From c8eac4a583da3b9bd8eb1d9ef0bd37b4160d6f68 Mon Sep 17 00:00:00 2001 From: Cheun Kitabayashi Date: Fri, 31 Jul 2026 11:34:54 +0200 Subject: [PATCH 1/8] docs(webhooks): add webhook event reference page Co-Authored-By: Claude Fable 5 --- payments/webhook-event-reference.mdx | 537 +++++++++++++++++++++++++++ 1 file changed, 537 insertions(+) create mode 100644 payments/webhook-event-reference.mdx diff --git a/payments/webhook-event-reference.mdx b/payments/webhook-event-reference.mdx new file mode 100644 index 0000000..b4e9345 --- /dev/null +++ b/payments/webhook-event-reference.mdx @@ -0,0 +1,537 @@ +--- +title: "Webhook event reference" +description: "Field-level reference for every payment webhook event: the envelope, the payment snapshot, each stage object, and a full example payload per event type." +sidebarTitle: "Webhook events" +--- + +This page is the field-level reference for the payment webhook payload, `api_version` `2026-05-18`. It documents every field, its type and nullability, and which stage objects are populated for each event type. For endpoint setup, signature verification, and delivery guarantees, see [Webhooks](/payments/webhooks). + + +**The contract is additive-only.** Within this `api_version`, fields are never removed or renamed, and types and nullability never change. New **optional** fields and new values for extensible strings (such as `fee.kind`) can be added at any time. Ignore fields you don't recognize, and never validate payloads with a schema that rejects unknown fields; strict validation will break your integration. + + +All timestamps in the payload are ISO 8601 UTC strings. + +## The event envelope + +Every event has the same top-level shape. + +| Field | Type | Description | +|---|---|---| +| `id` | string | Unique event identifier, prefixed `evt_`. The deduplication key: delivery is at-least-once, so the same `id` can arrive more than once. Do not deduplicate by `payment_id`; one payment emits multiple events. | +| `type` | string | The event type. One of the seven [event types](#event-types) below. | +| `api_version` | string | The payload contract version, `2026-05-18`. Changes within a version are additive-only. | +| `created_at` | string | When the event occurred, not when it was delivered. | +| `data` | object | The [payment snapshot](#the-payment-snapshot-data) at event time. | + +## The payment snapshot (`data`) + +`data` is always a **full snapshot of the payment at the moment the event occurred**, never a delta. Every event type carries the same field set; what varies is `status` and which stage objects are non-null. + +| Field | Type | Description | +|---|---|---| +| `payment_id` | string | Payment identifier, the same id used across the Merchant API. | +| `merchant_id` | string | Merchant identifier, the same external merchant id used across the Merchant API. | +| `live` | boolean | Whether this is a live payment. Test-mode payments carry `false` and are only delivered to test-mode endpoints. Verify `live` is `true` before driving real fulfillment. | +| `payment_state_version` | integer | Monotonic per-payment version, starting at `0` on the first event. The ordering guard: ignore any event whose version is lower than or equal to one already processed for the same `payment_id`. See [Out-of-order delivery](/payments/webhooks#out-of-order-delivery). | +| `reference_id` | string \| null | The merchant-provided order reference attached at payment creation, or `null`. Primary reconciliation key against your own records. | +| `status` | string | Payment status at event time: `requires_action`, `processing`, `succeeded`, `failed`, `expired`, or `cancelled`. There is no `settled` status; settlement is a stage of a succeeded payment. | +| `amount` | [Amount](#amount) | The requested payment amount. | +| `created_at` | string | When the payment was created. | +| `expires_at` | string \| null | When the payment stops accepting buyer action, or `null` if it does not expire. | +| `processing` | [processing](#processing) \| null | Populated once the buyer has committed to a payment option. | +| `success` | [success](#success) \| null | Populated once the payment succeeded. | +| `failed` | [failed](#failed) \| null | Populated once the payment failed. | +| `cancelled` | [cancelled](#cancelled) \| null | Populated once the payment was cancelled. | +| `expired` | [expired](#expired) \| null | Populated once the payment expired. | +| `settled` | [settled](#settled) \| null | Populated once merchant settlement completed. | + +### Stage objects by event type + +Which stage objects are non-null depends on the event type. + +| Event type | `status` | `processing` | `success` | `failed` | `cancelled` | `expired` | `settled` | +|---|---|---|---|---|---|---|---| +| `payment.created` | `requires_action` | null | null | null | null | null | null | +| `payment.processing` | `processing` | present | null | null | null | null | null | +| `payment.succeeded` | `succeeded` | present | present | null | null | null | usually null | +| `payment.failed` | `failed` | may be present | null | present | null | null | null | +| `payment.expired` | `expired` | may be present | null | null | null | present | null | +| `payment.cancelled` | `cancelled` | may be present | null | null | present | null | null | +| `payment.settled` | `succeeded` | present | present | null | null | null | present | + +"May be present" means the payment can reach that outcome either before or after processing started: `processing` is `null` when the buyer never committed to a payment option, and populated otherwise. On `payment.succeeded`, `settled` is `null` unless settlement had already completed at snapshot time; settlement completion is carried by `payment.settled`. + +## Objects + +### Amount + +An asset amount. Used for `data.amount` and inside the [processing](#processing), [fee](#fee), and [settled](#settled) objects. + +| Field | Type | Description | +|---|---|---| +| `unit` | string | The asset: `iso4217/XXX` for fiat currencies (for example `iso4217/USD`) or `caip19/...` for on-chain assets. | +| `value` | string | Integer string in the smallest unit of the asset: minor units for fiat, raw base units for tokens. Token decimals are not included in the payload. | + +### fee + +Fee applied to the payment, inside [processing](#processing). + +| Field | Type | Description | +|---|---|---| +| `kind` | string | Extensible string. The only value at launch is `base_plus_percent`. Do not switch exhaustively on it; new kinds can be added within this `api_version`. Whatever the kind, `total_amount` is always present. | +| `base_amount` | [Amount](#amount) | The fixed component of the fee. | +| `percent` | object | The percentage as an exact rational: `numerator` and `denominator`, both integer strings. For example `29/1000` is 2.9%. | +| `percent_amount` | [Amount](#amount) | The percentage component of the fee. | +| `total_amount` | [Amount](#amount) | The total fee charged. `base_amount` and `percent_amount` break it down. | + +### processing + +Present once the buyer has committed to a payment option. + +| Field | Type | Description | +|---|---|---| +| `processing_at` | string | When the buyer committed to the payment option. | +| `option_amount` | [Amount](#amount) | What the buyer pays gross, in the asset of the payment option they selected. | +| `fee` | [fee](#fee) \| null | Fee applied to this payment, or `null` when no fee is configured. | +| `settlement_amount` | [Amount](#amount) | What the merchant is expected to receive, net of `fee`. | +| `buyer_caip10` | string \| null | The buyer's on-chain account ([CAIP-10](https://chainagnostic.org/CAIPs/caip-10)), or `null` when the payment has no on-chain buyer (for example, payments settled inside an exchange). | +| `chain_caip2` | string | The chain the payment executes on ([CAIP-2](https://chainagnostic.org/CAIPs/caip-2)). Needed to interpret transaction ids. | + +### success + +Present once the payment succeeded. + +| Field | Type | Description | +|---|---|---| +| `succeeded_at` | string | When the payment succeeded. | +| `tx_id` | string \| null | On-chain transaction id, or `null` when there is no on-chain transaction. Format is chain-specific; interpret with `processing.chain_caip2`. | + +### failed + +Present once the payment failed. + +| Field | Type | Description | +|---|---|---| +| `failed_at` | string | When the payment failed. | +| `failure_reason` | string \| null | Human-readable diagnostic, or `null`. Not machine-parseable: values may change without notice. Display it if useful, but never branch on it. | + +### cancelled + +Present once the payment was cancelled. + +| Field | Type | Description | +|---|---|---| +| `cancelled_at` | string | When the payment was cancelled. | +| `cancellation_reason` | string \| null | Human-readable diagnostic, or `null`. Not machine-parseable: values may change without notice. Display it if useful, but never branch on it. | + +### expired + +Present once the payment expired. + +| Field | Type | Description | +|---|---|---| +| `expired_at` | string | When the payment expired. | + +### settled + +Present once merchant settlement completed. + +| Field | Type | Description | +|---|---|---| +| `settled_at` | string | When settlement completed. | +| `tx_id` | string \| null | Settlement transaction id, or `null` when there is no on-chain settlement transaction. Format is chain-specific; interpret with `processing.chain_caip2`. | +| `amount` | [Amount](#amount) | What actually settled to the merchant, as opposed to `processing.settlement_amount`, which is the expected net amount at processing time. | + +## Event types + +One example per event type. The examples are the golden fixtures the payload contract is tested against. + +### payment.created + +A payment was created and awaits buyer action. Initial snapshot: no lifecycle stage has happened yet, so every stage object is `null` and `payment_state_version` is `0`. + +```json payment.created expandable +{ + "id": "evt_fixture_payment_created", + "type": "payment.created", + "api_version": "2026-05-18", + "created_at": "2026-06-30T09:59:00.084Z", + "data": { + "payment_id": "pay_fixture_01HZX4V9K3T", + "merchant_id": "merchant_fixture_01HZX4V9K3T", + "live": true, + "payment_state_version": 0, + "reference_id": "order_fixture_1042", + "status": "requires_action", + "amount": { + "unit": "iso4217/USD", + "value": "1000" + }, + "created_at": "2026-06-30T09:59:00.084Z", + "expires_at": "2026-06-30T10:14:00.000Z", + "processing": null, + "success": null, + "failed": null, + "cancelled": null, + "expired": null, + "settled": null + } +} +``` + +### payment.processing + +The buyer committed to a payment option and the payment started processing. `processing` is populated from this point on. + +In this example the buyer pays 10 USDC on Base for a $10.00 payment, a 2.9% fee applies, and the merchant is expected to receive 9.71 USDC. `processing.buyer_caip10` is `null` instead when the payment has no on-chain buyer, and `live` is `false` for test-mode payments. + +```json payment.processing expandable +{ + "id": "evt_fixture_payment_processing", + "type": "payment.processing", + "api_version": "2026-05-18", + "created_at": "2026-06-30T09:59:10.512Z", + "data": { + "payment_id": "pay_fixture_01HZX4V9K3T", + "merchant_id": "merchant_fixture_01HZX4V9K3T", + "live": true, + "payment_state_version": 1, + "reference_id": "order_fixture_1042", + "status": "processing", + "amount": { + "unit": "iso4217/USD", + "value": "1000" + }, + "created_at": "2026-06-30T09:59:00.084Z", + "expires_at": "2026-06-30T10:14:00.000Z", + "processing": { + "processing_at": "2026-06-30T09:59:10.512Z", + "option_amount": { + "unit": "caip19/eip155:8453/erc20:0x833589fCD6eDb6E08f4c7C32D4f71b54bdA02913", + "value": "10000000" + }, + "fee": { + "kind": "base_plus_percent", + "base_amount": { + "unit": "caip19/eip155:8453/erc20:0x833589fCD6eDb6E08f4c7C32D4f71b54bdA02913", + "value": "0" + }, + "percent": { + "numerator": "29", + "denominator": "1000" + }, + "percent_amount": { + "unit": "caip19/eip155:8453/erc20:0x833589fCD6eDb6E08f4c7C32D4f71b54bdA02913", + "value": "290000" + }, + "total_amount": { + "unit": "caip19/eip155:8453/erc20:0x833589fCD6eDb6E08f4c7C32D4f71b54bdA02913", + "value": "290000" + } + }, + "settlement_amount": { + "unit": "caip19/eip155:8453/erc20:0x833589fCD6eDb6E08f4c7C32D4f71b54bdA02913", + "value": "9710000" + }, + "buyer_caip10": "eip155:8453:0x000000000000000000000000000000000000dEaD", + "chain_caip2": "eip155:8453" + }, + "success": null, + "failed": null, + "cancelled": null, + "expired": null, + "settled": null + } +} +``` + +### payment.succeeded + +The payment succeeded. `success` is populated, and `processing` carries the committed option. `settled` is usually `null` here; settlement completion is carried by [`payment.settled`](#payment-settled). It may be non-null when settlement had already completed at snapshot time. + +```json payment.succeeded expandable +{ + "id": "evt_fixture_payment_succeeded", + "type": "payment.succeeded", + "api_version": "2026-05-18", + "created_at": "2026-06-30T10:00:00.271Z", + "data": { + "payment_id": "pay_fixture_01HZX4V9K3T", + "merchant_id": "merchant_fixture_01HZX4V9K3T", + "live": true, + "payment_state_version": 2, + "reference_id": "order_fixture_1042", + "status": "succeeded", + "amount": { + "unit": "iso4217/USD", + "value": "1000" + }, + "created_at": "2026-06-30T09:59:00.084Z", + "expires_at": "2026-06-30T10:14:00.000Z", + "processing": { + "processing_at": "2026-06-30T09:59:10.512Z", + "option_amount": { + "unit": "caip19/eip155:8453/erc20:0x833589fCD6eDb6E08f4c7C32D4f71b54bdA02913", + "value": "10000000" + }, + "fee": { + "kind": "base_plus_percent", + "base_amount": { + "unit": "caip19/eip155:8453/erc20:0x833589fCD6eDb6E08f4c7C32D4f71b54bdA02913", + "value": "0" + }, + "percent": { + "numerator": "29", + "denominator": "1000" + }, + "percent_amount": { + "unit": "caip19/eip155:8453/erc20:0x833589fCD6eDb6E08f4c7C32D4f71b54bdA02913", + "value": "290000" + }, + "total_amount": { + "unit": "caip19/eip155:8453/erc20:0x833589fCD6eDb6E08f4c7C32D4f71b54bdA02913", + "value": "290000" + } + }, + "settlement_amount": { + "unit": "caip19/eip155:8453/erc20:0x833589fCD6eDb6E08f4c7C32D4f71b54bdA02913", + "value": "9710000" + }, + "buyer_caip10": "eip155:8453:0x000000000000000000000000000000000000dEaD", + "chain_caip2": "eip155:8453" + }, + "success": { + "succeeded_at": "2026-06-30T10:00:00.271Z", + "tx_id": "0x8f1e5c1b9a4a1d2e3f405162738495a6b7c8d9e0f1a2b3c4d5e6f708192a3b4c" + }, + "failed": null, + "cancelled": null, + "expired": null, + "settled": null + } +} +``` + +### payment.failed + +The payment failed. `failed` is populated. `processing` is `null` when the payment failed before the buyer committed, as in this example, and populated when the payment had started processing before failing. + +```json payment.failed expandable +{ + "id": "evt_fixture_payment_failed", + "type": "payment.failed", + "api_version": "2026-05-18", + "created_at": "2026-06-30T10:00:00.271Z", + "data": { + "payment_id": "pay_fixture_01HZX4V9K3T", + "merchant_id": "merchant_fixture_01HZX4V9K3T", + "live": true, + "payment_state_version": 1, + "reference_id": "order_fixture_1042", + "status": "failed", + "amount": { + "unit": "iso4217/USD", + "value": "1000" + }, + "created_at": "2026-06-30T09:59:00.084Z", + "expires_at": "2026-06-30T10:14:00.000Z", + "processing": null, + "success": null, + "failed": { + "failed_at": "2026-06-30T10:00:00.271Z", + "failure_reason": "insufficient allowance" + }, + "cancelled": null, + "expired": null, + "settled": null + } +} +``` + +### payment.expired + +The payment expired before completing. `expired` is populated. A payment can expire after processing started, as in this example; `processing` is `null` when it expired while still awaiting buyer action. + +```json payment.expired expandable +{ + "id": "evt_fixture_payment_expired_after_processing", + "type": "payment.expired", + "api_version": "2026-05-18", + "created_at": "2026-06-30T10:14:00.030Z", + "data": { + "payment_id": "pay_fixture_01HZX4V9K3T", + "merchant_id": "merchant_fixture_01HZX4V9K3T", + "live": true, + "payment_state_version": 2, + "reference_id": "order_fixture_1042", + "status": "expired", + "amount": { + "unit": "iso4217/USD", + "value": "1000" + }, + "created_at": "2026-06-30T09:59:00.084Z", + "expires_at": "2026-06-30T10:14:00.000Z", + "processing": { + "processing_at": "2026-06-30T09:59:10.512Z", + "option_amount": { + "unit": "caip19/eip155:8453/erc20:0x833589fCD6eDb6E08f4c7C32D4f71b54bdA02913", + "value": "10000000" + }, + "fee": { + "kind": "base_plus_percent", + "base_amount": { + "unit": "caip19/eip155:8453/erc20:0x833589fCD6eDb6E08f4c7C32D4f71b54bdA02913", + "value": "0" + }, + "percent": { + "numerator": "29", + "denominator": "1000" + }, + "percent_amount": { + "unit": "caip19/eip155:8453/erc20:0x833589fCD6eDb6E08f4c7C32D4f71b54bdA02913", + "value": "290000" + }, + "total_amount": { + "unit": "caip19/eip155:8453/erc20:0x833589fCD6eDb6E08f4c7C32D4f71b54bdA02913", + "value": "290000" + } + }, + "settlement_amount": { + "unit": "caip19/eip155:8453/erc20:0x833589fCD6eDb6E08f4c7C32D4f71b54bdA02913", + "value": "9710000" + }, + "buyer_caip10": "eip155:8453:0x000000000000000000000000000000000000dEaD", + "chain_caip2": "eip155:8453" + }, + "success": null, + "failed": null, + "cancelled": null, + "expired": { + "expired_at": "2026-06-30T10:14:00.030Z" + }, + "settled": null + } +} +``` + +### payment.cancelled + +The payment was cancelled. `cancelled` is populated. `processing` is `null` when the payment was cancelled before the buyer committed, as in this example, and populated when the payment had started processing before cancellation. + +```json payment.cancelled expandable +{ + "id": "evt_fixture_payment_cancelled", + "type": "payment.cancelled", + "api_version": "2026-05-18", + "created_at": "2026-06-30T10:00:00.271Z", + "data": { + "payment_id": "pay_fixture_01HZX4V9K3T", + "merchant_id": "merchant_fixture_01HZX4V9K3T", + "live": true, + "payment_state_version": 1, + "reference_id": "order_fixture_1042", + "status": "cancelled", + "amount": { + "unit": "iso4217/USD", + "value": "1000" + }, + "created_at": "2026-06-30T09:59:00.084Z", + "expires_at": "2026-06-30T10:14:00.000Z", + "processing": null, + "success": null, + "failed": null, + "cancelled": { + "cancelled_at": "2026-06-30T10:00:00.271Z", + "cancellation_reason": "merchant cancelled" + }, + "expired": null, + "settled": null + } +} +``` + +### payment.settled + +Merchant settlement completed for a succeeded payment. `settled` is populated, and `status` stays `succeeded`; settlement is a stage, not a status. `settled.amount` is what actually settled to the merchant. + +```json payment.settled expandable +{ + "id": "evt_fixture_payment_settled", + "type": "payment.settled", + "api_version": "2026-05-18", + "created_at": "2026-06-30T10:05:00.696Z", + "data": { + "payment_id": "pay_fixture_01HZX4V9K3T", + "merchant_id": "merchant_fixture_01HZX4V9K3T", + "live": true, + "payment_state_version": 3, + "reference_id": "order_fixture_1042", + "status": "succeeded", + "amount": { + "unit": "iso4217/USD", + "value": "1000" + }, + "created_at": "2026-06-30T09:59:00.084Z", + "expires_at": "2026-06-30T10:14:00.000Z", + "processing": { + "processing_at": "2026-06-30T09:59:10.512Z", + "option_amount": { + "unit": "caip19/eip155:8453/erc20:0x833589fCD6eDb6E08f4c7C32D4f71b54bdA02913", + "value": "10000000" + }, + "fee": { + "kind": "base_plus_percent", + "base_amount": { + "unit": "caip19/eip155:8453/erc20:0x833589fCD6eDb6E08f4c7C32D4f71b54bdA02913", + "value": "0" + }, + "percent": { + "numerator": "29", + "denominator": "1000" + }, + "percent_amount": { + "unit": "caip19/eip155:8453/erc20:0x833589fCD6eDb6E08f4c7C32D4f71b54bdA02913", + "value": "290000" + }, + "total_amount": { + "unit": "caip19/eip155:8453/erc20:0x833589fCD6eDb6E08f4c7C32D4f71b54bdA02913", + "value": "290000" + } + }, + "settlement_amount": { + "unit": "caip19/eip155:8453/erc20:0x833589fCD6eDb6E08f4c7C32D4f71b54bdA02913", + "value": "9710000" + }, + "buyer_caip10": "eip155:8453:0x000000000000000000000000000000000000dEaD", + "chain_caip2": "eip155:8453" + }, + "success": { + "succeeded_at": "2026-06-30T10:00:00.271Z", + "tx_id": "0x8f1e5c1b9a4a1d2e3f405162738495a6b7c8d9e0f1a2b3c4d5e6f708192a3b4c" + }, + "failed": null, + "cancelled": null, + "expired": null, + "settled": { + "settled_at": "2026-06-30T10:05:00.696Z", + "tx_id": "0x93d2f1a0b9c8e7d6f5a4b3c2d1e0f9a8b7c6d5e4f3a2b1c0d9e8f7a6b5c4d3e2", + "amount": { + "unit": "caip19/eip155:8453/erc20:0x833589fCD6eDb6E08f4c7C32D4f71b54bdA02913", + "value": "9710000" + } + } + } +} +``` + +## Next steps + + + + Endpoint setup, signature verification, and delivery guarantees. + + + Drive a test payment through every transition and watch these events arrive. + + From 77fbdb50d5ceeecbd79449a4c47897d31f65556e Mon Sep 17 00:00:00 2001 From: Cheun Kitabayashi Date: Fri, 31 Jul 2026 11:34:54 +0200 Subject: [PATCH 2/8] docs(nav): add webhook event reference to Merchant API group Co-Authored-By: Claude Fable 5 --- docs.json | 2 ++ 1 file changed, 2 insertions(+) diff --git a/docs.json b/docs.json index 4c8612b..494e95a 100644 --- a/docs.json +++ b/docs.json @@ -58,6 +58,7 @@ { "group": "Merchant API", "pages": [ + "payments/webhook-event-reference", "payments/merchant-api/logo-specification" ] }, @@ -217,6 +218,7 @@ { "group": "Merchant API", "pages": [ + "payments/webhook-event-reference", "payments/merchant-api/logo-specification" ] }, From dc957be230a86096a1f1c79906ab13faf1fda942 Mon Sep 17 00:00:00 2001 From: Cheun Kitabayashi Date: Fri, 31 Jul 2026 12:07:10 +0200 Subject: [PATCH 3/8] docs(webhooks): document expires_at as non-null The frozen webhook schema is being re-cut to make data.expires_at non-null before any partner consumes it, aligning with the REST contract and pay-core, which always defaults expiry to 15 minutes. Co-Authored-By: Claude Fable 5 --- payments/webhook-event-reference.mdx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/payments/webhook-event-reference.mdx b/payments/webhook-event-reference.mdx index b4e9345..6faa5c0 100644 --- a/payments/webhook-event-reference.mdx +++ b/payments/webhook-event-reference.mdx @@ -38,7 +38,7 @@ Every event has the same top-level shape. | `status` | string | Payment status at event time: `requires_action`, `processing`, `succeeded`, `failed`, `expired`, or `cancelled`. There is no `settled` status; settlement is a stage of a succeeded payment. | | `amount` | [Amount](#amount) | The requested payment amount. | | `created_at` | string | When the payment was created. | -| `expires_at` | string \| null | When the payment stops accepting buyer action, or `null` if it does not expire. | +| `expires_at` | string | When the payment stops accepting buyer action. | | `processing` | [processing](#processing) \| null | Populated once the buyer has committed to a payment option. | | `success` | [success](#success) \| null | Populated once the payment succeeded. | | `failed` | [failed](#failed) \| null | Populated once the payment failed. | From 434fcf54d6bd56010fb7ceb56f475d2165b6169e Mon Sep 17 00:00:00 2001 From: Cheun Kitabayashi Date: Fri, 31 Jul 2026 12:19:07 +0200 Subject: [PATCH 4/8] docs(webhooks): document reference_id as non-null Same re-cut rationale as expires_at: reference_id is required at payment creation in core and in the REST contract, so the webhook schema is being tightened before any partner consumes it. Co-Authored-By: Claude Fable 5 --- payments/webhook-event-reference.mdx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/payments/webhook-event-reference.mdx b/payments/webhook-event-reference.mdx index 6faa5c0..6e7cd79 100644 --- a/payments/webhook-event-reference.mdx +++ b/payments/webhook-event-reference.mdx @@ -34,7 +34,7 @@ Every event has the same top-level shape. | `merchant_id` | string | Merchant identifier, the same external merchant id used across the Merchant API. | | `live` | boolean | Whether this is a live payment. Test-mode payments carry `false` and are only delivered to test-mode endpoints. Verify `live` is `true` before driving real fulfillment. | | `payment_state_version` | integer | Monotonic per-payment version, starting at `0` on the first event. The ordering guard: ignore any event whose version is lower than or equal to one already processed for the same `payment_id`. See [Out-of-order delivery](/payments/webhooks#out-of-order-delivery). | -| `reference_id` | string \| null | The merchant-provided order reference attached at payment creation, or `null`. Primary reconciliation key against your own records. | +| `reference_id` | string | The merchant-provided order reference attached at payment creation. Primary reconciliation key against your own records. | | `status` | string | Payment status at event time: `requires_action`, `processing`, `succeeded`, `failed`, `expired`, or `cancelled`. There is no `settled` status; settlement is a stage of a succeeded payment. | | `amount` | [Amount](#amount) | The requested payment amount. | | `created_at` | string | When the payment was created. | From ab265cec7288c579ec854ee08c44dec538e5c123 Mon Sep 17 00:00:00 2001 From: Cheun Kitabayashi Date: Fri, 31 Jul 2026 12:19:27 +0200 Subject: [PATCH 5/8] docs(webhooks): payment.cancelled always carries processing null Core only allows cancellation from requires_action, so the schema is being tightened to drop the cancelled-after-processing variant before any partner consumes the webhook. Co-Authored-By: Claude Fable 5 --- payments/webhook-event-reference.mdx | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/payments/webhook-event-reference.mdx b/payments/webhook-event-reference.mdx index 6e7cd79..658eac9 100644 --- a/payments/webhook-event-reference.mdx +++ b/payments/webhook-event-reference.mdx @@ -57,10 +57,10 @@ Which stage objects are non-null depends on the event type. | `payment.succeeded` | `succeeded` | present | present | null | null | null | usually null | | `payment.failed` | `failed` | may be present | null | present | null | null | null | | `payment.expired` | `expired` | may be present | null | null | null | present | null | -| `payment.cancelled` | `cancelled` | may be present | null | null | present | null | null | +| `payment.cancelled` | `cancelled` | null | null | null | present | null | null | | `payment.settled` | `succeeded` | present | present | null | null | null | present | -"May be present" means the payment can reach that outcome either before or after processing started: `processing` is `null` when the buyer never committed to a payment option, and populated otherwise. On `payment.succeeded`, `settled` is `null` unless settlement had already completed at snapshot time; settlement completion is carried by `payment.settled`. +"May be present" means the payment can fail or expire either before or after processing started: `processing` is `null` when the buyer never committed to a payment option, and populated otherwise. Cancellation is only possible while the payment awaits buyer action, so `payment.cancelled` always carries `processing: null`. On `payment.succeeded`, `settled` is `null` unless settlement had already completed at snapshot time; settlement completion is carried by `payment.settled`. ## Objects @@ -417,7 +417,7 @@ The payment expired before completing. `expired` is populated. A payment can exp ### payment.cancelled -The payment was cancelled. `cancelled` is populated. `processing` is `null` when the payment was cancelled before the buyer committed, as in this example, and populated when the payment had started processing before cancellation. +The payment was cancelled. `cancelled` is populated. A payment can only be cancelled while it awaits buyer action, so `processing` is always `null`. ```json payment.cancelled expandable { From b0ddce0b7911cef3b5ec8a0a4112c8b8df8ce441 Mon Sep 17 00:00:00 2001 From: Cheun Kitabayashi Date: Fri, 31 Jul 2026 12:47:15 +0200 Subject: [PATCH 6/8] docs(nav): keep webhook event reference out of navigation until rollout Co-Authored-By: Claude Fable 5 --- docs.json | 2 -- 1 file changed, 2 deletions(-) diff --git a/docs.json b/docs.json index 494e95a..4c8612b 100644 --- a/docs.json +++ b/docs.json @@ -58,7 +58,6 @@ { "group": "Merchant API", "pages": [ - "payments/webhook-event-reference", "payments/merchant-api/logo-specification" ] }, @@ -218,7 +217,6 @@ { "group": "Merchant API", "pages": [ - "payments/webhook-event-reference", "payments/merchant-api/logo-specification" ] }, From 0cfa48ae2e44604bc6b1a2e601c85c06e071b2bb Mon Sep 17 00:00:00 2001 From: Cheun Kitabayashi Date: Fri, 31 Jul 2026 17:30:05 +0200 Subject: [PATCH 7/8] docs(webhooks): link the guide to the event reference Trims the stage-block details the reference now owns and points to it from the payload table and the next-steps cards. Co-Authored-By: Claude Fable 5 --- payments/webhooks.mdx | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/payments/webhooks.mdx b/payments/webhooks.mdx index a8c6cc6..7f65906 100644 --- a/payments/webhooks.mdx +++ b/payments/webhooks.mdx @@ -68,7 +68,7 @@ The fields inside `data` you'll use most: | `payment_state_version` | Monotonically increasing integer per payment. **Your ordering key**; see [Out-of-order delivery](#out-of-order-delivery). | | `live` | `false` for test-mode payments and test deliveries. **Check `live === true` before driving real fulfillment.** | | `amount` | The requested amount: `{ "unit": "iso4217/USD", "value": "1000" }`, where `value` is an integer string in the smallest unit. | -| `processing`, `success`, `failed`, `cancelled`, `expired`, `settled` | Stage detail blocks, `null` until the payment reaches that stage. `success` and `settled` include `tx_id` (chain-specific transaction id, may be `null`); `failed` and `cancelled` include human-readable `failure_reason` / `cancellation_reason`. | +| `processing`, `success`, `failed`, `cancelled`, `expired`, `settled` | Stage detail blocks, `null` until the payment reaches that stage. Every field, with types and a full example per event type: [Webhook event reference](/payments/webhook-event-reference). | A `payment.succeeded` event looks like this: @@ -522,6 +522,9 @@ Webhook configuration does **not** carry over from test to live; they are separa ## Next steps + + Every field of every event type, with types, nullability, and examples. + Simulate every payment status transition and watch the webhooks arrive. From 0133a3d21f36d5712e2dab6d15223b2ec8a9ad4f Mon Sep 17 00:00:00 2001 From: Cheun Kitabayashi Date: Fri, 31 Jul 2026 18:20:14 +0200 Subject: [PATCH 8/8] docs(webhooks): reference_id is not unique across payments Co-Authored-By: Claude Fable 5 --- payments/webhook-event-reference.mdx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/payments/webhook-event-reference.mdx b/payments/webhook-event-reference.mdx index 658eac9..cbe473e 100644 --- a/payments/webhook-event-reference.mdx +++ b/payments/webhook-event-reference.mdx @@ -34,7 +34,7 @@ Every event has the same top-level shape. | `merchant_id` | string | Merchant identifier, the same external merchant id used across the Merchant API. | | `live` | boolean | Whether this is a live payment. Test-mode payments carry `false` and are only delivered to test-mode endpoints. Verify `live` is `true` before driving real fulfillment. | | `payment_state_version` | integer | Monotonic per-payment version, starting at `0` on the first event. The ordering guard: ignore any event whose version is lower than or equal to one already processed for the same `payment_id`. See [Out-of-order delivery](/payments/webhooks#out-of-order-delivery). | -| `reference_id` | string | The merchant-provided order reference attached at payment creation. Primary reconciliation key against your own records. | +| `reference_id` | string | The merchant-provided order reference attached at payment creation. Primary reconciliation key against your own records. Not unique: a new payment for the same order (for example a checkout retry) carries the same reference. | | `status` | string | Payment status at event time: `requires_action`, `processing`, `succeeded`, `failed`, `expired`, or `cancelled`. There is no `settled` status; settlement is a stage of a succeeded payment. | | `amount` | [Amount](#amount) | The requested payment amount. | | `created_at` | string | When the payment was created. |