Skip to main content

Events and payloads

The event type tells you what changed; the snapshots in data tell you the current state.

Payload structure​

Order and payment events, payout events and dispute events each have their own envelope. Branch on type, or call isPayoutWebhook and isDisputeWebhook from @zkp2p/pay-sdk, before reading data.

type OrderWebhookPayload = {
id: string;
type: OrderWebhookEventTypeValue; // every event type except PAYOUT_ORDER_* and DISPUTE_*
timestamp: string;
data: {
order: CheckoutOrder | null;
payment: CheckoutPayment | null;
refund: Record<string, unknown> | null;
paymentBridge: Record<string, unknown> | null;
// Chargeback events only
trigger?:
| { type: 'CHARGEBACK_APPLIED'; chargeback: PaymentChargebackFact }
| { type: 'PAYMENT_SETTLED'; paymentId: string };
// ORDER_RESIZED only
resize?: { previousAmountUsdc: string; newAmountUsdc: string };
// ORDER_AMOUNT_SET only
amountChange?: {
previousAmountUsdc: string | null;
newAmountUsdc: string;
fiat: { amount: string; currency: string }; // as typed, in the customer's selected currency
amountVersion: number;
};
};
};

type PayoutWebhookPayload =
| {
id: string;
type: Exclude<PayoutWebhookEventTypeValue, 'PAYOUT_ORDER_PARTIALLY_PAID'>; // every other PAYOUT_ORDER_*
timestamp: string;
version: 2;
data: PayoutView;
}
| {
id: string;
type: 'PAYOUT_ORDER_PARTIALLY_PAID';
timestamp: string;
version: 2;
data: PayoutView;
partialPayment: PayoutPartialPayment;
};

type DisputeWebhookPayload = {
id: string; // dispute_<disputeId>_<type>
type: DisputeWebhookEventTypeValue;
timestamp: string;
version: 1;
data: DisputeWebhookData;
};

type WebhookPayload = OrderWebhookPayload | PayoutWebhookPayload | DisputeWebhookPayload;

id is the same on every retry of a delivery; deduplicate on it. Payout and dispute ids are also the same on every webhook endpoint you have.

What each event carries​

EventFires whendata.paymentExtras
ORDER_CREATEDAn order is creatednullTest deliveries reuse this type with data.test: true and every snapshot null
ORDER_FULFILLEDThe order reaches FULFILLEDThe payment whose settlement completed the order
ORDER_CANCELLEDYou cancel a CREATED order with no payment attempts, from the dashboardnull
ORDER_RESIZEDA customer resizes a dynamic ordernulldata.resize
ORDER_AMOUNT_SETA customer starts a payment with a new amount on an open-amount ordernulldata.amountChange
PAYMENT_CREATEDA payment attempt startsPopulated
PAYMENT_SETTLEDA payment settlesPopulatedOn Relay crypto payments, fee and amount fields can be refined a few minutes later; read the payment back for final values
PAYMENT_PAYOUT_RELEASEDA held payment's USDC is released to youPopulated, with payout.status: "RELEASED"See deferred payment payouts
PAYMENT_FAILEDAn attempt could not be started, or a crypto transfer failed or stalledPopulatedA stalled crypto attempt can still settle if its deposit lands late
PAYMENT_EXPIREDAn open attempt passes its payment windowPopulatedSee PAYMENT_EXPIRED
PAYMENT_CANCELLEDA payment attempt is cancelledPopulated
REFUND_PENDINGA refund startsnulldata.refund
REFUND_COMPLETEDA refund completesnulldata.refund
PAYMENT_BRIDGE_PENDINGA conversion to your payout chain or token is queued after settlementPopulateddata.paymentBridge
PAYMENT_BRIDGE_SUBMITTEDThe conversion is submittedPopulateddata.paymentBridge
PAYMENT_BRIDGE_COMPLETEDThe destination transfer is confirmedPopulateddata.paymentBridge
PAYMENT_BRIDGE_FAILEDThe destination transfer failedPopulateddata.paymentBridge
PAYMENT_CHARGEBACKEDA settled payment is charged backPopulateddata.trigger
ORDER_PARTIALLY_CHARGEBACKEDSome, but not all, settled payments on the order carry a chargebackPopulateddata.trigger
ORDER_CHARGEBACKEDEvery settled payment on the order carries a chargebackPopulateddata.trigger
PAYOUT_ORDER_*A payout changesn/adata is a PayoutView; see payout events
DISPUTE_OPENEDA seller reports a payment-app case on your order that no buyer stake covers, and you're asked to pay the sellern/adata is DisputeWebhookData; see dispute events
DISPUTE_PAIDYour USDC payment to the seller is confirmed on Basen/aAs above
DISPUTE_ESCALATEDThe 7-day deadline passed unpaid, and the Peer team takes overn/aAs above

Dispute events​

At the 7.1.0 rollout, existing LIVE webhook endpoints are auto-subscribed to DISPUTE_OPENED, DISPUTE_PAID, and DISPUTE_ESCALATED. This is a breaking integration change for handlers that assume data.order exists on every delivery: disputes use DisputeWebhookData, which has no data.order. Branch on the event type or isDisputeWebhook before reading order data. New endpoints and sandbox endpoints must explicitly subscribe to dispute events.

A seller dispute asks you to pay a seller for a reported payment-app case that no buyer stake covers. It is separate from a chargeback, which records compensation from buyer stake onchain. Dispute events never change order fulfillment or payment settlement.

DisputeWebhookPayload has version: 1 (DISPUTE_WEBHOOK_VERSION) and the dedupe id dispute_<disputeId>_<type>. data is DisputeWebhookData:

FieldTypeMeaning
disputeIdstringDispute id
orderIdstringYour order id
paymentIdstringThe settled fiat payment
intentHashstringLower-case 0x intent hash
amountUsdcstringUSDC owed to the seller, in raw 6-decimal units: "12500000" is 12.5 USDC
caseAmountstringFiat case amount with 2 decimals, such as "12.40"
caseCurrencystringFiat currency code, such as USD
sellerAddressstringSeller's lower-case 0x wallet address on Base
statusDisputeStatusTypeOPEN, PAID, or ESCALATED
dueAtstringISO 8601 deadline, 7 days after opening
paidTxHashstring | nullLower-case Base transaction hash once paid; otherwise null

An escalated dispute can still be paid, which sends DISPUTE_PAID.

data.order​

The CheckoutOrder, present on every order and payment event except the test delivery. Key fields: id, merchantId, status, amountMode, requestedUsdcAmount and remainingUsdcAmount (both null on an open-amount order until a payment saves its amount), openAmount and amountVersion (open-amount orders only), and the destination* fields.

metadata is null unless the order was created with a fiat price. Then it carries requestAmountInputMode, requestedFiatAmount, requestedFiatCurrency and requestedUsdcAmountAtCreation. An open-amount order gains the same keys when a payment saves its amount, holding the saved amount in the currency the customer typed.

A resized order keeps its original fiat snapshot

After ORDER_RESIZED, metadata.requestAmountResized is true, but the fiat fields still describe the price at creation. Reconcile a resized order on data.order.requestedUsdcAmount and data.resize, not on them.

data.payment​

The CheckoutPayment, present on ORDER_FULFILLED, every PAYMENT_* event, the bridge events and the chargeback events. On ORDER_FULFILLED it is the payment that completed the order, not a summary of every payment.

Key fields:

  • id, orderId, rail
  • paymentAmount and currency: what the customer paid, in their currency. "0" until the payment settles
  • currencyPerUsdRate: the exchange rate captured for that payment
  • netSettledUsdcAmount: what settled to you after fees
  • totalUsdcFeeAmount: fees in USDC (gross released minus net settled), not counting the seller's spread
  • penalties: always present, empty when nothing applied
  • fulfillTransaction: the settlement transaction hash, on Base for payment-app settlements and on the destination chain for Relay crypto payments (use the order's destinationChainId to pick the explorer)

On Relay crypto payments, totalUsdcFeeAmount, referralFees, paymentAmount, currency and currencyPerUsdRate in PAYMENT_SETTLED can be estimates that Peer refines within minutes, without another event. fulfillTransaction never changes, and netSettledUsdcAmount only increases, when another deposit to the same address also succeeds.

How these reconcile against the order amount under each fee mode is in fees, settlement and refunds.

Penalised payment​

The customer sent 50.00 with purchase protection on, on an order where you pay the fees. The attestation credited 48.50 (penaltyBps is authoritative; see payment penalties) and fees came off that. paymentAmount still records the 50.00 sent; if the customer paid the fees, it would be the credited 48.50.

{
"id": "pay_456",
"rail": "venmo",
"currency": "USD",
"paymentAmount": "50.00",
"penalties": [
{ "kind": "PURCHASE_PROTECTION", "penaltyBps": 300, "originalAmount": "50.00", "attestedAmount": "48.50" }
],
"netSettledUsdcAmount": "48.015",
"totalUsdcFeeAmount": "0.485"
}

Settled payment example​

{
"id": "evt_abc123",
"type": "PAYMENT_SETTLED",
"timestamp": "2026-04-03T10:00:00.000Z",
"data": {
"order": {
"id": "ord_123",
"merchantId": "merchant_123",
"status": "FULFILLED",
"requestedUsdcAmount": "50",
"remainingUsdcAmount": "0",
"destinationAddress": "0x742d...",
"destinationToken": "USDC",
"destinationChainId": "8453"
},
"payment": {
"id": "pay_123",
"orderId": "ord_123",
"rail": "venmo",
"paymentAmount": "50.00",
"penalties": [],
"netSettledUsdcAmount": "49.50",
"fulfillTransaction": "0xabc..."
},
"refund": null,
"paymentBridge": null
}
}

PAYMENT_EXPIRED​

PAYMENT_EXPIRED means the payment window elapsed. It doesn't cancel the order, and a late settlement can still send PAYMENT_SETTLED and ORDER_FULFILLED for the same payment.

AttemptOutcome
FiatExpires when its 1-hour window elapses, and sends PAYMENT_EXPIRED
Live Relay cryptoNever expires. A transfer that fails, or is still unresolved about 6 hours 20 minutes after creation, sends PAYMENT_FAILED instead
Live Zcash, never fundedExpires about 6 hours 20 minutes after creation, sends PAYMENT_EXPIRED, and can still settle if a deposit lands later
Sandbox cryptoExpires on its quote window like fiat, and sends PAYMENT_EXPIRED

ORDER_RESIZED​

data.order carries the resized amounts and data.resize the previous and new amounts; data.payment, data.refund and data.paymentBridge are null. A customer can resize only a CREATED order with nothing paid and no open or expired payment attempt. Credit the amounts from data.order and data.resize, not the amount you sent at creation.

{
"id": "evt_resize123",
"type": "ORDER_RESIZED",
"timestamp": "2026-04-03T10:15:00.000Z",
"data": {
"order": {
"id": "ord_456",
"merchantId": "merchant_123",
"status": "CREATED",
"requestedUsdcAmount": "45.5",
"remainingUsdcAmount": "45.5",
"destinationAddress": "0x742d...",
"destinationToken": "USDC",
"destinationChainId": "8453"
},
"payment": null,
"refund": null,
"paymentBridge": null,
"resize": {
"previousAmountUsdc": "50.000000",
"newAmountUsdc": "45.500000"
}
}
}

ORDER_AMOUNT_SET​

Sent when a customer starts a payment with a new amount on an open-amount order, so always on the first payment. data.order carries the saved amounts; data.payment, data.refund and data.paymentBridge are null. data.amountChange holds the previous and new USDC amounts, the amount the customer typed with its currency (fiat, which can differ from openAmount.currency), and the order's new amountVersion.

amountVersion goes up by one each time the amount is saved. Deliveries can arrive out of order, so keep the highest version you have seen. The event is informational: credit the customer on ORDER_FULFILLED.

{
"id": "evt_amount123",
"type": "ORDER_AMOUNT_SET",
"timestamp": "2026-10-01T13:05:12.000Z",
"data": {
"order": {
"id": "ord_789",
"merchantId": "merchant_123",
"status": "CREATED",
"amountMode": "OPEN",
"requestedUsdcAmount": "100",
"remainingUsdcAmount": "100",
"openAmount": {
"currency": "USD",
"minAmount": "10.00",
"maxAmount": "2000.00",
"presets": ["25.00", "50.00", "100.00", "250.00"]
},
"amountVersion": 1
},
"payment": null,
"refund": null,
"paymentBridge": null,
"amountChange": {
"previousAmountUsdc": null,
"newAmountUsdc": "100.000000",
"fiat": { "amount": "100.00", "currency": "USD" },
"amountVersion": 1
}
}
}

Refund events​

On REFUND_PENDING and REFUND_COMPLETED, data.order has refundStatus, refundAmountUsdc, refundDepositId and refundTransactionHash set, and data.payment and data.paymentBridge are null. data.refund carries:

{
orderId: string;
status: 'PENDING' | 'COMPLETED';
amountUsdc: string | null;
depositId: string | null;
txHash: string | null;
sellerUsername: string | null;
processorName: string | null;
payeeDetails: string | null;
metadata: unknown | null;
}

Chargeback events​

Venmo and PayPal payments can be charged back after settlement. A chargeback never changes data.order.status or data.payment.status, and each event carries full current snapshots, not a delta.

  • PAYMENT_CHARGEBACKED: a fiat payment was charged back, always in full.
  • ORDER_PARTIALLY_CHARGEBACKED: some, but not all, settled payments on the order carry a chargeback.
  • ORDER_CHARGEBACKED: every settled payment on the order carries a chargeback. A charged-back single-payment order goes straight here.

The order's chargeback status can move backwards: from CHARGEBACKED to PARTIALLY_CHARGEBACKED when another payment settles, and back if that one is charged back too. Read data.order.chargebackStatus, data.order.chargebacks, data.payment.chargebackStatus and data.payment.chargeback. data.trigger says why the order changed: a chargeback applied (CHARGEBACK_APPLIED, with the chargeback fact) or another payment settled (PAYMENT_SETTLED, with its paymentId).

Bridge state, when present, stays in data.paymentBridge. The fact's bridgeOrSweepOverlap records the latest bridge attempt and its status when the chargeback was applied; a chargeback never rewrites bridge history.

{
"id": "evt_chargeback123",
"type": "PAYMENT_CHARGEBACKED",
"timestamp": "2026-08-20T10:00:00.000Z",
"data": {
"order": {
"id": "ord_123",
"status": "FULFILLED",
"chargebackStatus": "CHARGEBACKED",
"chargebacks": [
{
"paymentId": "pay_123",
"intentHash": "0xintent",
"disputeId": "7",
"status": "CHARGEBACKED",
"compensatedUsdcAmount": "50",
"disputedAt": "2026-08-20T09:59:00.000Z",
"disputeTxHash": "0xdispute",
"refundOverlap": "NONE",
"bridgeOrSweepOverlap": null
}
]
},
"payment": {
"id": "pay_123",
"status": "SETTLED",
"chargebackStatus": "CHARGEBACKED",
"chargeback": { "paymentId": "pay_123", "disputeId": "7", "...": "same fact as above" }
},
"refund": null,
"paymentBridge": null,
"trigger": {
"type": "CHARGEBACK_APPLIED",
"chargeback": { "paymentId": "pay_123", "disputeId": "7", "...": "same fact as above" }
}
}
}

Bridge events​

PAYMENT_BRIDGE_PENDING, PAYMENT_BRIDGE_SUBMITTED, PAYMENT_BRIDGE_COMPLETED and PAYMENT_BRIDGE_FAILED track the conversion of a settled payment to your payout chain or token. They are sent in addition to the order and payment events, only when a conversion applies.

data.paymentBridge carries id, paymentId, status (PENDING, SUBMITTED, COMPLETED or FAILED), attemptNumber, inputUsdcAmount, destinationChainId, destinationToken, destinationAddress, quotedOutputAmount, minOutputAmount, actualOutputAmount, destinationTxHash, and failureCode and failureMessage, which are set only on FAILED.

Bridge failures and automatic retries​

Peer can retry a bridge automatically when it failed before any funds could have left your Base wallet:

  1. A failure a retry can't fix, such as a refund by the conversion service or the 24-hour cap, sends PAYMENT_BRIDGE_FAILED at once.
  2. A failure that is safe to retry sends nothing while it waits. The bridge returns to PENDING and is resubmitted up to 3 times, about 10 minutes, 1 hour and 3 hours after the first failure, sending PAYMENT_BRIDGE_SUBMITTED each time. It ends with PAYMENT_BRIDGE_COMPLETED, or with PAYMENT_BRIDGE_FAILED whose failureMessage ends with (after N automatic retries).
  3. A bridge already submitted stays SUBMITTED until it settles, for up to 24 hours.

While a retry is waiting, failureCode and failureMessage stay null. The payment's bridge view in the dashboard shows the retry schedule.

Payout events​

Every PAYOUT_ORDER_* event has data set to a PayoutView, the shape the Payouts API returns, not the { order, payment, refund, paymentBridge } shape. The envelope's version is 2; Peer bumps it only for a breaking change to the payout payload, so check it before reading data. PAYOUT_ORDER_PARTIALLY_PAID also carries partialPayment next to data.

EventFires when
PAYOUT_ORDER_CREATEDA payout is created
PAYOUT_ORDER_PARTIALLY_FUNDEDUSDC arrived below the payout and you sent less than funding.amount. Fires again each time more arrives; each on-time arrival moves funding.quoteExpiresAt to at least 24 hours later
PAYOUT_ORDER_FUNDEDThe payout is funded: depositAmount and fundedAt are set. A partly funded payout is funded with what arrived when the deadline passes
PAYOUT_ORDER_EXPIREDThe funding deadline passed with nothing received
PAYOUT_ORDER_CANCELLEDYou cancelled a payout with nothing received (cancelSource: "MERCHANT"), or the customer withdrew or spent it in the Peer app (cancelSource: "PEER_APP"). Only before any buyer paid part of it
PAYOUT_ORDER_MATCHEDA buyer starts paying the listing; status is PAYING. If their payment lapses, the payout returns to LISTING without a webhook, and the next buyer sends another PAYOUT_ORDER_MATCHED
PAYOUT_ORDER_PARTIALLY_PAIDA buyer paid part of the listing; once per payment
PAYOUT_ORDER_SETTLEDThe payout ended: a buyer paid what was left, a crypto payout arrived, the escrow swept a small rest as dust, or, after a partial payment, the customer took the rest. data.settlement says where the USDC went

Fiat fields are information only; payout, fee, refund and accounting amounts stay in USDC:

FieldMeaning
data.payoutCurrencyThe customer's fiat catalog currency, or null for crypto or no method
data.partialFills[].fiat{ currency, amount } for that buyer payment at its rate, or null when unknown
data.settlement.buyerPaidFiatOnce settled, one total per currency in first-paid order; null if any payment's fiat is unknown, [] if no buyer paid
partialPayment.fill.fiatPAYOUT_ORDER_PARTIALLY_PAID only: this payment's fiat at its rate, or null when unknown

After a currency change, totals can hold more than one currency, for example [{ "currency": "EUR", "amount": "44.65" }, { "currency": "GBP", "amount": "39.10" }]. A fiat amount is never estimated at a current market rate.

attempt.rail is a payout rail when set: venmo, cashapp, paypal, zelle, revolut, chime, a relay_<chainId> network or near_intents_133701 (see values).

A settled payout, paid by one buyer:

{
"id": "cashout_cmg1x7k2p0003s60h4f9z2q8d_CASHOUT_ORDER_SETTLED",
"type": "PAYOUT_ORDER_SETTLED",
"timestamp": "2026-09-24T18:52:40.017Z",
"version": 2,
"data": {
"payoutId": "cmg1x7k2p0003s60h4f9z2q8d",
"merchantReference": "withdrawal-8841",
"status": "SETTLED",
"fundingStatus": "FUNDED",
"payout": {
"amount": "100.00",
"chainId": 8453,
"tokenAddress": "0x833589fCD6eDb6E08f4c7C32D4f71b54bdA02913",
"decimals": 6
},
"merchantFee": { "bps": 100, "amount": "1.041237" },
"masterMerchantPayoutFee": { "bps": 0, "amount": "0.00" },
"funding": {
"chainId": 8453,
"tokenAddress": "0x833589fCD6eDb6E08f4c7C32D4f71b54bdA02913",
"decimals": 6,
"amount": "104.123712",
"depositAddress": null,
"quoteExpiresAt": "2026-09-25T18:31:12.408Z",
"receivedAmount": "101.00",
"sentAmount": "104.123712",
"missingAmount": "0.00",
"expectedAmount": "101.00"
},
"depositAmount": "100.00",
"paidAmount": "100.00",
"payoutCurrency": "USD",
"partialFills": [],
"settlement": {
"buyerPaidAmount": "100.00",
"buyerPaidFiat": [{ "currency": "USD", "amount": "100.00" }],
"sentAmount": "0.00",
"sentTo": null,
"returnedAmount": "0.00",
"dustAmount": "0.00",
"refundFeeAmount": "0.00"
},
"attempt": { "kind": "ESCROW", "rail": "venmo", "status": "SETTLED" },
"payoutTransfer": null,
"cancelSource": null,
"returnUrl": "https://casino.example.com/cashier",
"expiresAt": "2026-09-25T18:31:12.408Z",
"fundedAt": "2026-09-24T18:31:12.408Z",
"settledAt": "2026-09-24T18:52:40.017Z",
"cancelledAt": null,
"createdAt": "2026-09-24T18:06:36.911Z"
}
}

Other payout events have the same data shape. For example, PAYOUT_ORDER_MATCHED has status: "PAYING", the attempt ACTIVE, and settlement and settledAt null.

A buyer paid 150.00 of a 200.00 payout (other data fields as above):

{
"id": "cashout_cmg1y2b9r0007s60hc3m1k7pw_CASHOUT_ORDER_PARTIALLY_PAID_0x449a858ddb269b90fb779f3bf1942ab0273ccd86e8e1fde2f11a9040bbffe64f_12",
"type": "PAYOUT_ORDER_PARTIALLY_PAID",
"timestamp": "2026-09-24T19:14:05.220Z",
"version": 2,
"data": {
"payoutId": "cmg1y2b9r0007s60hc3m1k7pw",
"status": "LISTING",
"depositAmount": "200.00",
"paidAmount": "150.00",
"payoutCurrency": "USD",
"partialFills": [
{
"fiat": { "currency": "USD", "amount": "150.00" },
"amount": "150.00",
"txHash": "0x449a858ddb269b90fb779f3bf1942ab0273ccd86e8e1fde2f11a9040bbffe64f",
"paidAt": "2026-09-24T19:14:05.220Z"
}
],
"settlement": null,
"attempt": { "kind": "ESCROW", "rail": "venmo", "status": "ACTIVE" },
"...": "..."
},
"partialPayment": {
"expectedAmount": "200.00",
"paidAmount": "150.00",
"remainingAmount": "50.00",
"fill": {
"fiat": { "currency": "USD", "amount": "150.00" },
"amount": "150.00",
"txHash": "0x449a858ddb269b90fb779f3bf1942ab0273ccd86e8e1fde2f11a9040bbffe64f"
}
}
}
  • partialPayment is on this event only. expectedAmount is the payout's depositAmount, paidAmount what buyers have paid so far including this payment, and remainingAmount the difference. fill is this payment's USDC amount, lower-case txHash and fiat.
  • data is the payout after this payment and anything else Peer Pay found at the same time, normally LISTING. It can still be PAYING when the listing was already being withdrawn, or already SETTLED when the rest was swept as dust or another buyer paid it. Read partialPayment, not data.paidAmount, for this payment's numbers.
  • When several payments are found at once, Peer Pay sends one event per payment, oldest first, then PAYOUT_ORDER_SETTLED if the payout settled.

When a payout settles with crypto sent, payoutTransfer describes what reached the customer and settlement.sentAmount the USDC sent from Base. The same payout, after the customer sent the rest to an address:

"settlement": {
"buyerPaidAmount": "150.00",
"buyerPaidFiat": [{ "currency": "USD", "amount": "150.00" }],
"sentAmount": "50.00",
"sentTo": "0x1234567890123456789012345678901234567890",
"returnedAmount": "0.00",
"dustAmount": "0.00",
"refundFeeAmount": "0.00"
},
"attempt": { "kind": "ESCROW", "rail": "venmo", "status": "CLOSED" },
"payoutTransfer": {
"chainId": 8453,
"tokenAddress": "0x833589fCD6eDb6E08f4c7C32D4f71b54bdA02913",
"decimals": 6,
"address": "0x1234567890123456789012345678901234567890",
"amount": "50.00",
"txHash": "0x5a5a…",
"usdcTxHash": "0x5a5a…",
"provider": null
}

A withdrawal to the customer's Peer wallet reports the rest in returnedAmount instead, and a dust close in dustAmount.

A whole crypto payout sends PAYOUT_ORDER_SETTLED with no PAYOUT_ORDER_MATCHED before it, paidAmount: "0.00" (crypto never counts as a buyer payment), and a CRYPTO attempt on the network rail. It settles when delivery is proven, not when the Base transfer lands. This one paid USDC on Arbitrum after an earlier refund kept 0.50 USDC, so it sent 99.50 USDC from Base and delivered 99.20 USDC:

"settlement": {
"buyerPaidAmount": "0.00",
"buyerPaidFiat": [],
"sentAmount": "99.50",
"sentTo": "0xabcdefabcdefabcdefabcdefabcdefabcdefabcd",
"returnedAmount": "0.00",
"dustAmount": "0.00",
"refundFeeAmount": "0.50"
},
"attempt": { "kind": "CRYPTO", "rail": "relay_42161", "status": "SETTLED" },
"payoutTransfer": {
"chainId": 42161,
"tokenAddress": "0xaf88d065e77c8cC2239327C5EDb3A432268e5831",
"decimals": 6,
"address": "0xABcdEFABcdEFabcdEfAbCdefabcdeFABcDEFabCD",
"amount": "99.20",
"txHash": "0x6b6b6b6b6b6b6b6b6b6b6b6b6b6b6b6b6b6b6b6b6b6b6b6b6b6b6b6b6b6b6b6b",
"usdcTxHash": "0x5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a",
"provider": "RELAY"
}

provider is RELAY or NEAR_INTENTS for a bridge, and null for direct USDC on Base, where txHash equals usdcTxHash. For ZEC, chainId is 133701, tokenAddress is nep141:zec.omft.near, decimals is 8, address is the transparent Zcash address, and txHash is the Zcash txid (64 hex digits, no 0x). payoutTransfer.address keeps EVM checksum case while settlement.sentTo is lower-case. A bridge refund returns the payout to READY with no merchant webhook.

id is the dedupe key for payout events; the X-Webhook-Id header differs per endpoint. Treat id as opaque and never parse it. Partial funding, a new buyer and a new buyer payment each get their own id; a retry keeps its id.

Ordering and delivery notes​

  • PAYMENT_CREATED precedes the other payment events only when the attempt started. An attempt that fails while starting sends PAYMENT_FAILED with no PAYMENT_CREATED, so tolerate a terminal event for a payment id you have never seen.
  • PAYMENT_SETTLED may be followed by ORDER_FULFILLED.
  • ORDER_AMOUNT_SET and the PAYMENT_CREATED (or PAYMENT_FAILED) of the same payment can arrive in either order.
  • Any event can be retried or arrive out of order. Reconcile from the full snapshots and deduplicate on id.
  • To reconcile outside the event stream, read the Orders API and the Payments API.

Deferred payment payouts​

These concern checkout payments under deferred payouts, not PAYOUT_ORDER_* events.

PAYMENT_SETTLED and ORDER_FULFILLED fire when a payment settles even if its USDC is held: the customer has paid. Keep order fulfillment and availability of funds separate. The payment's payout shows { status, releaseEligibleAt, releasedAt }.

PAYMENT_PAYOUT_RELEASED uses the order and payment envelope and fires when held USDC is released, with payout.status: "RELEASED" and the release time. A conversion to another chain or token starts only after release, so PAYMENT_BRIDGE_PENDING follows PAYMENT_PAYOUT_RELEASED.