A card is created with a single POST /cards request and progresses
through a fixed lifecycle. This page covers the request shape, what
happens after issuance, and the errors you should handle.
Request shape
curl -X POST "$GRID_BASE_URL/cards" \
-u "$GRID_CLIENT_ID:$GRID_CLIENT_SECRET" \
-H "Content-Type: application/json" \
-d '{
"cardholderId": "Customer:019542f5-b3e7-1d02-0000-000000000001",
"platformCardId": "card-emp-aary-001",
"form": "VIRTUAL",
"fundingSources": [
"InternalAccount:019542f5-b3e7-1d02-0000-000000000002"
]
}'
| Field | Required | Notes |
|---|
cardholderId | Yes | The Customer that owns the card. Must be kycStatus: APPROVED. |
platformCardId | No | Your own identifier. System-generated when omitted, mirroring platformCustomerId. |
form | Yes | VIRTUAL in v1. PHYSICAL will be added later. |
fundingSources | Yes | Ordered array of InternalAccount ids. Each must belong to the cardholder and share one card-eligible currency. The first entry is tried first by Authorization Decisioning. |
The card’s currency is derived from the funding sources at issue time
and surfaces on the returned Card resource — all bound sources share
one currency.
The lifecycle
PROCESSING ──► ACTIVE ──► FROZEN ──► ACTIVE ──► CLOSED
│ │ ▲
│ └──────────────────────────────┘
│
└─► CLOSED (stateReason: ISSUER_REJECTED)
| State | When you see it |
|---|
PROCESSING | Returned synchronously from POST /cards. The card cannot transact yet. |
ACTIVE | Issuer provisioned the card. Reached via CARD.STATE_CHANGE webhook. |
FROZEN | You called PATCH /cards/{id} with state: "FROZEN". |
CLOSED | You called PATCH /cards/{id} with state: "CLOSED" (or the issuer rejected provisioning). Terminal. |
PENDING_KYC is also a valid state but you should not see it in v1 —
issuance is gated on KYC up front.
After issuance
POST /cards returns immediately with state: "PROCESSING". The
issuer provisions the card asynchronously; on success a
CARD.STATE_CHANGE webhook fires with the activated Card resource
including the populated last4, expMonth, and expYear.
If the issuer rejects provisioning, the same webhook fires with
state: "CLOSED" and stateReason: "ISSUER_REJECTED". That card is
terminal — issue a new one with a fresh platformCardId to retry.
Revealing the PAN
To show the cardholder their full PAN, CVV, and expiry, request a
reveal right before rendering:
curl -X POST "$GRID_BASE_URL/cards/Card:019542f5-b3e7-1d02-0000-000000000010/reveal" \
-u "$GRID_CLIENT_ID:$GRID_CLIENT_SECRET"
{
"panEmbedUrl": "https://embed.lithic.com/iframe/...?t=...",
"expiresAt": "2026-05-08T14:16:00Z"
}
panEmbedUrl is a signed URL for the card processor’s iframe that
renders the full credentials directly to the cardholder. The full PAN
and CVV never cross your servers or Grid’s.
POST /cards/{id}/reveal is the only way to obtain a reveal URL —
the Card resource never carries one (not in responses, not in webhook
payloads). The URL expires at expiresAt (within minutes), so request a
fresh reveal each time the cardholder asks for their details,
immediately before rendering the iframe. Never store, cache, or log the
URL. Every reveal is audit-logged.
Errors to handle
| Status | Code | What it means |
|---|
| 400 | CARDHOLDER_KYC_NOT_APPROVED | Cardholder is not kycStatus: APPROVED. Drive KYC to completion before retrying. |
| 400 | FUNDING_SOURCE_INELIGIBLE | The supplied internal account doesn’t belong to the cardholder or isn’t denominated in a card-eligible currency. |
| 400 | INVALID_INPUT | Validation failure on the request body. |
Changing funding sources later
The bound funding sources can be replaced after issuance via
PATCH /cards/{id} with a new fundingSources array. See
Funding sources for the rules
and the signed-retry flow.
Listing cards
curl -X GET "$GRID_BASE_URL/cards?cardholderId=Customer:019542f5-b3e7-1d02-0000-000000000001&limit=20" \
-u "$GRID_CLIENT_ID:$GRID_CLIENT_SECRET"
Filter by cardholderId, platformCardId, or state. The response is
paginated using the standard cursor shape used by other Grid list
endpoints.