Skip to content

feat(cards): add PIN management APIs and status webhooks - #897

Merged
DhruvPareek merged 5 commits into
mainfrom
dp/card-pin-management
Sep 25, 2026
Merged

DhruvPareek merged 5 commits into
mainfrom
dp/card-pin-management

Conversation

@DhruvPareek

@DhruvPareek DhruvPareek commented Sep 4, 2026 •

Copy link
Copy Markdown
Contributor

Reason

Platforms need to let cardholders choose or replace a PIN, recover a blocked PIN, and receive status changes. Platforms configure the website hosting the PIN iframe once, then request a temporary URL without repeating that setting.

Overview

Add three online-PIN operations: encrypted set/change (POST /cards/{id}/set-pin), hosted PIN-entry URL retrieval (GET /cards/{id}/set-pin-url), and unblock (POST /cards/{id}/pin/unblock). Read pinStatus from GET /cards/{id}. Offline PIN cards and PIN reveal are unsupported.

Configure cardConfigs.pinTargetOrigin using PATCH /platform/config. The value is a canonical HTTPS origin shared by all user-specific paths on that website. Omission preserves it; null clears it. GET URL requests take no body or origin parameter and return 409 CONFLICT if the setting is absent. Changes to the setting apply to newly requested URLs.

Each URL request creates a fresh temporary PIN-entry session and returns CardPinUrlResponse: iframeUrl, sessionToken, expiresAt, and environment. The URL permits one successful PIN submission. Responses specify Cache-Control: no-store; the operation requires card-management permission and an attributable audit actor. Fetching the URL does not change the PIN. The obsolete POST session operation and its request schema are removed.

The guide covers platform configuration, loading the iframe, submitting a PIN, and confirming the result. Custom PIN entry requires client-side encryption of the complete nonce/PIN payload with the downloadable public key. An OK status alone cannot confirm replacement of an existing PIN.

Card.pinStatus is the last observed status, present from issuance when PIN management is supported and absent when unsupported. CARD.PIN_STATUS_CHANGE carries the updated Card after an observed status change. Initial issuance and replacements that leave the status OK do not emit an event. The webhook guide covers signature verification, event deduplication, and out-of-order handling. No PIN material or session credentials are included in notifications.

The implementation stack is webdev #34392 (regeneration), #34393 (handlers), #34394 (Ops UI), and #36190 (platform notifications). This GET/platform-configuration revision updates only the Grid API contract and docs. Corresponding webdev regeneration, configuration persistence, handler, and caller changes are deferred; the webdev PRs have not been updated for this contract. Incoming PIN updates also depend on the Lithic webhook subscription.

Test Plan

  • make build and make lint: pass; Spectral reports 0 errors. Its broad pagination rule adds a warning for the single-object GET URL response, alongside existing warnings.
  • mint openapi-check mintlify/openapi.yaml: pass.
  • Checked that the GET operation has no request body, exposes the no-store response header, and returns the renamed response schema; the old POST session operation and schemas are absent.
  • Checked that both platform configuration schemas reference the updated CardConfig, null is allowed, the origin example matches the pattern, valid origins pass, and HTTP URLs, paths, queries, fragments, credentials, whitespace, and backslashes fail the pattern.
  • Verified the root and Mintlify OpenAPI bundles are identical, stale session-endpoint references are absent, and git diff --check passes.
  • No backend or live iframe validation for this contract revision; webdev changes are deferred.

@vercel

vercel Bot commented Sep 4, 2026 •

Copy link
Copy Markdown

The latest updates on your projects. Learn more about Vercel for GitHub.

Project Deployment Actions Updated
grid-statements-demo Error Error Sep 25, 2026 5:06pm UTC
3 Skipped Deployments
Project Deployment Actions Updated
grid-cards-demo Ignored Ignored Preview Sep 25, 2026 5:06pm UTC
grid-flow-builder Ignored Ignored Preview Sep 25, 2026 5:06pm UTC
grid-wallet-demo Ignored Ignored Preview Sep 25, 2026 5:06pm UTC

Request Review

@mintlify

mintlify Bot commented Sep 4, 2026 •

Copy link
Copy Markdown
Contributor

Preview deployment for your docs. Learn more about Mintlify Previews.

Project Status Preview Updated
Grid 🟢 Ready View Preview Sep 25, 2026, 5:06 PM

Copy link
Copy Markdown
Contributor Author

This stack of pull requests is managed by Graphite. Learn more about stacking.

@github-actions

github-actions Bot commented Sep 4, 2026 •

Copy link
Copy Markdown
Contributor

✱ Stainless preview builds for grid

This PR will update the grid SDKs with the following commit messages.

cli

chore(internal): regenerate SDK with no functional changes

go

feat(api): add pinStatus field to Card and webhook event responses

kotlin

feat(api): add pinStatus field to Card model

openapi

feat(api): add PIN management methods (set/session/unblock) and pinStatus field to cards

php

feat(api): add pinStatus field to Card model

python

feat(api): add pin_status field to Card

ruby

feat(api): add pin_status field to Card model

typescript

feat(api): add pinStatus field to Card response

Edit this comment to update them. They will appear in their respective SDK's changelogs.

✅ grid-typescript studio · code · diff

Your SDK build had at least one new note diagnostic, which is a regression from the base state.
generate ❗ → build ⏭️ → lint ⏭️ → test ⏭️

New diagnostics (4 note)
💡 Endpoint/NotConfigured: Skipped endpoint because it's not in your Stainless config: `get /cards/{id}/pin`
💡 Endpoint/NotConfigured: Skipped endpoint because it's not in your Stainless config: `post /cards/{id}/pin`
💡 Endpoint/NotConfigured: Skipped endpoint because it's not in your Stainless config: `post /cards/{id}/pin/session`
💡 Endpoint/NotConfigured: Skipped endpoint because it's not in your Stainless config: `post /cards/{id}/pin/unblock`
✅ grid-openapi studio · code · diff

Your SDK build had at least one new note diagnostic, which is a regression from the base state.
generate ❗

New diagnostics (4 note)
💡 Endpoint/NotConfigured: Skipped endpoint because it's not in your Stainless config: `get /cards/{id}/pin`
💡 Endpoint/NotConfigured: Skipped endpoint because it's not in your Stainless config: `post /cards/{id}/pin`
💡 Endpoint/NotConfigured: Skipped endpoint because it's not in your Stainless config: `post /cards/{id}/pin/session`
💡 Endpoint/NotConfigured: Skipped endpoint because it's not in your Stainless config: `post /cards/{id}/pin/unblock`
✅ grid-kotlin studio · code · diff

Your SDK build had at least one new note diagnostic, which is a regression from the base state.
generate ❗ → build ⏭️ (prev: build ✅) → lint ⏭️ (prev: lint ✅) → test ⏭️ (prev: test ❗)

New diagnostics (4 note)
💡 Endpoint/NotConfigured: Skipped endpoint because it's not in your Stainless config: `get /cards/{id}/pin`
💡 Endpoint/NotConfigured: Skipped endpoint because it's not in your Stainless config: `post /cards/{id}/pin`
💡 Endpoint/NotConfigured: Skipped endpoint because it's not in your Stainless config: `post /cards/{id}/pin/session`
💡 Endpoint/NotConfigured: Skipped endpoint because it's not in your Stainless config: `post /cards/{id}/pin/unblock`
✅ grid-ruby studio · code · diff

Your SDK build had at least one new note diagnostic, which is a regression from the base state.
generate ❗ → build ⏭️ → lint ⏭️ → test ⏭️

New diagnostics (4 note)
💡 Endpoint/NotConfigured: Skipped endpoint because it's not in your Stainless config: `get /cards/{id}/pin`
💡 Endpoint/NotConfigured: Skipped endpoint because it's not in your Stainless config: `post /cards/{id}/pin`
💡 Endpoint/NotConfigured: Skipped endpoint because it's not in your Stainless config: `post /cards/{id}/pin/session`
💡 Endpoint/NotConfigured: Skipped endpoint because it's not in your Stainless config: `post /cards/{id}/pin/unblock`
✅ grid-go studio · code · diff

Your SDK build had at least one new note diagnostic, which is a regression from the base state.
generate ❗ → build ⏭️ → lint ⏭️ → test ⏭️

go get github.com/stainless-sdks/grid-go@029033a1e06e66bd4e9d620077088797fbe86b4a
New diagnostics (4 note)
💡 Endpoint/NotConfigured: Skipped endpoint because it's not in your Stainless config: `get /cards/{id}/pin`
💡 Endpoint/NotConfigured: Skipped endpoint because it's not in your Stainless config: `post /cards/{id}/pin`
💡 Endpoint/NotConfigured: Skipped endpoint because it's not in your Stainless config: `post /cards/{id}/pin/session`
💡 Endpoint/NotConfigured: Skipped endpoint because it's not in your Stainless config: `post /cards/{id}/pin/unblock`
✅ grid-python studio · code · diff

Your SDK build had at least one new note diagnostic, which is a regression from the base state.
generate ❗ → build ⏭️ → lint ⏭️ → test ⏭️

New diagnostics (4 note)
💡 Endpoint/NotConfigured: Skipped endpoint because it's not in your Stainless config: `get /cards/{id}/pin`
💡 Endpoint/NotConfigured: Skipped endpoint because it's not in your Stainless config: `post /cards/{id}/pin`
💡 Endpoint/NotConfigured: Skipped endpoint because it's not in your Stainless config: `post /cards/{id}/pin/session`
💡 Endpoint/NotConfigured: Skipped endpoint because it's not in your Stainless config: `post /cards/{id}/pin/unblock`
✅ grid-php studio · code · diff

Your SDK build had at least one new note diagnostic, which is a regression from the base state.
generate ❗ → lint ⏭️ → test ⏭️

New diagnostics (4 note)
💡 Endpoint/NotConfigured: Skipped endpoint because it's not in your Stainless config: `get /cards/{id}/pin`
💡 Endpoint/NotConfigured: Skipped endpoint because it's not in your Stainless config: `post /cards/{id}/pin`
💡 Endpoint/NotConfigured: Skipped endpoint because it's not in your Stainless config: `post /cards/{id}/pin/session`
💡 Endpoint/NotConfigured: Skipped endpoint because it's not in your Stainless config: `post /cards/{id}/pin/unblock`
✅ grid-cli studio · code · diff

Your SDK build had at least one new note diagnostic, which is a regression from the base state.
generate ❗ → build ⏭️ → lint ⏭️ → test ⏭️

New diagnostics (4 note)
💡 Endpoint/NotConfigured: Skipped endpoint because it's not in your Stainless config: `get /cards/{id}/pin`
💡 Endpoint/NotConfigured: Skipped endpoint because it's not in your Stainless config: `post /cards/{id}/pin`
💡 Endpoint/NotConfigured: Skipped endpoint because it's not in your Stainless config: `post /cards/{id}/pin/session`
💡 Endpoint/NotConfigured: Skipped endpoint because it's not in your Stainless config: `post /cards/{id}/pin/unblock`

This comment is auto-generated by GitHub Actions and is automatically kept up to date as you push.
If you push custom code to the preview branch, re-run this workflow to update the comment.
Last updated: 2026-09-22 22:42:54 UTC

@greptile-apps

greptile-apps Bot commented Sep 22, 2026 •

Copy link
Copy Markdown
Contributor

RetriggerConfidence Score: 4/5

The API contract appears functionally safe, but the explicit Mintlify procedure requirement should be satisfied before merging and the session-origin schema should be tightened.

Findings

  1. P2 Origin Constraint Is Too Broad ▶
  2. P2 Hosted Flow Lacks Steps ▶
Fix with agent prompt
### Issue 1
openapi/components/schemas/cards/CardPinSessionRequest.yaml:7
`format: uri` accepts values such as `http://app.example.com` and `https://app.example.com/path`, even though the endpoint only accepts a canonical HTTPS origin. Generated validators may therefore approve requests that the server rejects with `400 INVALID_INPUT`. Add schema constraints for the HTTPS, origin-only requirement.

### Issue 2
mintlify/cards/card-management/pins.mdx:12-14
The hosted PIN setup presents SDK initialization, session creation, control mounting, submission, and result verification as prose. The repository's Mintlify writing guide requires complex procedures to use numbered steps and provide expected outcomes. Restructure this flow so readers can follow the required order and confirm success; this repository requirement must be satisfied before merging.

Note: If this suggestion doesn't match your team's coding style, reply to this and let me know. I'll remember it for next time!

---

For each issue above, determine whether it is valid and should be fixed. If so, fix it directly.

Summary

This PR adds the public contract and integration documentation for setting, hosting, reading, and unblocking card PINs.

  • Registers four PIN operations and their request, response, and status schemas.
  • Adds optional last-observed PIN status to the card resource.
  • Adds a cardholder integration guide and navigation entry.
  • Keeps the modular specification and both generated bundles synchronized.
Diagram
%%{init: {'theme': 'neutral'}}%%
flowchart TD
  Client[Authenticated platform backend] --> Choice{PIN entry flow}
  Choice -->|Hosted| Session[POST /cards/id/pin/session]
  Session --> SDK[Lithic hosted PIN control]
  Choice -->|Client encrypted| Set[POST /cards/id/pin]
  SDK --> Status[GET /cards/id/pin]
  Set --> Status
  Status -->|BLOCKED, retain PIN| Unblock[POST /cards/id/pin/unblock]
  Status -->|BLOCKED or forgotten, replace PIN| Choice
Loading

Reviews (1) · Last reviewed commit: "feat(cards): add PIN management endpoint..."

Comment thread openapi/components/schemas/cards/CardPinSessionRequest.yaml Outdated
Comment thread mintlify/cards/card-management/pins.mdx Outdated
@@ -0,0 +1,135 @@
parameters:

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

do we need this? seems like get card already gives you the pin

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

get card only gives you the pin status, like if the pin was configured, not configured, or blocked

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

oh sry i meant the get below. seems like it only has the status

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Oh yea get pin here is redundant with the existing get card. I can remove it, i'm just curious if it is something where it would seem confusing to an integrator that they cant do GET /cards/{id}/pin when we have an existing POST /cards/{id}/pin?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If i'm wrong about that, i can def remove GET /cards/{id}/pin

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

i dont think we need it but maybe @pengying has some thoughts. should we also emit some webhooks on these status changes?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

For chaning a pin to OK or NOT_SET I don't think we need webhooks because the platform itself is making API calls to get the PIN to that state. For BLOCKED i can add follow up PRs for the webhook

@ls-bolt

ls-bolt Bot commented Sep 23, 2026

Copy link
Copy Markdown
Contributor

🦣 Congratulations @shreyav - your substantive review earned a Argentavis! (rare)

With a wingspan around seven meters, it was among the largest flying birds ever known.

View your Frost-dex: https://zeus.dev.dev.sparkinfra.net/#/dinodex/shreyav?section=ice-age

description: |
State of the card's PIN. On the Card resource, this is the last known status
and is present whenever PIN management is supported. An absent value means
PIN management is unavailable for this card; it does not mean `NOT_SET`.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

should this be a card capability

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

that sentence is just there because Striga cards operate differently with PINs so this is just an absent value / null for striga cards

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

just wanted to clarify the difference between null and NOT_SET

@@ -0,0 +1,15 @@
type: object

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

what happens if you call create session twice? should the session resource have an id?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

wait one sec, turns out Ben implemented card reveal iframes using their old flow which doesnt use sessions

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

but why do you think a session id would be necessary? sessions are short lived and not really like an object that would be used later on or need to be referred to

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

the question underneath is what happens to session 1 when you create session 2. because the status, and the unblock api are scoped to the card and not the session

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

that's a lithic thing but they aren't explicit in their docs about it, it does Look like either session can still update the card though

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

so it shouldnt matter

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yea highly confident either can update the PIN still

@DhruvPareek DhruvPareek changed the title feat(cards): add PIN management endpoints feat(cards): add PIN management APIs and status webhooks Sep 24, 2026
@DhruvPareek
DhruvPareek force-pushed the dp/card-pin-management branch from b414afd to ea631b8 Compare September 24, 2026 18:36
Adds three card PIN endpoints and a pinStatus field on the Card resource.

- POST /cards/{id}/pin takes a PIN block the client encrypted for the card
  processor. Grid forwards it verbatim and never sees the PIN.
- POST /cards/{id}/pin/session mints a short-lived session token for the
  processor's hosted PIN-entry iframe, for platforms without their own PIN
  UI. The processor returns a token rather than a URL, so the response
  carries the token and the client's embed SDK builds the iframe from it.
- POST /cards/{id}/pin/unblock clears a PIN blocked by three consecutive
  incorrect entries, without changing it.
- Card.pinStatus reports NOT_SET / OK / BLOCKED, and is absent on cards whose
  processor does not offer PIN management.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01GsstRe1f8aoPXm1kYQw2Vm
@DhruvPareek
DhruvPareek force-pushed the dp/card-pin-management branch from ef03da1 to 6d7ba06 Compare September 24, 2026 23:14
@github-actions github-actions Bot removed the breaking-change Introduces a breaking change to the OpenAPI spec label Sep 24, 2026
Comment thread openapi/paths/cards/cards_{id}_set_pin.yaml
@ls-bolt

ls-bolt Bot commented Sep 24, 2026

Copy link
Copy Markdown
Contributor

🦣 Congratulations @pengying - your substantive review earned a Metailurus! (common)

Its canines were intermediate between the conical teeth of cats and true sabers.

View your Frost-dex: https://zeus.dev.dev.sparkinfra.net/#/dinodex/pengying?section=ice-age

required:
- targetOrigin
properties:
targetOrigin:

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

can we make this on the platform config instead of each set pin request? And then we can make pin url a get.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

wdym by "make pin url a get." ?

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

POST /cards/{id}/pin/session
{
"targetOrigin": "https://app.example.com"
}

becomes

GET /cards/{id}/set-pin-link

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

fixed

Comment thread openapi/openapi.yaml Outdated
Comment thread openapi/openapi.yaml Outdated

DhruvPareek commented Sep 25, 2026 •

Copy link
Copy Markdown
Contributor Author

Merge activity

  • Sep 25, 7:21 PM UTC: A user started a stack merge that includes this pull request via Graphite.
  • Sep 25, 7:21 PM UTC: @DhruvPareek merged this pull request with Graphite.

@DhruvPareek
DhruvPareek merged commit bf17b3f into main Sep 25, 2026
10 checks passed
@DhruvPareek
DhruvPareek deleted the dp/card-pin-management branch September 25, 2026 19:21

This branch had an error being deployed

1 failed (outdated) and 1 active deployments
staging - mintlify — 556e6ed7 Deployed Sep 25, 2026 by mintlify[bot]
Preview – grid-statements-demo — e64c7ef1 Deployed Sep 21, 2026 by vercel[bot]
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants