> ## Documentation Index
> Fetch the complete documentation index at: https://docs.lightspark.com/llms.txt
> Use this file to discover all available pages before exploring further.

# Issuing Cards

> Create a virtual card and observe its lifecycle

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

```bash theme={null}
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

```text theme={null}
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:

```bash theme={null}
curl -X POST "$GRID_BASE_URL/cards/Card:019542f5-b3e7-1d02-0000-000000000010/reveal" \
  -u "$GRID_CLIENT_ID:$GRID_CLIENT_SECRET"
```

```json theme={null}
{
  "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.

<Note>
  `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.
</Note>

## 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](/cards/card-management/funding-sources) for the rules
and the signed-retry flow.

## Listing cards

```bash theme={null}
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.
