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
| Event | Fires when | data.payment | Extras |
|---|---|---|---|
ORDER_CREATED | An order is created | null | Test deliveries reuse this type with data.test: true and every snapshot null |
ORDER_FULFILLED | The order reaches FULFILLED | The payment whose settlement completed the order | |
ORDER_CANCELLED | You cancel a CREATED order with no payment attempts, from the dashboard | null | |
ORDER_RESIZED | A customer resizes a dynamic order | null | data.resize |
ORDER_AMOUNT_SET | A customer starts a payment with a new amount on an open-amount order | null | data.amountChange |
PAYMENT_CREATED | A payment attempt starts | Populated | |
PAYMENT_SETTLED | A payment settles | Populated | On Relay crypto payments, fee and amount fields can be refined a few minutes later; read the payment back for final values |
PAYMENT_PAYOUT_RELEASED | A held payment's USDC is released to you | Populated, with payout.status: "RELEASED" | See deferred payment payouts |
PAYMENT_FAILED | An attempt could not be started, or a crypto transfer failed or stalled | Populated | A stalled crypto attempt can still settle if its deposit lands late |
PAYMENT_EXPIRED | An open attempt passes its payment window | Populated | See PAYMENT_EXPIRED |
PAYMENT_CANCELLED | A payment attempt is cancelled | Populated | |
REFUND_PENDING | A refund starts | null | data.refund |
REFUND_COMPLETED | A refund completes | null | data.refund |
PAYMENT_BRIDGE_PENDING | A conversion to your payout chain or token is queued after settlement | Populated | data.paymentBridge |
PAYMENT_BRIDGE_SUBMITTED | The conversion is submitted | Populated | data.paymentBridge |
PAYMENT_BRIDGE_COMPLETED | The destination transfer is confirmed | Populated | data.paymentBridge |
PAYMENT_BRIDGE_FAILED | The destination transfer failed | Populated | data.paymentBridge |
PAYMENT_CHARGEBACKED | A settled payment is charged back | Populated | data.trigger |
ORDER_PARTIALLY_CHARGEBACKED | Some, but not all, settled payments on the order carry a chargeback | Populated | data.trigger |
ORDER_CHARGEBACKED | Every settled payment on the order carries a chargeback | Populated | data.trigger |
PAYOUT_ORDER_* | A payout changes | n/a | data is a PayoutView; see payout events |
DISPUTE_OPENED | A seller reports a payment-app case on your order that no buyer stake covers, and you're asked to pay the seller | n/a | data is DisputeWebhookData; see dispute events |
DISPUTE_PAID | Your USDC payment to the seller is confirmed on Base | n/a | As above |
DISPUTE_ESCALATED | The 7-day deadline passed unpaid, and the Peer team takes over | n/a | As 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:
| Field | Type | Meaning |
|---|---|---|
disputeId | string | Dispute id |
orderId | string | Your order id |
paymentId | string | The settled fiat payment |
intentHash | string | Lower-case 0x intent hash |
amountUsdc | string | USDC owed to the seller, in raw 6-decimal units: "12500000" is 12.5 USDC |
caseAmount | string | Fiat case amount with 2 decimals, such as "12.40" |
caseCurrency | string | Fiat currency code, such as USD |
sellerAddress | string | Seller's lower-case 0x wallet address on Base |
status | DisputeStatusType | OPEN, PAID, or ESCALATED |
dueAt | string | ISO 8601 deadline, 7 days after opening |
paidTxHash | string | null | Lower-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.
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,railpaymentAmountandcurrency: what the customer paid, in their currency."0"until the payment settlescurrencyPerUsdRate: the exchange rate captured for that paymentnetSettledUsdcAmount: what settled to you after feestotalUsdcFeeAmount: fees in USDC (gross released minus net settled), not counting the seller's spreadpenalties: always present, empty when nothing appliedfulfillTransaction: the settlement transaction hash, on Base for payment-app settlements and on the destination chain for Relay crypto payments (use the order'sdestinationChainIdto 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.
| Attempt | Outcome |
|---|---|
| Fiat | Expires when its 1-hour window elapses, and sends PAYMENT_EXPIRED |
| Live Relay crypto | Never expires. A transfer that fails, or is still unresolved about 6 hours 20 minutes after creation, sends PAYMENT_FAILED instead |
| Live Zcash, never funded | Expires about 6 hours 20 minutes after creation, sends PAYMENT_EXPIRED, and can still settle if a deposit lands later |
| Sandbox crypto | Expires 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:
- A failure a retry can't fix, such as a refund by the conversion service or the 24-hour cap,
sends
PAYMENT_BRIDGE_FAILEDat once. - A failure that is safe to retry sends nothing while it waits. The bridge returns to
PENDINGand is resubmitted up to 3 times, about 10 minutes, 1 hour and 3 hours after the first failure, sendingPAYMENT_BRIDGE_SUBMITTEDeach time. It ends withPAYMENT_BRIDGE_COMPLETED, or withPAYMENT_BRIDGE_FAILEDwhosefailureMessageends with(after N automatic retries). - A bridge already submitted stays
SUBMITTEDuntil 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.
| Event | Fires when |
|---|---|
PAYOUT_ORDER_CREATED | A payout is created |
PAYOUT_ORDER_PARTIALLY_FUNDED | USDC 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_FUNDED | The payout is funded: depositAmount and fundedAt are set. A partly funded payout is funded with what arrived when the deadline passes |
PAYOUT_ORDER_EXPIRED | The funding deadline passed with nothing received |
PAYOUT_ORDER_CANCELLED | You 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_MATCHED | A 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_PAID | A buyer paid part of the listing; once per payment |
PAYOUT_ORDER_SETTLED | The 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:
| Field | Meaning |
|---|---|
data.payoutCurrency | The 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.buyerPaidFiat | Once settled, one total per currency in first-paid order; null if any payment's fiat is unknown, [] if no buyer paid |
partialPayment.fill.fiat | PAYOUT_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"
}
}
}
partialPaymentis on this event only.expectedAmountis the payout'sdepositAmount,paidAmountwhat buyers have paid so far including this payment, andremainingAmountthe difference.fillis this payment's USDCamount, lower-casetxHashandfiat.datais the payout after this payment and anything else Peer Pay found at the same time, normallyLISTING. It can still bePAYINGwhen the listing was already being withdrawn, or alreadySETTLEDwhen the rest was swept as dust or another buyer paid it. ReadpartialPayment, notdata.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_SETTLEDif 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_CREATEDprecedes the other payment events only when the attempt started. An attempt that fails while starting sendsPAYMENT_FAILEDwith noPAYMENT_CREATED, so tolerate a terminal event for a payment id you have never seen.PAYMENT_SETTLEDmay be followed byORDER_FULFILLED.ORDER_AMOUNT_SETand thePAYMENT_CREATED(orPAYMENT_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.