Skip to content

feat(cards): add PIN management endpoints - #897

Draft
DhruvPareek wants to merge 1 commit into
mainfrom
dp/card-pin-management
Draft

feat(cards): add PIN management endpoints#897
DhruvPareek wants to merge 1 commit into
mainfrom
dp/card-pin-management

Conversation

@DhruvPareek

@DhruvPareek DhruvPareek commented Sep 4, 2026

Copy link
Copy Markdown
Contributor

Reason

Cardholders have no way to set a PIN on a Grid-issued card. Without one, PIN-verified transactions decline, and a cardholder who blocks their PIN through incorrect attempts has no recovery path through Grid at all.

Overview

Adds three endpoints and one field. The two set paths are alternatives — a platform picks whichever matches how much of the PIN-entry experience it wants to own — and neither lets a PIN reach Grid.

Endpoint Purpose
POST /cards/{id}/pin Set/replace the PIN from a PIN block the client encrypted for the card processor. Grid forwards the block verbatim; it is never stored, logged, or readable by Grid. For platforms with their own PIN-entry UI.
POST /cards/{id}/pin/session Mint a short-lived session token for the processor's hosted PIN-entry iframe. The PIN crosses neither the platform's servers nor Grid's. For platforms without their own PIN UI.
POST /cards/{id}/pin/unblock Clear a PIN blocked by three consecutive incorrect entries, leaving the PIN itself unchanged.

Card.pinStatus (NOT_SET / OK / BLOCKED) makes the state observable, and is absent on cards whose processor offers no PIN management.

Notes for review:

  • The session response is a token, not a URL. This is the one place the shape departs from POST /cards/{id}/reveal: the processor's embed-session endpoint answers with an opaque session credential and no URL, and the iframe src its SDK builds is not publicly documented. Returning a pinEmbedUrl would have meant inventing one, so the response carries sessionToken and the client's embed SDK builds the iframe from it.
  • encryptedPinBlock is deliberately opaque. Grid cannot validate it, so the docs put the burden of correct encryption on the client and say plainly that a rejected block returns 400 INVALID_INPUT with no finer diagnosis.
  • The iframe path is fire-and-forget. Grid is not told whether the cardholder finished PIN entry, so CardPinSessionResponse points readers at pinStatus to confirm the outcome rather than implying the mint itself means success.
  • Unblock is a dedicated endpoint rather than PATCH /cards/{id} with a status flag: CardUpdateRequest already requires one of state/fundingSources/maxSpend* and carries CLOSED exclusivity rules, and threading a one-way status through that is more churn than a 204.

Everything else follows the reveal endpoint — same security scheme, same audit-logging language, same short-lived-bearer-secret warnings.

Test Plan

  • make build — bundle regenerates cleanly.
  • npx @redocly/cli lint openapi.yaml — valid, and the warning count is unchanged from main (55 before, 55 after), so these files add none.
  • npx @stoplight/spectral-cli lint openapi.yaml --fail-severity=error — 0 errors, and no finding references any new schema or path (the info count drops by one against baseline, since CardPinStatus carries the example CardState lacks).

The sparkcore implementation and the ops UI are stacked in webdev: lightsparkdev/webdev#34390 → #34391 → #34392 → #34393 → #34394.

🤖 Generated with Claude Code

https://claude.ai/code/session_01GsstRe1f8aoPXm1kYQw2Vm

@vercel

vercel Bot commented Sep 4, 2026

Copy link
Copy Markdown

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

2 Skipped Deployments
Project Deployment Actions Updated
grid-flow-builder Ignored Ignored Preview Sep 4, 2026 12:54am UTC
grid-wallet-demo Ignored Ignored Preview Sep 4, 2026 12:54am 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 4, 2026, 12:45 AM

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 ✅ (prev: build ❗) → lint ❗test ✅

npm install https://pkg.stainless.com/s/grid-typescript/20d239a8133c216b51218d3044305e90eb8ccc74/dist.tar.gz
New diagnostics (3 note)
💡 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 (3 note)
💡 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 (3 note)
💡 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 ✅lint ✅test ❗

New diagnostics (3 note)
💡 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@bea5948e25e3f5c7a6b95d0b14b0036fb623aabf
New diagnostics (3 note)
💡 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 ❗

pip install https://pkg.stainless.com/s/grid-python/13f6dacc24f0539dfa1b76045885a22de09f247d/grid-0.0.1-py3-none-any.whl
New diagnostics (3 note)
💡 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 (3 note)
💡 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 (3 note)
💡 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-04 01:02:07 UTC

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
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.

1 participant