docs(cards): panEmbedUrl is nullable and minted only on GET /cards/{id}#657
docs(cards): panEmbedUrl is nullable and minted only on GET /cards/{id}#657benwgold wants to merge 6 commits into
Conversation
Make Card.panEmbedUrl nullable in the OpenAPI schema and document the fetch-on-demand reveal pattern: the signed URL is populated only on the single-card GET, is short-lived, and must be fetched right before rendering the PAN-reveal iframe. Update the issuing-cards, quickstart, webhooks, and terminology docs (and the CARD.STATE_CHANGE webhook example) that previously showed panEmbedUrl arriving populated in the webhook payload. Follow-up #2 from webdev#29877. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
|
This stack of pull requests is managed by Graphite. Learn more about stacking. |
✱ Stainless preview builds for gridThis PR will update the cli csharp go kotlin openapi php python ruby typescript Edit this comment to update them. They will appear in their respective SDK's changelogs. ✅ grid-openapi studio · code · diff
✅ grid-ruby studio · code · diff
✅ grid-go studio · code · diff
✅ grid-kotlin studio · code · diff
|
Greptile SummaryThis PR corrects the public documentation to accurately reflect that
Confidence Score: 5/5Documentation-only change that corrects an inaccurate description; no application logic is touched. All nine changed files are documentation (OpenAPI YAML descriptions/examples and Mintlify MDX snippets). The schema change to nullable openapi/webhooks/card-state-change.yaml — the
|
| Filename | Overview |
|---|---|
| openapi/components/schemas/cards/Card.yaml | Adds nullable type ([string, 'null']) to panEmbedUrl — correct OpenAPI 3.1 syntax — and updates the description to document the single-GET-only, short-lived nature of the URL. |
| openapi/paths/cards/cards_{id}.yaml | Extends the GET /cards/{id} description to explicitly call out that this is the only endpoint that mints a fresh panEmbedUrl. No schema changes. |
| openapi/webhooks/card-state-change.yaml | Updates the activated example to show panEmbedUrl: null. The frozen and issuerRejected examples still omit the field rather than explicitly setting it to null, creating a minor inconsistency with the activated example. |
| mintlify/snippets/cards/issuing-cards.mdx | Removes panEmbedUrl from the list of fields populated in the activation webhook and adds a clear "Revealing the PAN" section with the fetch-on-demand pattern. |
| mintlify/snippets/cards/quickstart.mdx | Webhook example payload updated to panEmbedUrl: null and the note rewritten to point users to GET /cards/{id} for the reveal URL. |
| mintlify/snippets/cards/webhooks.mdx | Webhook payload example updated to show panEmbedUrl: null and the bullet for the ACTIVE branch rewritten to direct integrators to call GET /cards/{id} before rendering the iframe. |
| mintlify/snippets/cards/terminology.mdx | Appends a sentence to the panEmbedUrl definition clarifying it's short-lived and available only on the single-card GET. |
Sequence Diagram
%%{init: {'theme': 'neutral'}}%%
sequenceDiagram
participant Platform
participant Grid API
participant Webhook
Platform->>Grid API: POST /cards
Grid API-->>Platform: 200 { state: PROCESSING, panEmbedUrl: null }
Grid API->>Webhook: CARD.STATE_CHANGE { state: ACTIVE, panEmbedUrl: null }
Note over Webhook: panEmbedUrl is always null in webhook payloads
Platform->>Grid API: GET /cards/{id}
Grid API-->>Platform: 200 { state: ACTIVE, panEmbedUrl: "https://embed.lithic.com/iframe/...?t=..." }
Note over Platform,Grid API: Fresh signed URL minted on each GET /cards/{id} call (~5 min TTL)
Platform->>Platform: Render panEmbedUrl in iframe
Note over Platform: Cardholder sees PAN/CVV/expiry — credentials never cross Grid or platform servers
%%{init: {'theme': 'base', 'themeVariables': {"darkMode": true, "background": "#0d1117", "primaryColor": "#21262d", "primaryTextColor": "#e6edf3", "primaryBorderColor": "#8b949e", "lineColor": "#8b949e", "textColor": "#e6edf3", "edgeLabelBackground": "#161b22", "actorBkg": "#21262d", "actorBorder": "#8b949e", "actorTextColor": "#e6edf3", "actorLineColor": "#8b949e", "signalColor": "#8b949e", "signalTextColor": "#e6edf3", "noteBkgColor": "#373320", "noteBorderColor": "#d4a72c", "noteTextColor": "#f0e6c0", "labelBoxBkgColor": "#21262d", "labelBoxBorderColor": "#8b949e", "labelTextColor": "#e6edf3", "loopTextColor": "#e6edf3", "activationBkgColor": "#30363d", "activationBorderColor": "#8b949e"}}}%%
sequenceDiagram
participant Platform
participant Grid API
participant Webhook
Platform->>Grid API: POST /cards
Grid API-->>Platform: 200 { state: PROCESSING, panEmbedUrl: null }
Grid API->>Webhook: CARD.STATE_CHANGE { state: ACTIVE, panEmbedUrl: null }
Note over Webhook: panEmbedUrl is always null in webhook payloads
Platform->>Grid API: GET /cards/{id}
Grid API-->>Platform: 200 { state: ACTIVE, panEmbedUrl: "https://embed.lithic.com/iframe/...?t=..." }
Note over Platform,Grid API: Fresh signed URL minted on each GET /cards/{id} call (~5 min TTL)
Platform->>Platform: Render panEmbedUrl in iframe
Note over Platform: Cardholder sees PAN/CVV/expiry — credentials never cross Grid or platform servers
Comments Outside Diff (1)
-
openapi/webhooks/card-state-change.yaml, line 86-106 (link)The
frozenandissuerRejectedexamples silently omitpanEmbedUrl, while theactivatedexample now explicitly showspanEmbedUrl: null. This mixed pattern — null vs. absent — leaves integrators unsure whether the field is present-but-null or not included in those payloads. Since the PR establishes thatpanEmbedUrlis always null in webhook payloads, adding the field explicitly to the other examples would make the examples self-consistent and reinforce that guarantee.Prompt To Fix With AI
This is a comment left during a code review. Path: openapi/webhooks/card-state-change.yaml Line: 86-106 Comment: The `frozen` and `issuerRejected` examples silently omit `panEmbedUrl`, while the `activated` example now explicitly shows `panEmbedUrl: null`. This mixed pattern — null vs. absent — leaves integrators unsure whether the field is present-but-null or not included in those payloads. Since the PR establishes that `panEmbedUrl` is always null in webhook payloads, adding the field explicitly to the other examples would make the examples self-consistent and reinforce that guarantee. How can I resolve this? If you propose a fix, please make it concise.
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!
Prompt To Fix All With AI
Fix the following 1 code review issue. Work through them one at a time, proposing concise fixes.
---
### Issue 1 of 1
openapi/webhooks/card-state-change.yaml:86-106
The `frozen` and `issuerRejected` examples silently omit `panEmbedUrl`, while the `activated` example now explicitly shows `panEmbedUrl: null`. This mixed pattern — null vs. absent — leaves integrators unsure whether the field is present-but-null or not included in those payloads. Since the PR establishes that `panEmbedUrl` is always null in webhook payloads, adding the field explicitly to the other examples would make the examples self-consistent and reinforce that guarantee.
```suggestion
frozen:
summary: Card frozen by the platform
value:
id: Webhook:019542f5-b3e7-1d02-0000-000000000022
type: CARD.STATE_CHANGE
timestamp: '2026-05-09T09:00:00Z'
data:
id: Card:019542f5-b3e7-1d02-0000-000000000010
cardholderId: Customer:019542f5-b3e7-1d02-0000-000000000001
state: FROZEN
stateReason: null
brand: VISA
form: VIRTUAL
last4: '4242'
expMonth: 12
expYear: 2029
panEmbedUrl: null
fundingSources:
- InternalAccount:019542f5-b3e7-1d02-0000-000000000002
currency: USD
createdAt: '2026-05-08T14:10:00Z'
updatedAt: '2026-05-09T09:00:00Z'
```
Reviews (1): Last reviewed commit: "docs(cards): panEmbedUrl is nullable and..." | Re-trigger Greptile
…ples The frozen and issuerRejected examples omitted the field while activated showed it as null; the serializer always emits the key, so all three now match the wire format. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
|
The latest updates on your projects. Learn more about Vercel for GitHub. 2 Skipped Deployments
|
|
Applied. Added explicit |
Folded in from #656 so this PR's preview check goes green on its own. Co-Authored-By: mohamedwane <mohamedwane@users.noreply.github.com> Co-Authored-By: benwgold <benwgold@users.noreply.github.com>
|
Folded #656 into this PR per ben's request ("can probably merge into one"): commit 0bc4ba1 adds |
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
skip: true doesn't stop the target's build CI job from gating the preview check (656's green runs passed only because their base build had no cached csharp success to regress from). With no csharp target in the config there is no head build outcome to compare, so the broken upstream csharp build CI can't fail the check. Co-Authored-By: benwgold <benwgold@users.noreply.github.com>
|
Correction to my earlier comment: the folded-in What the sticky Stainless comment revealed: on this PR the head csharp build ran its build CI ( With no csharp target in the config, there's no head csharp build outcome to compare at all, which deterministically takes the broken upstream csharp build CI out of the gate. csharp was unpublished ( Watching the fresh |
|
Removing the csharp target from the repo config doesn't fix it either. Evidence from the run on 069ad9a (job 85516928968):
So the project's target set persists server-side (Stainless Studio, org Meanwhile this PR doesn't need to wait: every real gate is green (Lint, Docs build, Breaking Changes, Greptile 5/5), the red |
|
Verdict from probe PR #659: the csharp removal in this PR WILL fix the The probe: #659 stacked a doc-comment-only codegen delta (the exact class proven to fail The mechanism (from the probe's sticky comment + the action source): the gate is a per-language head-vs-base regression comparison, and Stainless builds csharp regardless of the repo config (the project's target registry persists server-side — that's why
Practical upshot:
Probe #659 is closed and its branch deleted. |
Keep csharp configured; the preview check stays red on codegen-delta PRs until Stainless fixes their csharp build CI upstream. Co-Authored-By: benwgold <benwgold@users.noreply.github.com>
|
Per ben's preference to keep csharp: commit 371594f restores the csharp target block, so |

Summary
Follow-up #2 from webdev#29877 (Lithic card PAN-reveal POC) — brings the public docs in line with the actual reveal model: the signed
panEmbedUrlis minted only onGET /cards/{id}, is short-lived (~5 min TTL), and is null everywhere else (list/create/update responses and webhook payloads).openapi/components/schemas/cards/Card.yaml—panEmbedUrlis now nullable (type: [string, 'null']), with a description stating it's populated only on the single-card GET, is short-lived, and should be fetched right before rendering the iframe (never cached).openapi/paths/cards/cards_{id}.yaml—GET /cards/{id}description now calls out that it's the only endpoint that mints a freshpanEmbedUrl.mintlify/snippets/cards/issuing-cards.mdx(rendered bymintlify/cards/card-management/issuing-cards.mdx, which just imports the snippet) — removedpanEmbedUrlfrom the fields theCARD.STATE_CHANGEwebhook delivers; added a "Revealing the PAN" section with the fetch-on-demand pattern.mintlify/snippets/cards/webhooks.mdxandquickstart.mdx(webhook example payloads now showpanEmbedUrl: null; reveal notes rewritten to fetch-on-demand),terminology.mdx(PAN embed URL definition mentions single-GET-only + short-lived), and theactivatedexample inopenapi/webhooks/card-state-change.yaml.Verification
make build— rebundledopenapi.yaml+mintlify/openapi.yaml(both included).make lint— passes with 0 errors; remaining warnings/infos are pre-existing on main (verified by diffing against a baseline lint run onmain).🤖 thundering-graviton(#1) | Feedback
Original PR: #654