Events and payloads
Every delivery uses the same envelope. The event type tells you what changed; the snapshots
inside data tell you the current state.
Payload structure
Order and payment, payout, 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;
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 set to true and every snapshot null |
ORDER_FULFILLED | The order reaches FULFILLED | The payment whose settlement completed the order | |
ORDER_CANCELLED | A merchant cancels a CREATED order that has no payment attempts yet | null | Dashboard action only; an API key cannot cancel, and an order with any payment attempt cannot be cancelled |
ORDER_RESIZED | A customer resizes a dynamic order to a nearby available amount | 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 is started | Populated | |
PAYMENT_SETTLED | A payment settles | Populated | On Relay crypto payments, fee and deposit fields can be estimates; read the payment back a few minutes later for final values |
PAYMENT_FAILED | An attempt could not be started, or a live crypto transfer failed or stalled | Populated | Usually terminal; a stalled crypto attempt reopens and settles 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 and is awaiting completion | null | data.refund |
REFUND_COMPLETED | A refund reaches completed status | null | data.refund |
PAYMENT_BRIDGE_PENDING | Bridge transfer queued after settlement | Populated | data.paymentBridge |
PAYMENT_BRIDGE_SUBMITTED | Bridge transfer submitted to the provider | Populated | data.paymentBridge |
PAYMENT_BRIDGE_COMPLETED | Destination transfer confirmed | Populated | data.paymentBridge |
PAYMENT_BRIDGE_FAILED | Destination transfer failed | Populated | data.paymentBridge |
PAYMENT_CHARGEBACKED | A settled payment is disputed and 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_CREATED | A payout is created | n/a | data is a PayoutView; see payout events |
PAYOUT_ORDER_CANCELLED | You cancel a payout before any funding arrives, or the customer moves its money in the Peer app before any buyer paid part of it | n/a | data is a PayoutView; see payout events |
PAYOUT_ORDER_PARTIALLY_FUNDED | USDC arrived below the payout and you sent less than funding.amount | n/a | Can fire several times; see payout events |
PAYOUT_ORDER_FUNDED | A payout is funded, or its funding deadline passed with part of it received, and its depositAmount is fixed | n/a | data is a PayoutView; see payout events |
PAYOUT_ORDER_EXPIRED | The funding deadline passed and nothing arrived | n/a | data is a PayoutView; see payout events |
PAYOUT_ORDER_MATCHED | A buyer starts paying a listed payout | n/a | Once per buyer; see payout events |
PAYOUT_ORDER_PARTIALLY_PAID | A buyer paid part of a listed payout; once per payment | n/a | partialPayment beside data; see payout events |
PAYOUT_ORDER_SETTLED | A payout ended: buyers paid it, a crypto payout arrived, the escrow swept a small rest as dust, or the customer took the rest after a partial payment | n/a | data.settlement says where the USDC went; see payout events |
DISPUTE_OPENED | A seller reports a payment-app case on your order and no buyer stake covers it; 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 | data is DisputeWebhookData; see dispute events |
DISPUTE_ESCALATED | The 7-day deadline passed unpaid; the Peer team handles the dispute | n/a | data is DisputeWebhookData; see dispute events |
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 the merchant to pay the seller when no buyer stake covers a reported payment-app case. It is separate from a chargeback: chargeback events record compensation from buyer stake on-chain. Dispute events leave order fulfillment and payment settlement unchanged.
DisputeWebhookPayload uses version: 1 (DISPUTE_WEBHOOK_VERSION). Its stable dedupe key
is id: "dispute_<disputeId>_<type>", shared across endpoints for the same event.
timestamp is the event timestamp in ISO 8601. data is DisputeWebhookData:
| Field | Type | Meaning |
|---|---|---|
disputeId | string | Peer Pay dispute ID |
orderId | string | Merchant's order ID |
paymentId | string | Settled fiat payment ID |
intentHash | string | Lower-case 0x intent hash |
amountUsdc | string | Gross USDC release owed to the seller, in raw 6-decimal units: "12500000" means 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 | Confirmed lower-case Base transaction hash when paid; otherwise null |
An escalated dispute can still be paid and emit DISPUTE_PAID.
LIVE webhooks that existed at the 7.1 rollout were subscribed automatically. Webhooks created
since and all sandbox webhooks must list DISPUTE_OPENED, DISPUTE_PAID, and DISPUTE_ESCALATED.
data.order
CheckoutOrder is present on order-family events except the synthetic test delivery, and includes:
idmerchantIdstatusamountMode:"FIXED", or"OPEN"for an open-amount orderrequestedUsdcAmountremainingUsdcAmount, bothnullon an open-amount order until a payment saves its amountopenAmountandamountVersion, on open-amount orders onlydestinationAddressdestinationTokendestinationChainIdmetadata
metadata is null unless the order was created with a fiat price. When it was, it carries
requestAmountInputMode, requestedFiatAmount, requestedFiatCurrency, and
requestedUsdcAmountAtCreation. An open-amount order gains the same keys when a payment saves
its amount. On an open-amount order these keys hold the saved amount and the currency the
customer typed it in.
After ORDER_RESIZED, metadata also carries requestAmountResized set to true, but the
fiat fields still describe the price at creation. Do not reconcile a resized order against
them. Use data.order.requestedUsdcAmount and data.resize.
data.payment
CheckoutPayment is present on ORDER_FULFILLED, every PAYMENT_* event, the bridge events,
and all three chargeback events. It is null on ORDER_CREATED, ORDER_RESIZED,
ORDER_AMOUNT_SET, ORDER_CANCELLED, REFUND_PENDING, and REFUND_COMPLETED.
On ORDER_FULFILLED it is the payment whose settlement completed the order, not a summary of
every payment on the order.
Useful fields:
idorderIdrailpaymentAmountandcurrency, what the customer actually paid, in their own currency. It is"0"until the payment settlescurrencyPerUsdRate, the market rate snapshotted for that paymentnetSettledUsdcAmount, what settled to you after feestotalUsdcFeeAmount, onchain fees in USDC (gross released minus net settled), excluding the peer's spread- On Relay crypto payments,
totalUsdcFeeAmount,referralFees,paymentAmount,currencyandcurrencyPerUsdRateinPAYMENT_SETTLEDcan be estimates that Peer refines within minutes without another event.fulfillTransactionnever changes, andnetSettledUsdcAmountonly ever increases, which happens only when another deposit to the same address also succeeded penalties, always present and empty when nothing appliedfulfillTransaction, the settlement transaction hash: Base for payment app settlements, or the destination chain hash for Relay crypto payments (use the order'sdestinationChainIdto pick the explorer)
How these reconcile against the order amount under each fee mode is in fees, settlement and refunds.
Penalised payment
The customer sent 50.00 on a merchant-pays-fees order with purchase protection on. The
attestation credited 45.00 (penaltyBps is authoritative; see
payment penalties) and fees came off that. paymentAmount
still records the 50.00 the customer sent; on a customer-pays-fees order it would be the
credited 45.00 instead.
{
"id": "pay_456",
"rail": "venmo",
"currency": "USD",
"paymentAmount": "50.00",
"penalties": [
{ "kind": "PURCHASE_PROTECTION", "penaltyBps": 1000, "originalAmount": "50.00", "attestedAmount": "45.00" }
],
"netSettledUsdcAmount": "44.55",
"totalUsdcFeeAmount": "0.45"
}
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 marks the payment window as elapsed. It does not cancel the merchant order.
If a late settlement succeeds, PAYMENT_SETTLED and ORDER_FULFILLED can still follow for
the same payment.
Which attempts expire depends on the rail and the environment:
| Attempt | Outcome |
|---|---|
| Fiat | Expires when the 1 hour payment window elapses, and emits PAYMENT_EXPIRED |
| Live Relay-backed crypto | Does not expire. A transfer that fails, or that is still unresolved about 6 hours 20 minutes after creation, emits PAYMENT_FAILED instead |
| Live Zcash, never funded | Expires about 6 hours 20 minutes after creation, emits PAYMENT_EXPIRED, and stays eligible for late settlement |
| Sandbox crypto | Expires on its quote window like a fiat attempt, and emits PAYMENT_EXPIRED |
ORDER_RESIZED
The event carries post-resize order amounts in data.order, with data.payment,
data.refund, and data.paymentBridge all null. data.resize holds the previous and new
amounts. Resizing is only possible while the order is CREATED, untouched, and has no open or
expired payment attempt.
Credit the amounts from data.order and data.resize, not the amount you sent at session
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. It is recorded together
with that 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 and its currency (fiat; the checkout's selected currency,
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 for the order. 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
For REFUND_PENDING and REFUND_COMPLETED, data.order is the current order snapshot with
refundStatus, refundAmountUsdc, refundDepositId, and refundTransactionHash populated.
data.payment and data.paymentBridge are null. data.refund carries:
{
orderId: string;
status: 'PENDING' | 'COMPLETED';
amountUsdc: string;
depositId: string | null;
txHash: string | null;
sellerUsername: string | null;
processorName: string | null;
payeeDetails: unknown | null;
metadata: unknown | null;
}
Chargeback events
Settled payments on disputable platforms can be charged back after the fact. A chargeback is
an additive fact: it never changes data.order.status or data.payment.status, it is
delivered as a full current snapshot rather than a delta, and the order chargeback status is
not monotonic. An order can move from CHARGEBACKED to PARTIALLY_CHARGEBACKED when a new,
uncharged payment settles, then return to CHARGEBACKED if that payment is later charged
back.
PAYMENT_CHARGEBACKEDis emitted once for a fully charged-back fiat payment. One protocol payment is never partially charged back.ORDER_PARTIALLY_CHARGEBACKEDis emitted when some, but not all, currently settled payments on an order have chargebacks.ORDER_CHARGEBACKEDis emitted when every currently settled payment on an order has a chargeback. A charged-back single-payment order goes directly to this state.
Read data.order.chargebackStatus, data.order.chargebacks,
data.payment.chargebackStatus, and data.payment.chargeback. data.trigger identifies
whether an order aggregate changed because a chargeback was applied:
{
type: 'CHARGEBACK_APPLIED';
chargeback: PaymentChargebackFact;
}
or because another payment settled and moved the denominator:
{
type: 'PAYMENT_SETTLED';
paymentId: string;
}
The chargeback fact has this shape:
interface PaymentChargebackFact {
paymentId: string;
intentHash: string;
disputeId: string;
status: 'CHARGEBACKED';
compensatedUsdcAmount: string;
disputedAt: string;
disputeTxHash: string;
refundOverlap:
| 'NONE'
| 'PENDING_HALTED'
| 'SUBMITTING_OR_UNKNOWN'
| 'ALREADY_SUBMITTED'
| 'ALREADY_REFUNDED';
bridgeOrSweepOverlap: null | {
paymentBridgeId: string;
status: 'PENDING' | 'SUBMITTED' | 'COMPLETED' | 'FAILED';
};
}
Bridge state, when present, stays in data.paymentBridge. bridgeOrSweepOverlap records the
latest bridge attempt and its status at the moment the chargeback was applied, whatever that
status was. A chargeback does not reverse or rewrite 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",
"intentHash": "0xintent",
"disputeId": "7",
"status": "CHARGEBACKED",
"compensatedUsdcAmount": "50",
"disputedAt": "2026-08-20T09:59:00.000Z",
"disputeTxHash": "0xdispute",
"refundOverlap": "NONE",
"bridgeOrSweepOverlap": null
}
},
"refund": null,
"paymentBridge": null,
"trigger": {
"type": "CHARGEBACK_APPLIED",
"chargeback": {
"paymentId": "pay_123",
"intentHash": "0xintent",
"disputeId": "7",
"status": "CHARGEBACKED",
"compensatedUsdcAmount": "50",
"disputedAt": "2026-08-20T09:59:00.000Z",
"disputeTxHash": "0xdispute",
"refundOverlap": "NONE",
"bridgeOrSweepOverlap": null
}
}
}
}
Bridge events
PAYMENT_BRIDGE_PENDING, PAYMENT_BRIDGE_SUBMITTED, PAYMENT_BRIDGE_COMPLETED, and
PAYMENT_BRIDGE_FAILED track the payout on the destination chain. data.paymentBridge
carries bridge state and destination transfer metadata, alongside the order and payment
snapshots.
They are additive to the core payment and order events, and are only emitted when bridge
execution applies.
Bridge failures and automatic retries
When automatic bridge retry is enabled for your environment, a payout bridge that fails before
any funds could have left your Base wallet is retried on the same attempt before you are told.
PAYMENT_BRIDGE_FAILED timing falls into three cases:
- Failures a retry cannot fix send
PAYMENT_BRIDGE_FAILEDat once, as before: no source wallet, a wallet the payout signer cannot use, a Relayfailure,refundorrefundedstatus, the 24-hour cap, or a stall that cannot be proven unsent. - Failures proven safe to retry send nothing while the attempt waits: a Relay quote or
submission error before any transfer was sent, or a stall where Relay still waits and the
transfer was never sent or was rejected by the wallet provider. The attempt returns to
PENDINGand is resubmitted up to 3 times, about 10 minutes, 1 hour and 3 hours after the first failure, sendingPAYMENT_BRIDGE_SUBMITTEDagain each time. It ends withPAYMENT_BRIDGE_COMPLETED, or withPAYMENT_BRIDGE_FAILEDabout 3 to 3.5 hours after the first failure, whose failure message ends with(after N automatic retries). - Bridges already in flight at Relay keep today's behavior: they stay
SUBMITTEDuntil Relay settles them, up to the 24-hour cap.
GET /api/v1/merchants/me/payments/:paymentId/bridge reports the schedule in automaticRetry:
scheduledCount counts retries scheduled so far, including one still waiting; maxRetries is
3 while automatic retry is enabled and 0 while it is off; nextRetryAt is when a waiting retry
is due, otherwise null. Failure fields appear only on failed attempts: while an attempt waits
for a retry it stays PENDING with failureCode and failureMessage set to null in
latestAttempt, attempts and the order's bridgeInfo. automaticRetry is how you tell a
retry is in progress.
Payout events
Every PAYOUT_ORDER_* event uses the payout envelope: data is a
PayoutView, the same shape the Payouts API returns, not
the { order, payment, refund, paymentBridge } shape of the order and payment events. The
envelope also carries version, which is 2 today. Peer bumps it only for a breaking change to
the payout payload, so check it before reading data. Order and payment payloads have no
version. PAYOUT_ORDER_PARTIALLY_PAID also carries partialPayment next to data.
Deliveries queued before the deploy keep their stored v1 payloads, even on retry. Drain them or keep a v1 receiver until they finish; see the v7 migration guide.
Multi-currency fields are included in version 2, alongside the payout event and field rename. Payout, fee, refund and accounting amounts remain USDC; fiat fields are information only, never a funding or refund instruction.
| Field | Meaning |
|---|---|
data.payoutCurrency | Saved fiat method's catalog currency, or null for crypto or no method |
data.partialFills[].fiat | { currency, amount } for that buyer payment at its bound rate; two-decimal fiat, or null when currency or rate is unknown |
data.settlement.buyerPaidFiat | Once settled, one fiat total per currency in first-paid order. Null if any buyer payment's fiat is unknown; [] if no buyer paid |
partialPayment.fill.fiat | Only on PAYOUT_ORDER_PARTIALLY_PAID; that event's own payment's fiat at its bound rate, or null when unknown |
After a currency change, totals can contain more than one currency, for example
[{ "currency": "EUR", "amount": "44.65" }, { "currency": "GBP", "amount": "39.10" }].
Rates bind when buyers signal; webhooks never substitute a current market estimate for
missing bound-rate evidence.
| Event | Fires when |
|---|---|
PAYOUT_ORDER_CREATED | A payout is created |
PAYOUT_ORDER_CANCELLED | You cancel a payout that has nothing received (cancelSource: "MERCHANT"), or the customer withdraws or spends it in the Peer app (cancelSource: "PEER_APP"); the customer checkout never cancels. Fires only before any buyer paid part of the payout; after a partial payment the same exits end in PAYOUT_ORDER_SETTLED |
PAYOUT_ORDER_PARTIALLY_FUNDED | USDC arrived below the payout and you sent less than funding.amount. Fires on becoming partial and again each time the received amount grows. Each on-time arrival moves funding.quoteExpiresAt to at least 24 hours (the funding TTL, by default) after Pay sees it; it never moves earlier |
PAYOUT_ORDER_FUNDED | The payout is funded; depositAmount and fundedAt are set. A partly funded payout is funded with what arrived when funding.quoteExpiresAt passes |
PAYOUT_ORDER_EXPIRED | funding.quoteExpiresAt passed with nothing received; funding.depositAddress is null |
PAYOUT_ORDER_MATCHED | A buyer starts paying the listing; status is PAYING. If their payment lapses, the payout returns to LISTING with no webhook, and the next buyer sends another PAYOUT_ORDER_MATCHED with its own id |
PAYOUT_ORDER_PARTIALLY_PAID | A buyer paid part of the listing; once per payment. partialPayment has this payment's numbers and data usually has status: "LISTING" with the rest listed again. It can still be PAYING when the listing was already withdrawn, or already SETTLED when the same check closed the payout. The rest is not listed again when a withdraw landed while a buyer held the deposit and it stopped taking new buyers |
PAYOUT_ORDER_SETTLED | The payout ended: a buyer paid what was left, the crypto payout landed, the escrow swept a small rest as dust, or after a partial payment the customer withdrew the rest to their Peer wallet, kept it there or sent it to an address. status is SETTLED, settledAt is set, and data.settlement says where the USDC went |
attempt.rail is typed string | null. When set, it is a payout rail: venmo, cashapp, paypal, zelle, revolut, chime, a supported relay_<chainId> network rail, or near_intents_133701 (see values).
Webhooks created during dashboard onboarding subscribe to every event type, so a receiver
must branch on type before parsing data.
{
"id": "cashout_cmg1x7k2p0003s60h4f9z2q8d_CASHOUT_ORDER_CANCELLED",
"type": "PAYOUT_ORDER_CANCELLED",
"timestamp": "2026-09-24T18:07:02.114Z",
"version": 2,
"data": {
"payoutId": "cmg1x7k2p0003s60h4f9z2q8d",
"merchantReference": "withdrawal-8841",
"status": "CANCELLED",
"fundingStatus": "AWAITING_FUNDING",
"payout": {
"amount": "100.00",
"chainId": 8453,
"tokenAddress": "0x833589fCD6eDb6E08f4c7C32D4f71b54bdA02913",
"decimals": 6
},
"merchantFee": { "bps": 100, "amount": "1.041237" },
"funding": {
"chainId": 8453,
"tokenAddress": "0x833589fCD6eDb6E08f4c7C32D4f71b54bdA02913",
"decimals": 6,
"amount": "104.123712",
"depositAddress": null,
"quoteExpiresAt": "2026-09-25T18:06:36.911Z",
"receivedAmount": "0.00",
"sentAmount": "0.00",
"missingAmount": "104.123712",
"expectedAmount": "101.00"
},
"depositAmount": null,
"paidAmount": "0.00",
"payoutCurrency": null,
"partialFills": [],
"settlement": null,
"attempt": null,
"payoutTransfer": null,
"cancelSource": "MERCHANT",
"returnUrl": "https://casino.example.com/cashier",
"expiresAt": "2026-09-25T18:06:36.911Z",
"fundedAt": null,
"settledAt": null,
"cancelledAt": "2026-09-24T18:07:02.114Z",
"createdAt": "2026-09-24T18:06:36.911Z"
}
}
A funded payout, after partial funding and a top-up:
{
"id": "cashout_cmg1x7k2p0003s60h4f9z2q8d_CASHOUT_ORDER_FUNDED",
"type": "PAYOUT_ORDER_FUNDED",
"timestamp": "2026-09-24T18:31:12.408Z",
"version": 2,
"data": {
"payoutId": "cmg1x7k2p0003s60h4f9z2q8d",
"merchantReference": "withdrawal-8841",
"status": "FUNDED",
"fundingStatus": "FUNDED",
"payout": {
"amount": "100.00",
"chainId": 8453,
"tokenAddress": "0x833589fCD6eDb6E08f4c7C32D4f71b54bdA02913",
"decimals": 6
},
"merchantFee": { "bps": 100, "amount": "1.041237" },
"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": "0.00",
"payoutCurrency": null,
"partialFills": [],
"settlement": null,
"attempt": null,
"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": null,
"cancelledAt": null,
"createdAt": "2026-09-24T18:06:36.911Z"
}
}
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" },
"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"
}
}
PAYOUT_ORDER_MATCHED has the same data shape, with status: "PAYING", the attempt still
ACTIVE, settlement: null and settledAt: null.
A buyer paid 150.00 of a 200.00 payout:
{
"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",
"merchantReference": "withdrawal-9120",
"status": "LISTING",
"fundingStatus": "FUNDED",
"payout": {
"amount": "200.00",
"chainId": 8453,
"tokenAddress": "0x833589fCD6eDb6E08f4c7C32D4f71b54bdA02913",
"decimals": 6
},
"merchantFee": { "bps": 100, "amount": "2.082474" },
"funding": {
"chainId": 8453,
"tokenAddress": "0x833589fCD6eDb6E08f4c7C32D4f71b54bdA02913",
"decimals": 6,
"amount": "208.247423",
"depositAddress": null,
"quoteExpiresAt": "2026-09-25T18:41:30.118Z",
"receivedAmount": "202.00",
"sentAmount": "208.247423",
"missingAmount": "0.00",
"expectedAmount": "202.00"
},
"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" },
"payoutTransfer": null,
"cancelSource": null,
"returnUrl": "https://casino.example.com/cashier",
"expiresAt": "2026-09-25T18:41:30.118Z",
"fundedAt": "2026-09-24T18:41:30.118Z",
"settledAt": null,
"cancelledAt": null,
"createdAt": "2026-09-24T18:40:51.302Z"
},
"partialPayment": {
"expectedAmount": "200.00",
"paidAmount": "150.00",
"remainingAmount": "50.00",
"fill": {
"fiat": { "currency": "USD", "amount": "150.00" },
"amount": "150.00",
"txHash": "0x449a858ddb269b90fb779f3bf1942ab0273ccd86e8e1fde2f11a9040bbffe64f"
}
}
}
partialPaymentsits besidedataand only on this event.expectedAmountis the payout'sdepositAmount;paidAmountis what buyers have paid so far, this payment included;remainingAmountis the difference.fillis this payment's USDCamount, lower-casetxHashand informationalfiat.datais the payout after Peer Pay applied this payment and anything else the same check found, normallyLISTING. It can still bePAYINGwhen the listing was already withdrawn, or alreadySETTLEDwhen the same check closed the payout, because the rest was swept as dust or another buyer paid the rest. ReadpartialPaymentfor this payment's own numbers, notdata.paidAmount.- When one check applies several payments, Peer Pay sends one event per payment, oldest first,
then
PAYOUT_ORDER_SETTLEDif the payout settled.
The same payout, settled after the customer sent the rest to an address:
"status": "SETTLED",
"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": {
"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 withdraw to the Peer wallet reports the rest in returnedAmount instead, and a dust close in
dustAmount.
A direct Base USDC crypto payout sends the same
PAYOUT_ORDER_SETTLED, with no PAYOUT_ORDER_MATCHED before it, and:
"paidAmount": "0.00",
"payoutCurrency": null,
"partialFills": [],
"settlement": {
"buyerPaidAmount": "0.00",
"buyerPaidFiat": [],
"sentAmount": "100.00",
"sentTo": "0x1234567890123456789012345678901234567890",
"returnedAmount": "0.00",
"dustAmount": "0.00",
"refundFeeAmount": "0.00"
},
"attempt": { "kind": "CRYPTO", "rail": "relay_8453", "status": "SETTLED" },
"payoutTransfer": {
"chainId": 8453,
"tokenAddress": "0x833589fCD6eDb6E08f4c7C32D4f71b54bdA02913",
"decimals": 6,
"address": "0x1234567890123456789012345678901234567890",
"amount": "100.00",
"txHash": "0x5a5a…",
"usdcTxHash": "0x5a5a…",
"provider": null
}
A bridged payout settles only when the destination delivery is proven, not when the Base transfer lands. This example paid USDC on Arbitrum after an earlier refund retained 0.50 USDC; the retry sent 99.50 USDC from Base and delivered 99.20 USDC on Arbitrum:
{
"id": "cashout_cmg1z5d4t000bs60h8q2x6vne_CASHOUT_ORDER_SETTLED",
"type": "PAYOUT_ORDER_SETTLED",
"timestamp": "2026-09-24T18:52:40.017Z",
"version": 2,
"data": {
"payoutId": "cmg1z5d4t000bs60h8q2x6vne",
"merchantReference": "withdrawal-8841",
"status": "SETTLED",
"fundingStatus": "FUNDED",
"payout": {
"amount": "100.00",
"chainId": 8453,
"tokenAddress": "0x833589fCD6eDb6E08f4c7C32D4f71b54bdA02913",
"decimals": 6
},
"merchantFee": {
"bps": 100,
"amount": "1.041237"
},
"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": "0.00",
"payoutCurrency": null,
"partialFills": [],
"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"
},
"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"
}
}
ZEC on Zcash — PAYOUT_ORDER_SETTLED. This synthetic example uses a 10 USDC
payout, a 10 USDC payout step and no merchant fee; the delivered amount is illustrative.
{
"id": "cashout_cmgf3k8w2000fs60h1r9y4cza_CASHOUT_ORDER_SETTLED",
"type": "PAYOUT_ORDER_SETTLED",
"timestamp": "2026-10-07T12:20:00.000Z",
"version": 2,
"data": {
"payoutId": "cmgf3k8w2000fs60h1r9y4cza",
"merchantReference": "withdrawal-zec-001",
"status": "SETTLED",
"fundingStatus": "FUNDED",
"payout": {
"amount": "10.00",
"chainId": 8453,
"tokenAddress": "0x833589fCD6eDb6E08f4c7C32D4f71b54bdA02913",
"decimals": 6
},
"merchantFee": {
"bps": 0,
"amount": "0.00"
},
"funding": {
"chainId": 8453,
"tokenAddress": "0x833589fCD6eDb6E08f4c7C32D4f71b54bdA02913",
"decimals": 6,
"amount": "10.306123",
"depositAddress": null,
"quoteExpiresAt": "2026-10-08T12:00:00.000Z",
"receivedAmount": "10.10",
"sentAmount": "10.306123",
"missingAmount": "0.00",
"expectedAmount": "10.10"
},
"depositAmount": "10.00",
"paidAmount": "0.00",
"payoutCurrency": null,
"partialFills": [],
"settlement": {
"buyerPaidAmount": "0.00",
"buyerPaidFiat": [],
"sentAmount": "10.00",
"sentTo": "t1Hsc1LR8yKnbbe3twRp88p6vFfC5t7DLbs",
"returnedAmount": "0.00",
"dustAmount": "0.00",
"refundFeeAmount": "0.00"
},
"attempt": {
"kind": "CRYPTO",
"rail": "near_intents_133701",
"status": "SETTLED"
},
"payoutTransfer": {
"chainId": 133701,
"tokenAddress": "nep141:zec.omft.near",
"decimals": 8,
"address": "t1Hsc1LR8yKnbbe3twRp88p6vFfC5t7DLbs",
"amount": "0.099",
"txHash": "efefefefefefefefefefefefefefefefefefefefefefefefefefefefefefefef",
"usdcTxHash": "0xabababababababababababababababababababababababababababababababab",
"provider": "NEAR_INTENTS"
},
"cancelSource": null,
"returnUrl": "https://casino.example.com/cashier",
"expiresAt": "2026-10-08T12:00:00.000Z",
"fundedAt": "2026-10-07T12:01:00.000Z",
"settledAt": "2026-10-07T12:20:00.000Z",
"cancelledAt": null,
"createdAt": "2026-10-07T12:00:00.000Z"
}
}
payoutTransfer.txHash is the Zcash txid (64 hex digits, no 0x); usdcTxHash is the Base
USDC transfer to NEAR Intents. The recipient is a transparent Zcash address.
payoutTransfer describes what reached the customer: destination chain, token, decimals,
canonical recipient, delivered token amount and delivering txHash. usdcTxHash identifies
the Base USDC send; provider is RELAY or NEAR_INTENTS for a bridge, or null for direct Base USDC where
both hashes are equal. It is set only on a settled payout with a sent amount, including a
send-to-address remainder. It is null for an entirely buyer-paid payout; fiat payee handles
are never included. settlement.sentTo is the customer’s recipient, never a deposit address:
EVM lower-case, Solana, Tron, Bitcoin and transparent Zcash canonical. payoutTransfer.address keeps EVM checksum case.
paidAmount counts buyer payments only. A plain crypto payout keeps it at "0.00".
settlement.sentAmount counts the USDC sent on Base, while payoutTransfer.amount counts the
delivered destination token. settlement.refundFeeAmount totals USDC retained on refunds and
is "0.00" when none occurred. A refund returns the payout to READY without a merchant webhook;
the customer gets an action-required email and a PAYOUT_REFUNDED timeline entry. The payout
webhook version is 2.
Deduplicate payout events on payload.id, which is the same for every endpoint. It is
cashout_<storedId>_<storedEventType>, with three exceptions. These ids are opaque: the stored event type retains its CASHOUT_ORDER_* spelling even when the public type is PAYOUT_ORDER_*. Never parse an id to determine the event type:
PAYOUT_ORDER_PARTIALLY_FUNDEDends in the USDC received so far in base units (1 USDC = 1000000): partial funding of 40.5 USDC iscashout_<storedId>_CASHOUT_ORDER_PARTIALLY_FUNDED_40500000.PAYOUT_ORDER_MATCHEDends in the buyer's intent hash:cashout_<storedId>_CASHOUT_ORDER_MATCHED_0x…, so each buyer gets one.PAYOUT_ORDER_PARTIALLY_PAIDends in the payment's lower-case transaction hash and log index:cashout_<storedId>_CASHOUT_ORDER_PARTIALLY_PAID_0x…_<logIndex>, so each buyer payment gets one.
A retry of the same event keeps its id; new funding, a new buyer or a new buyer payment gets a new one. The X-Webhook-Id header is unique per
endpoint, so it is not the dedupe key for payout events.
Ordering and delivery notes
PAYMENT_CREATEDprecedes the terminal payment events only when the attempt was successfully started. An attempt that fails while being started emitsPAYMENT_FAILEDwith no precedingPAYMENT_CREATED, so a handler must tolerate a terminal event for a payment id it has 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, including
PAYMENT_SETTLEDandORDER_FULFILLED, can be retried or arrive out of order. Reconcile the full snapshots and deduplicate byX-Webhook-Id(payout events: bypayload.id). - To reconcile outside the event stream, read the order back through the Orders API and the Payments API.