> ## Documentation Index
> Fetch the complete documentation index at: https://ramps-kph-cards-idempotency-txn-webhook.mintlify.site/llms.txt
> Use this file to discover all available pages before exploring further.

# Webhooks

> Card webhook events and how to consume them

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.

## 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 of them carry the standard envelope:

```json theme={null}
{
  "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 theme={null}
{
  "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,
    "fundingSources": [
      "InternalAccount:019542f5-b3e7-1d02-0000-000000000002"
    ],
    "currency": "USD",
    "processorRef": "card_b81c2a4f",
    "issuerRef": "lead_card_7a1b9c3d",
    "createdAt": "2026-05-08T14:10:00Z",
    "updatedAt": "2026-05-08T14:11:00Z"
  }
}
```

Common branches to handle in your consumer:

* `state: "ACTIVE"` after `PROCESSING` — the card is live. To reveal
  the full card details, request a reveal with
  `POST /cards/{id}/reveal` right before rendering its short-lived
  `panEmbedUrl` in an iframe — webhook payloads never carry a reveal
  URL.
* `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.

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

See [Reconciliation](/cards/transactions/reconciliation) for the
underlying event model and `EXCEPTION` handling.

## 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.
