diff --git a/mintlify/cards/card-management/freezing-and-closing.mdx b/mintlify/cards/card-management/freezing-and-closing.mdx
new file mode 100644
index 00000000..23964b21
--- /dev/null
+++ b/mintlify/cards/card-management/freezing-and-closing.mdx
@@ -0,0 +1,9 @@
+---
+title: "Freezing & Closing Cards"
+description: "Freeze, unfreeze, and close cards via the signed-retry pattern"
+icon: "/images/icons/lock.svg"
+---
+
+import CardsFreezingAndClosing from '/snippets/cards/freezing-and-closing.mdx';
+
+
diff --git a/mintlify/cards/card-management/funding-sources.mdx b/mintlify/cards/card-management/funding-sources.mdx
new file mode 100644
index 00000000..f1d662f1
--- /dev/null
+++ b/mintlify/cards/card-management/funding-sources.mdx
@@ -0,0 +1,9 @@
+---
+title: "Funding Sources"
+description: "Bind and update internal accounts as card funding sources"
+icon: "/images/icons/wallet1.svg"
+---
+
+import CardsFundingSources from '/snippets/cards/funding-sources.mdx';
+
+
diff --git a/mintlify/cards/card-management/issuing-cards.mdx b/mintlify/cards/card-management/issuing-cards.mdx
new file mode 100644
index 00000000..9483f761
--- /dev/null
+++ b/mintlify/cards/card-management/issuing-cards.mdx
@@ -0,0 +1,9 @@
+---
+title: "Issuing Cards"
+description: "Create a virtual card and observe its lifecycle"
+icon: "/images/icons/credit-card1.svg"
+---
+
+import CardsIssuingCards from '/snippets/cards/issuing-cards.mdx';
+
+
diff --git a/mintlify/cards/index.mdx b/mintlify/cards/index.mdx
new file mode 100644
index 00000000..ed955dd9
--- /dev/null
+++ b/mintlify/cards/index.mdx
@@ -0,0 +1,11 @@
+---
+title: "Cards"
+sidebarTitle: "Introduction"
+description: "Issue virtual debit cards, decision authorizations in real time, and reconcile transactions against internal accounts."
+icon: "/images/icons/credit-card1.svg"
+mode: "wide"
+---
+
+import CardsIntro from '/snippets/cards/intro.mdx';
+
+
diff --git a/mintlify/cards/onboarding/cardholder-setup.mdx b/mintlify/cards/onboarding/cardholder-setup.mdx
new file mode 100644
index 00000000..a0ce40d9
--- /dev/null
+++ b/mintlify/cards/onboarding/cardholder-setup.mdx
@@ -0,0 +1,9 @@
+---
+title: "Cardholder Setup"
+description: "Prepare a customer to receive a card"
+icon: "/images/icons/people.svg"
+---
+
+import CardsCardholderSetup from '/snippets/cards/cardholder-setup.mdx';
+
+
diff --git a/mintlify/cards/onboarding/implementation-overview.mdx b/mintlify/cards/onboarding/implementation-overview.mdx
new file mode 100644
index 00000000..30c000b5
--- /dev/null
+++ b/mintlify/cards/onboarding/implementation-overview.mdx
@@ -0,0 +1,9 @@
+---
+title: "Implementation Overview"
+description: "End-to-end architecture for issuing and operating cards"
+icon: "/images/icons/code.svg"
+---
+
+import CardsImplementationOverview from '/snippets/cards/implementation-overview.mdx';
+
+
diff --git a/mintlify/cards/platform-tools/sandbox-testing.mdx b/mintlify/cards/platform-tools/sandbox-testing.mdx
new file mode 100644
index 00000000..9c1cee3b
--- /dev/null
+++ b/mintlify/cards/platform-tools/sandbox-testing.mdx
@@ -0,0 +1,9 @@
+---
+title: "Sandbox Testing"
+description: "Drive deterministic card outcomes with the simulate endpoints"
+icon: "/images/icons/sandbox.svg"
+---
+
+import CardsSandboxTesting from '/snippets/cards/sandbox-testing.mdx';
+
+
diff --git a/mintlify/cards/platform-tools/webhooks.mdx b/mintlify/cards/platform-tools/webhooks.mdx
new file mode 100644
index 00000000..c9c2d71b
--- /dev/null
+++ b/mintlify/cards/platform-tools/webhooks.mdx
@@ -0,0 +1,9 @@
+---
+title: "Webhooks"
+description: "Card webhook events and how to consume them"
+icon: "/images/icons/bell.svg"
+---
+
+import CardsWebhooks from '/snippets/cards/webhooks.mdx';
+
+
diff --git a/mintlify/cards/quickstart.mdx b/mintlify/cards/quickstart.mdx
new file mode 100644
index 00000000..ce89fbdb
--- /dev/null
+++ b/mintlify/cards/quickstart.mdx
@@ -0,0 +1,9 @@
+---
+title: "Quickstart"
+description: "Issue your first card and simulate a transaction end to end"
+icon: "/images/icons/rocket.svg"
+---
+
+import CardsQuickstart from '/snippets/cards/quickstart.mdx';
+
+
diff --git a/mintlify/cards/terminology.mdx b/mintlify/cards/terminology.mdx
new file mode 100644
index 00000000..8eb3fb17
--- /dev/null
+++ b/mintlify/cards/terminology.mdx
@@ -0,0 +1,9 @@
+---
+title: "Terminology"
+description: "Concepts and resources specific to the Cards API"
+icon: "/images/icons/file-text.svg"
+---
+
+import CardsTerminology from '/snippets/cards/terminology.mdx';
+
+
diff --git a/mintlify/cards/transactions/reconciliation.mdx b/mintlify/cards/transactions/reconciliation.mdx
new file mode 100644
index 00000000..a99087a1
--- /dev/null
+++ b/mintlify/cards/transactions/reconciliation.mdx
@@ -0,0 +1,9 @@
+---
+title: "Reconciliation"
+description: "How card transactions reconcile, and what exceptions to act on"
+icon: "/images/icons/checkmark1.svg"
+---
+
+import CardsReconciliation from '/snippets/cards/reconciliation.mdx';
+
+
diff --git a/mintlify/docs.json b/mintlify/docs.json
index d3280b5d..5bc96138 100644
--- a/mintlify/docs.json
+++ b/mintlify/docs.json
@@ -329,6 +329,47 @@
}
]
},
+ {
+ "tab": "Cards",
+ "groups": [
+ {
+ "group": "Overview",
+ "pages": [
+ "cards/index",
+ "cards/terminology",
+ "cards/onboarding/implementation-overview",
+ "cards/quickstart"
+ ]
+ },
+ {
+ "group": "Onboarding",
+ "pages": [
+ "cards/onboarding/cardholder-setup"
+ ]
+ },
+ {
+ "group": "Card Management",
+ "pages": [
+ "cards/card-management/issuing-cards",
+ "cards/card-management/funding-sources",
+ "cards/card-management/freezing-and-closing"
+ ]
+ },
+ {
+ "group": "Transactions",
+ "pages": [
+ "cards/transactions/reconciliation"
+ ]
+ },
+ {
+ "group": "Platform Tools",
+ "pages": [
+ "cards/platform-tools/webhooks",
+ "cards/platform-tools/sandbox-testing"
+ ]
+ }
+ ]
+ },
{
"tab": "API reference",
"groups": [
diff --git a/mintlify/openapi.yaml b/mintlify/openapi.yaml
index 7ca52cea..6a2a8401 100644
--- a/mintlify/openapi.yaml
+++ b/mintlify/openapi.yaml
@@ -53,6 +53,8 @@ tags:
description: 'Endpoints for creating and managing agents (experimental), called by the partner''s backend using platform credentials. Covers the full agent lifecycle: creation, policy configuration, pausing, deletion, the device code installation flow, and approving or rejecting transactions initiated by agents.'
- name: Agent Operations
description: Endpoints called by the agent itself using its own credentials (obtained via device code redemption). Scoped to the agent's associated customer — all requests automatically operate on behalf of that customer and are subject to the agent's policy. When an action requires approval, the resulting transaction enters a pending state and must be approved by the platform via `POST /transactions/{transactionId}/approve`.
+ - name: Cards
+ description: Card management endpoints. Issue debit cards against an internal account, freeze / unfreeze, close, manage card funding sources, and list card transactions.
paths:
/config:
get:
@@ -6163,6 +6165,533 @@ paths:
application/json:
schema:
$ref: '#/components/schemas/Error500'
+ /cards:
+ post:
+ summary: Issue a card
+ description: |
+ Issue a new card for a cardholder. Every card must be bound to at least one funding source at create time. The cardholder must have KYC status `APPROVED` before a card can be issued; otherwise the request is rejected with `CARDHOLDER_KYC_NOT_APPROVED`.
+
+ New cards start in `state: "PENDING_ISSUE"` while the card issuer provisions the card. The `card.state_change` webhook fires on the transition to `ACTIVE` (or to `CLOSED` with `stateReason: "ISSUER_REJECTED"` if provisioning fails).
+ operationId: createCard
+ tags:
+ - Cards
+ security:
+ - BasicAuth: []
+ requestBody:
+ required: true
+ content:
+ application/json:
+ schema:
+ $ref: '#/components/schemas/CardCreateRequest'
+ examples:
+ virtualCard:
+ summary: Issue a virtual card with one funding source
+ value:
+ cardholderId: Customer:019542f5-b3e7-1d02-0000-000000000001
+ platformCardId: card-emp-aary-001
+ form: VIRTUAL
+ fundingSources:
+ - InternalAccount:019542f5-b3e7-1d02-0000-000000000002
+ responses:
+ '201':
+ description: Card issued successfully
+ content:
+ application/json:
+ schema:
+ $ref: '#/components/schemas/Card'
+ '400':
+ description: Bad request. Returned with `CARDHOLDER_KYC_NOT_APPROVED` when the cardholder's KYC status is not `APPROVED`, with `FUNDING_SOURCE_INELIGIBLE` when the supplied funding source does not belong to the cardholder or is not denominated in a card-eligible currency, and for general invalid parameters.
+ content:
+ application/json:
+ schema:
+ $ref: '#/components/schemas/Error400'
+ '401':
+ description: Unauthorized
+ content:
+ application/json:
+ schema:
+ $ref: '#/components/schemas/Error401'
+ '500':
+ description: Internal service error
+ content:
+ application/json:
+ schema:
+ $ref: '#/components/schemas/Error500'
+ get:
+ summary: List cards
+ description: |
+ Retrieve a paginated list of cards. Cards can be filtered by cardholder, bound funding-source internal account, state, and platform-specific card identifier. If no filters are provided, returns all cards visible to the caller.
+ operationId: listCards
+ tags:
+ - Cards
+ security:
+ - BasicAuth: []
+ parameters:
+ - name: cardholderId
+ in: query
+ description: Filter by cardholder (customer) id.
+ required: false
+ schema:
+ type: string
+ - name: accountId
+ in: query
+ description: Filter by internal account id. Returns cards whose `fundingSources` array contains the given internal account id.
+ required: false
+ schema:
+ type: string
+ - name: platformCardId
+ in: query
+ description: Filter by platform-specific card identifier.
+ required: false
+ schema:
+ type: string
+ - name: state
+ in: query
+ description: Filter by card state.
+ required: false
+ schema:
+ $ref: '#/components/schemas/CardState'
+ - name: limit
+ in: query
+ description: Maximum number of results to return (default 20, max 100)
+ required: false
+ schema:
+ type: integer
+ minimum: 1
+ maximum: 100
+ default: 20
+ - name: cursor
+ in: query
+ description: Cursor for pagination (returned from previous request)
+ required: false
+ schema:
+ type: string
+ - name: sortOrder
+ in: query
+ description: Order to sort results in
+ required: false
+ schema:
+ type: string
+ enum:
+ - asc
+ - desc
+ default: desc
+ responses:
+ '200':
+ description: Successful operation
+ content:
+ application/json:
+ schema:
+ $ref: '#/components/schemas/CardListResponse'
+ '400':
+ description: Bad request - Invalid parameters
+ content:
+ application/json:
+ schema:
+ $ref: '#/components/schemas/Error400'
+ '401':
+ description: Unauthorized
+ content:
+ application/json:
+ schema:
+ $ref: '#/components/schemas/Error401'
+ '500':
+ description: Internal service error
+ content:
+ application/json:
+ schema:
+ $ref: '#/components/schemas/Error500'
+ /cards/{id}:
+ parameters:
+ - name: id
+ in: path
+ description: System-generated unique card identifier
+ required: true
+ schema:
+ type: string
+ get:
+ summary: Get a card
+ description: Retrieve a card by its system-generated id.
+ operationId: getCardById
+ tags:
+ - Cards
+ security:
+ - BasicAuth: []
+ responses:
+ '200':
+ description: Successful operation
+ content:
+ application/json:
+ schema:
+ $ref: '#/components/schemas/Card'
+ '401':
+ description: Unauthorized
+ content:
+ application/json:
+ schema:
+ $ref: '#/components/schemas/Error401'
+ '404':
+ description: Card not found
+ content:
+ application/json:
+ schema:
+ $ref: '#/components/schemas/Error404'
+ '500':
+ description: Internal service error
+ content:
+ application/json:
+ schema:
+ $ref: '#/components/schemas/Error500'
+ patch:
+ summary: Update a card
+ description: |
+ Update a card's `state` and / or its bound `fundingSources`. At least one of the two fields must be supplied.
+
+ - `state` transitions are limited to `ACTIVE ⇄ FROZEN` and `ACTIVE | FROZEN → CLOSED`. `CLOSED` is terminal and irreversible. Any other transition returns `409 INVALID_STATE_TRANSITION`.
+ - `fundingSources`, when supplied, fully replaces the card's bound funding sources. Array order determines the priority Authorization Decisioning tries them in. Each id must belong to the cardholder and be denominated in the card's currency; the list must contain at least one source. `fundingSources` cannot be supplied alongside `state: CLOSED`.
+
+ Because both updates are sensitive state changes, this endpoint uses Grid's 202 → signed-retry pattern (same shape as `DELETE /auth/credentials/{id}` and `POST /internal-accounts/{id}/export`):
+
+ 1. Call `PATCH /cards/{id}` with the target fields and no signing headers. The response is `202` with a `payloadToSign`, `requestId`, and `expiresAt`.
+
+ 2. Sign the `payloadToSign` with the session private key of a verified authentication credential on the card's owning internal account and retry with the signature as the `Grid-Wallet-Signature` header and the `requestId` echoed back as the `Request-Id` header. The signed retry returns `200` with the updated `Card`.
+
+ Effects:
+ - `state: FROZEN`: Authorization Decisioning declines new auths with `CARD_PAUSED`. Existing pulls and in-flight reconciliation continue — freezing does not pause the lifecycle of authorizations that already passed.
+ - `state: ACTIVE`: normal authorization behavior resumes.
+ - `state: CLOSED`: terminal close. The card transitions to `state: "CLOSED"` with `stateReason: "CLOSED_BY_PLATFORM"` and stays in the system for audit and reconciliation. All pending auths reconcile to a terminal state via the existing reconcile primitive. Inbound clearings received after close follow the standard force-post / late-presentment path — Lightspark absorbs the loss if a post-hoc pull on the now-unbound source fails. Funding-source bindings are detached. Refunds already in flight still complete because Lightspark holds the card-reserve keys.
+ - `fundingSources` change: emits `card.funding_source_change` reflecting the new ordered binding.
+
+ The `card.state_change` webhook fires on every successful `state` transition; the `card.funding_source_change` webhook fires whenever `fundingSources` is updated.
+ operationId: updateCardById
+ tags:
+ - Cards
+ security:
+ - BasicAuth: []
+ parameters:
+ - name: Grid-Wallet-Signature
+ in: header
+ required: false
+ description: Signature over the `payloadToSign` returned in a prior `202` response, produced with the session private key of a verified authentication credential on the card's owning internal account and base64-encoded. Required on the signed retry; ignored on the initial call.
+ schema:
+ type: string
+ example: MEUCIQDx7k2N0aK4p8f3vR9J6yT5wL1mB0sXnG2hQ4vJ8zYkCgIgZ4rP9dT7eWfU3oM6KjR1qSpNvBwL0tXyA2iG8fH5dE=
+ - name: Request-Id
+ in: header
+ required: false
+ description: The `requestId` returned in a prior `202` response, echoed back on the signed retry so the server can correlate it with the issued challenge. Required on the signed retry; must be paired with `Grid-Wallet-Signature`.
+ schema:
+ type: string
+ example: 7c4a8d09-ca37-4e3e-9e0d-8c2b3e9a1f21
+ requestBody:
+ required: true
+ content:
+ application/json:
+ schema:
+ $ref: '#/components/schemas/CardUpdateRequest'
+ examples:
+ freeze:
+ summary: Freeze an active card
+ value:
+ state: FROZEN
+ unfreeze:
+ summary: Unfreeze a frozen card
+ value:
+ state: ACTIVE
+ updateFundingSources:
+ summary: Replace the card's bound funding sources
+ value:
+ fundingSources:
+ - InternalAccount:019542f5-b3e7-1d02-0000-000000000002
+ - InternalAccount:019542f5-b3e7-1d02-0000-000000000003
+ freezeAndUpdateSources:
+ summary: Freeze the card and replace its funding sources in one call
+ value:
+ state: FROZEN
+ fundingSources:
+ - InternalAccount:019542f5-b3e7-1d02-0000-000000000002
+ close:
+ summary: Permanently close the card
+ value:
+ state: CLOSED
+ responses:
+ '200':
+ description: Signed retry accepted. Returns the updated card.
+ content:
+ application/json:
+ schema:
+ $ref: '#/components/schemas/Card'
+ '202':
+ description: Challenge issued. The response contains a `payloadToSign` that must be signed with the session private key of a verified authentication credential on the card's owning internal account, along with a `requestId` that must be echoed back on the retry.
+ content:
+ application/json:
+ schema:
+ $ref: '#/components/schemas/SignedRequestChallenge'
+ '400':
+ description: Bad request. Returned with `FUNDING_SOURCE_INELIGIBLE` when a supplied funding source does not belong to the cardholder or is not denominated in the card's currency, and for general invalid parameters.
+ content:
+ application/json:
+ schema:
+ $ref: '#/components/schemas/Error400'
+ '401':
+ description: Unauthorized. Returned when the provided `Grid-Wallet-Signature` is missing, malformed, or does not match a pending update challenge for this card, or when the `Request-Id` does not match an unexpired pending challenge.
+ content:
+ application/json:
+ schema:
+ $ref: '#/components/schemas/Error401'
+ '404':
+ description: Card not found
+ content:
+ application/json:
+ schema:
+ $ref: '#/components/schemas/Error404'
+ '409':
+ description: 'Conflict. Returned with `INVALID_STATE_TRANSITION` when the requested `state` transition is not one of `ACTIVE ⇄ FROZEN` or `ACTIVE | FROZEN → CLOSED` (e.g. trying to un-freeze a `CLOSED` card); with `CARD_ALREADY_CLOSED` when `state: CLOSED` is requested for a card that is already `CLOSED`; and with `CARD_NOT_MUTABLE` when the card is `CLOSED`.'
+ content:
+ application/json:
+ schema:
+ $ref: '#/components/schemas/Error409'
+ '500':
+ description: Internal service error
+ content:
+ application/json:
+ schema:
+ $ref: '#/components/schemas/Error500'
+ /sandbox/cards/{id}/simulate/authorization:
+ post:
+ summary: Simulate a card authorization
+ description: |
+ Simulate an inbound card authorization in the sandbox environment. Drives the same internal `authorize` + `reconcile` paths the card issuer would call in production, so platforms can exercise Grid's decisioning + funding-source pull behavior end-to-end without an external network round-trip.
+
+ The decisioning outcome is controlled by the last three characters of `merchant.descriptor`:
+
+ | Suffix | Outcome | | ------ | ------- | | `002` | Decline — `INSUFFICIENT_FUNDS` (the pull on the funding source fails) | | `003` | Decline — `CARD_PAUSED` (intended to verify a frozen card refuses auths) | | `005` | Delayed pull (~30s) — exercises the `PENDING → CONFIRMED` path | | `006` | Pull succeeds but the confirmation event reports `FAILED` — exercises the high-urgency `EXCEPTION` alert | | any other | Approved |
+
+ Production returns `404` on this path.
+ operationId: sandboxSimulateCardAuthorization
+ tags:
+ - Sandbox
+ security:
+ - BasicAuth: []
+ parameters:
+ - name: id
+ in: path
+ required: true
+ description: The id of the card to simulate an authorization against.
+ schema:
+ type: string
+ example: Card:019542f5-b3e7-1d02-0000-000000000010
+ requestBody:
+ required: true
+ content:
+ application/json:
+ schema:
+ $ref: '#/components/schemas/SandboxCardAuthorizationRequest'
+ examples:
+ coffeeAuth:
+ summary: Approved $12.50 auth at a coffee shop
+ value:
+ amount: 1250
+ currency:
+ code: USD
+ merchant:
+ descriptor: BLUE BOTTLE COFFEE SF
+ mcc: '5814'
+ country: US
+ declinedInsufficientFunds:
+ summary: Declined — insufficient funds (descriptor suffix `002`)
+ value:
+ amount: 50000
+ currency:
+ code: USD
+ merchant:
+ descriptor: AMAZON RETAIL US-002
+ mcc: '5942'
+ country: US
+ responses:
+ '200':
+ description: Simulated authorization processed. Returns the resulting card transaction.
+ content:
+ application/json:
+ schema:
+ $ref: '#/components/schemas/CardTransaction'
+ '400':
+ description: Bad request - Invalid parameters
+ content:
+ application/json:
+ schema:
+ $ref: '#/components/schemas/Error400'
+ '401':
+ description: Unauthorized
+ content:
+ application/json:
+ schema:
+ $ref: '#/components/schemas/Error401'
+ '403':
+ description: Forbidden - request was made with a production platform token
+ content:
+ application/json:
+ schema:
+ $ref: '#/components/schemas/Error403'
+ '404':
+ description: Card not found (also returned in production for this path)
+ content:
+ application/json:
+ schema:
+ $ref: '#/components/schemas/Error404'
+ '500':
+ description: Internal service error
+ content:
+ application/json:
+ schema:
+ $ref: '#/components/schemas/Error500'
+ /sandbox/cards/{id}/simulate/clearing:
+ post:
+ summary: Simulate a card clearing
+ description: |
+ Simulate a clearing (settlement) event against an existing `CardTransaction` in the sandbox environment.
+
+ - A clearing `amount` greater than the authorized amount exercises the over-auth post-hoc-pull path (e.g. restaurant tip on top of a 20% over-auth).
+ - A clearing `amount` of `0` exercises the `AUTHORIZATION_EXPIRY` path — the auth expires with no clearing posted.
+ - Suffix-driven outcomes on the parent transaction's id govern whether the post-hoc pull succeeds (use the suffix table from `simulate/authorization` to construct deterministic test cases).
+
+ Production returns `404` on this path.
+ operationId: sandboxSimulateCardClearing
+ tags:
+ - Sandbox
+ security:
+ - BasicAuth: []
+ parameters:
+ - name: id
+ in: path
+ required: true
+ description: The id of the card the clearing applies to.
+ schema:
+ type: string
+ example: Card:019542f5-b3e7-1d02-0000-000000000010
+ requestBody:
+ required: true
+ content:
+ application/json:
+ schema:
+ $ref: '#/components/schemas/SandboxCardClearingRequest'
+ examples:
+ tipOnTopClearing:
+ summary: Clearing larger than auth — exercises post-hoc pull
+ value:
+ cardTransactionId: CardTransaction:019542f5-b3e7-1d02-0000-000000000100
+ amount: 1500
+ authorizationExpiry:
+ summary: Clearing of 0 — exercises authorization expiry
+ value:
+ cardTransactionId: CardTransaction:019542f5-b3e7-1d02-0000-000000000100
+ amount: 0
+ responses:
+ '200':
+ description: Simulated clearing processed. Returns the updated card transaction.
+ content:
+ application/json:
+ schema:
+ $ref: '#/components/schemas/CardTransaction'
+ '400':
+ description: Bad request - Invalid parameters
+ content:
+ application/json:
+ schema:
+ $ref: '#/components/schemas/Error400'
+ '401':
+ description: Unauthorized
+ content:
+ application/json:
+ schema:
+ $ref: '#/components/schemas/Error401'
+ '403':
+ description: Forbidden - request was made with a production platform token
+ content:
+ application/json:
+ schema:
+ $ref: '#/components/schemas/Error403'
+ '404':
+ description: Card or card transaction not found
+ content:
+ application/json:
+ schema:
+ $ref: '#/components/schemas/Error404'
+ '500':
+ description: Internal service error
+ content:
+ application/json:
+ schema:
+ $ref: '#/components/schemas/Error500'
+ /sandbox/cards/{id}/simulate/return:
+ post:
+ summary: Simulate a card return
+ description: |
+ Simulate a merchant-initiated `RETURN` against an existing settled card transaction in the sandbox environment. Creates a `CardRefund` on the parent and either flips the parent to `REFUNDED` (full refund) or keeps it `SETTLED` with a non-zero `refundedAmount` (partial refund).
+
+ Production returns `404` on this path.
+ operationId: sandboxSimulateCardReturn
+ tags:
+ - Sandbox
+ security:
+ - BasicAuth: []
+ parameters:
+ - name: id
+ in: path
+ required: true
+ description: The id of the card the return applies to.
+ schema:
+ type: string
+ example: Card:019542f5-b3e7-1d02-0000-000000000010
+ requestBody:
+ required: true
+ content:
+ application/json:
+ schema:
+ $ref: '#/components/schemas/SandboxCardReturnRequest'
+ examples:
+ fullRefund:
+ summary: Full refund of a $15.00 settled transaction
+ value:
+ cardTransactionId: CardTransaction:019542f5-b3e7-1d02-0000-000000000100
+ amount: 1500
+ responses:
+ '200':
+ description: Simulated return processed. Returns the updated card transaction.
+ content:
+ application/json:
+ schema:
+ $ref: '#/components/schemas/CardTransaction'
+ '400':
+ description: Bad request - Invalid parameters
+ content:
+ application/json:
+ schema:
+ $ref: '#/components/schemas/Error400'
+ '401':
+ description: Unauthorized
+ content:
+ application/json:
+ schema:
+ $ref: '#/components/schemas/Error401'
+ '403':
+ description: Forbidden - request was made with a production platform token
+ content:
+ application/json:
+ schema:
+ $ref: '#/components/schemas/Error403'
+ '404':
+ description: Card or card transaction not found
+ content:
+ application/json:
+ schema:
+ $ref: '#/components/schemas/Error404'
+ '500':
+ description: Internal service error
+ content:
+ application/json:
+ schema:
+ $ref: '#/components/schemas/Error500'
webhooks:
agent-action:
post:
@@ -7004,23 +7533,218 @@ webhooks:
application/json:
schema:
$ref: '#/components/schemas/Error409'
- verification-update:
+ verification-update:
+ post:
+ summary: Verification status change
+ description: |
+ Webhook that is called when a customer's KYC/KYB verification status changes.
+ This endpoint should be implemented by clients of the Grid API.
+
+ ### Authentication
+ The webhook includes a signature in the `X-Grid-Signature` header that allows you to verify that the webhook was sent by Grid.
+ To verify the signature:
+ 1. Get the Grid API public key provided to you during integration
+ 2. Decode the base64 signature from the header
+ 3. Create a SHA-256 hash of the request body
+ 4. Verify the signature using the public key and the hash
+
+ If the signature verification succeeds, the webhook is authentic. If not, it should be rejected.
+ operationId: verificationStatusWebhook
+ tags:
+ - Webhooks
+ security:
+ - WebhookSignature: []
+ requestBody:
+ required: true
+ content:
+ application/json:
+ schema:
+ $ref: '#/components/schemas/VerificationWebhook'
+ examples:
+ approved:
+ summary: Verification approved
+ value:
+ id: Webhook:019542f5-b3e7-1d02-0000-000000000030
+ type: VERIFICATION.APPROVED
+ timestamp: '2025-08-15T14:32:00Z'
+ data:
+ id: Verification:019542f5-b3e7-1d02-0000-000000000010
+ customerId: Customer:019542f5-b3e7-1d02-0000-000000000001
+ verificationStatus: APPROVED
+ errors: []
+ createdAt: '2025-08-15T14:00:00Z'
+ resolveErrors:
+ summary: Verification requires action
+ value:
+ id: Webhook:019542f5-b3e7-1d02-0000-000000000031
+ type: VERIFICATION.RESOLVE_ERRORS
+ timestamp: '2025-08-15T14:32:00Z'
+ data:
+ id: Verification:019542f5-b3e7-1d02-0000-000000000011
+ customerId: Customer:019542f5-b3e7-1d02-0000-000000000001
+ verificationStatus: RESOLVE_ERRORS
+ errors:
+ - resourceId: Customer:019542f5-b3e7-1d02-0000-000000000001
+ type: MISSING_PROOF_OF_ADDRESS_DOCUMENT
+ acceptedDocumentTypes:
+ - PROOF_OF_ADDRESS
+ reason: Proof of address document is required
+ createdAt: '2025-08-15T14:00:00Z'
+ responses:
+ '200':
+ description: |
+ Webhook received successfully
+ '400':
+ description: Bad request
+ content:
+ application/json:
+ schema:
+ $ref: '#/components/schemas/Error400'
+ '401':
+ description: Unauthorized - Signature validation failed
+ content:
+ application/json:
+ schema:
+ $ref: '#/components/schemas/Error401'
+ '409':
+ description: Conflict - Webhook has already been processed (duplicate id)
+ content:
+ application/json:
+ schema:
+ $ref: '#/components/schemas/Error409'
+ card-state-change:
+ post:
+ summary: Card state change
+ description: |
+ Webhook that is called when a card's lifecycle state changes. Fires on `PENDING_ISSUE → ACTIVE`, on `PENDING_ISSUE → CLOSED (ISSUER_REJECTED)` when issuer provisioning fails, and on every subsequent `ACTIVE ⇄ FROZEN` and `→ CLOSED` transition.
+
+ This endpoint should be implemented by clients of the Grid API.
+
+ ### Authentication
+
+ The webhook includes a signature in the `X-Grid-Signature` header that allows you to verify that the webhook was sent by Grid.
+ To verify the signature:
+ 1. Get the Grid public key provided to you during integration
+ 2. Decode the base64 signature from the header
+ 3. Create a SHA-256 hash of the request body
+ 4. Verify the signature using the public key and the hash
+
+ If the signature verification succeeds, the webhook is authentic. If not, it should be rejected.
+ operationId: cardStateChangeWebhook
+ tags:
+ - Webhooks
+ security:
+ - WebhookSignature: []
+ requestBody:
+ required: true
+ content:
+ application/json:
+ schema:
+ $ref: '#/components/schemas/CardStateChangeWebhook'
+ examples:
+ activated:
+ summary: Card transitioned from PENDING_ISSUE to ACTIVE
+ value:
+ id: Webhook:019542f5-b3e7-1d02-0000-000000000020
+ type: CARD.STATE_CHANGE
+ timestamp: '2026-05-08T14:11:00Z'
+ data:
+ id: Card:019542f5-b3e7-1d02-0000-000000000010
+ cardholderId: Customer:019542f5-b3e7-1d02-0000-000000000001
+ platformCardId: card-emp-aary-001
+ state: ACTIVE
+ stateReason: null
+ brand: VISA
+ form: VIRTUAL
+ last4: '4242'
+ expMonth: 12
+ expYear: 2029
+ panEmbedUrl: https://embed.lithic.com/iframe/...?t=...
+ fundingSources:
+ - InternalAccount:019542f5-b3e7-1d02-0000-000000000002
+ currency: USD
+ issuerRef: lithic_card_4f8d3a2b1c
+ createdAt: '2026-05-08T14:10:00Z'
+ updatedAt: '2026-05-08T14:11:00Z'
+ issuerRejected:
+ summary: Card rejected by issuer during provisioning
+ value:
+ id: Webhook:019542f5-b3e7-1d02-0000-000000000021
+ type: CARD.STATE_CHANGE
+ timestamp: '2026-05-08T14:12:00Z'
+ data:
+ id: Card:019542f5-b3e7-1d02-0000-000000000011
+ cardholderId: Customer:019542f5-b3e7-1d02-0000-000000000001
+ state: CLOSED
+ stateReason: ISSUER_REJECTED
+ form: VIRTUAL
+ fundingSources:
+ - InternalAccount:019542f5-b3e7-1d02-0000-000000000002
+ currency: USD
+ createdAt: '2026-05-08T14:10:00Z'
+ updatedAt: '2026-05-08T14:12:00Z'
+ 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
+ fundingSources:
+ - InternalAccount:019542f5-b3e7-1d02-0000-000000000002
+ currency: USD
+ createdAt: '2026-05-08T14:10:00Z'
+ updatedAt: '2026-05-09T09:00:00Z'
+ responses:
+ '200':
+ description: |
+ Webhook received successfully
+ '400':
+ description: Bad request
+ content:
+ application/json:
+ schema:
+ $ref: '#/components/schemas/Error400'
+ '401':
+ description: Unauthorized - Signature validation failed
+ content:
+ application/json:
+ schema:
+ $ref: '#/components/schemas/Error401'
+ '409':
+ description: Conflict - Webhook has already been processed (duplicate id)
+ content:
+ application/json:
+ schema:
+ $ref: '#/components/schemas/Error409'
+ card-funding-source-change:
post:
- summary: Verification status change
+ summary: Card funding source change
description: |
- Webhook that is called when a customer's KYC/KYB verification status changes.
+ Webhook that is called when the funding sources bound to a card change. Fires whenever `PATCH /cards/{id}` updates the `fundingSources` array. The payload carries the full `Card` resource with the post-change `fundingSources` array.
+
This endpoint should be implemented by clients of the Grid API.
### Authentication
+
The webhook includes a signature in the `X-Grid-Signature` header that allows you to verify that the webhook was sent by Grid.
To verify the signature:
- 1. Get the Grid API public key provided to you during integration
+ 1. Get the Grid public key provided to you during integration
2. Decode the base64 signature from the header
3. Create a SHA-256 hash of the request body
4. Verify the signature using the public key and the hash
If the signature verification succeeds, the webhook is authentic. If not, it should be rejected.
- operationId: verificationStatusWebhook
+ operationId: cardFundingSourceChangeWebhook
tags:
- Webhooks
security:
@@ -7030,37 +7754,30 @@ webhooks:
content:
application/json:
schema:
- $ref: '#/components/schemas/VerificationWebhook'
+ $ref: '#/components/schemas/CardFundingSourceChangeWebhook'
examples:
- approved:
- summary: Verification approved
+ fundingSourcesReplaced:
+ summary: Funding sources replaced via PATCH /cards/{id}
value:
id: Webhook:019542f5-b3e7-1d02-0000-000000000030
- type: VERIFICATION.APPROVED
- timestamp: '2025-08-15T14:32:00Z'
- data:
- id: Verification:019542f5-b3e7-1d02-0000-000000000010
- customerId: Customer:019542f5-b3e7-1d02-0000-000000000001
- verificationStatus: APPROVED
- errors: []
- createdAt: '2025-08-15T14:00:00Z'
- resolveErrors:
- summary: Verification requires action
- value:
- id: Webhook:019542f5-b3e7-1d02-0000-000000000031
- type: VERIFICATION.RESOLVE_ERRORS
- timestamp: '2025-08-15T14:32:00Z'
+ type: CARD.FUNDING_SOURCE_CHANGE
+ timestamp: '2026-05-08T14:30:00Z'
data:
- id: Verification:019542f5-b3e7-1d02-0000-000000000011
- customerId: Customer:019542f5-b3e7-1d02-0000-000000000001
- verificationStatus: RESOLVE_ERRORS
- errors:
- - resourceId: Customer:019542f5-b3e7-1d02-0000-000000000001
- type: MISSING_PROOF_OF_ADDRESS_DOCUMENT
- acceptedDocumentTypes:
- - PROOF_OF_ADDRESS
- reason: Proof of address document is required
- createdAt: '2025-08-15T14:00:00Z'
+ id: Card:019542f5-b3e7-1d02-0000-000000000010
+ cardholderId: Customer:019542f5-b3e7-1d02-0000-000000000001
+ state: ACTIVE
+ stateReason: null
+ brand: VISA
+ form: VIRTUAL
+ last4: '4242'
+ expMonth: 12
+ expYear: 2029
+ fundingSources:
+ - InternalAccount:019542f5-b3e7-1d02-0000-000000000002
+ - InternalAccount:019542f5-b3e7-1d02-0000-000000000003
+ currency: USD
+ createdAt: '2026-05-08T14:10:00Z'
+ updatedAt: '2026-05-08T14:30:00Z'
responses:
'200':
description: |
@@ -17125,6 +17842,414 @@ components:
example: gat_ed0ad25881e234cc28fb2dec0a4fe64e4172a3b9
policy:
$ref: '#/components/schemas/AgentPolicy'
+ CardState:
+ type: string
+ enum:
+ - PENDING_KYC
+ - PENDING_ISSUE
+ - ACTIVE
+ - FROZEN
+ - CLOSED
+ description: |
+ Lifecycle state of a card.
+
+ | State | Description |
+ |-------|-------------|
+ | `PENDING_KYC` | The cardholder has not yet completed KYC. Cards in this state cannot transact. |
+ | `PENDING_ISSUE` | The card has been requested and is being provisioned with the issuer. |
+ | `ACTIVE` | The card is live and can authorize transactions. |
+ | `FROZEN` | The card is temporarily disabled by the platform. New authorizations are declined with `CARD_PAUSED`. Existing settlements and refunds continue to reconcile. |
+ | `CLOSED` | The card is permanently closed. Terminal, irreversible state. |
+ CardStateReason:
+ type: string
+ enum:
+ - ISSUER_REJECTED
+ - CLOSED_BY_PLATFORM
+ - CLOSED_BY_GRID
+ description: |
+ Reason a card reached a terminal or non-active state. Present on
+ `CLOSED` cards, and on cards that fail provisioning before reaching
+ `ACTIVE`.
+
+ | Reason | Description |
+ |--------|-------------|
+ | `ISSUER_REJECTED` | The card issuer rejected provisioning during `PENDING_ISSUE`. |
+ | `CLOSED_BY_PLATFORM` | The card was closed via `PATCH /cards/{id}` (`state: CLOSED`) by the platform. |
+ | `CLOSED_BY_GRID` | The card was closed by Grid (e.g. compliance or risk action). |
+ CardBrand:
+ type: string
+ enum:
+ - VISA
+ - MASTERCARD
+ description: |
+ Card network brand. Read-only — determined by Grid when the card is
+ provisioned with the issuer.
+ CardForm:
+ type: string
+ enum:
+ - VIRTUAL
+ description: |
+ Physical form factor of the card. Only `VIRTUAL` is supported in v1;
+ `PHYSICAL` will be added in a later release.
+ Card:
+ type: object
+ required:
+ - id
+ - cardholderId
+ - state
+ - form
+ - fundingSources
+ - createdAt
+ - updatedAt
+ properties:
+ id:
+ type: string
+ description: System-generated unique card identifier
+ readOnly: true
+ example: Card:019542f5-b3e7-1d02-0000-000000000010
+ cardholderId:
+ type: string
+ description: The id of the `Customer` who holds this card.
+ example: Customer:019542f5-b3e7-1d02-0000-000000000001
+ platformCardId:
+ type: string
+ description: Platform-specific card identifier. Optional on create — system-generated if omitted, mirroring `platformCustomerId` semantics.
+ example: card-emp-aary-001
+ state:
+ $ref: '#/components/schemas/CardState'
+ stateReason:
+ oneOf:
+ - $ref: '#/components/schemas/CardStateReason'
+ - type: 'null'
+ description: Reason associated with the current `state`. Populated when the card is `CLOSED` or when provisioning was rejected; otherwise null.
+ brand:
+ $ref: '#/components/schemas/CardBrand'
+ form:
+ $ref: '#/components/schemas/CardForm'
+ last4:
+ type: string
+ description: Last four digits of the card PAN.
+ example: '4242'
+ expMonth:
+ type: integer
+ minimum: 1
+ maximum: 12
+ description: Card expiration month (1–12).
+ example: 12
+ expYear:
+ type: integer
+ description: Card expiration year (four digits).
+ example: 2029
+ panEmbedUrl:
+ type: string
+ format: uri
+ description: URL of the card issuer's iframe that securely displays the PAN, CVV, and expiry to the cardholder. The full PAN and CVV never cross Grid's servers — render this URL in an iframe in your client to reveal card details.
+ example: https://embed.lithic.com/iframe/...?t=...
+ fundingSources:
+ type: array
+ description: Internal account ids bound to this card as funding sources, in priority order — the first entry is tried first by Authorization Decisioning. Every card has at least one funding source.
+ items:
+ type: string
+ example:
+ - InternalAccount:019542f5-b3e7-1d02-0000-000000000002
+ - InternalAccount:019542f5-b3e7-1d02-0000-000000000003
+ currency:
+ type: string
+ description: Currency the card transacts in (ISO 4217 for fiat, tickers for crypto). Derived from the funding sources at issue time — all funding sources bound to a card must be denominated in the same card-eligible currency.
+ example: USD
+ readOnly: true
+ issuerRef:
+ type: string
+ description: Opaque identifier for the card on the underlying issuer. Useful for cross-referencing in issuer dashboards; not used for any Grid request routing.
+ example: lithic_card_4f8d3a2b1c
+ readOnly: true
+ createdAt:
+ type: string
+ format: date-time
+ description: Creation timestamp
+ readOnly: true
+ example: '2026-05-08T14:10:00Z'
+ updatedAt:
+ type: string
+ format: date-time
+ description: Last update timestamp
+ readOnly: true
+ example: '2026-05-08T14:11:00Z'
+ CardListResponse:
+ type: object
+ required:
+ - data
+ - hasMore
+ properties:
+ data:
+ type: array
+ description: List of cards matching the filter criteria
+ items:
+ $ref: '#/components/schemas/Card'
+ hasMore:
+ type: boolean
+ description: Indicates if more results are available beyond this page
+ nextCursor:
+ type: string
+ description: Cursor to retrieve the next page of results (only present if hasMore is true)
+ totalCount:
+ type: integer
+ description: Total number of cards matching the criteria (excluding pagination)
+ CardCreateRequest:
+ type: object
+ required:
+ - cardholderId
+ - form
+ - fundingSources
+ properties:
+ cardholderId:
+ type: string
+ description: The id of the `Customer` to issue the card to. The customer must have KYC status `APPROVED`; otherwise the request is rejected with `CARDHOLDER_KYC_NOT_APPROVED`.
+ example: Customer:019542f5-b3e7-1d02-0000-000000000001
+ platformCardId:
+ type: string
+ description: Optional platform-specific card identifier. System-generated when omitted, mirroring `platformCustomerId` semantics.
+ example: card-emp-aary-001
+ form:
+ $ref: '#/components/schemas/CardForm'
+ fundingSources:
+ type: array
+ description: Internal account ids to bind as funding sources, in priority order. The first entry is tried first by Authorization Decisioning. Every card must be bound to at least one source, and every source must belong to the cardholder and be denominated in a card-eligible currency (USDB in v1); otherwise the request is rejected with `FUNDING_SOURCE_INELIGIBLE`.
+ minItems: 1
+ items:
+ type: string
+ example:
+ - InternalAccount:019542f5-b3e7-1d02-0000-000000000002
+ CardUpdateRequest:
+ type: object
+ description: Update request for `PATCH /cards/{id}`. At least one of `state` or `fundingSources` must be supplied. `state` transitions are limited to `ACTIVE ⇄ FROZEN` and `ACTIVE | FROZEN → CLOSED`; any other transition returns `409 INVALID_STATE_TRANSITION`. `CLOSED` is terminal and irreversible and cannot be combined with `fundingSources`. `fundingSources`, when supplied, fully replaces the card's bound funding sources — the array order determines the priority Authorization Decisioning tries them in.
+ properties:
+ state:
+ type: string
+ enum:
+ - ACTIVE
+ - FROZEN
+ - CLOSED
+ description: Target state for the card. Permitted transitions are `ACTIVE ⇄ FROZEN` and `ACTIVE | FROZEN → CLOSED`. `CLOSED` is terminal and irreversible; once closed, the card stays in the system for audit and reconciliation but cannot transact again.
+ example: FROZEN
+ fundingSources:
+ type: array
+ description: 'New ordered list of internal account ids to bind as funding sources. Fully replaces the previous binding. Each id must belong to the cardholder and be denominated in the card''s currency. The list must contain at least one source — to stop a card from spending without removing all sources, transition it to `FROZEN` instead. Cannot be supplied alongside `state: CLOSED`.'
+ minItems: 1
+ items:
+ type: string
+ example:
+ - InternalAccount:019542f5-b3e7-1d02-0000-000000000002
+ - InternalAccount:019542f5-b3e7-1d02-0000-000000000003
+ CardMerchant:
+ type: object
+ required:
+ - descriptor
+ properties:
+ descriptor:
+ type: string
+ description: Merchant descriptor string captured from the card network at authorization time.
+ example: BLUE BOTTLE COFFEE SF
+ mcc:
+ type: string
+ description: Merchant Category Code (ISO 18245) — four-digit numeric string.
+ example: '5814'
+ country:
+ type: string
+ description: Two-letter ISO 3166-1 alpha-2 country code of the merchant.
+ example: US
+ SandboxCardAuthorizationRequest:
+ type: object
+ required:
+ - amount
+ - currency
+ - merchant
+ description: Sandbox-only request body for `POST /sandbox/cards/{id}/simulate/authorization`. Drives the same internal authorization + reconcile paths that the issuer would call in production. The decisioning outcome is controlled by the last three characters of `merchant.descriptor` — see the endpoint documentation for the suffix table.
+ properties:
+ amount:
+ type: integer
+ format: int64
+ description: Authorization amount in the smallest unit of `currency` (e.g. cents for USD).
+ exclusiveMinimum: 0
+ example: 1250
+ currency:
+ $ref: '#/components/schemas/Currency'
+ merchant:
+ $ref: '#/components/schemas/CardMerchant'
+ CardTransactionStatus:
+ type: string
+ enum:
+ - AUTHORIZED
+ - PARTIALLY_SETTLED
+ - SETTLED
+ - REFUNDED
+ - EXCEPTION
+ description: |
+ Lifecycle status of a card transaction.
+
+ | Status | Description |
+ |--------|-------------|
+ | `AUTHORIZED` | The auth has been approved and a hold placed on the funding source; no clearing has arrived yet. |
+ | `PARTIALLY_SETTLED` | At least one clearing has arrived and posted, but more clearings are still expected (split shipments, tips, multi-leg trips). |
+ | `SETTLED` | All clearings for the auth have posted and the transaction is closed against the funding source. |
+ | `REFUNDED` | A `RETURN` was received from the merchant; the net settled amount has been refunded in part or whole. |
+ | `EXCEPTION` | The transaction settled to the card network but the corresponding pull from the funding source failed (e.g. balance no longer covers the post-hoc clearing). Surfaces high-urgency alerts and is the dashboard query for stuck reconciliations. |
+ CardPullSummary:
+ type: object
+ required:
+ - count
+ - totalAmount
+ properties:
+ count:
+ type: integer
+ description: Total number of pulls (debits) executed against the funding source for this transaction. `> 1` indicates one or more post-hoc pulls — e.g. restaurant tip / over-auth clearings.
+ example: 2
+ totalAmount:
+ type: integer
+ format: int64
+ description: Sum of all pull amounts in the smallest unit of the funding source's currency.
+ example: 1500
+ pendingCount:
+ type: integer
+ description: Number of pulls still in the `PENDING` state. Drops to zero when every pull has reached a terminal state. Non-zero values that persist beyond the expected settlement window are an early signal for the `EXCEPTION` path.
+ example: 0
+ CardRefundSummary:
+ type: object
+ required:
+ - count
+ - totalAmount
+ properties:
+ count:
+ type: integer
+ description: Number of refund (return) events received for this transaction.
+ example: 0
+ totalAmount:
+ type: integer
+ format: int64
+ description: Sum of all refund amounts in the smallest unit of the funding source's currency.
+ example: 0
+ CardSettlementSummary:
+ type: object
+ required:
+ - count
+ - totalAmount
+ properties:
+ count:
+ type: integer
+ description: Number of settlement (clearing) events received for this transaction.
+ example: 1
+ totalAmount:
+ type: integer
+ format: int64
+ description: Sum of all settled amounts in the smallest unit of the funding source's currency.
+ example: 1500
+ CardTransaction:
+ type: object
+ required:
+ - id
+ - cardId
+ - status
+ - merchant
+ - authorizedAmount
+ - accountId
+ - pullSummary
+ - refundSummary
+ - settlementSummary
+ - authorizedAt
+ - createdAt
+ - updatedAt
+ description: Parent transaction row for a card authorization and all of the pulls / settlements / refunds that reconcile against it. Child events are rolled up into the `pullSummary`, `refundSummary`, and `settlementSummary` aggregates. Delivered as the payload of the generic transaction webhook stream (extends the Transaction model with a card destination type) on every transition.
+ properties:
+ id:
+ type: string
+ description: System-generated unique card transaction identifier
+ readOnly: true
+ example: CardTransaction:019542f5-b3e7-1d02-0000-000000000100
+ cardId:
+ type: string
+ description: The id of the `Card` this transaction was made on.
+ example: Card:019542f5-b3e7-1d02-0000-000000000010
+ issuerTransactionToken:
+ type: string
+ description: Opaque identifier for the transaction on the underlying issuer. Used to cross-reference Grid records against issuer dashboards and webhooks.
+ example: lithic_txn_b81c2a4f
+ readOnly: true
+ status:
+ $ref: '#/components/schemas/CardTransactionStatus'
+ merchant:
+ $ref: '#/components/schemas/CardMerchant'
+ authorizedAmount:
+ $ref: '#/components/schemas/CurrencyAmount'
+ settledAmount:
+ $ref: '#/components/schemas/CurrencyAmount'
+ refundedAmount:
+ $ref: '#/components/schemas/CurrencyAmount'
+ accountId:
+ type: string
+ description: Internal account id that funded this transaction (the funding source selected by Authorization Decisioning at auth time).
+ example: InternalAccount:019542f5-b3e7-1d02-0000-000000000002
+ pullSummary:
+ $ref: '#/components/schemas/CardPullSummary'
+ refundSummary:
+ $ref: '#/components/schemas/CardRefundSummary'
+ settlementSummary:
+ $ref: '#/components/schemas/CardSettlementSummary'
+ authorizedAt:
+ type: string
+ format: date-time
+ description: When the auth was approved.
+ example: '2026-05-08T14:30:00Z'
+ lastEventAt:
+ type: string
+ format: date-time
+ description: Timestamp of the most recent reconcile event (pull / clearing / refund) against this transaction.
+ example: '2026-05-08T15:42:11Z'
+ createdAt:
+ type: string
+ format: date-time
+ description: Creation timestamp (same as `authorizedAt` for card transactions).
+ readOnly: true
+ example: '2026-05-08T14:30:00Z'
+ updatedAt:
+ type: string
+ format: date-time
+ description: Last update timestamp.
+ readOnly: true
+ example: '2026-05-08T15:42:11Z'
+ SandboxCardClearingRequest:
+ type: object
+ required:
+ - cardTransactionId
+ - amount
+ description: Sandbox-only request body for `POST /sandbox/cards/{id}/simulate/clearing`. Drives a clearing event against an existing `CardTransaction`. Pass an `amount` greater than the authorized amount to exercise the over-auth / restaurant-tip post-hoc-pull path; pass `0` to exercise `AUTHORIZATION_EXPIRY`. Suffix-driven outcomes on the parent transaction's id govern whether the post-hoc pull succeeds.
+ properties:
+ cardTransactionId:
+ type: string
+ description: The id of the `CardTransaction` to clear against. Must be in `AUTHORIZED` or `PARTIALLY_SETTLED` state.
+ example: CardTransaction:019542f5-b3e7-1d02-0000-000000000100
+ amount:
+ type: integer
+ format: int64
+ description: Clearing amount in the smallest unit of the transaction's currency. Set to `0` to simulate an authorization expiry with no clearing.
+ minimum: 0
+ example: 1500
+ SandboxCardReturnRequest:
+ type: object
+ required:
+ - cardTransactionId
+ - amount
+ description: Sandbox-only request body for `POST /sandbox/cards/{id}/simulate/return`. Drives a `RETURN` event against an existing settled `CardTransaction`, which creates a `CardRefund` and pushes the parent transaction towards `REFUNDED` (full) or keeps it `SETTLED` (partial).
+ properties:
+ cardTransactionId:
+ type: string
+ description: The id of the `CardTransaction` to refund against. Must have at least one settled clearing.
+ example: CardTransaction:019542f5-b3e7-1d02-0000-000000000100
+ amount:
+ type: integer
+ format: int64
+ description: Return amount in the smallest unit of the transaction's currency. Must be less than or equal to the net settled amount (settled minus previously-refunded).
+ exclusiveMinimum: 0
+ example: 1500
WebhookType:
type: string
enum:
@@ -17157,6 +18282,8 @@ components:
- BULK_UPLOAD.COMPLETED
- BULK_UPLOAD.FAILED
- AGENT_ACTION.PENDING_APPROVAL
+ - CARD.STATE_CHANGE
+ - CARD.FUNDING_SOURCE_CHANGE
- TEST
description: Type of webhook event in OBJECT.EVENT dot-notation. The part before the dot identifies the resource, the part after identifies the event. This lets consumers route purely on type without inspecting data.status.
BaseWebhook:
@@ -17351,3 +18478,29 @@ components:
- VERIFICATION.RESOLVE_ERRORS
- VERIFICATION.IN_PROGRESS
- VERIFICATION.PENDING_MANUAL_REVIEW
+ CardStateChangeWebhook:
+ allOf:
+ - $ref: '#/components/schemas/BaseWebhook'
+ - type: object
+ required:
+ - data
+ properties:
+ data:
+ $ref: '#/components/schemas/Card'
+ type:
+ type: string
+ enum:
+ - CARD.STATE_CHANGE
+ CardFundingSourceChangeWebhook:
+ allOf:
+ - $ref: '#/components/schemas/BaseWebhook'
+ - type: object
+ required:
+ - data
+ properties:
+ data:
+ $ref: '#/components/schemas/Card'
+ type:
+ type: string
+ enum:
+ - CARD.FUNDING_SOURCE_CHANGE
diff --git a/mintlify/snippets/cards/cardholder-setup.mdx b/mintlify/snippets/cards/cardholder-setup.mdx
new file mode 100644
index 00000000..723bc065
--- /dev/null
+++ b/mintlify/snippets/cards/cardholder-setup.mdx
@@ -0,0 +1,60 @@
+Before you can issue a card, the cardholder must be a Grid `Customer`
+in good standing with at least one funded internal account. This page
+covers the requirements and the order they must be satisfied in.
+
+## KYC must be APPROVED
+
+`POST /cards` is rejected with `409 CARDHOLDER_KYC_NOT_APPROVED` if the
+cardholder's `kycStatus` is anything other than `APPROVED`. There is no
+"issue and verify later" path.
+
+If you're a regulated platform that creates customers directly with
+KYC data, the customer reaches `APPROVED` as soon as the verification
+returns approved. If you're using the hosted KYC link flow, gate
+issuance on the `CUSTOMER.KYC_APPROVED` webhook.
+
+```bash
+# Check KYC status before issuing
+curl -X GET "$GRID_BASE_URL/customers/Customer:019542f5-b3e7-1d02-0000-000000000001" \
+ -u "$GRID_CLIENT_ID:$GRID_CLIENT_SECRET"
+```
+
+## The cardholder needs a funding source
+
+Every card must be bound to at least one `InternalAccount` at issue
+time. The account must:
+
+- Belong to the cardholder (no cross-customer funding in v1).
+- Be denominated in a card-eligible currency. In v1 this is USDB; the
+ request is rejected with `409 FUNDING_SOURCE_INELIGIBLE` otherwise.
+
+If the cardholder doesn't have an internal account yet, internal
+accounts are created automatically when the customer is created based
+on your platform configuration. You can list them with:
+
+```bash
+curl -X GET "$GRID_BASE_URL/customers/internal-accounts?customerId=Customer:019542f5-b3e7-1d02-0000-000000000001¤cy=USDB" \
+ -u "$GRID_CLIENT_ID:$GRID_CLIENT_SECRET"
+```
+
+## Pre-fund before authorizations arrive
+
+Cards decline at auth time if the bound funding source can't cover the
+transaction. The decline code surfaces as `INSUFFICIENT_FUNDS` and is
+visible on the resulting `CardTransaction`. Fund the source the same
+way you would for any other internal account — via the funding
+payment instructions or, in Sandbox, with
+`/sandbox/internal-accounts/{id}/fund`.
+
+
+Just-in-time funding works the same as for other Grid flows: receive a
+deposit into the funding source, let it confirm, then expect the
+cardholder to transact. There is no separate JIT path for cards in v1.
+
+
+## Ready to issue
+
+Once the cardholder is `APPROVED` and a funded internal account exists,
+issue the card with `POST /cards`. See
+[Issuing cards](/cards/card-management/issuing-cards) for the request
+shape and lifecycle states.
diff --git a/mintlify/snippets/cards/freezing-and-closing.mdx b/mintlify/snippets/cards/freezing-and-closing.mdx
new file mode 100644
index 00000000..2a28e4d2
--- /dev/null
+++ b/mintlify/snippets/cards/freezing-and-closing.mdx
@@ -0,0 +1,133 @@
+Freeze, close, and other sensitive card updates use Grid's
+`202 → signed-retry` pattern — the same one used by Embedded Wallet
+credential revocation and wallet export. This page covers the flow,
+what each transition does, and how to handle the signing step.
+
+
+`PATCH /cards/{id}` covers both freeze / unfreeze (`state`) and funding
+source updates (`fundingSources`); see
+[Funding sources](/cards/card-management/funding-sources) for the
+funding-source-only flow. The signed-retry mechanics below apply to all
+three.
+
+## Valid state transitions
+
+| From | To | Endpoint |
+|------|----|----------|
+| `ACTIVE` | `FROZEN` | `PATCH /cards/{id}` body `{ "state": "FROZEN" }` |
+| `FROZEN` | `ACTIVE` | `PATCH /cards/{id}` body `{ "state": "ACTIVE" }` |
+| `ACTIVE` or `FROZEN` | `CLOSED` | `PATCH /cards/{id}` body `{ "state": "CLOSED" }` |
+
+Any other transition returns `409 INVALID_STATE_TRANSITION`. In
+particular, you cannot un-freeze a `CLOSED` card — close is terminal.
+
+
+You can also combine a state change with a funding source replacement
+in one PATCH — just include both fields in the body.
+
+## The signed-retry flow
+
+Each request follows the same two-call shape:
+
+```text
+1. PATCH /cards/{id} ─► 202 with payloadToSign, requestId, expiresAt
+2. PATCH /cards/{id} ─► 200 with the updated Card
+ Headers:
+ Grid-Wallet-Signature:
+ Request-Id:
+```
+
+The signature is produced with the session private key of a verified
+authentication credential on the card's owning internal account.
+
+### Step 1 — initial call
+
+```bash
+curl -X PATCH "$GRID_BASE_URL/cards/Card:019542f5-b3e7-1d02-0000-000000000010" \
+ -u "$GRID_CLIENT_ID:$GRID_CLIENT_SECRET" \
+ -H "Content-Type: application/json" \
+ -d '{ "state": "FROZEN" }'
+```
+
+Response — `202 Accepted`:
+
+```json
+{
+ "payloadToSign": "Y2hhbGxlbmdlLXBheWxvYWQtdG8tc2lnbg==",
+ "requestId": "7c4a8d09-ca37-4e3e-9e0d-8c2b3e9a1f21",
+ "expiresAt": "2026-05-08T15:35:00Z"
+}
+```
+
+### Step 2 — signed retry
+
+Sign `payloadToSign` with the session private key of a verified
+authentication credential on the card's owning internal account, then
+retry the same request with the signature and the request id echoed
+back:
+
+```bash
+curl -X PATCH "$GRID_BASE_URL/cards/Card:019542f5-b3e7-1d02-0000-000000000010" \
+ -u "$GRID_CLIENT_ID:$GRID_CLIENT_SECRET" \
+ -H "Content-Type: application/json" \
+ -H "Grid-Wallet-Signature: " \
+ -H "Request-Id: 7c4a8d09-ca37-4e3e-9e0d-8c2b3e9a1f21" \
+ -d '{ "state": "FROZEN" }'
+```
+
+Response — `200 OK` with the updated `Card` and a
+`CARD.STATE_CHANGE` webhook.
+
+
+The signing flow is identical to the one used by Embedded Wallet
+credential revocation. If you've already wired that up, you can reuse
+the same key-handling code for cards.
+
+
+## What freeze does
+
+Setting a card to `FROZEN`:
+
+- Causes Authorization Decisioning to decline new auths with
+ `CARD_PAUSED`.
+- Does **not** pause the lifecycle of authorizations that already
+ passed. Pulls, clearings, and refunds against existing transactions
+ continue to reconcile normally.
+- Emits `CARD.STATE_CHANGE` with `state: "FROZEN"`.
+
+Unfreeze (`state: "ACTIVE"`) reverses this — new auths flow normally
+again.
+
+## What close does
+
+Closing a card is done with the same `PATCH /cards/{id}` endpoint by
+setting `state: "CLOSED"`. The operation is permanent:
+
+- Card state transitions to `CLOSED`, `stateReason: "CLOSED_BY_PLATFORM"`.
+- All pending authorizations reconcile to a terminal state via the
+ existing reconcile primitive.
+- Funding-source bindings are detached. Refunds already in flight
+ continue to complete because Lightspark holds the card-reserve keys.
+- Inbound clearings received after close follow the standard
+ force-post / late-presentment path — Lightspark absorbs the loss if
+ a post-hoc pull on the now-unbound source fails.
+- `CARD.STATE_CHANGE` fires with `state: "CLOSED"`.
+
+```bash
+curl -X PATCH "$GRID_BASE_URL/cards/Card:019542f5-b3e7-1d02-0000-000000000010" \
+ -u "$GRID_CLIENT_ID:$GRID_CLIENT_SECRET" \
+ -H "Content-Type: application/json" \
+ -H "Grid-Wallet-Signature: " \
+ -H "Request-Id: " \
+ -d '{ "state": "CLOSED" }'
+```
+
+`fundingSources` cannot be supplied alongside `state: CLOSED`.
+`409 CARD_ALREADY_CLOSED` is returned if the card is already in the
+terminal `CLOSED` state.
+
+## Sandbox behavior
+
+In Sandbox the state changes are instant — no issuer round-trip is
+simulated, but the signed-retry shape is the same as production so
+you can exercise the full client flow.
diff --git a/mintlify/snippets/cards/funding-sources.mdx b/mintlify/snippets/cards/funding-sources.mdx
new file mode 100644
index 00000000..d41550bf
--- /dev/null
+++ b/mintlify/snippets/cards/funding-sources.mdx
@@ -0,0 +1,97 @@
+A card's `fundingSources` array is the ordered list of internal accounts
+Authorization Decisioning can pull from when an auth lands. The first
+entry is tried first. This page covers binding at issue time and
+replacing the binding via `PATCH /cards/{id}`.
+
+## At issue time
+
+You supply the initial `fundingSources` array on `POST /cards`. Every
+card must be bound to at least one source.
+
+```bash
+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",
+ "form": "VIRTUAL",
+ "fundingSources": [
+ "InternalAccount:019542f5-b3e7-1d02-0000-000000000002"
+ ]
+ }'
+```
+
+Each source must:
+
+- Belong to the cardholder (no cross-customer funding in v1).
+- Be denominated in a card-eligible currency (USDB in v1).
+- Match the card's currency. All sources bound to a single card share
+ one currency.
+
+If any source fails these checks, the request is rejected with
+`409 FUNDING_SOURCE_INELIGIBLE`.
+
+## Replacing the binding
+
+`PATCH /cards/{id}` accepts a `fundingSources` field that fully
+replaces the previous binding. Array order is the new priority order —
+first entry is tried first by Authorization Decisioning.
+
+```bash
+curl -X PATCH "$GRID_BASE_URL/cards/Card:019542f5-b3e7-1d02-0000-000000000010" \
+ -u "$GRID_CLIENT_ID:$GRID_CLIENT_SECRET" \
+ -H "Content-Type: application/json" \
+ -d '{
+ "fundingSources": [
+ "InternalAccount:019542f5-b3e7-1d02-0000-000000000002",
+ "InternalAccount:019542f5-b3e7-1d02-0000-000000000003"
+ ]
+ }'
+```
+
+`PATCH` is a sensitive state change, so it uses the
+`202 → signed-retry` flow described in
+[Freezing and closing cards](/cards/card-management/freezing-and-closing).
+The same flow covers `state`, `fundingSources`, or both fields supplied
+together.
+
+`CARD.FUNDING_SOURCE_CHANGE` fires on every successful update with the
+post-change `Card` resource.
+
+### Errors
+
+| Status | Code | What it means |
+|--------|------|---------------|
+| 409 | `FUNDING_SOURCE_INELIGIBLE` | A supplied account doesn't belong to the cardholder or isn't denominated in the card's currency. |
+| 409 | `CARD_NOT_MUTABLE` | The card is `CLOSED`. |
+| 400 | `INVALID_INPUT` | The `fundingSources` array is empty (a card must have at least one source). |
+
+
+`fundingSources` is a full replacement, not a delta. Always send the
+complete ordered list you want bound to the card; omitting an existing
+source removes it.
+
+
+## Stopping a card from spending
+
+You cannot remove all funding sources from a card — the array must
+contain at least one entry. To stop a card from spending without
+detaching it from its funding source, transition it to `FROZEN`:
+
+```bash
+curl -X PATCH "$GRID_BASE_URL/cards/Card:019542f5-b3e7-1d02-0000-000000000010" \
+ -u "$GRID_CLIENT_ID:$GRID_CLIENT_SECRET" \
+ -H "Content-Type: application/json" \
+ -d '{ "state": "FROZEN" }'
+```
+
+To permanently retire a card, close it with `PATCH /cards/{id}` and
+`state: "CLOSED"`.
+
+## v1 behavior: single active source
+
+`PATCH /cards/{id}` accepts an arbitrary-length ordered array, but in
+v1 Authorization Decisioning only reads the first entry. Additional
+sources are accepted and stored so you can stage multi-source
+decisioning ahead of the v1.5+ rollout, but they don't change
+auth-time behavior today.
diff --git a/mintlify/snippets/cards/implementation-overview.mdx b/mintlify/snippets/cards/implementation-overview.mdx
new file mode 100644
index 00000000..7151d0d8
--- /dev/null
+++ b/mintlify/snippets/cards/implementation-overview.mdx
@@ -0,0 +1,100 @@
+This page gives you a 10,000-ft view of an end-to-end Cards
+implementation. The detailed guides that follow cover concrete request
+shapes, edge cases, and step-by-step instructions.
+
+
+Cards sit on top of the same customer and internal-account primitives
+you already use for payouts. If you've already onboarded customers and
+funded internal accounts in Grid, the work to add cards is small.
+
+
+## Platform configuration
+
+You need an existing Grid platform configuration before you can issue
+cards. Cards do not require new webhook endpoints or new API
+credentials — they reuse what's already configured for the rest of
+Grid. You'll only need to:
+
+- Subscribe to the new card-specific webhook types (`CARD.STATE_CHANGE`
+ and `CARD.FUNDING_SOURCE_CHANGE`). Card-transaction lifecycle events
+ ride on the generic transaction webhook stream that already covers
+ outgoing-payment activity.
+- Confirm with your Lightspark contact that cards are enabled for your
+ platform — issuance requires an issuer-side onboarding.
+
+## Cardholder readiness
+
+A card can only be issued to a `Customer` with `kycStatus: APPROVED`.
+This is the same gate you use for Grid's other features. If the
+cardholder hasn't completed KYC, `POST /cards` returns
+`409 CARDHOLDER_KYC_NOT_APPROVED` — see
+[Cardholder setup](/cards/onboarding/cardholder-setup) for how to drive
+KYC to completion before issuing.
+
+## Funding sources
+
+Every card is bound to at least one `InternalAccount` as its funding
+source at issue time. Authorization Decisioning checks the source's
+balance before approving each auth, so:
+
+- Top up the funding source before you expect transactions.
+- Use existing funding instructions (ACH, SEPA, wires, stablecoin) the
+ same way you would for any other internal account.
+- See [Funding sources](/cards/card-management/funding-sources) for
+ rules around binding, unbinding, and the future multi-source path.
+
+## Issuing and lifecycle
+
+Issuance is a single `POST /cards` call. New cards start in
+`PENDING_ISSUE` while the issuer provisions them and transition to
+`ACTIVE` automatically — you observe both transitions via the
+`CARD.STATE_CHANGE` webhook. Day-to-day operational changes are:
+
+- `PATCH /cards/{id}` to freeze, unfreeze, or close permanently
+ (`state: "CLOSED"`).
+
+Both freeze and close use Grid's `202 → signed-retry` pattern (the same
+pattern as Embedded Wallet credential revocation). See
+[Freezing and closing cards](/cards/card-management/freezing-and-closing).
+
+## Transactions and reconciliation
+
+Each authorization on a card produces a parent `CardTransaction` row.
+Children (pulls, clearings, refunds) are reconciled against the parent
+and rolled up into `pullSummary`, `settlementSummary`, and
+`refundSummary` aggregates. The lifecycle status moves
+`AUTHORIZED → PARTIALLY_SETTLED → SETTLED → REFUNDED`, with
+`EXCEPTION` as the failure path for stuck post-hoc pulls.
+
+The full event model is covered in
+[Reconciliation](/cards/transactions/reconciliation).
+
+## Testing in Sandbox
+
+Sandbox cannot receive real authorizations from the card network, so
+it exposes three simulate endpoints that drive the same internal paths
+the issuer would call in production:
+
+- `POST /sandbox/cards/{id}/simulate/authorization`
+- `POST /sandbox/cards/{id}/simulate/clearing`
+- `POST /sandbox/cards/{id}/simulate/return`
+
+Outcomes are deterministic — driven by magic-value suffixes on the
+relevant id. See [Sandbox testing](/cards/platform-tools/sandbox-testing).
+
+## Enabling Production
+
+When you're ready to go live:
+
+- Complete card-issuer onboarding through your Lightspark contact.
+- Confirm webhook security, monitoring, and alerting cover the
+ `CARD.*` event types plus card-destination transactions on the
+ generic transaction webhook stream.
+- Build the `EXCEPTION` dashboard view from card-destination
+ transaction webhooks (filter by `status: "EXCEPTION"`) and wire it
+ into on-call alerting.
+
+
+Contact your Lightspark representative to enable Production card
+issuance and finalize issuer activations.
+
diff --git a/mintlify/snippets/cards/intro.mdx b/mintlify/snippets/cards/intro.mdx
new file mode 100644
index 00000000..ac78f5f8
--- /dev/null
+++ b/mintlify/snippets/cards/intro.mdx
@@ -0,0 +1,53 @@
+import { topLevelProductName } from '/snippets/variables.mdx';
+import { FeatureCard, FeatureCardGrid } from '/snippets/feature-card.mdx';
+
+With {topLevelProductName} Cards, you can issue virtual debit cards backed by an
+internal account, decision authorizations in real time against that
+account's balance, and reconcile every pull, clearing, and refund against
+the same ledger you already use for payouts.
+
+
+
+ Authorizations are checked against the bound internal account at auth
+ time, so the funding source and the card share a single source of
+ truth.
+
+
+ The full PAN and CVV are rendered directly to the cardholder through
+ the issuer's iframe. Card credentials never cross your servers.
+
+
+
+## Card lifecycle at a glance
+
+A card moves through five states:
+
+| State | Meaning |
+|-------|---------|
+| `PENDING_KYC` | Cardholder has not finished KYC; the card cannot transact yet. |
+| `PENDING_ISSUE` | Card has been requested and is being provisioned with the issuer. |
+| `ACTIVE` | Card is live and can authorize transactions. |
+| `FROZEN` | Card is temporarily disabled. New authorizations are declined; in-flight settlements continue. |
+| `CLOSED` | Card is permanently closed. Terminal, irreversible. |
+
+Every transition emits a `CARD.STATE_CHANGE` webhook so you can mirror
+state changes into your application.
+
+## What's covered in this tab
+
+
+
+ Issue your first card, simulate an authorization, and watch it
+ reconcile against an internal account.
+
+
+ Issue cards, bind funding sources, freeze, unfreeze, and close.
+
+
+ The relationship between authorizations, pulls, clearings, and
+ refunds — and how to surface exceptions.
+
+
+ Drive deterministic outcomes with the magic-value suffix tables.
+
+
diff --git a/mintlify/snippets/cards/issuing-cards.mdx b/mintlify/snippets/cards/issuing-cards.mdx
new file mode 100644
index 00000000..156fce9f
--- /dev/null
+++ b/mintlify/snippets/cards/issuing-cards.mdx
@@ -0,0 +1,94 @@
+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
+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
+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 |
+|--------|------|---------------|
+| 409 | `CARDHOLDER_KYC_NOT_APPROVED` | Cardholder is not `kycStatus: APPROVED`. Drive KYC to completion before retrying. |
+| 409 | `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
+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.
diff --git a/mintlify/snippets/cards/quickstart.mdx b/mintlify/snippets/cards/quickstart.mdx
new file mode 100644
index 00000000..067c3ed6
--- /dev/null
+++ b/mintlify/snippets/cards/quickstart.mdx
@@ -0,0 +1,144 @@
+This quickstart walks you from an empty Sandbox to a card transaction
+you can see in your dashboard. We'll:
+
+1. Confirm the cardholder is KYC-approved.
+2. Fund their internal account.
+3. Issue a virtual card against that account.
+4. Simulate an inbound authorization, then a clearing.
+5. List the resulting `CardTransaction`.
+
+## Prerequisites
+
+- Sandbox API credentials.
+- A `Customer` with `kycStatus: APPROVED` and at least one `InternalAccount`.
+ If you don't have one yet, follow the
+ [Payouts quickstart](/payouts-and-b2b/quickstart) up to "Get the Customer's
+ Internal Account".
+
+```bash
+export GRID_BASE_URL="https://api.lightspark.com/grid/2025-10-13"
+export GRID_CLIENT_ID="YOUR_SANDBOX_CLIENT_ID"
+export GRID_CLIENT_SECRET="YOUR_SANDBOX_CLIENT_SECRET"
+```
+
+## Fund the cardholder's internal account
+
+Cards decline at auth time if the bound funding source can't cover the
+transaction. Top up the cardholder's internal account first.
+
+```bash
+curl -X POST "$GRID_BASE_URL/sandbox/internal-accounts/InternalAccount:019542f5-b3e7-1d02-0000-000000000002/fund" \
+ -u "$GRID_CLIENT_ID:$GRID_CLIENT_SECRET" \
+ -H "Content-Type: application/json" \
+ -d '{ "amount": 50000 }'
+```
+
+## Issue the card
+
+```bash
+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"
+ ]
+ }'
+```
+
+The card comes back in `state: "PENDING_ISSUE"` while the issuer
+provisions it. In Sandbox, activation is near-instant for any
+`platformCardId` whose last three characters aren't a magic suffix —
+see the [Sandbox testing guide](/cards/platform-tools/sandbox-testing)
+for the full table. When activation completes, a
+`CARD.STATE_CHANGE` webhook fires with `state: "ACTIVE"`:
+
+```json
+{
+ "id": "Webhook:019542f5-b3e7-1d02-0000-000000000020",
+ "type": "CARD.STATE_CHANGE",
+ "timestamp": "2026-05-08T14:11:00Z",
+ "data": {
+ "id": "Card:019542f5-b3e7-1d02-0000-000000000010",
+ "cardholderId": "Customer:019542f5-b3e7-1d02-0000-000000000001",
+ "state": "ACTIVE",
+ "brand": "VISA",
+ "form": "VIRTUAL",
+ "last4": "4242",
+ "expMonth": 12,
+ "expYear": 2029,
+ "panEmbedUrl": "https://embed.lithic.com/iframe/...?t=...",
+ "fundingSources": [
+ "InternalAccount:019542f5-b3e7-1d02-0000-000000000002"
+ ],
+ "currency": "USD",
+ "createdAt": "2026-05-08T14:10:00Z",
+ "updatedAt": "2026-05-08T14:11:00Z"
+ }
+}
+```
+
+
+Render `panEmbedUrl` in an iframe in your client to display the full
+PAN, CVV, and expiry to the cardholder. The full card credentials never
+cross your servers.
+
+
+## Simulate an authorization
+
+Sandbox exposes simulate endpoints that drive the same internal paths
+the card issuer would call in production. The decisioning outcome is
+controlled by the last three characters of `merchant.descriptor` — any
+non-magic suffix is approved.
+
+```bash
+curl -X POST "$GRID_BASE_URL/sandbox/cards/Card:019542f5-b3e7-1d02-0000-000000000010/simulate/authorization" \
+ -u "$GRID_CLIENT_ID:$GRID_CLIENT_SECRET" \
+ -H "Content-Type: application/json" \
+ -d '{
+ "amount": 1250,
+ "currency": { "code": "USD" },
+ "merchant": {
+ "descriptor": "BLUE BOTTLE COFFEE SF",
+ "mcc": "5814",
+ "country": "US"
+ }
+ }'
+```
+
+The response is the resulting `CardTransaction` in `status:
+"AUTHORIZED"` with a single pull on the funding source.
+
+## Simulate the clearing
+
+The merchant adds a tip and clears for more than the original auth
+($15.00 on a $12.50 hold). Grid handles the over-auth by issuing a
+post-hoc pull for the difference.
+
+```bash
+curl -X POST "$GRID_BASE_URL/sandbox/cards/Card:019542f5-b3e7-1d02-0000-000000000010/simulate/clearing" \
+ -u "$GRID_CLIENT_ID:$GRID_CLIENT_SECRET" \
+ -H "Content-Type: application/json" \
+ -d '{
+ "cardTransactionId": "CardTransaction:019542f5-b3e7-1d02-0000-000000000100",
+ "amount": 1500
+ }'
+```
+
+A transaction webhook fires with the updated parent: the transaction
+moves to `SETTLED`, `pullSummary.count` becomes `2`, and
+`settlementSummary.totalAmount` is `1500`.
+
+The webhook payload carries the full parent `CardTransaction` with
+child pull, clearing, and refund events rolled up into the
+`pullSummary`, `settlementSummary`, and `refundSummary` aggregates —
+see [Reconciliation](/cards/transactions/reconciliation) for the full
+event model.
+
+
+You've issued a card, watched it activate, and driven an over-auth
+transaction through pull and clearing.
+
diff --git a/mintlify/snippets/cards/reconciliation.mdx b/mintlify/snippets/cards/reconciliation.mdx
new file mode 100644
index 00000000..bb4e3b4c
--- /dev/null
+++ b/mintlify/snippets/cards/reconciliation.mdx
@@ -0,0 +1,83 @@
+A card transaction is not a single event — it's a parent row plus a
+stream of child events from the card network. This page covers the
+event model, the status transitions, and how to handle the
+`EXCEPTION` path.
+
+## The event model
+
+For each card authorization, Grid produces:
+
+1. **One parent `CardTransaction`** — created at auth time, persists for
+ the life of the transaction.
+2. **Pulls** — debits against the funding source that fund approved
+ auths and any post-hoc settlements.
+3. **Clearings** — the network's confirmation that funds have moved.
+4. **Refunds** — merchant-initiated `RETURN` events.
+
+Children are reconciled against the parent and rolled up into three
+aggregates: `pullSummary`, `settlementSummary`, and `refundSummary`.
+You don't see per-child rows on the list endpoint — they're summarized
+on the parent.
+
+## Status transitions
+
+```text
+ ┌─────────────────────────────────────┐
+ │ ▼
+AUTHORIZED ──► PARTIALLY_SETTLED ──► SETTLED ──► REFUNDED
+ │
+ └──► EXCEPTION (pull failed after settlement)
+```
+
+| Status | Meaning |
+|--------|---------|
+| `AUTHORIZED` | Auth approved, hold placed, no clearings yet. |
+| `PARTIALLY_SETTLED` | At least one clearing landed, but more are still expected (split shipments, multi-leg trips). |
+| `SETTLED` | All clearings for the auth have posted. The transaction is closed against the funding source. |
+| `REFUNDED` | A `RETURN` was received and the net settled amount has been refunded in full or part. |
+| `EXCEPTION` | The transaction settled to the network but the corresponding pull from the funding source failed. |
+
+Every transition is delivered via the generic transaction webhook
+stream carrying the post-change parent (a follow-up extends the
+Transaction model with a card destination type — see
+[Webhooks](/cards/platform-tools/webhooks)).
+
+## The over-auth path
+
+The most common non-trivial flow is the over-auth (e.g. restaurant
+tip). The auth comes in at $12.50, but the merchant clears for $15.00.
+
+1. Auth approved → one pull for $12.50 → parent is `AUTHORIZED`.
+2. Clearing for $15.00 → second post-hoc pull for $2.50 → parent is
+ `SETTLED` with `pullSummary.count = 2`,
+ `settlementSummary.totalAmount = 1500`.
+
+The post-settlement parent carries `authorizedAmount: 1250`,
+`settledAmount: 1500`, and two pulls in its `pullSummary`.
+
+## The EXCEPTION path
+
+An exception happens when the card network has already moved funds for
+a settlement but Grid can't pull the matching amount from the funding
+source — typically because the cardholder's balance no longer covers
+the post-hoc difference.
+
+Signal to watch: a transaction webhook with `status: "EXCEPTION"` for
+a card-destination transaction. The payload includes the full parent
+record, so your dashboard's exception view is driven entirely by
+webhook deliveries — there's no list endpoint to poll.
+
+Exceptions don't roll back automatically. The standard response is to
+top up the funding source (or move the customer to a state where their
+balance can be collected) and contact Lightspark support to drive the
+exception to resolution.
+
+## Idempotency on webhooks
+
+Every transaction webhook carries a unique `id`. Track processed
+webhook IDs and treat duplicates as no-ops — Grid retries failed
+deliveries, and your reconciliation should be safe under at-least-once
+delivery.
+
+See [Webhooks](/cards/platform-tools/webhooks) for signature
+verification and the full payload shape.
diff --git a/mintlify/snippets/cards/sandbox-testing.mdx b/mintlify/snippets/cards/sandbox-testing.mdx
new file mode 100644
index 00000000..af097b90
--- /dev/null
+++ b/mintlify/snippets/cards/sandbox-testing.mdx
@@ -0,0 +1,145 @@
+The card network can't reach into Sandbox to send real authorizations,
+so Sandbox exposes three simulate helpers that drive the same internal
+`authorize` and `reconcile` paths the issuer would call in production.
+Production returns `404` on these paths.
+
+```text
+POST /sandbox/cards/{id}/simulate/authorization
+POST /sandbox/cards/{id}/simulate/clearing
+POST /sandbox/cards/{id}/simulate/return
+```
+
+Outcomes are deterministic — driven by magic-value suffixes on the
+relevant id. The same approach is used by
+`/sandbox/internal-accounts/{id}/fund`.
+
+## Issuance suffixes (platformCardId)
+
+The last three characters of `platformCardId` (or `cardholderId` when
+`platformCardId` is omitted) control how `POST /cards` resolves:
+
+| Suffix | Behavior |
+|--------|----------|
+| `001` | Stays `PENDING_ISSUE` indefinitely (test the polling path) |
+| `002` | Issuer provisioning rejected → `state: CLOSED`, `stateReason: "ISSUER_REJECTED"` |
+| `005` | Delayed activation (~30s) before the `CARD.STATE_CHANGE` webhook fires |
+| any other | Instant activation → `state: ACTIVE`, `last4` deterministic from the suffix |
+
+## Funding-source suffixes (accountId)
+
+Binding a funding source resolves based on the last three characters
+of `accountId`:
+
+| Suffix | Behavior |
+|--------|----------|
+| `002` | `FUNDING_SOURCE_INELIGIBLE` (insufficient balance check) |
+| `003` | `FUNDING_SOURCE_INELIGIBLE` (account closed) |
+| any other | Success |
+
+## Authorization simulate
+
+```bash
+curl -X POST "$GRID_BASE_URL/sandbox/cards/Card:019542f5-b3e7-1d02-0000-000000000010/simulate/authorization" \
+ -u "$GRID_CLIENT_ID:$GRID_CLIENT_SECRET" \
+ -H "Content-Type: application/json" \
+ -d '{
+ "amount": 1250,
+ "currency": { "code": "USD" },
+ "merchant": {
+ "descriptor": "BLUE BOTTLE COFFEE SF",
+ "mcc": "5814",
+ "country": "US"
+ }
+ }'
+```
+
+Outcomes are controlled by the last three characters of
+`merchant.descriptor`:
+
+| Suffix | Outcome |
+|--------|---------|
+| `002` | Decline — `INSUFFICIENT_FUNDS` (the pull on the funding source fails) |
+| `003` | Decline — `CARD_PAUSED` (use against a `FROZEN` card to verify) |
+| `005` | Delayed pull (~30s) — exercises the `PENDING → CONFIRMED` path |
+| `006` | Pull succeeds but the confirmation event reports `FAILED` — exercises the high-urgency `EXCEPTION` alert |
+| any other | Approved |
+
+The response is the resulting `CardTransaction`.
+
+## Clearing simulate
+
+```bash
+curl -X POST "$GRID_BASE_URL/sandbox/cards/Card:019542f5-b3e7-1d02-0000-000000000010/simulate/clearing" \
+ -u "$GRID_CLIENT_ID:$GRID_CLIENT_SECRET" \
+ -H "Content-Type: application/json" \
+ -d '{
+ "cardTransactionId": "CardTransaction:019542f5-b3e7-1d02-0000-000000000100",
+ "amount": 1500
+ }'
+```
+
+- `amount > authorizedAmount` exercises the over-auth post-hoc pull
+ path (restaurant tip / tip-on-top).
+- `amount = 0` exercises `AUTHORIZATION_EXPIRY` — the auth expires
+ with no clearing posted.
+
+Suffix-driven outcomes on the parent transaction's id govern whether
+the post-hoc pull succeeds — use them with the
+[merchant descriptor suffixes](#authorization-simulate) above to
+construct deterministic exception scenarios.
+
+## Return simulate
+
+```bash
+curl -X POST "$GRID_BASE_URL/sandbox/cards/Card:019542f5-b3e7-1d02-0000-000000000010/simulate/return" \
+ -u "$GRID_CLIENT_ID:$GRID_CLIENT_SECRET" \
+ -H "Content-Type: application/json" \
+ -d '{
+ "cardTransactionId": "CardTransaction:019542f5-b3e7-1d02-0000-000000000100",
+ "amount": 1500
+ }'
+```
+
+A full refund flips the parent to `REFUNDED`; a partial refund keeps
+it `SETTLED` with a non-zero `refundedAmount`.
+
+## End-to-end happy path
+
+A simple Sandbox loop that exercises issue → activate → auth → clear
+→ refund:
+
+```bash
+# 1. Issue (suffix not in the magic set → instant activation)
+curl -X POST "$GRID_BASE_URL/cards" \
+ -u "$GRID_CLIENT_ID:$GRID_CLIENT_SECRET" \
+ -H "Content-Type: application/json" \
+ -d '{
+ "cardholderId": "Customer:...",
+ "platformCardId": "card-test-happy",
+ "form": "VIRTUAL",
+ "fundingSources": ["InternalAccount:..."]
+ }'
+
+# 2. Simulate auth — any non-magic descriptor approves
+curl -X POST "$GRID_BASE_URL/sandbox/cards/Card:.../simulate/authorization" \
+ -u "$GRID_CLIENT_ID:$GRID_CLIENT_SECRET" \
+ -H "Content-Type: application/json" \
+ -d '{ "amount": 1250, "currency": {"code":"USD"}, "merchant": {"descriptor":"BLUE BOTTLE COFFEE SF"} }'
+
+# 3. Clear the auth
+curl -X POST "$GRID_BASE_URL/sandbox/cards/Card:.../simulate/clearing" \
+ -u "$GRID_CLIENT_ID:$GRID_CLIENT_SECRET" \
+ -H "Content-Type: application/json" \
+ -d '{ "cardTransactionId": "CardTransaction:...", "amount": 1500 }'
+
+# 4. Refund the cleared transaction
+curl -X POST "$GRID_BASE_URL/sandbox/cards/Card:.../simulate/return" \
+ -u "$GRID_CLIENT_ID:$GRID_CLIENT_SECRET" \
+ -H "Content-Type: application/json" \
+ -d '{ "cardTransactionId": "CardTransaction:...", "amount": 1500 }'
+```
+
+At each step you'll see `CARD.STATE_CHANGE` or
+`CARD.FUNDING_SOURCE_CHANGE` webhooks plus transaction webhooks for
+the simulated authorization, clearing, and return — wire those into
+your local webhook handler to validate end-to-end.
diff --git a/mintlify/snippets/cards/terminology.mdx b/mintlify/snippets/cards/terminology.mdx
new file mode 100644
index 00000000..db888832
--- /dev/null
+++ b/mintlify/snippets/cards/terminology.mdx
@@ -0,0 +1,63 @@
+The Cards API builds on top of the entities already used by the rest of
+{` `}Grid (Platform, Customer, Internal Account). The terms below are the
+ones that are new or have a card-specific meaning.
+
+## Cardholder
+
+The `Customer` that a card is issued to. Cards are bound to a single
+cardholder, and the cardholder must have `kycStatus: APPROVED` before a
+card can be issued — otherwise `POST /cards` is rejected with
+`CARDHOLDER_KYC_NOT_APPROVED`.
+
+## Funding source
+
+An `InternalAccount` bound to a card. Every card must be bound to at
+least one funding source, and Authorization Decisioning checks the
+source's balance at auth time before approving a transaction. The
+`fundingSources` array on the `Card` resource is ordered by priority —
+the first entry is tried first. In v1 only the first source is read by
+Authorization Decisioning; additional sources are stored for future
+multi-source decisioning.
+
+## Authorization
+
+The real-time request from the card network ("can this card spend
+$12.50 at Blue Bottle Coffee?"). {` `}Grid runs Authorization Decisioning
+against the funding source and either approves the auth (placing a hold)
+or declines it (`INSUFFICIENT_FUNDS`, `CARD_PAUSED`, etc.).
+
+## Pull
+
+A debit against the bound internal account that funds an approved
+authorization. Most transactions have a single pull at auth time. A
+restaurant tip or any settlement larger than the original auth produces
+a second, post-hoc pull — this is the over-auth path.
+
+## Clearing (settlement)
+
+The network's confirmation that funds have moved for an authorization.
+A transaction can have multiple clearings (e.g. split shipments) and
+goes through `AUTHORIZED → PARTIALLY_SETTLED → SETTLED` as clearings
+land.
+
+## Refund
+
+A merchant-initiated `RETURN` against a settled transaction. Refunds
+flow back to the funding source. A full refund moves the parent
+transaction to `REFUNDED`; a partial refund keeps it `SETTLED` with a
+non-zero `refundedAmount`.
+
+## Exception
+
+A transaction that settled to the card network but whose corresponding
+pull from the funding source failed — for example, the cardholder's
+balance no longer covers a post-hoc tip clearing. Exceptions are the
+high-urgency reconciliation alerts and are surfaced as
+`status: "EXCEPTION"` on the parent `CardTransaction`.
+
+## PAN embed URL
+
+`panEmbedUrl` on the `Card` resource is the issuer's iframe URL that
+renders the full PAN, CVV, and expiry directly to the cardholder. Render
+it in an iframe in your client; the full credentials never cross
+{` `}Grid's servers.
diff --git a/mintlify/snippets/cards/webhooks.mdx b/mintlify/snippets/cards/webhooks.mdx
new file mode 100644
index 00000000..64b818a2
--- /dev/null
+++ b/mintlify/snippets/cards/webhooks.mdx
@@ -0,0 +1,93 @@
+Cards add two webhook event types on top of Grid's existing webhook
+infrastructure. Signature verification (`X-Grid-Signature`) and
+retry behavior are identical to the rest of Grid — see
+[Authentication](/api-reference/authentication) and
+[Webhooks](/api-reference/webhooks) for the underlying mechanics.
+
+Card-transaction lifecycle events are not card-specific webhooks —
+they ride on the generic transaction webhook stream (a follow-up
+extends the Transaction model with a card destination type).
+
+## Event types
+
+| Type | Fires on |
+|------|----------|
+| `CARD.STATE_CHANGE` | `PENDING_ISSUE → ACTIVE`, `→ CLOSED (ISSUER_REJECTED)`, and every subsequent `ACTIVE ⇄ FROZEN` and `→ CLOSED` transition. |
+| `CARD.FUNDING_SOURCE_CHANGE` | Whenever `PATCH /cards/{id}` updates the `fundingSources` array. |
+
+All three carry the standard envelope:
+
+```json
+{
+ "id": "Webhook:019542f5-b3e7-1d02-0000-000000000020",
+ "type": "CARD.STATE_CHANGE",
+ "timestamp": "2026-05-08T14:11:00Z",
+ "data": { /* the affected Card or CardTransaction resource */ }
+}
+```
+
+The `id` is unique per delivery and safe to use for idempotency.
+
+## CARD.STATE_CHANGE
+
+The `data` payload is the post-change `Card` resource. Example —
+activation after issuance:
+
+```json
+{
+ "id": "Webhook:019542f5-b3e7-1d02-0000-000000000020",
+ "type": "CARD.STATE_CHANGE",
+ "timestamp": "2026-05-08T14:11:00Z",
+ "data": {
+ "id": "Card:019542f5-b3e7-1d02-0000-000000000010",
+ "cardholderId": "Customer:019542f5-b3e7-1d02-0000-000000000001",
+ "state": "ACTIVE",
+ "brand": "VISA",
+ "form": "VIRTUAL",
+ "last4": "4242",
+ "expMonth": 12,
+ "expYear": 2029,
+ "panEmbedUrl": "https://embed.lithic.com/iframe/...?t=...",
+ "fundingSources": [
+ "InternalAccount:019542f5-b3e7-1d02-0000-000000000002"
+ ],
+ "currency": "USD",
+ "createdAt": "2026-05-08T14:10:00Z",
+ "updatedAt": "2026-05-08T14:11:00Z"
+ }
+}
+```
+
+Common branches to handle in your consumer:
+
+- `state: "ACTIVE"` after `PENDING_ISSUE` — the card is live; surface
+ `panEmbedUrl` to the cardholder.
+- `state: "CLOSED"`, `stateReason: "ISSUER_REJECTED"` — the issuer
+ rejected provisioning; offer to issue a new card.
+- `state: "FROZEN"` / `state: "ACTIVE"` — reflect the freeze toggle in
+ your UI.
+- `state: "CLOSED"`, `stateReason: "CLOSED_BY_PLATFORM"` — close
+ confirmed; stop showing the card.
+
+## CARD.FUNDING_SOURCE_CHANGE
+
+Fires whenever a `PATCH /cards/{id}` call changes the `fundingSources`
+array. The `data` payload is the full `Card` resource with the
+post-change `fundingSources`, so a consumer that only cares about the
+current set of bindings can replace state wholesale.
+
+## Card-transaction lifecycle
+
+Authorization, pull, clearing, refund, and `EXCEPTION` transitions are
+not delivered through a dedicated card webhook. They flow through
+the generic transaction webhook stream that already carries
+outgoing-payment lifecycle events; a follow-up PR adds the card
+destination type to that stream. See
+[Reconciliation](/cards/transactions/reconciliation) for the
+underlying event model.
+
+## Idempotency & retries
+
+Webhook deliveries are at-least-once. Track processed `id` values and
+return `200` on duplicates, or return `409` and let Grid stop
+retrying. Both shapes are accepted by Grid's webhook infrastructure.
diff --git a/openapi.yaml b/openapi.yaml
index 7ca52cea..6a2a8401 100644
--- a/openapi.yaml
+++ b/openapi.yaml
@@ -53,6 +53,8 @@ tags:
description: 'Endpoints for creating and managing agents (experimental), called by the partner''s backend using platform credentials. Covers the full agent lifecycle: creation, policy configuration, pausing, deletion, the device code installation flow, and approving or rejecting transactions initiated by agents.'
- name: Agent Operations
description: Endpoints called by the agent itself using its own credentials (obtained via device code redemption). Scoped to the agent's associated customer — all requests automatically operate on behalf of that customer and are subject to the agent's policy. When an action requires approval, the resulting transaction enters a pending state and must be approved by the platform via `POST /transactions/{transactionId}/approve`.
+ - name: Cards
+ description: Card management endpoints. Issue debit cards against an internal account, freeze / unfreeze, close, manage card funding sources, and list card transactions.
paths:
/config:
get:
@@ -6163,6 +6165,533 @@ paths:
application/json:
schema:
$ref: '#/components/schemas/Error500'
+ /cards:
+ post:
+ summary: Issue a card
+ description: |
+ Issue a new card for a cardholder. Every card must be bound to at least one funding source at create time. The cardholder must have KYC status `APPROVED` before a card can be issued; otherwise the request is rejected with `CARDHOLDER_KYC_NOT_APPROVED`.
+
+ New cards start in `state: "PENDING_ISSUE"` while the card issuer provisions the card. The `card.state_change` webhook fires on the transition to `ACTIVE` (or to `CLOSED` with `stateReason: "ISSUER_REJECTED"` if provisioning fails).
+ operationId: createCard
+ tags:
+ - Cards
+ security:
+ - BasicAuth: []
+ requestBody:
+ required: true
+ content:
+ application/json:
+ schema:
+ $ref: '#/components/schemas/CardCreateRequest'
+ examples:
+ virtualCard:
+ summary: Issue a virtual card with one funding source
+ value:
+ cardholderId: Customer:019542f5-b3e7-1d02-0000-000000000001
+ platformCardId: card-emp-aary-001
+ form: VIRTUAL
+ fundingSources:
+ - InternalAccount:019542f5-b3e7-1d02-0000-000000000002
+ responses:
+ '201':
+ description: Card issued successfully
+ content:
+ application/json:
+ schema:
+ $ref: '#/components/schemas/Card'
+ '400':
+ description: Bad request. Returned with `CARDHOLDER_KYC_NOT_APPROVED` when the cardholder's KYC status is not `APPROVED`, with `FUNDING_SOURCE_INELIGIBLE` when the supplied funding source does not belong to the cardholder or is not denominated in a card-eligible currency, and for general invalid parameters.
+ content:
+ application/json:
+ schema:
+ $ref: '#/components/schemas/Error400'
+ '401':
+ description: Unauthorized
+ content:
+ application/json:
+ schema:
+ $ref: '#/components/schemas/Error401'
+ '500':
+ description: Internal service error
+ content:
+ application/json:
+ schema:
+ $ref: '#/components/schemas/Error500'
+ get:
+ summary: List cards
+ description: |
+ Retrieve a paginated list of cards. Cards can be filtered by cardholder, bound funding-source internal account, state, and platform-specific card identifier. If no filters are provided, returns all cards visible to the caller.
+ operationId: listCards
+ tags:
+ - Cards
+ security:
+ - BasicAuth: []
+ parameters:
+ - name: cardholderId
+ in: query
+ description: Filter by cardholder (customer) id.
+ required: false
+ schema:
+ type: string
+ - name: accountId
+ in: query
+ description: Filter by internal account id. Returns cards whose `fundingSources` array contains the given internal account id.
+ required: false
+ schema:
+ type: string
+ - name: platformCardId
+ in: query
+ description: Filter by platform-specific card identifier.
+ required: false
+ schema:
+ type: string
+ - name: state
+ in: query
+ description: Filter by card state.
+ required: false
+ schema:
+ $ref: '#/components/schemas/CardState'
+ - name: limit
+ in: query
+ description: Maximum number of results to return (default 20, max 100)
+ required: false
+ schema:
+ type: integer
+ minimum: 1
+ maximum: 100
+ default: 20
+ - name: cursor
+ in: query
+ description: Cursor for pagination (returned from previous request)
+ required: false
+ schema:
+ type: string
+ - name: sortOrder
+ in: query
+ description: Order to sort results in
+ required: false
+ schema:
+ type: string
+ enum:
+ - asc
+ - desc
+ default: desc
+ responses:
+ '200':
+ description: Successful operation
+ content:
+ application/json:
+ schema:
+ $ref: '#/components/schemas/CardListResponse'
+ '400':
+ description: Bad request - Invalid parameters
+ content:
+ application/json:
+ schema:
+ $ref: '#/components/schemas/Error400'
+ '401':
+ description: Unauthorized
+ content:
+ application/json:
+ schema:
+ $ref: '#/components/schemas/Error401'
+ '500':
+ description: Internal service error
+ content:
+ application/json:
+ schema:
+ $ref: '#/components/schemas/Error500'
+ /cards/{id}:
+ parameters:
+ - name: id
+ in: path
+ description: System-generated unique card identifier
+ required: true
+ schema:
+ type: string
+ get:
+ summary: Get a card
+ description: Retrieve a card by its system-generated id.
+ operationId: getCardById
+ tags:
+ - Cards
+ security:
+ - BasicAuth: []
+ responses:
+ '200':
+ description: Successful operation
+ content:
+ application/json:
+ schema:
+ $ref: '#/components/schemas/Card'
+ '401':
+ description: Unauthorized
+ content:
+ application/json:
+ schema:
+ $ref: '#/components/schemas/Error401'
+ '404':
+ description: Card not found
+ content:
+ application/json:
+ schema:
+ $ref: '#/components/schemas/Error404'
+ '500':
+ description: Internal service error
+ content:
+ application/json:
+ schema:
+ $ref: '#/components/schemas/Error500'
+ patch:
+ summary: Update a card
+ description: |
+ Update a card's `state` and / or its bound `fundingSources`. At least one of the two fields must be supplied.
+
+ - `state` transitions are limited to `ACTIVE ⇄ FROZEN` and `ACTIVE | FROZEN → CLOSED`. `CLOSED` is terminal and irreversible. Any other transition returns `409 INVALID_STATE_TRANSITION`.
+ - `fundingSources`, when supplied, fully replaces the card's bound funding sources. Array order determines the priority Authorization Decisioning tries them in. Each id must belong to the cardholder and be denominated in the card's currency; the list must contain at least one source. `fundingSources` cannot be supplied alongside `state: CLOSED`.
+
+ Because both updates are sensitive state changes, this endpoint uses Grid's 202 → signed-retry pattern (same shape as `DELETE /auth/credentials/{id}` and `POST /internal-accounts/{id}/export`):
+
+ 1. Call `PATCH /cards/{id}` with the target fields and no signing headers. The response is `202` with a `payloadToSign`, `requestId`, and `expiresAt`.
+
+ 2. Sign the `payloadToSign` with the session private key of a verified authentication credential on the card's owning internal account and retry with the signature as the `Grid-Wallet-Signature` header and the `requestId` echoed back as the `Request-Id` header. The signed retry returns `200` with the updated `Card`.
+
+ Effects:
+ - `state: FROZEN`: Authorization Decisioning declines new auths with `CARD_PAUSED`. Existing pulls and in-flight reconciliation continue — freezing does not pause the lifecycle of authorizations that already passed.
+ - `state: ACTIVE`: normal authorization behavior resumes.
+ - `state: CLOSED`: terminal close. The card transitions to `state: "CLOSED"` with `stateReason: "CLOSED_BY_PLATFORM"` and stays in the system for audit and reconciliation. All pending auths reconcile to a terminal state via the existing reconcile primitive. Inbound clearings received after close follow the standard force-post / late-presentment path — Lightspark absorbs the loss if a post-hoc pull on the now-unbound source fails. Funding-source bindings are detached. Refunds already in flight still complete because Lightspark holds the card-reserve keys.
+ - `fundingSources` change: emits `card.funding_source_change` reflecting the new ordered binding.
+
+ The `card.state_change` webhook fires on every successful `state` transition; the `card.funding_source_change` webhook fires whenever `fundingSources` is updated.
+ operationId: updateCardById
+ tags:
+ - Cards
+ security:
+ - BasicAuth: []
+ parameters:
+ - name: Grid-Wallet-Signature
+ in: header
+ required: false
+ description: Signature over the `payloadToSign` returned in a prior `202` response, produced with the session private key of a verified authentication credential on the card's owning internal account and base64-encoded. Required on the signed retry; ignored on the initial call.
+ schema:
+ type: string
+ example: MEUCIQDx7k2N0aK4p8f3vR9J6yT5wL1mB0sXnG2hQ4vJ8zYkCgIgZ4rP9dT7eWfU3oM6KjR1qSpNvBwL0tXyA2iG8fH5dE=
+ - name: Request-Id
+ in: header
+ required: false
+ description: The `requestId` returned in a prior `202` response, echoed back on the signed retry so the server can correlate it with the issued challenge. Required on the signed retry; must be paired with `Grid-Wallet-Signature`.
+ schema:
+ type: string
+ example: 7c4a8d09-ca37-4e3e-9e0d-8c2b3e9a1f21
+ requestBody:
+ required: true
+ content:
+ application/json:
+ schema:
+ $ref: '#/components/schemas/CardUpdateRequest'
+ examples:
+ freeze:
+ summary: Freeze an active card
+ value:
+ state: FROZEN
+ unfreeze:
+ summary: Unfreeze a frozen card
+ value:
+ state: ACTIVE
+ updateFundingSources:
+ summary: Replace the card's bound funding sources
+ value:
+ fundingSources:
+ - InternalAccount:019542f5-b3e7-1d02-0000-000000000002
+ - InternalAccount:019542f5-b3e7-1d02-0000-000000000003
+ freezeAndUpdateSources:
+ summary: Freeze the card and replace its funding sources in one call
+ value:
+ state: FROZEN
+ fundingSources:
+ - InternalAccount:019542f5-b3e7-1d02-0000-000000000002
+ close:
+ summary: Permanently close the card
+ value:
+ state: CLOSED
+ responses:
+ '200':
+ description: Signed retry accepted. Returns the updated card.
+ content:
+ application/json:
+ schema:
+ $ref: '#/components/schemas/Card'
+ '202':
+ description: Challenge issued. The response contains a `payloadToSign` that must be signed with the session private key of a verified authentication credential on the card's owning internal account, along with a `requestId` that must be echoed back on the retry.
+ content:
+ application/json:
+ schema:
+ $ref: '#/components/schemas/SignedRequestChallenge'
+ '400':
+ description: Bad request. Returned with `FUNDING_SOURCE_INELIGIBLE` when a supplied funding source does not belong to the cardholder or is not denominated in the card's currency, and for general invalid parameters.
+ content:
+ application/json:
+ schema:
+ $ref: '#/components/schemas/Error400'
+ '401':
+ description: Unauthorized. Returned when the provided `Grid-Wallet-Signature` is missing, malformed, or does not match a pending update challenge for this card, or when the `Request-Id` does not match an unexpired pending challenge.
+ content:
+ application/json:
+ schema:
+ $ref: '#/components/schemas/Error401'
+ '404':
+ description: Card not found
+ content:
+ application/json:
+ schema:
+ $ref: '#/components/schemas/Error404'
+ '409':
+ description: 'Conflict. Returned with `INVALID_STATE_TRANSITION` when the requested `state` transition is not one of `ACTIVE ⇄ FROZEN` or `ACTIVE | FROZEN → CLOSED` (e.g. trying to un-freeze a `CLOSED` card); with `CARD_ALREADY_CLOSED` when `state: CLOSED` is requested for a card that is already `CLOSED`; and with `CARD_NOT_MUTABLE` when the card is `CLOSED`.'
+ content:
+ application/json:
+ schema:
+ $ref: '#/components/schemas/Error409'
+ '500':
+ description: Internal service error
+ content:
+ application/json:
+ schema:
+ $ref: '#/components/schemas/Error500'
+ /sandbox/cards/{id}/simulate/authorization:
+ post:
+ summary: Simulate a card authorization
+ description: |
+ Simulate an inbound card authorization in the sandbox environment. Drives the same internal `authorize` + `reconcile` paths the card issuer would call in production, so platforms can exercise Grid's decisioning + funding-source pull behavior end-to-end without an external network round-trip.
+
+ The decisioning outcome is controlled by the last three characters of `merchant.descriptor`:
+
+ | Suffix | Outcome | | ------ | ------- | | `002` | Decline — `INSUFFICIENT_FUNDS` (the pull on the funding source fails) | | `003` | Decline — `CARD_PAUSED` (intended to verify a frozen card refuses auths) | | `005` | Delayed pull (~30s) — exercises the `PENDING → CONFIRMED` path | | `006` | Pull succeeds but the confirmation event reports `FAILED` — exercises the high-urgency `EXCEPTION` alert | | any other | Approved |
+
+ Production returns `404` on this path.
+ operationId: sandboxSimulateCardAuthorization
+ tags:
+ - Sandbox
+ security:
+ - BasicAuth: []
+ parameters:
+ - name: id
+ in: path
+ required: true
+ description: The id of the card to simulate an authorization against.
+ schema:
+ type: string
+ example: Card:019542f5-b3e7-1d02-0000-000000000010
+ requestBody:
+ required: true
+ content:
+ application/json:
+ schema:
+ $ref: '#/components/schemas/SandboxCardAuthorizationRequest'
+ examples:
+ coffeeAuth:
+ summary: Approved $12.50 auth at a coffee shop
+ value:
+ amount: 1250
+ currency:
+ code: USD
+ merchant:
+ descriptor: BLUE BOTTLE COFFEE SF
+ mcc: '5814'
+ country: US
+ declinedInsufficientFunds:
+ summary: Declined — insufficient funds (descriptor suffix `002`)
+ value:
+ amount: 50000
+ currency:
+ code: USD
+ merchant:
+ descriptor: AMAZON RETAIL US-002
+ mcc: '5942'
+ country: US
+ responses:
+ '200':
+ description: Simulated authorization processed. Returns the resulting card transaction.
+ content:
+ application/json:
+ schema:
+ $ref: '#/components/schemas/CardTransaction'
+ '400':
+ description: Bad request - Invalid parameters
+ content:
+ application/json:
+ schema:
+ $ref: '#/components/schemas/Error400'
+ '401':
+ description: Unauthorized
+ content:
+ application/json:
+ schema:
+ $ref: '#/components/schemas/Error401'
+ '403':
+ description: Forbidden - request was made with a production platform token
+ content:
+ application/json:
+ schema:
+ $ref: '#/components/schemas/Error403'
+ '404':
+ description: Card not found (also returned in production for this path)
+ content:
+ application/json:
+ schema:
+ $ref: '#/components/schemas/Error404'
+ '500':
+ description: Internal service error
+ content:
+ application/json:
+ schema:
+ $ref: '#/components/schemas/Error500'
+ /sandbox/cards/{id}/simulate/clearing:
+ post:
+ summary: Simulate a card clearing
+ description: |
+ Simulate a clearing (settlement) event against an existing `CardTransaction` in the sandbox environment.
+
+ - A clearing `amount` greater than the authorized amount exercises the over-auth post-hoc-pull path (e.g. restaurant tip on top of a 20% over-auth).
+ - A clearing `amount` of `0` exercises the `AUTHORIZATION_EXPIRY` path — the auth expires with no clearing posted.
+ - Suffix-driven outcomes on the parent transaction's id govern whether the post-hoc pull succeeds (use the suffix table from `simulate/authorization` to construct deterministic test cases).
+
+ Production returns `404` on this path.
+ operationId: sandboxSimulateCardClearing
+ tags:
+ - Sandbox
+ security:
+ - BasicAuth: []
+ parameters:
+ - name: id
+ in: path
+ required: true
+ description: The id of the card the clearing applies to.
+ schema:
+ type: string
+ example: Card:019542f5-b3e7-1d02-0000-000000000010
+ requestBody:
+ required: true
+ content:
+ application/json:
+ schema:
+ $ref: '#/components/schemas/SandboxCardClearingRequest'
+ examples:
+ tipOnTopClearing:
+ summary: Clearing larger than auth — exercises post-hoc pull
+ value:
+ cardTransactionId: CardTransaction:019542f5-b3e7-1d02-0000-000000000100
+ amount: 1500
+ authorizationExpiry:
+ summary: Clearing of 0 — exercises authorization expiry
+ value:
+ cardTransactionId: CardTransaction:019542f5-b3e7-1d02-0000-000000000100
+ amount: 0
+ responses:
+ '200':
+ description: Simulated clearing processed. Returns the updated card transaction.
+ content:
+ application/json:
+ schema:
+ $ref: '#/components/schemas/CardTransaction'
+ '400':
+ description: Bad request - Invalid parameters
+ content:
+ application/json:
+ schema:
+ $ref: '#/components/schemas/Error400'
+ '401':
+ description: Unauthorized
+ content:
+ application/json:
+ schema:
+ $ref: '#/components/schemas/Error401'
+ '403':
+ description: Forbidden - request was made with a production platform token
+ content:
+ application/json:
+ schema:
+ $ref: '#/components/schemas/Error403'
+ '404':
+ description: Card or card transaction not found
+ content:
+ application/json:
+ schema:
+ $ref: '#/components/schemas/Error404'
+ '500':
+ description: Internal service error
+ content:
+ application/json:
+ schema:
+ $ref: '#/components/schemas/Error500'
+ /sandbox/cards/{id}/simulate/return:
+ post:
+ summary: Simulate a card return
+ description: |
+ Simulate a merchant-initiated `RETURN` against an existing settled card transaction in the sandbox environment. Creates a `CardRefund` on the parent and either flips the parent to `REFUNDED` (full refund) or keeps it `SETTLED` with a non-zero `refundedAmount` (partial refund).
+
+ Production returns `404` on this path.
+ operationId: sandboxSimulateCardReturn
+ tags:
+ - Sandbox
+ security:
+ - BasicAuth: []
+ parameters:
+ - name: id
+ in: path
+ required: true
+ description: The id of the card the return applies to.
+ schema:
+ type: string
+ example: Card:019542f5-b3e7-1d02-0000-000000000010
+ requestBody:
+ required: true
+ content:
+ application/json:
+ schema:
+ $ref: '#/components/schemas/SandboxCardReturnRequest'
+ examples:
+ fullRefund:
+ summary: Full refund of a $15.00 settled transaction
+ value:
+ cardTransactionId: CardTransaction:019542f5-b3e7-1d02-0000-000000000100
+ amount: 1500
+ responses:
+ '200':
+ description: Simulated return processed. Returns the updated card transaction.
+ content:
+ application/json:
+ schema:
+ $ref: '#/components/schemas/CardTransaction'
+ '400':
+ description: Bad request - Invalid parameters
+ content:
+ application/json:
+ schema:
+ $ref: '#/components/schemas/Error400'
+ '401':
+ description: Unauthorized
+ content:
+ application/json:
+ schema:
+ $ref: '#/components/schemas/Error401'
+ '403':
+ description: Forbidden - request was made with a production platform token
+ content:
+ application/json:
+ schema:
+ $ref: '#/components/schemas/Error403'
+ '404':
+ description: Card or card transaction not found
+ content:
+ application/json:
+ schema:
+ $ref: '#/components/schemas/Error404'
+ '500':
+ description: Internal service error
+ content:
+ application/json:
+ schema:
+ $ref: '#/components/schemas/Error500'
webhooks:
agent-action:
post:
@@ -7004,23 +7533,218 @@ webhooks:
application/json:
schema:
$ref: '#/components/schemas/Error409'
- verification-update:
+ verification-update:
+ post:
+ summary: Verification status change
+ description: |
+ Webhook that is called when a customer's KYC/KYB verification status changes.
+ This endpoint should be implemented by clients of the Grid API.
+
+ ### Authentication
+ The webhook includes a signature in the `X-Grid-Signature` header that allows you to verify that the webhook was sent by Grid.
+ To verify the signature:
+ 1. Get the Grid API public key provided to you during integration
+ 2. Decode the base64 signature from the header
+ 3. Create a SHA-256 hash of the request body
+ 4. Verify the signature using the public key and the hash
+
+ If the signature verification succeeds, the webhook is authentic. If not, it should be rejected.
+ operationId: verificationStatusWebhook
+ tags:
+ - Webhooks
+ security:
+ - WebhookSignature: []
+ requestBody:
+ required: true
+ content:
+ application/json:
+ schema:
+ $ref: '#/components/schemas/VerificationWebhook'
+ examples:
+ approved:
+ summary: Verification approved
+ value:
+ id: Webhook:019542f5-b3e7-1d02-0000-000000000030
+ type: VERIFICATION.APPROVED
+ timestamp: '2025-08-15T14:32:00Z'
+ data:
+ id: Verification:019542f5-b3e7-1d02-0000-000000000010
+ customerId: Customer:019542f5-b3e7-1d02-0000-000000000001
+ verificationStatus: APPROVED
+ errors: []
+ createdAt: '2025-08-15T14:00:00Z'
+ resolveErrors:
+ summary: Verification requires action
+ value:
+ id: Webhook:019542f5-b3e7-1d02-0000-000000000031
+ type: VERIFICATION.RESOLVE_ERRORS
+ timestamp: '2025-08-15T14:32:00Z'
+ data:
+ id: Verification:019542f5-b3e7-1d02-0000-000000000011
+ customerId: Customer:019542f5-b3e7-1d02-0000-000000000001
+ verificationStatus: RESOLVE_ERRORS
+ errors:
+ - resourceId: Customer:019542f5-b3e7-1d02-0000-000000000001
+ type: MISSING_PROOF_OF_ADDRESS_DOCUMENT
+ acceptedDocumentTypes:
+ - PROOF_OF_ADDRESS
+ reason: Proof of address document is required
+ createdAt: '2025-08-15T14:00:00Z'
+ responses:
+ '200':
+ description: |
+ Webhook received successfully
+ '400':
+ description: Bad request
+ content:
+ application/json:
+ schema:
+ $ref: '#/components/schemas/Error400'
+ '401':
+ description: Unauthorized - Signature validation failed
+ content:
+ application/json:
+ schema:
+ $ref: '#/components/schemas/Error401'
+ '409':
+ description: Conflict - Webhook has already been processed (duplicate id)
+ content:
+ application/json:
+ schema:
+ $ref: '#/components/schemas/Error409'
+ card-state-change:
+ post:
+ summary: Card state change
+ description: |
+ Webhook that is called when a card's lifecycle state changes. Fires on `PENDING_ISSUE → ACTIVE`, on `PENDING_ISSUE → CLOSED (ISSUER_REJECTED)` when issuer provisioning fails, and on every subsequent `ACTIVE ⇄ FROZEN` and `→ CLOSED` transition.
+
+ This endpoint should be implemented by clients of the Grid API.
+
+ ### Authentication
+
+ The webhook includes a signature in the `X-Grid-Signature` header that allows you to verify that the webhook was sent by Grid.
+ To verify the signature:
+ 1. Get the Grid public key provided to you during integration
+ 2. Decode the base64 signature from the header
+ 3. Create a SHA-256 hash of the request body
+ 4. Verify the signature using the public key and the hash
+
+ If the signature verification succeeds, the webhook is authentic. If not, it should be rejected.
+ operationId: cardStateChangeWebhook
+ tags:
+ - Webhooks
+ security:
+ - WebhookSignature: []
+ requestBody:
+ required: true
+ content:
+ application/json:
+ schema:
+ $ref: '#/components/schemas/CardStateChangeWebhook'
+ examples:
+ activated:
+ summary: Card transitioned from PENDING_ISSUE to ACTIVE
+ value:
+ id: Webhook:019542f5-b3e7-1d02-0000-000000000020
+ type: CARD.STATE_CHANGE
+ timestamp: '2026-05-08T14:11:00Z'
+ data:
+ id: Card:019542f5-b3e7-1d02-0000-000000000010
+ cardholderId: Customer:019542f5-b3e7-1d02-0000-000000000001
+ platformCardId: card-emp-aary-001
+ state: ACTIVE
+ stateReason: null
+ brand: VISA
+ form: VIRTUAL
+ last4: '4242'
+ expMonth: 12
+ expYear: 2029
+ panEmbedUrl: https://embed.lithic.com/iframe/...?t=...
+ fundingSources:
+ - InternalAccount:019542f5-b3e7-1d02-0000-000000000002
+ currency: USD
+ issuerRef: lithic_card_4f8d3a2b1c
+ createdAt: '2026-05-08T14:10:00Z'
+ updatedAt: '2026-05-08T14:11:00Z'
+ issuerRejected:
+ summary: Card rejected by issuer during provisioning
+ value:
+ id: Webhook:019542f5-b3e7-1d02-0000-000000000021
+ type: CARD.STATE_CHANGE
+ timestamp: '2026-05-08T14:12:00Z'
+ data:
+ id: Card:019542f5-b3e7-1d02-0000-000000000011
+ cardholderId: Customer:019542f5-b3e7-1d02-0000-000000000001
+ state: CLOSED
+ stateReason: ISSUER_REJECTED
+ form: VIRTUAL
+ fundingSources:
+ - InternalAccount:019542f5-b3e7-1d02-0000-000000000002
+ currency: USD
+ createdAt: '2026-05-08T14:10:00Z'
+ updatedAt: '2026-05-08T14:12:00Z'
+ 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
+ fundingSources:
+ - InternalAccount:019542f5-b3e7-1d02-0000-000000000002
+ currency: USD
+ createdAt: '2026-05-08T14:10:00Z'
+ updatedAt: '2026-05-09T09:00:00Z'
+ responses:
+ '200':
+ description: |
+ Webhook received successfully
+ '400':
+ description: Bad request
+ content:
+ application/json:
+ schema:
+ $ref: '#/components/schemas/Error400'
+ '401':
+ description: Unauthorized - Signature validation failed
+ content:
+ application/json:
+ schema:
+ $ref: '#/components/schemas/Error401'
+ '409':
+ description: Conflict - Webhook has already been processed (duplicate id)
+ content:
+ application/json:
+ schema:
+ $ref: '#/components/schemas/Error409'
+ card-funding-source-change:
post:
- summary: Verification status change
+ summary: Card funding source change
description: |
- Webhook that is called when a customer's KYC/KYB verification status changes.
+ Webhook that is called when the funding sources bound to a card change. Fires whenever `PATCH /cards/{id}` updates the `fundingSources` array. The payload carries the full `Card` resource with the post-change `fundingSources` array.
+
This endpoint should be implemented by clients of the Grid API.
### Authentication
+
The webhook includes a signature in the `X-Grid-Signature` header that allows you to verify that the webhook was sent by Grid.
To verify the signature:
- 1. Get the Grid API public key provided to you during integration
+ 1. Get the Grid public key provided to you during integration
2. Decode the base64 signature from the header
3. Create a SHA-256 hash of the request body
4. Verify the signature using the public key and the hash
If the signature verification succeeds, the webhook is authentic. If not, it should be rejected.
- operationId: verificationStatusWebhook
+ operationId: cardFundingSourceChangeWebhook
tags:
- Webhooks
security:
@@ -7030,37 +7754,30 @@ webhooks:
content:
application/json:
schema:
- $ref: '#/components/schemas/VerificationWebhook'
+ $ref: '#/components/schemas/CardFundingSourceChangeWebhook'
examples:
- approved:
- summary: Verification approved
+ fundingSourcesReplaced:
+ summary: Funding sources replaced via PATCH /cards/{id}
value:
id: Webhook:019542f5-b3e7-1d02-0000-000000000030
- type: VERIFICATION.APPROVED
- timestamp: '2025-08-15T14:32:00Z'
- data:
- id: Verification:019542f5-b3e7-1d02-0000-000000000010
- customerId: Customer:019542f5-b3e7-1d02-0000-000000000001
- verificationStatus: APPROVED
- errors: []
- createdAt: '2025-08-15T14:00:00Z'
- resolveErrors:
- summary: Verification requires action
- value:
- id: Webhook:019542f5-b3e7-1d02-0000-000000000031
- type: VERIFICATION.RESOLVE_ERRORS
- timestamp: '2025-08-15T14:32:00Z'
+ type: CARD.FUNDING_SOURCE_CHANGE
+ timestamp: '2026-05-08T14:30:00Z'
data:
- id: Verification:019542f5-b3e7-1d02-0000-000000000011
- customerId: Customer:019542f5-b3e7-1d02-0000-000000000001
- verificationStatus: RESOLVE_ERRORS
- errors:
- - resourceId: Customer:019542f5-b3e7-1d02-0000-000000000001
- type: MISSING_PROOF_OF_ADDRESS_DOCUMENT
- acceptedDocumentTypes:
- - PROOF_OF_ADDRESS
- reason: Proof of address document is required
- createdAt: '2025-08-15T14:00:00Z'
+ id: Card:019542f5-b3e7-1d02-0000-000000000010
+ cardholderId: Customer:019542f5-b3e7-1d02-0000-000000000001
+ state: ACTIVE
+ stateReason: null
+ brand: VISA
+ form: VIRTUAL
+ last4: '4242'
+ expMonth: 12
+ expYear: 2029
+ fundingSources:
+ - InternalAccount:019542f5-b3e7-1d02-0000-000000000002
+ - InternalAccount:019542f5-b3e7-1d02-0000-000000000003
+ currency: USD
+ createdAt: '2026-05-08T14:10:00Z'
+ updatedAt: '2026-05-08T14:30:00Z'
responses:
'200':
description: |
@@ -17125,6 +17842,414 @@ components:
example: gat_ed0ad25881e234cc28fb2dec0a4fe64e4172a3b9
policy:
$ref: '#/components/schemas/AgentPolicy'
+ CardState:
+ type: string
+ enum:
+ - PENDING_KYC
+ - PENDING_ISSUE
+ - ACTIVE
+ - FROZEN
+ - CLOSED
+ description: |
+ Lifecycle state of a card.
+
+ | State | Description |
+ |-------|-------------|
+ | `PENDING_KYC` | The cardholder has not yet completed KYC. Cards in this state cannot transact. |
+ | `PENDING_ISSUE` | The card has been requested and is being provisioned with the issuer. |
+ | `ACTIVE` | The card is live and can authorize transactions. |
+ | `FROZEN` | The card is temporarily disabled by the platform. New authorizations are declined with `CARD_PAUSED`. Existing settlements and refunds continue to reconcile. |
+ | `CLOSED` | The card is permanently closed. Terminal, irreversible state. |
+ CardStateReason:
+ type: string
+ enum:
+ - ISSUER_REJECTED
+ - CLOSED_BY_PLATFORM
+ - CLOSED_BY_GRID
+ description: |
+ Reason a card reached a terminal or non-active state. Present on
+ `CLOSED` cards, and on cards that fail provisioning before reaching
+ `ACTIVE`.
+
+ | Reason | Description |
+ |--------|-------------|
+ | `ISSUER_REJECTED` | The card issuer rejected provisioning during `PENDING_ISSUE`. |
+ | `CLOSED_BY_PLATFORM` | The card was closed via `PATCH /cards/{id}` (`state: CLOSED`) by the platform. |
+ | `CLOSED_BY_GRID` | The card was closed by Grid (e.g. compliance or risk action). |
+ CardBrand:
+ type: string
+ enum:
+ - VISA
+ - MASTERCARD
+ description: |
+ Card network brand. Read-only — determined by Grid when the card is
+ provisioned with the issuer.
+ CardForm:
+ type: string
+ enum:
+ - VIRTUAL
+ description: |
+ Physical form factor of the card. Only `VIRTUAL` is supported in v1;
+ `PHYSICAL` will be added in a later release.
+ Card:
+ type: object
+ required:
+ - id
+ - cardholderId
+ - state
+ - form
+ - fundingSources
+ - createdAt
+ - updatedAt
+ properties:
+ id:
+ type: string
+ description: System-generated unique card identifier
+ readOnly: true
+ example: Card:019542f5-b3e7-1d02-0000-000000000010
+ cardholderId:
+ type: string
+ description: The id of the `Customer` who holds this card.
+ example: Customer:019542f5-b3e7-1d02-0000-000000000001
+ platformCardId:
+ type: string
+ description: Platform-specific card identifier. Optional on create — system-generated if omitted, mirroring `platformCustomerId` semantics.
+ example: card-emp-aary-001
+ state:
+ $ref: '#/components/schemas/CardState'
+ stateReason:
+ oneOf:
+ - $ref: '#/components/schemas/CardStateReason'
+ - type: 'null'
+ description: Reason associated with the current `state`. Populated when the card is `CLOSED` or when provisioning was rejected; otherwise null.
+ brand:
+ $ref: '#/components/schemas/CardBrand'
+ form:
+ $ref: '#/components/schemas/CardForm'
+ last4:
+ type: string
+ description: Last four digits of the card PAN.
+ example: '4242'
+ expMonth:
+ type: integer
+ minimum: 1
+ maximum: 12
+ description: Card expiration month (1–12).
+ example: 12
+ expYear:
+ type: integer
+ description: Card expiration year (four digits).
+ example: 2029
+ panEmbedUrl:
+ type: string
+ format: uri
+ description: URL of the card issuer's iframe that securely displays the PAN, CVV, and expiry to the cardholder. The full PAN and CVV never cross Grid's servers — render this URL in an iframe in your client to reveal card details.
+ example: https://embed.lithic.com/iframe/...?t=...
+ fundingSources:
+ type: array
+ description: Internal account ids bound to this card as funding sources, in priority order — the first entry is tried first by Authorization Decisioning. Every card has at least one funding source.
+ items:
+ type: string
+ example:
+ - InternalAccount:019542f5-b3e7-1d02-0000-000000000002
+ - InternalAccount:019542f5-b3e7-1d02-0000-000000000003
+ currency:
+ type: string
+ description: Currency the card transacts in (ISO 4217 for fiat, tickers for crypto). Derived from the funding sources at issue time — all funding sources bound to a card must be denominated in the same card-eligible currency.
+ example: USD
+ readOnly: true
+ issuerRef:
+ type: string
+ description: Opaque identifier for the card on the underlying issuer. Useful for cross-referencing in issuer dashboards; not used for any Grid request routing.
+ example: lithic_card_4f8d3a2b1c
+ readOnly: true
+ createdAt:
+ type: string
+ format: date-time
+ description: Creation timestamp
+ readOnly: true
+ example: '2026-05-08T14:10:00Z'
+ updatedAt:
+ type: string
+ format: date-time
+ description: Last update timestamp
+ readOnly: true
+ example: '2026-05-08T14:11:00Z'
+ CardListResponse:
+ type: object
+ required:
+ - data
+ - hasMore
+ properties:
+ data:
+ type: array
+ description: List of cards matching the filter criteria
+ items:
+ $ref: '#/components/schemas/Card'
+ hasMore:
+ type: boolean
+ description: Indicates if more results are available beyond this page
+ nextCursor:
+ type: string
+ description: Cursor to retrieve the next page of results (only present if hasMore is true)
+ totalCount:
+ type: integer
+ description: Total number of cards matching the criteria (excluding pagination)
+ CardCreateRequest:
+ type: object
+ required:
+ - cardholderId
+ - form
+ - fundingSources
+ properties:
+ cardholderId:
+ type: string
+ description: The id of the `Customer` to issue the card to. The customer must have KYC status `APPROVED`; otherwise the request is rejected with `CARDHOLDER_KYC_NOT_APPROVED`.
+ example: Customer:019542f5-b3e7-1d02-0000-000000000001
+ platformCardId:
+ type: string
+ description: Optional platform-specific card identifier. System-generated when omitted, mirroring `platformCustomerId` semantics.
+ example: card-emp-aary-001
+ form:
+ $ref: '#/components/schemas/CardForm'
+ fundingSources:
+ type: array
+ description: Internal account ids to bind as funding sources, in priority order. The first entry is tried first by Authorization Decisioning. Every card must be bound to at least one source, and every source must belong to the cardholder and be denominated in a card-eligible currency (USDB in v1); otherwise the request is rejected with `FUNDING_SOURCE_INELIGIBLE`.
+ minItems: 1
+ items:
+ type: string
+ example:
+ - InternalAccount:019542f5-b3e7-1d02-0000-000000000002
+ CardUpdateRequest:
+ type: object
+ description: Update request for `PATCH /cards/{id}`. At least one of `state` or `fundingSources` must be supplied. `state` transitions are limited to `ACTIVE ⇄ FROZEN` and `ACTIVE | FROZEN → CLOSED`; any other transition returns `409 INVALID_STATE_TRANSITION`. `CLOSED` is terminal and irreversible and cannot be combined with `fundingSources`. `fundingSources`, when supplied, fully replaces the card's bound funding sources — the array order determines the priority Authorization Decisioning tries them in.
+ properties:
+ state:
+ type: string
+ enum:
+ - ACTIVE
+ - FROZEN
+ - CLOSED
+ description: Target state for the card. Permitted transitions are `ACTIVE ⇄ FROZEN` and `ACTIVE | FROZEN → CLOSED`. `CLOSED` is terminal and irreversible; once closed, the card stays in the system for audit and reconciliation but cannot transact again.
+ example: FROZEN
+ fundingSources:
+ type: array
+ description: 'New ordered list of internal account ids to bind as funding sources. Fully replaces the previous binding. Each id must belong to the cardholder and be denominated in the card''s currency. The list must contain at least one source — to stop a card from spending without removing all sources, transition it to `FROZEN` instead. Cannot be supplied alongside `state: CLOSED`.'
+ minItems: 1
+ items:
+ type: string
+ example:
+ - InternalAccount:019542f5-b3e7-1d02-0000-000000000002
+ - InternalAccount:019542f5-b3e7-1d02-0000-000000000003
+ CardMerchant:
+ type: object
+ required:
+ - descriptor
+ properties:
+ descriptor:
+ type: string
+ description: Merchant descriptor string captured from the card network at authorization time.
+ example: BLUE BOTTLE COFFEE SF
+ mcc:
+ type: string
+ description: Merchant Category Code (ISO 18245) — four-digit numeric string.
+ example: '5814'
+ country:
+ type: string
+ description: Two-letter ISO 3166-1 alpha-2 country code of the merchant.
+ example: US
+ SandboxCardAuthorizationRequest:
+ type: object
+ required:
+ - amount
+ - currency
+ - merchant
+ description: Sandbox-only request body for `POST /sandbox/cards/{id}/simulate/authorization`. Drives the same internal authorization + reconcile paths that the issuer would call in production. The decisioning outcome is controlled by the last three characters of `merchant.descriptor` — see the endpoint documentation for the suffix table.
+ properties:
+ amount:
+ type: integer
+ format: int64
+ description: Authorization amount in the smallest unit of `currency` (e.g. cents for USD).
+ exclusiveMinimum: 0
+ example: 1250
+ currency:
+ $ref: '#/components/schemas/Currency'
+ merchant:
+ $ref: '#/components/schemas/CardMerchant'
+ CardTransactionStatus:
+ type: string
+ enum:
+ - AUTHORIZED
+ - PARTIALLY_SETTLED
+ - SETTLED
+ - REFUNDED
+ - EXCEPTION
+ description: |
+ Lifecycle status of a card transaction.
+
+ | Status | Description |
+ |--------|-------------|
+ | `AUTHORIZED` | The auth has been approved and a hold placed on the funding source; no clearing has arrived yet. |
+ | `PARTIALLY_SETTLED` | At least one clearing has arrived and posted, but more clearings are still expected (split shipments, tips, multi-leg trips). |
+ | `SETTLED` | All clearings for the auth have posted and the transaction is closed against the funding source. |
+ | `REFUNDED` | A `RETURN` was received from the merchant; the net settled amount has been refunded in part or whole. |
+ | `EXCEPTION` | The transaction settled to the card network but the corresponding pull from the funding source failed (e.g. balance no longer covers the post-hoc clearing). Surfaces high-urgency alerts and is the dashboard query for stuck reconciliations. |
+ CardPullSummary:
+ type: object
+ required:
+ - count
+ - totalAmount
+ properties:
+ count:
+ type: integer
+ description: Total number of pulls (debits) executed against the funding source for this transaction. `> 1` indicates one or more post-hoc pulls — e.g. restaurant tip / over-auth clearings.
+ example: 2
+ totalAmount:
+ type: integer
+ format: int64
+ description: Sum of all pull amounts in the smallest unit of the funding source's currency.
+ example: 1500
+ pendingCount:
+ type: integer
+ description: Number of pulls still in the `PENDING` state. Drops to zero when every pull has reached a terminal state. Non-zero values that persist beyond the expected settlement window are an early signal for the `EXCEPTION` path.
+ example: 0
+ CardRefundSummary:
+ type: object
+ required:
+ - count
+ - totalAmount
+ properties:
+ count:
+ type: integer
+ description: Number of refund (return) events received for this transaction.
+ example: 0
+ totalAmount:
+ type: integer
+ format: int64
+ description: Sum of all refund amounts in the smallest unit of the funding source's currency.
+ example: 0
+ CardSettlementSummary:
+ type: object
+ required:
+ - count
+ - totalAmount
+ properties:
+ count:
+ type: integer
+ description: Number of settlement (clearing) events received for this transaction.
+ example: 1
+ totalAmount:
+ type: integer
+ format: int64
+ description: Sum of all settled amounts in the smallest unit of the funding source's currency.
+ example: 1500
+ CardTransaction:
+ type: object
+ required:
+ - id
+ - cardId
+ - status
+ - merchant
+ - authorizedAmount
+ - accountId
+ - pullSummary
+ - refundSummary
+ - settlementSummary
+ - authorizedAt
+ - createdAt
+ - updatedAt
+ description: Parent transaction row for a card authorization and all of the pulls / settlements / refunds that reconcile against it. Child events are rolled up into the `pullSummary`, `refundSummary`, and `settlementSummary` aggregates. Delivered as the payload of the generic transaction webhook stream (extends the Transaction model with a card destination type) on every transition.
+ properties:
+ id:
+ type: string
+ description: System-generated unique card transaction identifier
+ readOnly: true
+ example: CardTransaction:019542f5-b3e7-1d02-0000-000000000100
+ cardId:
+ type: string
+ description: The id of the `Card` this transaction was made on.
+ example: Card:019542f5-b3e7-1d02-0000-000000000010
+ issuerTransactionToken:
+ type: string
+ description: Opaque identifier for the transaction on the underlying issuer. Used to cross-reference Grid records against issuer dashboards and webhooks.
+ example: lithic_txn_b81c2a4f
+ readOnly: true
+ status:
+ $ref: '#/components/schemas/CardTransactionStatus'
+ merchant:
+ $ref: '#/components/schemas/CardMerchant'
+ authorizedAmount:
+ $ref: '#/components/schemas/CurrencyAmount'
+ settledAmount:
+ $ref: '#/components/schemas/CurrencyAmount'
+ refundedAmount:
+ $ref: '#/components/schemas/CurrencyAmount'
+ accountId:
+ type: string
+ description: Internal account id that funded this transaction (the funding source selected by Authorization Decisioning at auth time).
+ example: InternalAccount:019542f5-b3e7-1d02-0000-000000000002
+ pullSummary:
+ $ref: '#/components/schemas/CardPullSummary'
+ refundSummary:
+ $ref: '#/components/schemas/CardRefundSummary'
+ settlementSummary:
+ $ref: '#/components/schemas/CardSettlementSummary'
+ authorizedAt:
+ type: string
+ format: date-time
+ description: When the auth was approved.
+ example: '2026-05-08T14:30:00Z'
+ lastEventAt:
+ type: string
+ format: date-time
+ description: Timestamp of the most recent reconcile event (pull / clearing / refund) against this transaction.
+ example: '2026-05-08T15:42:11Z'
+ createdAt:
+ type: string
+ format: date-time
+ description: Creation timestamp (same as `authorizedAt` for card transactions).
+ readOnly: true
+ example: '2026-05-08T14:30:00Z'
+ updatedAt:
+ type: string
+ format: date-time
+ description: Last update timestamp.
+ readOnly: true
+ example: '2026-05-08T15:42:11Z'
+ SandboxCardClearingRequest:
+ type: object
+ required:
+ - cardTransactionId
+ - amount
+ description: Sandbox-only request body for `POST /sandbox/cards/{id}/simulate/clearing`. Drives a clearing event against an existing `CardTransaction`. Pass an `amount` greater than the authorized amount to exercise the over-auth / restaurant-tip post-hoc-pull path; pass `0` to exercise `AUTHORIZATION_EXPIRY`. Suffix-driven outcomes on the parent transaction's id govern whether the post-hoc pull succeeds.
+ properties:
+ cardTransactionId:
+ type: string
+ description: The id of the `CardTransaction` to clear against. Must be in `AUTHORIZED` or `PARTIALLY_SETTLED` state.
+ example: CardTransaction:019542f5-b3e7-1d02-0000-000000000100
+ amount:
+ type: integer
+ format: int64
+ description: Clearing amount in the smallest unit of the transaction's currency. Set to `0` to simulate an authorization expiry with no clearing.
+ minimum: 0
+ example: 1500
+ SandboxCardReturnRequest:
+ type: object
+ required:
+ - cardTransactionId
+ - amount
+ description: Sandbox-only request body for `POST /sandbox/cards/{id}/simulate/return`. Drives a `RETURN` event against an existing settled `CardTransaction`, which creates a `CardRefund` and pushes the parent transaction towards `REFUNDED` (full) or keeps it `SETTLED` (partial).
+ properties:
+ cardTransactionId:
+ type: string
+ description: The id of the `CardTransaction` to refund against. Must have at least one settled clearing.
+ example: CardTransaction:019542f5-b3e7-1d02-0000-000000000100
+ amount:
+ type: integer
+ format: int64
+ description: Return amount in the smallest unit of the transaction's currency. Must be less than or equal to the net settled amount (settled minus previously-refunded).
+ exclusiveMinimum: 0
+ example: 1500
WebhookType:
type: string
enum:
@@ -17157,6 +18282,8 @@ components:
- BULK_UPLOAD.COMPLETED
- BULK_UPLOAD.FAILED
- AGENT_ACTION.PENDING_APPROVAL
+ - CARD.STATE_CHANGE
+ - CARD.FUNDING_SOURCE_CHANGE
- TEST
description: Type of webhook event in OBJECT.EVENT dot-notation. The part before the dot identifies the resource, the part after identifies the event. This lets consumers route purely on type without inspecting data.status.
BaseWebhook:
@@ -17351,3 +18478,29 @@ components:
- VERIFICATION.RESOLVE_ERRORS
- VERIFICATION.IN_PROGRESS
- VERIFICATION.PENDING_MANUAL_REVIEW
+ CardStateChangeWebhook:
+ allOf:
+ - $ref: '#/components/schemas/BaseWebhook'
+ - type: object
+ required:
+ - data
+ properties:
+ data:
+ $ref: '#/components/schemas/Card'
+ type:
+ type: string
+ enum:
+ - CARD.STATE_CHANGE
+ CardFundingSourceChangeWebhook:
+ allOf:
+ - $ref: '#/components/schemas/BaseWebhook'
+ - type: object
+ required:
+ - data
+ properties:
+ data:
+ $ref: '#/components/schemas/Card'
+ type:
+ type: string
+ enum:
+ - CARD.FUNDING_SOURCE_CHANGE
diff --git a/openapi/components/schemas/cards/Card.yaml b/openapi/components/schemas/cards/Card.yaml
new file mode 100644
index 00000000..8a096a62
--- /dev/null
+++ b/openapi/components/schemas/cards/Card.yaml
@@ -0,0 +1,100 @@
+type: object
+required:
+ - id
+ - cardholderId
+ - state
+ - form
+ - fundingSources
+ - createdAt
+ - updatedAt
+properties:
+ id:
+ type: string
+ description: System-generated unique card identifier
+ readOnly: true
+ example: Card:019542f5-b3e7-1d02-0000-000000000010
+ cardholderId:
+ type: string
+ description: The id of the `Customer` who holds this card.
+ example: Customer:019542f5-b3e7-1d02-0000-000000000001
+ platformCardId:
+ type: string
+ description: >-
+ Platform-specific card identifier. Optional on create — system-generated
+ if omitted, mirroring `platformCustomerId` semantics.
+ example: card-emp-aary-001
+ state:
+ $ref: ./CardState.yaml
+ stateReason:
+ oneOf:
+ - $ref: ./CardStateReason.yaml
+ - type: 'null'
+ description: >-
+ Reason associated with the current `state`. Populated when the card is
+ `CLOSED` or when provisioning was rejected; otherwise null.
+ brand:
+ $ref: ./CardBrand.yaml
+ form:
+ $ref: ./CardForm.yaml
+ last4:
+ type: string
+ description: Last four digits of the card PAN.
+ example: '4242'
+ expMonth:
+ type: integer
+ minimum: 1
+ maximum: 12
+ description: Card expiration month (1–12).
+ example: 12
+ expYear:
+ type: integer
+ description: Card expiration year (four digits).
+ example: 2029
+ panEmbedUrl:
+ type: string
+ format: uri
+ description: >-
+ URL of the card issuer's iframe that securely displays the PAN, CVV,
+ and expiry to the cardholder. The full PAN and CVV never cross Grid's
+ servers — render this URL in an iframe in your client to reveal card
+ details.
+ example: https://embed.lithic.com/iframe/...?t=...
+ fundingSources:
+ type: array
+ description: >-
+ Internal account ids bound to this card as funding sources, in priority
+ order — the first entry is tried first by Authorization Decisioning.
+ Every card has at least one funding source.
+ items:
+ type: string
+ example:
+ - InternalAccount:019542f5-b3e7-1d02-0000-000000000002
+ - InternalAccount:019542f5-b3e7-1d02-0000-000000000003
+ currency:
+ type: string
+ description: >-
+ Currency the card transacts in (ISO 4217 for fiat, tickers for crypto).
+ Derived from the funding sources at issue time — all funding sources
+ bound to a card must be denominated in the same card-eligible currency.
+ example: USD
+ readOnly: true
+ issuerRef:
+ type: string
+ description: >-
+ Opaque identifier for the card on the underlying issuer. Useful for
+ cross-referencing in issuer dashboards; not used for any Grid request
+ routing.
+ example: lithic_card_4f8d3a2b1c
+ readOnly: true
+ createdAt:
+ type: string
+ format: date-time
+ description: Creation timestamp
+ readOnly: true
+ example: '2026-05-08T14:10:00Z'
+ updatedAt:
+ type: string
+ format: date-time
+ description: Last update timestamp
+ readOnly: true
+ example: '2026-05-08T14:11:00Z'
diff --git a/openapi/components/schemas/cards/CardBrand.yaml b/openapi/components/schemas/cards/CardBrand.yaml
new file mode 100644
index 00000000..ee079bab
--- /dev/null
+++ b/openapi/components/schemas/cards/CardBrand.yaml
@@ -0,0 +1,7 @@
+type: string
+enum:
+ - VISA
+ - MASTERCARD
+description: |
+ Card network brand. Read-only — determined by Grid when the card is
+ provisioned with the issuer.
diff --git a/openapi/components/schemas/cards/CardCreateRequest.yaml b/openapi/components/schemas/cards/CardCreateRequest.yaml
new file mode 100644
index 00000000..5cbd262a
--- /dev/null
+++ b/openapi/components/schemas/cards/CardCreateRequest.yaml
@@ -0,0 +1,35 @@
+type: object
+required:
+ - cardholderId
+ - form
+ - fundingSources
+properties:
+ cardholderId:
+ type: string
+ description: >-
+ The id of the `Customer` to issue the card to. The customer must have
+ KYC status `APPROVED`; otherwise the request is rejected with
+ `CARDHOLDER_KYC_NOT_APPROVED`.
+ example: Customer:019542f5-b3e7-1d02-0000-000000000001
+ platformCardId:
+ type: string
+ description: >-
+ Optional platform-specific card identifier. System-generated when
+ omitted, mirroring `platformCustomerId` semantics.
+ example: card-emp-aary-001
+ form:
+ $ref: ./CardForm.yaml
+ fundingSources:
+ type: array
+ description: >-
+ Internal account ids to bind as funding sources, in priority order.
+ The first entry is tried first by Authorization Decisioning. Every
+ card must be bound to at least one source, and every source must
+ belong to the cardholder and be denominated in a card-eligible
+ currency (USDB in v1); otherwise the request is rejected with
+ `FUNDING_SOURCE_INELIGIBLE`.
+ minItems: 1
+ items:
+ type: string
+ example:
+ - InternalAccount:019542f5-b3e7-1d02-0000-000000000002
diff --git a/openapi/components/schemas/cards/CardForm.yaml b/openapi/components/schemas/cards/CardForm.yaml
new file mode 100644
index 00000000..e44f710d
--- /dev/null
+++ b/openapi/components/schemas/cards/CardForm.yaml
@@ -0,0 +1,6 @@
+type: string
+enum:
+ - VIRTUAL
+description: |
+ Physical form factor of the card. Only `VIRTUAL` is supported in v1;
+ `PHYSICAL` will be added in a later release.
diff --git a/openapi/components/schemas/cards/CardListResponse.yaml b/openapi/components/schemas/cards/CardListResponse.yaml
new file mode 100644
index 00000000..f6f9727c
--- /dev/null
+++ b/openapi/components/schemas/cards/CardListResponse.yaml
@@ -0,0 +1,22 @@
+type: object
+required:
+ - data
+ - hasMore
+properties:
+ data:
+ type: array
+ description: List of cards matching the filter criteria
+ items:
+ $ref: ./Card.yaml
+ hasMore:
+ type: boolean
+ description: Indicates if more results are available beyond this page
+ nextCursor:
+ type: string
+ description: >-
+ Cursor to retrieve the next page of results (only present if
+ hasMore is true)
+ totalCount:
+ type: integer
+ description: >-
+ Total number of cards matching the criteria (excluding pagination)
diff --git a/openapi/components/schemas/cards/CardMerchant.yaml b/openapi/components/schemas/cards/CardMerchant.yaml
new file mode 100644
index 00000000..8b01f8b7
--- /dev/null
+++ b/openapi/components/schemas/cards/CardMerchant.yaml
@@ -0,0 +1,20 @@
+type: object
+required:
+ - descriptor
+properties:
+ descriptor:
+ type: string
+ description: >-
+ Merchant descriptor string captured from the card network at
+ authorization time.
+ example: BLUE BOTTLE COFFEE SF
+ mcc:
+ type: string
+ description: >-
+ Merchant Category Code (ISO 18245) — four-digit numeric string.
+ example: '5814'
+ country:
+ type: string
+ description: >-
+ Two-letter ISO 3166-1 alpha-2 country code of the merchant.
+ example: US
diff --git a/openapi/components/schemas/cards/CardPullSummary.yaml b/openapi/components/schemas/cards/CardPullSummary.yaml
new file mode 100644
index 00000000..002926e3
--- /dev/null
+++ b/openapi/components/schemas/cards/CardPullSummary.yaml
@@ -0,0 +1,27 @@
+type: object
+required:
+ - count
+ - totalAmount
+properties:
+ count:
+ type: integer
+ description: >-
+ Total number of pulls (debits) executed against the funding source for
+ this transaction. `> 1` indicates one or more post-hoc pulls — e.g.
+ restaurant tip / over-auth clearings.
+ example: 2
+ totalAmount:
+ type: integer
+ format: int64
+ description: >-
+ Sum of all pull amounts in the smallest unit of the funding source's
+ currency.
+ example: 1500
+ pendingCount:
+ type: integer
+ description: >-
+ Number of pulls still in the `PENDING` state. Drops to zero when every
+ pull has reached a terminal state. Non-zero values that persist beyond
+ the expected settlement window are an early signal for the
+ `EXCEPTION` path.
+ example: 0
diff --git a/openapi/components/schemas/cards/CardRefundSummary.yaml b/openapi/components/schemas/cards/CardRefundSummary.yaml
new file mode 100644
index 00000000..fb0e0916
--- /dev/null
+++ b/openapi/components/schemas/cards/CardRefundSummary.yaml
@@ -0,0 +1,16 @@
+type: object
+required:
+ - count
+ - totalAmount
+properties:
+ count:
+ type: integer
+ description: Number of refund (return) events received for this transaction.
+ example: 0
+ totalAmount:
+ type: integer
+ format: int64
+ description: >-
+ Sum of all refund amounts in the smallest unit of the funding source's
+ currency.
+ example: 0
diff --git a/openapi/components/schemas/cards/CardSettlementSummary.yaml b/openapi/components/schemas/cards/CardSettlementSummary.yaml
new file mode 100644
index 00000000..06cdca1f
--- /dev/null
+++ b/openapi/components/schemas/cards/CardSettlementSummary.yaml
@@ -0,0 +1,17 @@
+type: object
+required:
+ - count
+ - totalAmount
+properties:
+ count:
+ type: integer
+ description: >-
+ Number of settlement (clearing) events received for this transaction.
+ example: 1
+ totalAmount:
+ type: integer
+ format: int64
+ description: >-
+ Sum of all settled amounts in the smallest unit of the funding source's
+ currency.
+ example: 1500
diff --git a/openapi/components/schemas/cards/CardState.yaml b/openapi/components/schemas/cards/CardState.yaml
new file mode 100644
index 00000000..e3b9105a
--- /dev/null
+++ b/openapi/components/schemas/cards/CardState.yaml
@@ -0,0 +1,17 @@
+type: string
+enum:
+ - PENDING_KYC
+ - PENDING_ISSUE
+ - ACTIVE
+ - FROZEN
+ - CLOSED
+description: |
+ Lifecycle state of a card.
+
+ | State | Description |
+ |-------|-------------|
+ | `PENDING_KYC` | The cardholder has not yet completed KYC. Cards in this state cannot transact. |
+ | `PENDING_ISSUE` | The card has been requested and is being provisioned with the issuer. |
+ | `ACTIVE` | The card is live and can authorize transactions. |
+ | `FROZEN` | The card is temporarily disabled by the platform. New authorizations are declined with `CARD_PAUSED`. Existing settlements and refunds continue to reconcile. |
+ | `CLOSED` | The card is permanently closed. Terminal, irreversible state. |
diff --git a/openapi/components/schemas/cards/CardStateReason.yaml b/openapi/components/schemas/cards/CardStateReason.yaml
new file mode 100644
index 00000000..4dc582e6
--- /dev/null
+++ b/openapi/components/schemas/cards/CardStateReason.yaml
@@ -0,0 +1,15 @@
+type: string
+enum:
+ - ISSUER_REJECTED
+ - CLOSED_BY_PLATFORM
+ - CLOSED_BY_GRID
+description: |
+ Reason a card reached a terminal or non-active state. Present on
+ `CLOSED` cards, and on cards that fail provisioning before reaching
+ `ACTIVE`.
+
+ | Reason | Description |
+ |--------|-------------|
+ | `ISSUER_REJECTED` | The card issuer rejected provisioning during `PENDING_ISSUE`. |
+ | `CLOSED_BY_PLATFORM` | The card was closed via `PATCH /cards/{id}` (`state: CLOSED`) by the platform. |
+ | `CLOSED_BY_GRID` | The card was closed by Grid (e.g. compliance or risk action). |
diff --git a/openapi/components/schemas/cards/CardTransaction.yaml b/openapi/components/schemas/cards/CardTransaction.yaml
new file mode 100644
index 00000000..535bad48
--- /dev/null
+++ b/openapi/components/schemas/cards/CardTransaction.yaml
@@ -0,0 +1,84 @@
+type: object
+required:
+ - id
+ - cardId
+ - status
+ - merchant
+ - authorizedAmount
+ - accountId
+ - pullSummary
+ - refundSummary
+ - settlementSummary
+ - authorizedAt
+ - createdAt
+ - updatedAt
+description: >-
+ Parent transaction row for a card authorization and all of the
+ pulls / settlements / refunds that reconcile against it. Child events are
+ rolled up into the `pullSummary`, `refundSummary`, and `settlementSummary`
+ aggregates. Delivered as the payload of the generic transaction
+ webhook stream (extends the Transaction model with a card
+ destination type) on every transition.
+properties:
+ id:
+ type: string
+ description: System-generated unique card transaction identifier
+ readOnly: true
+ example: CardTransaction:019542f5-b3e7-1d02-0000-000000000100
+ cardId:
+ type: string
+ description: The id of the `Card` this transaction was made on.
+ example: Card:019542f5-b3e7-1d02-0000-000000000010
+ issuerTransactionToken:
+ type: string
+ description: >-
+ Opaque identifier for the transaction on the underlying issuer. Used to
+ cross-reference Grid records against issuer dashboards and webhooks.
+ example: lithic_txn_b81c2a4f
+ readOnly: true
+ status:
+ $ref: ./CardTransactionStatus.yaml
+ merchant:
+ $ref: ./CardMerchant.yaml
+ authorizedAmount:
+ $ref: ../common/CurrencyAmount.yaml
+ settledAmount:
+ $ref: ../common/CurrencyAmount.yaml
+ refundedAmount:
+ $ref: ../common/CurrencyAmount.yaml
+ accountId:
+ type: string
+ description: >-
+ Internal account id that funded this transaction (the funding source
+ selected by Authorization Decisioning at auth time).
+ example: InternalAccount:019542f5-b3e7-1d02-0000-000000000002
+ pullSummary:
+ $ref: ./CardPullSummary.yaml
+ refundSummary:
+ $ref: ./CardRefundSummary.yaml
+ settlementSummary:
+ $ref: ./CardSettlementSummary.yaml
+ authorizedAt:
+ type: string
+ format: date-time
+ description: When the auth was approved.
+ example: '2026-05-08T14:30:00Z'
+ lastEventAt:
+ type: string
+ format: date-time
+ description: >-
+ Timestamp of the most recent reconcile event (pull / clearing / refund)
+ against this transaction.
+ example: '2026-05-08T15:42:11Z'
+ createdAt:
+ type: string
+ format: date-time
+ description: Creation timestamp (same as `authorizedAt` for card transactions).
+ readOnly: true
+ example: '2026-05-08T14:30:00Z'
+ updatedAt:
+ type: string
+ format: date-time
+ description: Last update timestamp.
+ readOnly: true
+ example: '2026-05-08T15:42:11Z'
diff --git a/openapi/components/schemas/cards/CardTransactionStatus.yaml b/openapi/components/schemas/cards/CardTransactionStatus.yaml
new file mode 100644
index 00000000..01be9d92
--- /dev/null
+++ b/openapi/components/schemas/cards/CardTransactionStatus.yaml
@@ -0,0 +1,17 @@
+type: string
+enum:
+ - AUTHORIZED
+ - PARTIALLY_SETTLED
+ - SETTLED
+ - REFUNDED
+ - EXCEPTION
+description: |
+ Lifecycle status of a card transaction.
+
+ | Status | Description |
+ |--------|-------------|
+ | `AUTHORIZED` | The auth has been approved and a hold placed on the funding source; no clearing has arrived yet. |
+ | `PARTIALLY_SETTLED` | At least one clearing has arrived and posted, but more clearings are still expected (split shipments, tips, multi-leg trips). |
+ | `SETTLED` | All clearings for the auth have posted and the transaction is closed against the funding source. |
+ | `REFUNDED` | A `RETURN` was received from the merchant; the net settled amount has been refunded in part or whole. |
+ | `EXCEPTION` | The transaction settled to the card network but the corresponding pull from the funding source failed (e.g. balance no longer covers the post-hoc clearing). Surfaces high-urgency alerts and is the dashboard query for stuck reconciliations. |
diff --git a/openapi/components/schemas/cards/CardUpdateRequest.yaml b/openapi/components/schemas/cards/CardUpdateRequest.yaml
new file mode 100644
index 00000000..4664e3ec
--- /dev/null
+++ b/openapi/components/schemas/cards/CardUpdateRequest.yaml
@@ -0,0 +1,38 @@
+type: object
+description: >-
+ Update request for `PATCH /cards/{id}`. At least one of `state` or
+ `fundingSources` must be supplied. `state` transitions are limited to
+ `ACTIVE ⇄ FROZEN` and `ACTIVE | FROZEN → CLOSED`; any other transition
+ returns `409 INVALID_STATE_TRANSITION`. `CLOSED` is terminal and
+ irreversible and cannot be combined with `fundingSources`.
+ `fundingSources`, when supplied, fully replaces the card's bound
+ funding sources — the array order determines the priority Authorization
+ Decisioning tries them in.
+properties:
+ state:
+ type: string
+ enum:
+ - ACTIVE
+ - FROZEN
+ - CLOSED
+ description: >-
+ Target state for the card. Permitted transitions are
+ `ACTIVE ⇄ FROZEN` and `ACTIVE | FROZEN → CLOSED`. `CLOSED` is
+ terminal and irreversible; once closed, the card stays in the
+ system for audit and reconciliation but cannot transact again.
+ example: FROZEN
+ fundingSources:
+ type: array
+ description: >-
+ New ordered list of internal account ids to bind as funding sources.
+ Fully replaces the previous binding. Each id must belong to the
+ cardholder and be denominated in the card's currency. The list must
+ contain at least one source — to stop a card from spending without
+ removing all sources, transition it to `FROZEN` instead. Cannot be
+ supplied alongside `state: CLOSED`.
+ minItems: 1
+ items:
+ type: string
+ example:
+ - InternalAccount:019542f5-b3e7-1d02-0000-000000000002
+ - InternalAccount:019542f5-b3e7-1d02-0000-000000000003
diff --git a/openapi/components/schemas/cards/SandboxCardAuthorizationRequest.yaml b/openapi/components/schemas/cards/SandboxCardAuthorizationRequest.yaml
new file mode 100644
index 00000000..d7e24917
--- /dev/null
+++ b/openapi/components/schemas/cards/SandboxCardAuthorizationRequest.yaml
@@ -0,0 +1,24 @@
+type: object
+required:
+ - amount
+ - currency
+ - merchant
+description: >-
+ Sandbox-only request body for `POST /sandbox/cards/{id}/simulate/authorization`.
+ Drives the same internal authorization + reconcile paths that the issuer
+ would call in production. The decisioning outcome is controlled by the
+ last three characters of `merchant.descriptor` — see the endpoint
+ documentation for the suffix table.
+properties:
+ amount:
+ type: integer
+ format: int64
+ description: >-
+ Authorization amount in the smallest unit of `currency` (e.g. cents
+ for USD).
+ exclusiveMinimum: 0
+ example: 1250
+ currency:
+ $ref: ../common/Currency.yaml
+ merchant:
+ $ref: ./CardMerchant.yaml
diff --git a/openapi/components/schemas/cards/SandboxCardClearingRequest.yaml b/openapi/components/schemas/cards/SandboxCardClearingRequest.yaml
new file mode 100644
index 00000000..9c5b5804
--- /dev/null
+++ b/openapi/components/schemas/cards/SandboxCardClearingRequest.yaml
@@ -0,0 +1,26 @@
+type: object
+required:
+ - cardTransactionId
+ - amount
+description: >-
+ Sandbox-only request body for `POST /sandbox/cards/{id}/simulate/clearing`.
+ Drives a clearing event against an existing `CardTransaction`. Pass an
+ `amount` greater than the authorized amount to exercise the over-auth /
+ restaurant-tip post-hoc-pull path; pass `0` to exercise
+ `AUTHORIZATION_EXPIRY`. Suffix-driven outcomes on the parent transaction's
+ id govern whether the post-hoc pull succeeds.
+properties:
+ cardTransactionId:
+ type: string
+ description: >-
+ The id of the `CardTransaction` to clear against. Must be in
+ `AUTHORIZED` or `PARTIALLY_SETTLED` state.
+ example: CardTransaction:019542f5-b3e7-1d02-0000-000000000100
+ amount:
+ type: integer
+ format: int64
+ description: >-
+ Clearing amount in the smallest unit of the transaction's currency.
+ Set to `0` to simulate an authorization expiry with no clearing.
+ minimum: 0
+ example: 1500
diff --git a/openapi/components/schemas/cards/SandboxCardReturnRequest.yaml b/openapi/components/schemas/cards/SandboxCardReturnRequest.yaml
new file mode 100644
index 00000000..e64a9979
--- /dev/null
+++ b/openapi/components/schemas/cards/SandboxCardReturnRequest.yaml
@@ -0,0 +1,25 @@
+type: object
+required:
+ - cardTransactionId
+ - amount
+description: >-
+ Sandbox-only request body for `POST /sandbox/cards/{id}/simulate/return`.
+ Drives a `RETURN` event against an existing settled `CardTransaction`,
+ which creates a `CardRefund` and pushes the parent transaction towards
+ `REFUNDED` (full) or keeps it `SETTLED` (partial).
+properties:
+ cardTransactionId:
+ type: string
+ description: >-
+ The id of the `CardTransaction` to refund against. Must have at least
+ one settled clearing.
+ example: CardTransaction:019542f5-b3e7-1d02-0000-000000000100
+ amount:
+ type: integer
+ format: int64
+ description: >-
+ Return amount in the smallest unit of the transaction's currency. Must
+ be less than or equal to the net settled amount (settled minus
+ previously-refunded).
+ exclusiveMinimum: 0
+ example: 1500
diff --git a/openapi/components/schemas/webhooks/CardFundingSourceChangeWebhook.yaml b/openapi/components/schemas/webhooks/CardFundingSourceChangeWebhook.yaml
new file mode 100644
index 00000000..a2d6da26
--- /dev/null
+++ b/openapi/components/schemas/webhooks/CardFundingSourceChangeWebhook.yaml
@@ -0,0 +1,12 @@
+allOf:
+ - $ref: ./BaseWebhook.yaml
+ - type: object
+ required:
+ - data
+ properties:
+ data:
+ $ref: ../cards/Card.yaml
+ type:
+ type: string
+ enum:
+ - CARD.FUNDING_SOURCE_CHANGE
diff --git a/openapi/components/schemas/webhooks/CardStateChangeWebhook.yaml b/openapi/components/schemas/webhooks/CardStateChangeWebhook.yaml
new file mode 100644
index 00000000..d809de79
--- /dev/null
+++ b/openapi/components/schemas/webhooks/CardStateChangeWebhook.yaml
@@ -0,0 +1,12 @@
+allOf:
+ - $ref: ./BaseWebhook.yaml
+ - type: object
+ required:
+ - data
+ properties:
+ data:
+ $ref: ../cards/Card.yaml
+ type:
+ type: string
+ enum:
+ - CARD.STATE_CHANGE
diff --git a/openapi/components/schemas/webhooks/WebhookType.yaml b/openapi/components/schemas/webhooks/WebhookType.yaml
index 5c96ac96..c587cc6b 100644
--- a/openapi/components/schemas/webhooks/WebhookType.yaml
+++ b/openapi/components/schemas/webhooks/WebhookType.yaml
@@ -29,6 +29,8 @@ enum:
- BULK_UPLOAD.COMPLETED
- BULK_UPLOAD.FAILED
- AGENT_ACTION.PENDING_APPROVAL
+ - CARD.STATE_CHANGE
+ - CARD.FUNDING_SOURCE_CHANGE
- TEST
description: >-
Type of webhook event in OBJECT.EVENT dot-notation. The part before the dot
diff --git a/openapi/openapi.yaml b/openapi/openapi.yaml
index 19b26f8d..125ec249 100644
--- a/openapi/openapi.yaml
+++ b/openapi/openapi.yaml
@@ -71,6 +71,11 @@ tags:
policy. When an action requires approval, the resulting transaction enters a pending
state and must be approved by the platform via
`POST /transactions/{transactionId}/approve`.
+ - name: Cards
+ description: >-
+ Card management endpoints. Issue debit cards against an internal
+ account, freeze / unfreeze, close, manage card funding sources, and
+ list card transactions.
servers:
- url: https://api.lightspark.com/grid/2025-10-13
description: Production server
@@ -262,6 +267,16 @@ paths:
$ref: paths/agents/agents_me_external-accounts.yaml
/agents/me/external-accounts/{externalAccountId}:
$ref: paths/agents/agents_me_external-accounts_{externalAccountId}.yaml
+ /cards:
+ $ref: paths/cards/cards.yaml
+ /cards/{id}:
+ $ref: paths/cards/cards_{id}.yaml
+ /sandbox/cards/{id}/simulate/authorization:
+ $ref: paths/sandbox/cards/sandbox_cards_{id}_simulate_authorization.yaml
+ /sandbox/cards/{id}/simulate/clearing:
+ $ref: paths/sandbox/cards/sandbox_cards_{id}_simulate_clearing.yaml
+ /sandbox/cards/{id}/simulate/return:
+ $ref: paths/sandbox/cards/sandbox_cards_{id}_simulate_return.yaml
webhooks:
agent-action:
$ref: webhooks/agent-action.yaml
@@ -281,6 +296,10 @@ webhooks:
$ref: webhooks/internal-account-status.yaml
verification-update:
$ref: webhooks/verification-update.yaml
+ card-state-change:
+ $ref: webhooks/card-state-change.yaml
+ card-funding-source-change:
+ $ref: webhooks/card-funding-source-change.yaml
security:
- BasicAuth: []
- AgentAuth: []
diff --git a/openapi/paths/cards/cards.yaml b/openapi/paths/cards/cards.yaml
new file mode 100644
index 00000000..5b9dc461
--- /dev/null
+++ b/openapi/paths/cards/cards.yaml
@@ -0,0 +1,152 @@
+post:
+ summary: Issue a card
+ description: >
+ Issue a new card for a cardholder. Every card must be bound to at least
+ one funding source at create time. The cardholder must have KYC status
+ `APPROVED` before a card can be issued; otherwise the request is rejected
+ with `CARDHOLDER_KYC_NOT_APPROVED`.
+
+
+ New cards start in `state: "PENDING_ISSUE"` while the card issuer
+ provisions the card. The `card.state_change` webhook fires on the
+ transition to `ACTIVE` (or to `CLOSED` with `stateReason:
+ "ISSUER_REJECTED"` if provisioning fails).
+ operationId: createCard
+ tags:
+ - Cards
+ security:
+ - BasicAuth: []
+ requestBody:
+ required: true
+ content:
+ application/json:
+ schema:
+ $ref: ../../components/schemas/cards/CardCreateRequest.yaml
+ examples:
+ virtualCard:
+ summary: Issue a virtual card with one funding source
+ value:
+ cardholderId: Customer:019542f5-b3e7-1d02-0000-000000000001
+ platformCardId: card-emp-aary-001
+ form: VIRTUAL
+ fundingSources:
+ - InternalAccount:019542f5-b3e7-1d02-0000-000000000002
+ responses:
+ '201':
+ description: Card issued successfully
+ content:
+ application/json:
+ schema:
+ $ref: ../../components/schemas/cards/Card.yaml
+ '400':
+ description: >-
+ Bad request. Returned with `CARDHOLDER_KYC_NOT_APPROVED` when the
+ cardholder's KYC status is not `APPROVED`, with
+ `FUNDING_SOURCE_INELIGIBLE` when the supplied funding source does
+ not belong to the cardholder or is not denominated in a
+ card-eligible currency, and for general invalid parameters.
+ content:
+ application/json:
+ schema:
+ $ref: ../../components/schemas/errors/Error400.yaml
+ '401':
+ description: Unauthorized
+ content:
+ application/json:
+ schema:
+ $ref: ../../components/schemas/errors/Error401.yaml
+ '500':
+ description: Internal service error
+ content:
+ application/json:
+ schema:
+ $ref: ../../components/schemas/errors/Error500.yaml
+get:
+ summary: List cards
+ description: >
+ Retrieve a paginated list of cards. Cards can be filtered by cardholder,
+ bound funding-source internal account, state, and platform-specific
+ card identifier. If no filters are provided, returns all cards visible
+ to the caller.
+ operationId: listCards
+ tags:
+ - Cards
+ security:
+ - BasicAuth: []
+ parameters:
+ - name: cardholderId
+ in: query
+ description: Filter by cardholder (customer) id.
+ required: false
+ schema:
+ type: string
+ - name: accountId
+ in: query
+ description: >-
+ Filter by internal account id. Returns cards whose `fundingSources`
+ array contains the given internal account id.
+ required: false
+ schema:
+ type: string
+ - name: platformCardId
+ in: query
+ description: Filter by platform-specific card identifier.
+ required: false
+ schema:
+ type: string
+ - name: state
+ in: query
+ description: Filter by card state.
+ required: false
+ schema:
+ $ref: ../../components/schemas/cards/CardState.yaml
+ - name: limit
+ in: query
+ description: Maximum number of results to return (default 20, max 100)
+ required: false
+ schema:
+ type: integer
+ minimum: 1
+ maximum: 100
+ default: 20
+ - name: cursor
+ in: query
+ description: Cursor for pagination (returned from previous request)
+ required: false
+ schema:
+ type: string
+ - name: sortOrder
+ in: query
+ description: Order to sort results in
+ required: false
+ schema:
+ type: string
+ enum:
+ - asc
+ - desc
+ default: desc
+ responses:
+ '200':
+ description: Successful operation
+ content:
+ application/json:
+ schema:
+ $ref: ../../components/schemas/cards/CardListResponse.yaml
+ '400':
+ description: Bad request - Invalid parameters
+ content:
+ application/json:
+ schema:
+ $ref: ../../components/schemas/errors/Error400.yaml
+ '401':
+ description: Unauthorized
+ content:
+ application/json:
+ schema:
+ $ref: ../../components/schemas/errors/Error401.yaml
+ '500':
+ description: Internal service error
+ content:
+ application/json:
+ schema:
+ $ref: ../../components/schemas/errors/Error500.yaml
diff --git a/openapi/paths/cards/cards_{id}.yaml b/openapi/paths/cards/cards_{id}.yaml
new file mode 100644
index 00000000..b117a28c
--- /dev/null
+++ b/openapi/paths/cards/cards_{id}.yaml
@@ -0,0 +1,224 @@
+parameters:
+ - name: id
+ in: path
+ description: System-generated unique card identifier
+ required: true
+ schema:
+ type: string
+get:
+ summary: Get a card
+ description: Retrieve a card by its system-generated id.
+ operationId: getCardById
+ tags:
+ - Cards
+ security:
+ - BasicAuth: []
+ responses:
+ '200':
+ description: Successful operation
+ content:
+ application/json:
+ schema:
+ $ref: ../../components/schemas/cards/Card.yaml
+ '401':
+ description: Unauthorized
+ content:
+ application/json:
+ schema:
+ $ref: ../../components/schemas/errors/Error401.yaml
+ '404':
+ description: Card not found
+ content:
+ application/json:
+ schema:
+ $ref: ../../components/schemas/errors/Error404.yaml
+ '500':
+ description: Internal service error
+ content:
+ application/json:
+ schema:
+ $ref: ../../components/schemas/errors/Error500.yaml
+patch:
+ summary: Update a card
+ description: >
+ Update a card's `state` and / or its bound `fundingSources`. At least
+ one of the two fields must be supplied.
+
+
+ - `state` transitions are limited to `ACTIVE ⇄ FROZEN` and
+ `ACTIVE | FROZEN → CLOSED`. `CLOSED` is terminal and irreversible.
+ Any other transition returns `409 INVALID_STATE_TRANSITION`.
+
+ - `fundingSources`, when supplied, fully replaces the card's bound
+ funding sources. Array order determines the priority Authorization
+ Decisioning tries them in. Each id must belong to the cardholder and
+ be denominated in the card's currency; the list must contain at least
+ one source. `fundingSources` cannot be supplied alongside
+ `state: CLOSED`.
+
+
+ Because both updates are sensitive state changes, this endpoint uses
+ Grid's 202 → signed-retry pattern (same shape as
+ `DELETE /auth/credentials/{id}` and `POST /internal-accounts/{id}/export`):
+
+
+ 1. Call `PATCH /cards/{id}` with the target fields and no signing
+ headers. The response is `202` with a `payloadToSign`, `requestId`, and
+ `expiresAt`.
+
+
+ 2. Sign the `payloadToSign` with the session private key of a verified
+ authentication credential on the card's owning internal account and
+ retry with the signature as the `Grid-Wallet-Signature` header and the
+ `requestId` echoed back as the `Request-Id` header. The signed retry
+ returns `200` with the updated `Card`.
+
+
+ Effects:
+
+ - `state: FROZEN`: Authorization Decisioning declines new auths with
+ `CARD_PAUSED`. Existing pulls and in-flight reconciliation continue —
+ freezing does not pause the lifecycle of authorizations that already
+ passed.
+
+ - `state: ACTIVE`: normal authorization behavior resumes.
+
+ - `state: CLOSED`: terminal close. The card transitions to
+ `state: "CLOSED"` with `stateReason: "CLOSED_BY_PLATFORM"` and stays
+ in the system for audit and reconciliation. All pending auths
+ reconcile to a terminal state via the existing reconcile primitive.
+ Inbound clearings received after close follow the standard
+ force-post / late-presentment path — Lightspark absorbs the loss if
+ a post-hoc pull on the now-unbound source fails. Funding-source
+ bindings are detached. Refunds already in flight still complete
+ because Lightspark holds the card-reserve keys.
+
+ - `fundingSources` change: emits `card.funding_source_change` reflecting
+ the new ordered binding.
+
+
+ The `card.state_change` webhook fires on every successful `state`
+ transition; the `card.funding_source_change` webhook fires whenever
+ `fundingSources` is updated.
+ operationId: updateCardById
+ tags:
+ - Cards
+ security:
+ - BasicAuth: []
+ parameters:
+ - name: Grid-Wallet-Signature
+ in: header
+ required: false
+ description: >-
+ Signature over the `payloadToSign` returned in a prior `202`
+ response, produced with the session private key of a verified
+ authentication credential on the card's owning internal account and
+ base64-encoded. Required on the signed retry; ignored on the initial
+ call.
+ schema:
+ type: string
+ example: MEUCIQDx7k2N0aK4p8f3vR9J6yT5wL1mB0sXnG2hQ4vJ8zYkCgIgZ4rP9dT7eWfU3oM6KjR1qSpNvBwL0tXyA2iG8fH5dE=
+ - name: Request-Id
+ in: header
+ required: false
+ description: >-
+ The `requestId` returned in a prior `202` response, echoed back on
+ the signed retry so the server can correlate it with the issued
+ challenge. Required on the signed retry; must be paired with
+ `Grid-Wallet-Signature`.
+ schema:
+ type: string
+ example: 7c4a8d09-ca37-4e3e-9e0d-8c2b3e9a1f21
+ requestBody:
+ required: true
+ content:
+ application/json:
+ schema:
+ $ref: ../../components/schemas/cards/CardUpdateRequest.yaml
+ examples:
+ freeze:
+ summary: Freeze an active card
+ value:
+ state: FROZEN
+ unfreeze:
+ summary: Unfreeze a frozen card
+ value:
+ state: ACTIVE
+ updateFundingSources:
+ summary: Replace the card's bound funding sources
+ value:
+ fundingSources:
+ - InternalAccount:019542f5-b3e7-1d02-0000-000000000002
+ - InternalAccount:019542f5-b3e7-1d02-0000-000000000003
+ freezeAndUpdateSources:
+ summary: Freeze the card and replace its funding sources in one call
+ value:
+ state: FROZEN
+ fundingSources:
+ - InternalAccount:019542f5-b3e7-1d02-0000-000000000002
+ close:
+ summary: Permanently close the card
+ value:
+ state: CLOSED
+ responses:
+ '200':
+ description: Signed retry accepted. Returns the updated card.
+ content:
+ application/json:
+ schema:
+ $ref: ../../components/schemas/cards/Card.yaml
+ '202':
+ description: >-
+ Challenge issued. The response contains a `payloadToSign` that must
+ be signed with the session private key of a verified authentication
+ credential on the card's owning internal account, along with a
+ `requestId` that must be echoed back on the retry.
+ content:
+ application/json:
+ schema:
+ $ref: ../../components/schemas/common/SignedRequestChallenge.yaml
+ '400':
+ description: >-
+ Bad request. Returned with `FUNDING_SOURCE_INELIGIBLE` when a
+ supplied funding source does not belong to the cardholder or is
+ not denominated in the card's currency, and for general invalid
+ parameters.
+ content:
+ application/json:
+ schema:
+ $ref: ../../components/schemas/errors/Error400.yaml
+ '401':
+ description: >-
+ Unauthorized. Returned when the provided `Grid-Wallet-Signature` is
+ missing, malformed, or does not match a pending update challenge for
+ this card, or when the `Request-Id` does not match an unexpired
+ pending challenge.
+ content:
+ application/json:
+ schema:
+ $ref: ../../components/schemas/errors/Error401.yaml
+ '404':
+ description: Card not found
+ content:
+ application/json:
+ schema:
+ $ref: ../../components/schemas/errors/Error404.yaml
+ '409':
+ description: >-
+ Conflict. Returned with `INVALID_STATE_TRANSITION` when the
+ requested `state` transition is not one of
+ `ACTIVE ⇄ FROZEN` or `ACTIVE | FROZEN → CLOSED` (e.g. trying
+ to un-freeze a `CLOSED` card); with `CARD_ALREADY_CLOSED` when
+ `state: CLOSED` is requested for a card that is already
+ `CLOSED`; and with `CARD_NOT_MUTABLE` when the card is
+ `CLOSED`.
+ content:
+ application/json:
+ schema:
+ $ref: ../../components/schemas/errors/Error409.yaml
+ '500':
+ description: Internal service error
+ content:
+ application/json:
+ schema:
+ $ref: ../../components/schemas/errors/Error500.yaml
diff --git a/openapi/paths/sandbox/cards/sandbox_cards_{id}_simulate_authorization.yaml b/openapi/paths/sandbox/cards/sandbox_cards_{id}_simulate_authorization.yaml
new file mode 100644
index 00000000..3a36d533
--- /dev/null
+++ b/openapi/paths/sandbox/cards/sandbox_cards_{id}_simulate_authorization.yaml
@@ -0,0 +1,101 @@
+post:
+ summary: Simulate a card authorization
+ description: >
+ Simulate an inbound card authorization in the sandbox environment.
+ Drives the same internal `authorize` + `reconcile` paths the card
+ issuer would call in production, so platforms can exercise Grid's
+ decisioning + funding-source pull behavior end-to-end without an
+ external network round-trip.
+
+
+ The decisioning outcome is controlled by the last three characters of
+ `merchant.descriptor`:
+
+
+ | Suffix | Outcome |
+ | ------ | ------- |
+ | `002` | Decline — `INSUFFICIENT_FUNDS` (the pull on the funding source fails) |
+ | `003` | Decline — `CARD_PAUSED` (intended to verify a frozen card refuses auths) |
+ | `005` | Delayed pull (~30s) — exercises the `PENDING → CONFIRMED` path |
+ | `006` | Pull succeeds but the confirmation event reports `FAILED` — exercises the high-urgency `EXCEPTION` alert |
+ | any other | Approved |
+
+
+ Production returns `404` on this path.
+ operationId: sandboxSimulateCardAuthorization
+ tags:
+ - Sandbox
+ security:
+ - BasicAuth: []
+ parameters:
+ - name: id
+ in: path
+ required: true
+ description: The id of the card to simulate an authorization against.
+ schema:
+ type: string
+ example: Card:019542f5-b3e7-1d02-0000-000000000010
+ requestBody:
+ required: true
+ content:
+ application/json:
+ schema:
+ $ref: ../../../components/schemas/cards/SandboxCardAuthorizationRequest.yaml
+ examples:
+ coffeeAuth:
+ summary: Approved $12.50 auth at a coffee shop
+ value:
+ amount: 1250
+ currency:
+ code: USD
+ merchant:
+ descriptor: BLUE BOTTLE COFFEE SF
+ mcc: '5814'
+ country: US
+ declinedInsufficientFunds:
+ summary: Declined — insufficient funds (descriptor suffix `002`)
+ value:
+ amount: 50000
+ currency:
+ code: USD
+ merchant:
+ descriptor: AMAZON RETAIL US-002
+ mcc: '5942'
+ country: US
+ responses:
+ '200':
+ description: Simulated authorization processed. Returns the resulting card transaction.
+ content:
+ application/json:
+ schema:
+ $ref: ../../../components/schemas/cards/CardTransaction.yaml
+ '400':
+ description: Bad request - Invalid parameters
+ content:
+ application/json:
+ schema:
+ $ref: ../../../components/schemas/errors/Error400.yaml
+ '401':
+ description: Unauthorized
+ content:
+ application/json:
+ schema:
+ $ref: ../../../components/schemas/errors/Error401.yaml
+ '403':
+ description: Forbidden - request was made with a production platform token
+ content:
+ application/json:
+ schema:
+ $ref: ../../../components/schemas/errors/Error403.yaml
+ '404':
+ description: Card not found (also returned in production for this path)
+ content:
+ application/json:
+ schema:
+ $ref: ../../../components/schemas/errors/Error404.yaml
+ '500':
+ description: Internal service error
+ content:
+ application/json:
+ schema:
+ $ref: ../../../components/schemas/errors/Error500.yaml
diff --git a/openapi/paths/sandbox/cards/sandbox_cards_{id}_simulate_clearing.yaml b/openapi/paths/sandbox/cards/sandbox_cards_{id}_simulate_clearing.yaml
new file mode 100644
index 00000000..501fcbf7
--- /dev/null
+++ b/openapi/paths/sandbox/cards/sandbox_cards_{id}_simulate_clearing.yaml
@@ -0,0 +1,87 @@
+post:
+ summary: Simulate a card clearing
+ description: >
+ Simulate a clearing (settlement) event against an existing
+ `CardTransaction` in the sandbox environment.
+
+
+ - A clearing `amount` greater than the authorized amount exercises the
+ over-auth post-hoc-pull path (e.g. restaurant tip on top of a 20%
+ over-auth).
+
+ - A clearing `amount` of `0` exercises the `AUTHORIZATION_EXPIRY`
+ path — the auth expires with no clearing posted.
+
+ - Suffix-driven outcomes on the parent transaction's id govern whether
+ the post-hoc pull succeeds (use the suffix table from
+ `simulate/authorization` to construct deterministic test cases).
+
+
+ Production returns `404` on this path.
+ operationId: sandboxSimulateCardClearing
+ tags:
+ - Sandbox
+ security:
+ - BasicAuth: []
+ parameters:
+ - name: id
+ in: path
+ required: true
+ description: The id of the card the clearing applies to.
+ schema:
+ type: string
+ example: Card:019542f5-b3e7-1d02-0000-000000000010
+ requestBody:
+ required: true
+ content:
+ application/json:
+ schema:
+ $ref: ../../../components/schemas/cards/SandboxCardClearingRequest.yaml
+ examples:
+ tipOnTopClearing:
+ summary: Clearing larger than auth — exercises post-hoc pull
+ value:
+ cardTransactionId: CardTransaction:019542f5-b3e7-1d02-0000-000000000100
+ amount: 1500
+ authorizationExpiry:
+ summary: Clearing of 0 — exercises authorization expiry
+ value:
+ cardTransactionId: CardTransaction:019542f5-b3e7-1d02-0000-000000000100
+ amount: 0
+ responses:
+ '200':
+ description: Simulated clearing processed. Returns the updated card transaction.
+ content:
+ application/json:
+ schema:
+ $ref: ../../../components/schemas/cards/CardTransaction.yaml
+ '400':
+ description: Bad request - Invalid parameters
+ content:
+ application/json:
+ schema:
+ $ref: ../../../components/schemas/errors/Error400.yaml
+ '401':
+ description: Unauthorized
+ content:
+ application/json:
+ schema:
+ $ref: ../../../components/schemas/errors/Error401.yaml
+ '403':
+ description: Forbidden - request was made with a production platform token
+ content:
+ application/json:
+ schema:
+ $ref: ../../../components/schemas/errors/Error403.yaml
+ '404':
+ description: Card or card transaction not found
+ content:
+ application/json:
+ schema:
+ $ref: ../../../components/schemas/errors/Error404.yaml
+ '500':
+ description: Internal service error
+ content:
+ application/json:
+ schema:
+ $ref: ../../../components/schemas/errors/Error500.yaml
diff --git a/openapi/paths/sandbox/cards/sandbox_cards_{id}_simulate_return.yaml b/openapi/paths/sandbox/cards/sandbox_cards_{id}_simulate_return.yaml
new file mode 100644
index 00000000..6cf193c8
--- /dev/null
+++ b/openapi/paths/sandbox/cards/sandbox_cards_{id}_simulate_return.yaml
@@ -0,0 +1,72 @@
+post:
+ summary: Simulate a card return
+ description: >
+ Simulate a merchant-initiated `RETURN` against an existing settled
+ card transaction in the sandbox environment. Creates a `CardRefund` on
+ the parent and either flips the parent to `REFUNDED` (full refund) or
+ keeps it `SETTLED` with a non-zero `refundedAmount` (partial refund).
+
+
+ Production returns `404` on this path.
+ operationId: sandboxSimulateCardReturn
+ tags:
+ - Sandbox
+ security:
+ - BasicAuth: []
+ parameters:
+ - name: id
+ in: path
+ required: true
+ description: The id of the card the return applies to.
+ schema:
+ type: string
+ example: Card:019542f5-b3e7-1d02-0000-000000000010
+ requestBody:
+ required: true
+ content:
+ application/json:
+ schema:
+ $ref: ../../../components/schemas/cards/SandboxCardReturnRequest.yaml
+ examples:
+ fullRefund:
+ summary: Full refund of a $15.00 settled transaction
+ value:
+ cardTransactionId: CardTransaction:019542f5-b3e7-1d02-0000-000000000100
+ amount: 1500
+ responses:
+ '200':
+ description: Simulated return processed. Returns the updated card transaction.
+ content:
+ application/json:
+ schema:
+ $ref: ../../../components/schemas/cards/CardTransaction.yaml
+ '400':
+ description: Bad request - Invalid parameters
+ content:
+ application/json:
+ schema:
+ $ref: ../../../components/schemas/errors/Error400.yaml
+ '401':
+ description: Unauthorized
+ content:
+ application/json:
+ schema:
+ $ref: ../../../components/schemas/errors/Error401.yaml
+ '403':
+ description: Forbidden - request was made with a production platform token
+ content:
+ application/json:
+ schema:
+ $ref: ../../../components/schemas/errors/Error403.yaml
+ '404':
+ description: Card or card transaction not found
+ content:
+ application/json:
+ schema:
+ $ref: ../../../components/schemas/errors/Error404.yaml
+ '500':
+ description: Internal service error
+ content:
+ application/json:
+ schema:
+ $ref: ../../../components/schemas/errors/Error500.yaml
diff --git a/openapi/webhooks/card-funding-source-change.yaml b/openapi/webhooks/card-funding-source-change.yaml
new file mode 100644
index 00000000..ff917421
--- /dev/null
+++ b/openapi/webhooks/card-funding-source-change.yaml
@@ -0,0 +1,87 @@
+post:
+ summary: Card funding source change
+ description: >
+ Webhook that is called when the funding sources bound to a card change.
+ Fires whenever `PATCH /cards/{id}` updates the `fundingSources` array.
+ The payload carries the full `Card` resource with the post-change
+ `fundingSources` array.
+
+
+ This endpoint should be implemented by clients of the Grid API.
+
+
+ ### Authentication
+
+
+ The webhook includes a signature in the `X-Grid-Signature` header that
+ allows you to verify that the webhook was sent by Grid.
+
+ To verify the signature:
+
+ 1. Get the Grid public key provided to you during integration
+
+ 2. Decode the base64 signature from the header
+
+ 3. Create a SHA-256 hash of the request body
+
+ 4. Verify the signature using the public key and the hash
+
+
+ If the signature verification succeeds, the webhook is authentic. If not, it
+ should be rejected.
+ operationId: cardFundingSourceChangeWebhook
+ tags:
+ - Webhooks
+ security:
+ - WebhookSignature: []
+ requestBody:
+ required: true
+ content:
+ application/json:
+ schema:
+ $ref: ../components/schemas/webhooks/CardFundingSourceChangeWebhook.yaml
+ examples:
+ fundingSourcesReplaced:
+ summary: Funding sources replaced via PATCH /cards/{id}
+ value:
+ id: Webhook:019542f5-b3e7-1d02-0000-000000000030
+ type: CARD.FUNDING_SOURCE_CHANGE
+ timestamp: '2026-05-08T14:30:00Z'
+ data:
+ id: Card:019542f5-b3e7-1d02-0000-000000000010
+ cardholderId: Customer:019542f5-b3e7-1d02-0000-000000000001
+ state: ACTIVE
+ stateReason: null
+ brand: VISA
+ form: VIRTUAL
+ last4: '4242'
+ expMonth: 12
+ expYear: 2029
+ fundingSources:
+ - InternalAccount:019542f5-b3e7-1d02-0000-000000000002
+ - InternalAccount:019542f5-b3e7-1d02-0000-000000000003
+ currency: USD
+ createdAt: '2026-05-08T14:10:00Z'
+ updatedAt: '2026-05-08T14:30:00Z'
+ responses:
+ '200':
+ description: >
+ Webhook received successfully
+ '400':
+ description: Bad request
+ content:
+ application/json:
+ schema:
+ $ref: ../components/schemas/errors/Error400.yaml
+ '401':
+ description: Unauthorized - Signature validation failed
+ content:
+ application/json:
+ schema:
+ $ref: ../components/schemas/errors/Error401.yaml
+ '409':
+ description: Conflict - Webhook has already been processed (duplicate id)
+ content:
+ application/json:
+ schema:
+ $ref: ../components/schemas/errors/Error409.yaml
diff --git a/openapi/webhooks/card-state-change.yaml b/openapi/webhooks/card-state-change.yaml
new file mode 100644
index 00000000..76752d34
--- /dev/null
+++ b/openapi/webhooks/card-state-change.yaml
@@ -0,0 +1,127 @@
+post:
+ summary: Card state change
+ description: >
+ Webhook that is called when a card's lifecycle state changes. Fires on
+ `PENDING_ISSUE → ACTIVE`, on `PENDING_ISSUE → CLOSED (ISSUER_REJECTED)`
+ when issuer provisioning fails, and on every subsequent
+ `ACTIVE ⇄ FROZEN` and `→ CLOSED` transition.
+
+
+ This endpoint should be implemented by clients of the Grid API.
+
+
+ ### Authentication
+
+
+ The webhook includes a signature in the `X-Grid-Signature` header that
+ allows you to verify that the webhook was sent by Grid.
+
+ To verify the signature:
+
+ 1. Get the Grid public key provided to you during integration
+
+ 2. Decode the base64 signature from the header
+
+ 3. Create a SHA-256 hash of the request body
+
+ 4. Verify the signature using the public key and the hash
+
+
+ If the signature verification succeeds, the webhook is authentic. If not, it
+ should be rejected.
+ operationId: cardStateChangeWebhook
+ tags:
+ - Webhooks
+ security:
+ - WebhookSignature: []
+ requestBody:
+ required: true
+ content:
+ application/json:
+ schema:
+ $ref: ../components/schemas/webhooks/CardStateChangeWebhook.yaml
+ examples:
+ activated:
+ summary: Card transitioned from PENDING_ISSUE to ACTIVE
+ value:
+ id: Webhook:019542f5-b3e7-1d02-0000-000000000020
+ type: CARD.STATE_CHANGE
+ timestamp: '2026-05-08T14:11:00Z'
+ data:
+ id: Card:019542f5-b3e7-1d02-0000-000000000010
+ cardholderId: Customer:019542f5-b3e7-1d02-0000-000000000001
+ platformCardId: card-emp-aary-001
+ state: ACTIVE
+ stateReason: null
+ brand: VISA
+ form: VIRTUAL
+ last4: '4242'
+ expMonth: 12
+ expYear: 2029
+ panEmbedUrl: https://embed.lithic.com/iframe/...?t=...
+ fundingSources:
+ - InternalAccount:019542f5-b3e7-1d02-0000-000000000002
+ currency: USD
+ issuerRef: lithic_card_4f8d3a2b1c
+ createdAt: '2026-05-08T14:10:00Z'
+ updatedAt: '2026-05-08T14:11:00Z'
+ issuerRejected:
+ summary: Card rejected by issuer during provisioning
+ value:
+ id: Webhook:019542f5-b3e7-1d02-0000-000000000021
+ type: CARD.STATE_CHANGE
+ timestamp: '2026-05-08T14:12:00Z'
+ data:
+ id: Card:019542f5-b3e7-1d02-0000-000000000011
+ cardholderId: Customer:019542f5-b3e7-1d02-0000-000000000001
+ state: CLOSED
+ stateReason: ISSUER_REJECTED
+ form: VIRTUAL
+ fundingSources:
+ - InternalAccount:019542f5-b3e7-1d02-0000-000000000002
+ currency: USD
+ createdAt: '2026-05-08T14:10:00Z'
+ updatedAt: '2026-05-08T14:12:00Z'
+ 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
+ fundingSources:
+ - InternalAccount:019542f5-b3e7-1d02-0000-000000000002
+ currency: USD
+ createdAt: '2026-05-08T14:10:00Z'
+ updatedAt: '2026-05-09T09:00:00Z'
+ responses:
+ '200':
+ description: >
+ Webhook received successfully
+ '400':
+ description: Bad request
+ content:
+ application/json:
+ schema:
+ $ref: ../components/schemas/errors/Error400.yaml
+ '401':
+ description: Unauthorized - Signature validation failed
+ content:
+ application/json:
+ schema:
+ $ref: ../components/schemas/errors/Error401.yaml
+ '409':
+ description: Conflict - Webhook has already been processed (duplicate id)
+ content:
+ application/json:
+ schema:
+ $ref: ../components/schemas/errors/Error409.yaml