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
PENDING_ISSUE ──► ACTIVE ──► FROZEN ──► ACTIVE ──► CLOSED
│ │ ▲
│ └──────────────────────────────┘
│
└─► CLOSED (stateReason: ISSUER_REJECTED)
| State | When you see it |
|---|
PENDING_ISSUE | 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: "PENDING_ISSUE". The
issuer provisions the card asynchronously; on success a
CARD.STATE_CHANGE webhook fires with the activated Card resource
including the populated last4, expMonth, expYear, and
panEmbedUrl.
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.
Render panEmbedUrl in an iframe in your client to display the full
PAN, CVV, and expiry to the cardholder. The full credentials never
cross your servers.
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.