diff --git a/.redocly.lint-ignore.yaml b/.redocly.lint-ignore.yaml index 9652a98a5..82f71dea8 100644 --- a/.redocly.lint-ignore.yaml +++ b/.redocly.lint-ignore.yaml @@ -14,3 +14,8 @@ openapi.yaml: - '#/paths/~1quotes/post/requestBody/content/application~1json/schema' no-ambiguous-paths: - '#/paths/~1customers~1external-accounts~1{externalAccountId}' + # threeDSecurePassword: "threeDSecure" is the industry-standard casing for + # 3DS and matches the EU provider's wire field; the camelCase checker + # rejects the consecutive-capital hump. Deliberate exception. + field-names-camelCase: + - '#/components/schemas/CardCreateRequest/properties/threeDSecurePassword' diff --git a/.spectral.yaml b/.spectral.yaml index ad9f01377..915c7b49e 100644 --- a/.spectral.yaml +++ b/.spectral.yaml @@ -176,3 +176,14 @@ rules: function: pattern functionOptions: match: "^(\\/([a-z0-9]+(-[a-z0-9]+)*|\\{[a-zA-Z0-9]+\\}))+$" + +# Per-pointer exceptions (mirror .redocly.lint-ignore.yaml - both linters run +# the same conventions, and spectral has no ignore-file mechanism). +overrides: + # threeDSecurePassword: "threeDSecure" is the industry-standard casing for + # 3DS and matches the EU provider's wire field; the camelCase checker + # rejects the consecutive-capital hump. Deliberate exception. + - files: + - "**/openapi.yaml#/components/schemas/CardCreateRequest/properties/threeDSecurePassword" + rules: + field-names-camelCase: off diff --git a/mintlify/openapi.yaml b/mintlify/openapi.yaml index 5c5219a31..4c5b05186 100644 --- a/mintlify/openapi.yaml +++ b/mintlify/openapi.yaml @@ -6884,6 +6884,15 @@ paths: - Cards security: - BasicAuth: [] + parameters: + - name: Idempotency-Key + in: header + required: false + description: | + A unique identifier for the request. If the same key is sent multiple times, the server will return the same response as the first request. Required for card programs on EUR-funded (EU) platforms and recommended everywhere. + schema: + type: string + example: requestBody: required: true content: @@ -6918,6 +6927,12 @@ paths: application/json: schema: $ref: '#/components/schemas/Error401' + '409': + description: Conflict. Returned when an `Idempotency-Key` is replayed with a request body different from the original request, or while a request with the same key is still being processed. + content: + application/json: + schema: + $ref: '#/components/schemas/Error409' '500': description: Internal service error content: @@ -7171,7 +7186,7 @@ paths: 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`.' + 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`. Provider-reported state conflicts return `CONFLICT`.' content: application/json: schema: @@ -8842,7 +8857,7 @@ webhooks: post: summary: Card transaction description: | - Webhook that is called on every state transition of a card `CardTransaction`. Fires when an authorization is approved (`CARD_TRANSACTION.AUTHORIZED`), as clearings settle against it (`CARD_TRANSACTION.PARTIALLY_SETTLED`, `CARD_TRANSACTION.SETTLED`), when settled funds are returned (`CARD_TRANSACTION.REFUNDED`), and when a pull or confirmation fails (`CARD_TRANSACTION.EXCEPTION`). The payload carries the full `CardTransaction` resource. + Webhook that is called on every state transition of a card `CardTransaction`. Fires when an authorization is approved (`CARD_TRANSACTION.AUTHORIZED`), when an authorization is declined by the issuer or program (`CARD_TRANSACTION.DECLINED`), as clearings settle against it (`CARD_TRANSACTION.PARTIALLY_SETTLED`, `CARD_TRANSACTION.SETTLED`), when settled funds are returned (`CARD_TRANSACTION.REFUNDED`), and when a pull or confirmation fails (`CARD_TRANSACTION.EXCEPTION`). The payload carries the full `CardTransaction` resource. This endpoint should be implemented by clients of the Grid API. @@ -8908,6 +8923,46 @@ webhooks: authorizedAt: '2026-05-09T10:00:00Z' createdAt: '2026-05-09T10:00:00Z' updatedAt: '2026-05-09T10:00:00Z' + declined: + summary: Authorization declined; no funds moved + value: + id: Webhook:019542f5-b3e7-1d02-0000-000000000044 + type: CARD_TRANSACTION.DECLINED + timestamp: '2026-05-09T11:20:00Z' + data: + type: CARD + id: CardTransaction:019542f5-b3e7-1d02-0000-000000000102 + cardId: Card:019542f5-b3e7-1d02-0000-000000000010 + customerId: Customer:019542f5-b3e7-1d02-0000-000000000001 + platformCustomerId: 18d3e5f7b4a9c2 + issuerTransactionToken: lithic_txn_d03e4c6b + status: DECLINED + direction: DEBIT + merchant: + descriptor: BLUE BOTTLE COFFEE SF + mcc: '5814' + country: US + authorizedAmount: + amount: 8300 + currency: + code: USD + name: United States Dollar + symbol: $ + decimals: 2 + accountId: InternalAccount:019542f5-b3e7-1d02-0000-000000000002 + pullSummary: + count: 0 + totalAmount: 0 + pendingCount: 0 + refundSummary: + count: 0 + totalAmount: 0 + settlementSummary: + count: 0 + totalAmount: 0 + authorizedAt: '2026-05-09T11:20:00Z' + createdAt: '2026-05-09T11:20:00Z' + updatedAt: '2026-05-09T11:20:00Z' settled: summary: All clearings posted value: @@ -17841,6 +17896,7 @@ components: - PARTIALLY_SETTLED - SETTLED - REFUNDED + - DECLINED - EXCEPTION description: | Lifecycle status of a card transaction. @@ -17848,6 +17904,7 @@ components: | Status | Description | |--------|-------------| | `AUTHORIZED` | The auth has been approved and a hold placed on the funding source; no clearing has arrived yet. | + | `DECLINED` | The authorization was declined by the issuer or program; no funds moved and the transaction is terminal. | | `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. | @@ -17934,7 +17991,7 @@ components: - 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. + 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 `CARD_TRANSACTION.*` webhook family on every state transition; the webhook type carries the transaction's new status (for example `CARD_TRANSACTION.SETTLED`). properties: type: type: string @@ -17977,6 +18034,7 @@ components: $ref: '#/components/schemas/CurrencyAmount' refundedAmount: $ref: '#/components/schemas/CurrencyAmount' + description: Total merchant refunds linked to this transaction. Populated only when the card program's provider links refunds to the original purchase (US programs). EU card programs receive merchant refunds WITHOUT a purchase reference, so those refunds surface as standalone CREDIT-direction card transactions and never update the purchase's `refundedAmount`/`refundSummary`. accountId: type: string description: Internal account id that funded this transaction (the funding source selected by Authorization Decisioning at auth time). @@ -20353,6 +20411,13 @@ components: example: card-emp-aary-001 form: $ref: '#/components/schemas/CardForm' + threeDSecurePassword: + type: string + writeOnly: true + minLength: 8 + maxLength: 36 + description: Optional static 3-D Secure password to set on the card, for card programs whose issuer challenges online purchases with a static password factor (EU/Striga cards). 8-36 characters; Latin letters, digits, and the issuer's allowed special characters. Grid forwards it to the provider and never stores or returns it. When omitted, Grid generates a strong password; the cardholder-facing view/change flow is delivered through the secure card UI. Rejected for card programs without a static-password challenge factor (US/Lithic cards). + example: Xk9!qR2#mVp7 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`. @@ -20584,6 +20649,7 @@ components: - CARD.STATE_CHANGE - CARD.FUNDING_SOURCE_CHANGE - CARD_TRANSACTION.AUTHORIZED + - CARD_TRANSACTION.DECLINED - CARD_TRANSACTION.PARTIALLY_SETTLED - CARD_TRANSACTION.SETTLED - CARD_TRANSACTION.REFUNDED @@ -20821,6 +20887,7 @@ components: type: string enum: - CARD_TRANSACTION.AUTHORIZED + - CARD_TRANSACTION.DECLINED - CARD_TRANSACTION.PARTIALLY_SETTLED - CARD_TRANSACTION.SETTLED - CARD_TRANSACTION.REFUNDED diff --git a/mintlify/snippets/cards/implementation-overview.mdx b/mintlify/snippets/cards/implementation-overview.mdx index 03c878896..64c387392 100644 --- a/mintlify/snippets/cards/implementation-overview.mdx +++ b/mintlify/snippets/cards/implementation-overview.mdx @@ -15,10 +15,8 @@ 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. +- Subscribe to the new card-specific webhook types (`CARD.STATE_CHANGE`, + `CARD.FUNDING_SOURCE_CHANGE`, and the `CARD_TRANSACTION.*` family). - Confirm with your Lightspark contact that cards are enabled for your platform — issuance requires an issuer-side onboarding. @@ -88,11 +86,10 @@ 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. + `CARD.*` and `CARD_TRANSACTION.*` event types. +- Build the `EXCEPTION` dashboard view from + `CARD_TRANSACTION.EXCEPTION` webhooks and wire it into on-call + alerting. Contact your Lightspark representative to enable Production card diff --git a/mintlify/snippets/cards/quickstart.mdx b/mintlify/snippets/cards/quickstart.mdx index 1187a3e0f..d19c2fb26 100644 --- a/mintlify/snippets/cards/quickstart.mdx +++ b/mintlify/snippets/cards/quickstart.mdx @@ -130,9 +130,9 @@ curl -X POST "$GRID_BASE_URL/sandbox/cards/Card:019542f5-b3e7-1d02-0000-00000000 }' ``` -A transaction webhook fires with the updated parent: the transaction -moves to `SETTLED`, `pullSummary.count` becomes `2`, and -`settlementSummary.totalAmount` is `1500`. +A `CARD_TRANSACTION.SETTLED` 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 diff --git a/mintlify/snippets/cards/reconciliation.mdx b/mintlify/snippets/cards/reconciliation.mdx index bb4e3b4c4..4ec38da06 100644 --- a/mintlify/snippets/cards/reconciliation.mdx +++ b/mintlify/snippets/cards/reconciliation.mdx @@ -1,4 +1,4 @@ -A card transaction is not a single event — it's a parent row plus a +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. @@ -7,16 +7,16 @@ event model, the status transitions, and how to handle the For each card authorization, Grid produces: -1. **One parent `CardTransaction`** — created at auth time, persists for +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 +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. +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 +You don't see per-child rows on the list endpoint - they're summarized on the parent. ## Status transitions @@ -32,15 +32,26 @@ AUTHORIZED ──► PARTIALLY_SETTLED ──► SETTLED ──► REFUNDED | Status | Meaning | |--------|---------| | `AUTHORIZED` | Auth approved, hold placed, no clearings yet. | +| `DECLINED` | The authorization was declined by the issuer or program. No funds moved; the transaction is terminal. | | `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)). +Every state transition is delivered via a `CARD_TRANSACTION.*` webhook +whose type names the new status (`CARD_TRANSACTION.AUTHORIZED`, +`CARD_TRANSACTION.SETTLED`, ...) and carries the post-update parent. +See [Webhooks](/cards/platform-tools/webhooks) for the full payload +shape. + +**Refund linkage differs by card program.** US programs link merchant +refunds to the original purchase: the purchase transitions to `REFUNDED` +and its `refundedAmount`/`refundSummary` update. EU programs receive +merchant refunds from the network WITHOUT a purchase reference, so a +refund arrives as a **standalone CREDIT-direction card transaction** (its +own `CARD_TRANSACTION.*` lifecycle) and the original purchase keeps its +final status with `refundedAmount` unset. Reconcile EU refunds by +merchant/amount/time rather than by parent linkage. ## The over-auth path @@ -59,13 +70,13 @@ The post-settlement parent carries `authorizedAmount: 1250`, 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 +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. +Signal to watch: a `CARD_TRANSACTION.EXCEPTION` webhook. 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 @@ -74,10 +85,10 @@ 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. +Every `CARD_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 index d9c175c15..b10c53dd5 100644 --- a/mintlify/snippets/cards/sandbox-testing.mdx +++ b/mintlify/snippets/cards/sandbox-testing.mdx @@ -139,7 +139,7 @@ curl -X POST "$GRID_BASE_URL/sandbox/cards/Card:.../simulate/return" \ -d '{ "cardTransactionId": "CardTransaction:...", "amount": 1500 }' ``` -At each step you'll see `CARD.STATE_CHANGE` or -`CARD.FUNDING_SOURCE_CHANGE` webhooks plus transaction webhooks for +At each step you'll see `CARD.STATE_CHANGE`, +`CARD.FUNDING_SOURCE_CHANGE`, or `CARD_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/webhooks.mdx b/mintlify/snippets/cards/webhooks.mdx index c3318d863..03a2a4e4f 100644 --- a/mintlify/snippets/cards/webhooks.mdx +++ b/mintlify/snippets/cards/webhooks.mdx @@ -1,21 +1,24 @@ -Cards add two webhook event types on top of Grid's existing webhook +Cards add two card-lifecycle webhook event types plus the +`CARD_TRANSACTION.*` family 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` | `PROCESSING → ACTIVE`, `→ CLOSED (ISSUER_REJECTED)`, and every subsequent `ACTIVE ⇄ FROZEN` and `→ CLOSED` transition. | | `CARD.FUNDING_SOURCE_CHANGE` | Whenever `PATCH /cards/{id}` updates the `fundingSources` array. | +| `CARD_TRANSACTION.AUTHORIZED` | An authorization is approved and the parent `CardTransaction` is created. | +| `CARD_TRANSACTION.DECLINED` | An authorization is declined; the transaction is terminal and no funds moved. | +| `CARD_TRANSACTION.PARTIALLY_SETTLED` | A clearing posted but more are still expected. | +| `CARD_TRANSACTION.SETTLED` | All clearings for the auth have posted. | +| `CARD_TRANSACTION.REFUNDED` | A merchant `RETURN` refunded the net settled amount in part or whole. | +| `CARD_TRANSACTION.EXCEPTION` | The transaction settled to the network but the funding-source pull failed. | -All three carry the standard envelope: +All of them carry the standard envelope: ```json { @@ -80,15 +83,64 @@ 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 +## The CARD_TRANSACTION.* family + +Fires on every state transition of a `CardTransaction` — the webhook +type names the transaction's new status, so consumers can route purely +on `type` without inspecting `data.status`. The `data` payload is the +full post-transition `CardTransaction`, including its lifecycle status +and reconciliation aggregates. + +```json +{ + "id": "Webhook:019542f5-b3e7-1d02-0000-000000000040", + "type": "CARD_TRANSACTION.SETTLED", + "timestamp": "2026-05-08T15:42:11Z", + "data": { + "type": "CARD", + "id": "CardTransaction:019542f5-b3e7-1d02-0000-000000000100", + "cardId": "Card:019542f5-b3e7-1d02-0000-000000000010", + "customerId": "Customer:019542f5-b3e7-1d02-0000-000000000001", + "platformCustomerId": "18d3e5f7b4a9c2", + "status": "SETTLED", + "direction": "DEBIT", + "merchant": { + "descriptor": "BLUE BOTTLE COFFEE SF", + "mcc": "5814", + "country": "US" + }, + "authorizedAmount": { + "amount": 1250, + "currency": { "code": "USD" } + }, + "settledAmount": { + "amount": 1500, + "currency": { "code": "USD" } + }, + "accountId": "InternalAccount:019542f5-b3e7-1d02-0000-000000000002", + "pullSummary": { + "count": 2, + "totalAmount": 1500, + "pendingCount": 0 + }, + "refundSummary": { + "count": 0, + "totalAmount": 0 + }, + "settlementSummary": { + "count": 1, + "totalAmount": 1500 + }, + "authorizedAt": "2026-05-08T14:30:00Z", + "lastEventAt": "2026-05-08T15:42:11Z", + "createdAt": "2026-05-08T14:30:00Z", + "updatedAt": "2026-05-08T15:42:11Z" + } +} +``` -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. +See [Reconciliation](/cards/transactions/reconciliation) for the +underlying event model and `EXCEPTION` handling. ## Idempotency & retries diff --git a/openapi.yaml b/openapi.yaml index 5c5219a31..4c5b05186 100644 --- a/openapi.yaml +++ b/openapi.yaml @@ -6884,6 +6884,15 @@ paths: - Cards security: - BasicAuth: [] + parameters: + - name: Idempotency-Key + in: header + required: false + description: | + A unique identifier for the request. If the same key is sent multiple times, the server will return the same response as the first request. Required for card programs on EUR-funded (EU) platforms and recommended everywhere. + schema: + type: string + example: requestBody: required: true content: @@ -6918,6 +6927,12 @@ paths: application/json: schema: $ref: '#/components/schemas/Error401' + '409': + description: Conflict. Returned when an `Idempotency-Key` is replayed with a request body different from the original request, or while a request with the same key is still being processed. + content: + application/json: + schema: + $ref: '#/components/schemas/Error409' '500': description: Internal service error content: @@ -7171,7 +7186,7 @@ paths: 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`.' + 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`. Provider-reported state conflicts return `CONFLICT`.' content: application/json: schema: @@ -8842,7 +8857,7 @@ webhooks: post: summary: Card transaction description: | - Webhook that is called on every state transition of a card `CardTransaction`. Fires when an authorization is approved (`CARD_TRANSACTION.AUTHORIZED`), as clearings settle against it (`CARD_TRANSACTION.PARTIALLY_SETTLED`, `CARD_TRANSACTION.SETTLED`), when settled funds are returned (`CARD_TRANSACTION.REFUNDED`), and when a pull or confirmation fails (`CARD_TRANSACTION.EXCEPTION`). The payload carries the full `CardTransaction` resource. + Webhook that is called on every state transition of a card `CardTransaction`. Fires when an authorization is approved (`CARD_TRANSACTION.AUTHORIZED`), when an authorization is declined by the issuer or program (`CARD_TRANSACTION.DECLINED`), as clearings settle against it (`CARD_TRANSACTION.PARTIALLY_SETTLED`, `CARD_TRANSACTION.SETTLED`), when settled funds are returned (`CARD_TRANSACTION.REFUNDED`), and when a pull or confirmation fails (`CARD_TRANSACTION.EXCEPTION`). The payload carries the full `CardTransaction` resource. This endpoint should be implemented by clients of the Grid API. @@ -8908,6 +8923,46 @@ webhooks: authorizedAt: '2026-05-09T10:00:00Z' createdAt: '2026-05-09T10:00:00Z' updatedAt: '2026-05-09T10:00:00Z' + declined: + summary: Authorization declined; no funds moved + value: + id: Webhook:019542f5-b3e7-1d02-0000-000000000044 + type: CARD_TRANSACTION.DECLINED + timestamp: '2026-05-09T11:20:00Z' + data: + type: CARD + id: CardTransaction:019542f5-b3e7-1d02-0000-000000000102 + cardId: Card:019542f5-b3e7-1d02-0000-000000000010 + customerId: Customer:019542f5-b3e7-1d02-0000-000000000001 + platformCustomerId: 18d3e5f7b4a9c2 + issuerTransactionToken: lithic_txn_d03e4c6b + status: DECLINED + direction: DEBIT + merchant: + descriptor: BLUE BOTTLE COFFEE SF + mcc: '5814' + country: US + authorizedAmount: + amount: 8300 + currency: + code: USD + name: United States Dollar + symbol: $ + decimals: 2 + accountId: InternalAccount:019542f5-b3e7-1d02-0000-000000000002 + pullSummary: + count: 0 + totalAmount: 0 + pendingCount: 0 + refundSummary: + count: 0 + totalAmount: 0 + settlementSummary: + count: 0 + totalAmount: 0 + authorizedAt: '2026-05-09T11:20:00Z' + createdAt: '2026-05-09T11:20:00Z' + updatedAt: '2026-05-09T11:20:00Z' settled: summary: All clearings posted value: @@ -17841,6 +17896,7 @@ components: - PARTIALLY_SETTLED - SETTLED - REFUNDED + - DECLINED - EXCEPTION description: | Lifecycle status of a card transaction. @@ -17848,6 +17904,7 @@ components: | Status | Description | |--------|-------------| | `AUTHORIZED` | The auth has been approved and a hold placed on the funding source; no clearing has arrived yet. | + | `DECLINED` | The authorization was declined by the issuer or program; no funds moved and the transaction is terminal. | | `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. | @@ -17934,7 +17991,7 @@ components: - 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. + 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 `CARD_TRANSACTION.*` webhook family on every state transition; the webhook type carries the transaction's new status (for example `CARD_TRANSACTION.SETTLED`). properties: type: type: string @@ -17977,6 +18034,7 @@ components: $ref: '#/components/schemas/CurrencyAmount' refundedAmount: $ref: '#/components/schemas/CurrencyAmount' + description: Total merchant refunds linked to this transaction. Populated only when the card program's provider links refunds to the original purchase (US programs). EU card programs receive merchant refunds WITHOUT a purchase reference, so those refunds surface as standalone CREDIT-direction card transactions and never update the purchase's `refundedAmount`/`refundSummary`. accountId: type: string description: Internal account id that funded this transaction (the funding source selected by Authorization Decisioning at auth time). @@ -20353,6 +20411,13 @@ components: example: card-emp-aary-001 form: $ref: '#/components/schemas/CardForm' + threeDSecurePassword: + type: string + writeOnly: true + minLength: 8 + maxLength: 36 + description: Optional static 3-D Secure password to set on the card, for card programs whose issuer challenges online purchases with a static password factor (EU/Striga cards). 8-36 characters; Latin letters, digits, and the issuer's allowed special characters. Grid forwards it to the provider and never stores or returns it. When omitted, Grid generates a strong password; the cardholder-facing view/change flow is delivered through the secure card UI. Rejected for card programs without a static-password challenge factor (US/Lithic cards). + example: Xk9!qR2#mVp7 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`. @@ -20584,6 +20649,7 @@ components: - CARD.STATE_CHANGE - CARD.FUNDING_SOURCE_CHANGE - CARD_TRANSACTION.AUTHORIZED + - CARD_TRANSACTION.DECLINED - CARD_TRANSACTION.PARTIALLY_SETTLED - CARD_TRANSACTION.SETTLED - CARD_TRANSACTION.REFUNDED @@ -20821,6 +20887,7 @@ components: type: string enum: - CARD_TRANSACTION.AUTHORIZED + - CARD_TRANSACTION.DECLINED - CARD_TRANSACTION.PARTIALLY_SETTLED - CARD_TRANSACTION.SETTLED - CARD_TRANSACTION.REFUNDED diff --git a/openapi/components/schemas/cards/CardCreateRequest.yaml b/openapi/components/schemas/cards/CardCreateRequest.yaml index 5cbd262a3..8b727b632 100644 --- a/openapi/components/schemas/cards/CardCreateRequest.yaml +++ b/openapi/components/schemas/cards/CardCreateRequest.yaml @@ -19,6 +19,22 @@ properties: example: card-emp-aary-001 form: $ref: ./CardForm.yaml + threeDSecurePassword: + type: string + writeOnly: true + minLength: 8 + maxLength: 36 + description: >- + Optional static 3-D Secure password to set on the card, for card + programs whose issuer challenges online purchases with a static + password factor (EU/Striga cards). 8-36 characters; Latin letters, + digits, and the issuer's allowed special characters. Grid forwards it + to the provider and never stores or returns it. When omitted, Grid + generates a strong password; the cardholder-facing view/change flow + is delivered through the secure card UI. Rejected for card programs + without a static-password challenge factor (US/Lithic cards). + example: Xk9!qR2#mVp7 + fundingSources: type: array description: >- diff --git a/openapi/components/schemas/cards/CardTransaction.yaml b/openapi/components/schemas/cards/CardTransaction.yaml index 87caed2fa..058e32602 100644 --- a/openapi/components/schemas/cards/CardTransaction.yaml +++ b/openapi/components/schemas/cards/CardTransaction.yaml @@ -17,9 +17,9 @@ 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. + aggregates. Delivered as the payload of the `CARD_TRANSACTION.*` + webhook family on every state transition; the webhook type carries the + transaction's new status (for example `CARD_TRANSACTION.SETTLED`). properties: type: type: string @@ -64,6 +64,13 @@ properties: $ref: ../common/CurrencyAmount.yaml refundedAmount: $ref: ../common/CurrencyAmount.yaml + description: >- + Total merchant refunds linked to this transaction. Populated only when + the card program's provider links refunds to the original purchase + (US programs). EU card programs receive merchant refunds WITHOUT a + purchase reference, so those refunds surface as standalone + CREDIT-direction card transactions and never update the purchase's + `refundedAmount`/`refundSummary`. accountId: type: string description: >- diff --git a/openapi/components/schemas/cards/CardTransactionStatus.yaml b/openapi/components/schemas/cards/CardTransactionStatus.yaml index 01be9d92c..c53a71a9f 100644 --- a/openapi/components/schemas/cards/CardTransactionStatus.yaml +++ b/openapi/components/schemas/cards/CardTransactionStatus.yaml @@ -4,6 +4,7 @@ enum: - PARTIALLY_SETTLED - SETTLED - REFUNDED + - DECLINED - EXCEPTION description: | Lifecycle status of a card transaction. @@ -11,6 +12,7 @@ description: | | Status | Description | |--------|-------------| | `AUTHORIZED` | The auth has been approved and a hold placed on the funding source; no clearing has arrived yet. | + | `DECLINED` | The authorization was declined by the issuer or program; no funds moved and the transaction is terminal. | | `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. | diff --git a/openapi/components/schemas/webhooks/CardTransactionWebhook.yaml b/openapi/components/schemas/webhooks/CardTransactionWebhook.yaml index 0d46e69d5..95d28e4f0 100644 --- a/openapi/components/schemas/webhooks/CardTransactionWebhook.yaml +++ b/openapi/components/schemas/webhooks/CardTransactionWebhook.yaml @@ -10,6 +10,7 @@ allOf: type: string enum: - CARD_TRANSACTION.AUTHORIZED + - CARD_TRANSACTION.DECLINED - CARD_TRANSACTION.PARTIALLY_SETTLED - CARD_TRANSACTION.SETTLED - CARD_TRANSACTION.REFUNDED diff --git a/openapi/components/schemas/webhooks/WebhookType.yaml b/openapi/components/schemas/webhooks/WebhookType.yaml index 179007dc8..9628320b1 100644 --- a/openapi/components/schemas/webhooks/WebhookType.yaml +++ b/openapi/components/schemas/webhooks/WebhookType.yaml @@ -32,6 +32,7 @@ enum: - CARD.STATE_CHANGE - CARD.FUNDING_SOURCE_CHANGE - CARD_TRANSACTION.AUTHORIZED + - CARD_TRANSACTION.DECLINED - CARD_TRANSACTION.PARTIALLY_SETTLED - CARD_TRANSACTION.SETTLED - CARD_TRANSACTION.REFUNDED diff --git a/openapi/paths/cards/cards.yaml b/openapi/paths/cards/cards.yaml index 9d945fc07..e2485e24d 100644 --- a/openapi/paths/cards/cards.yaml +++ b/openapi/paths/cards/cards.yaml @@ -24,6 +24,17 @@ post: - Cards security: - BasicAuth: [] + parameters: + - name: Idempotency-Key + in: header + required: false + description: > + A unique identifier for the request. If the same key is sent multiple times, + the server will return the same response as the first request. Required for + card programs on EUR-funded (EU) platforms and recommended everywhere. + schema: + type: string + example: requestBody: required: true content: @@ -67,6 +78,15 @@ post: application/json: schema: $ref: ../../components/schemas/errors/Error401.yaml + '409': + description: >- + Conflict. Returned when an `Idempotency-Key` is replayed with a + request body different from the original request, or while a request + with the same key is still being processed. + content: + application/json: + schema: + $ref: ../../components/schemas/errors/Error409.yaml '500': description: Internal service error content: diff --git a/openapi/paths/cards/cards_{id}.yaml b/openapi/paths/cards/cards_{id}.yaml index c5149f206..b33889c8a 100644 --- a/openapi/paths/cards/cards_{id}.yaml +++ b/openapi/paths/cards/cards_{id}.yaml @@ -224,7 +224,7 @@ patch: 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`. + `CLOSED`. Provider-reported state conflicts return `CONFLICT`. content: application/json: schema: diff --git a/openapi/webhooks/card-transaction.yaml b/openapi/webhooks/card-transaction.yaml index 5a9a054bb..b4f7d81c1 100644 --- a/openapi/webhooks/card-transaction.yaml +++ b/openapi/webhooks/card-transaction.yaml @@ -3,11 +3,13 @@ post: description: > Webhook that is called on every state transition of a card `CardTransaction`. Fires when an authorization is approved - (`CARD_TRANSACTION.AUTHORIZED`), as clearings settle against it - (`CARD_TRANSACTION.PARTIALLY_SETTLED`, `CARD_TRANSACTION.SETTLED`), - when settled funds are returned (`CARD_TRANSACTION.REFUNDED`), and - when a pull or confirmation fails (`CARD_TRANSACTION.EXCEPTION`). - The payload carries the full `CardTransaction` resource. + (`CARD_TRANSACTION.AUTHORIZED`), when an authorization is declined by + the issuer or program (`CARD_TRANSACTION.DECLINED`), as clearings + settle against it (`CARD_TRANSACTION.PARTIALLY_SETTLED`, + `CARD_TRANSACTION.SETTLED`), when settled funds are returned + (`CARD_TRANSACTION.REFUNDED`), and when a pull or confirmation fails + (`CARD_TRANSACTION.EXCEPTION`). The payload carries the full + `CardTransaction` resource. This endpoint should be implemented by clients of the Grid API. @@ -84,6 +86,46 @@ post: authorizedAt: '2026-05-09T10:00:00Z' createdAt: '2026-05-09T10:00:00Z' updatedAt: '2026-05-09T10:00:00Z' + declined: + summary: Authorization declined; no funds moved + value: + id: Webhook:019542f5-b3e7-1d02-0000-000000000044 + type: CARD_TRANSACTION.DECLINED + timestamp: '2026-05-09T11:20:00Z' + data: + type: CARD + id: CardTransaction:019542f5-b3e7-1d02-0000-000000000102 + cardId: Card:019542f5-b3e7-1d02-0000-000000000010 + customerId: Customer:019542f5-b3e7-1d02-0000-000000000001 + platformCustomerId: 18d3e5f7b4a9c2 + issuerTransactionToken: lithic_txn_d03e4c6b + status: DECLINED + direction: DEBIT + merchant: + descriptor: BLUE BOTTLE COFFEE SF + mcc: '5814' + country: US + authorizedAmount: + amount: 8300 + currency: + code: USD + name: United States Dollar + symbol: $ + decimals: 2 + accountId: InternalAccount:019542f5-b3e7-1d02-0000-000000000002 + pullSummary: + count: 0 + totalAmount: 0 + pendingCount: 0 + refundSummary: + count: 0 + totalAmount: 0 + settlementSummary: + count: 0 + totalAmount: 0 + authorizedAt: '2026-05-09T11:20:00Z' + createdAt: '2026-05-09T11:20:00Z' + updatedAt: '2026-05-09T11:20:00Z' settled: summary: All clearings posted value: