Webhook Events reference
This page is a payload schema dictionary for the webhook events that CROSS PAY API delivers. For emission points, integration flow, and merchant-side handling patterns, see Webhooks Overview.
This page focuses on the JSON body schema of each envelope.
- Signature verification and replay protection → Verifying Webhooks
- Retry, idempotency, dedup → Delivery & Retry
- Production checklist → Production Checklist
HTTP request headers
| Header | Format | Description |
|---|---|---|
Content-Type | application/json | Body encoding |
webhook-id | UUID | Outbox event identifier. Use as the idempotency dedup key |
webhook-timestamp | Unix epoch seconds (string) | Send time. For replay protection (recommended ±300s) |
webhook-signature | v1,<base64> (space-separated for multiple) | HMAC-SHA256 signature over ${id}.${ts}.${body} |
For the signature verification algorithm and code examples, see Verifying Webhooks.
Common envelope
CROSS PAY uses the Standard Webhooks v1 envelope specification.
{
"type": "<dot-delimited event name>",
"timestamp": "<ISO-8601 UTC, time the domain event occurred>",
"data": {
"payment": { ... },
"refund": { ... }
}
}
| Field | Type | Description |
|---|---|---|
type | string | Dot-delimited event name. e.g., payment.completed |
timestamp | ISO-8601 | When the domain event occurred (paid_at / failed_at / ...). UTC Z |
data | object | Per-event payload. The payment object is always present |
data.payment common fields
The data.payment object across all events is drawn from the following field
set. Whether a field is present depends on the event type.
| Field | Type | Always? | Description |
|---|---|---|---|
id | string | ✓ | Payment identifier (pay_...) |
status | string | ✓ | Payment status (fixed per event — see table below) |
currency | string | ✓ | ISO-4217 currency code. e.g., KRW, USD |
amount | string | ✓ | Payment amount. Serialized as a string to avoid precision loss |
payer_uid | string | ✓ | Payer identifier |
metadata | object | ✓ | Metadata supplied by the merchant at payment creation. {} if empty |
receipt_id | string | PG-issued receipt ID (payment.completed only) | |
provider | string | PG that processed the payment (payment.completed only). e.g., binancepay | |
failure_reason | string | Failure reason enum (payment.failed only). See enum table below | |
paid_at | ISO-8601 | Payment confirmation time (payment.completed) | |
cancelled_at | ISO-8601 | Cancellation time (payment.cancelled) | |
failed_at | ISO-8601 | Failure time (payment.failed) | |
expired_at | ISO-8601 | Expiry time (payment.expired) |
status value per event
| Event | Previous state | status value |
|---|---|---|
payment.completed | PROCESSING | PAID |
payment.failed | PROCESSING | FAILED |
payment.expired | PENDING or PROCESSING | EXPIRED |
payment.cancelled | PENDING | CANCELLED |
PROCESSING transition rulepayment.completed and payment.failed always terminate via PROCESSING
(starting when POST /checkout/{id}/quotes creates a PG session). Until the
PG webhook arrives, the merchant cannot call cancel on its own — the
request is rejected with 409 INVALID_TRANSITION. For the full lifecycle,
see Webhooks Overview.
failure_reason enum
| Value | Meaning |
|---|---|
UNKNOWN | Failure not classified by the PG. Fallback |
DECLINED | The PG declined the payment |
TX_REVERTED | On-chain TX was mined but the contract call reverted |
AMOUNT_MISMATCH | On-chain event amount does not match the expected value |
TOKEN_MISMATCH | On-chain event token does not match the expected value |
ONCHAIN_TIMEOUT | The on-chain confirmation was not observed before expiry |
This set is open to extension. Merchants should fall back to UNKNOWN for
values they do not recognize.
Payment state machine
PENDING → PAID/FAILEDis not allowed directly. Before the PG/chain reports a result, the payment must first transition throughPROCESSING.PROCESSING → CANCELLEDis blocked. To avoid races with the PG, the merchant cancel API rejects this transition with409 INVALID_TRANSITION.- Terminal states (
PAID/FAILED/EXPIRED/CANCELLED/*_REFUNDED) do not transition to other terminal states. The same payment never emits two different terminal events.
payment.completed
When the PG or chain reports confirmation, payment transitions to PAID
and this event is emitted.
Fields
| Field | Required | Notes |
|---|---|---|
id | ✓ | |
status | ✓ | Always PAID |
currency | ✓ | |
amount | ✓ | |
payer_uid | ✓ | |
receipt_id | ✓ | PG-issued receipt ID |
provider | ✓ | PG identifier (binancepay, payletter, ...) |
paid_at | ✓ | |
metadata | ✓ |
Sample
{
"type": "payment.completed",
"timestamp": "2026-04-21T12:00:00Z",
"data": {
"payment": {
"id": "pay_abc123",
"status": "PAID",
"currency": "KRW",
"amount": "50000",
"payer_uid": "user_789",
"receipt_id": "rcpt_xyz",
"provider": "binancepay",
"paid_at": "2026-04-21T12:00:00Z",
"metadata": { "order_id": "shop-order-789" }
}
}
}
Settlement-level fee information is not included in this payload. If you
need it, enrich via GET /v1/payments/:id.
payment.failed
When the PG or chain reports failure, payment transitions to FAILED and
this event is emitted.
Fields
| Field | Required | Notes |
|---|---|---|
id | ✓ | |
status | ✓ | Always FAILED |
currency | ✓ | |
amount | ✓ | |
payer_uid | ✓ | |
failure_reason | ✓ | See failure_reason enum above |
failed_at | ✓ | |
metadata | ✓ |
Sample
{
"type": "payment.failed",
"timestamp": "2026-04-21T12:05:00Z",
"data": {
"payment": {
"id": "pay_abc123",
"status": "FAILED",
"currency": "KRW",
"amount": "50000",
"payer_uid": "user_789",
"failure_reason": "DECLINED",
"failed_at": "2026-04-21T12:05:00Z",
"metadata": { "order_id": "shop-order-789" }
}
}
}
payment.expired
When the session TTL elapses, the expiry worker transitions in-flight
payments (PENDING or PROCESSING) to EXPIRED and emits this event.
Fields
| Field | Required | Notes |
|---|---|---|
id | ✓ | |
status | ✓ | Always EXPIRED |
currency | ✓ | |
amount | ✓ | |
payer_uid | ✓ | |
expired_at | ✓ | |
metadata | ✓ |
Sample
{
"type": "payment.expired",
"timestamp": "2026-04-21T12:30:00Z",
"data": {
"payment": {
"id": "pay_abc123",
"status": "EXPIRED",
"currency": "KRW",
"amount": "50000",
"payer_uid": "user_789",
"expired_at": "2026-04-21T12:30:00Z",
"metadata": { "order_id": "shop-order-789" }
}
}
}
payment.cancelled
When the merchant explicitly cancels a PENDING payment via
POST /v1/payments/{id}/cancel, this event is emitted. Cancel calls on a
PROCESSING payment are rejected with 409 INVALID_TRANSITION, so this
event does not fire for that flow — it terminates as payment.expired or
payment.failed instead.
Fields
| Field | Required | Notes |
|---|---|---|
id | ✓ | |
status | ✓ | Always CANCELLED |
currency | ✓ | |
amount | ✓ | |
payer_uid | ✓ | |
cancelled_at | ✓ | |
metadata | ✓ |
Sample
{
"type": "payment.cancelled",
"timestamp": "2026-04-21T12:10:00Z",
"data": {
"payment": {
"id": "pay_abc123",
"status": "CANCELLED",
"currency": "KRW",
"amount": "50000",
"payer_uid": "user_789",
"cancelled_at": "2026-04-21T12:10:00Z",
"metadata": { "order_id": "shop-order-789" }
}
}
}
payment.refunded (Preview)
The refund-completed event envelope (including the data.refund object) is
defined in code, but no emission path is currently wired up in the CROSS
PAY API. This event does not reach the merchant endpoint. To check refund
results, query GET /v1/payments/:id.
Once the emission path is wired up, this page will be updated with the official schema and samples.
Known constraints
payment.createdis not emitted. Payment creation results are delivered synchronously in thePOST /v1/paymentsresponse.- Empty
metadatais serialized as{}. It is nevernull. - All time fields are ISO-8601 UTC (
Zsuffix). amountis a string. Serialized as a string rather than a number to avoid precision loss. Parse with a decimal library on the merchant side.