Skip to main content

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​

EventFires whendata.paymentExtras
ORDER_CREATEDAn order is creatednullTest deliveries reuse this type with data.test set to true and every snapshot null
ORDER_FULFILLEDThe order reaches FULFILLEDThe payment whose settlement completed the order
ORDER_CANCELLEDA merchant cancels a CREATED order that has no payment attempts yetnullDashboard action only; an API key cannot cancel, and an order with any payment attempt cannot be cancelled
ORDER_RESIZEDA customer resizes a dynamic order to a nearby available amountnulldata.resize
ORDER_AMOUNT_SETA customer starts a payment with a new amount on an open-amount ordernulldata.amountChange
PAYMENT_CREATEDA payment attempt is startedPopulated
PAYMENT_SETTLEDA payment settlesPopulatedOn Relay crypto payments, fee and deposit fields can be estimates; read the payment back a few minutes later for final values
PAYMENT_FAILEDAn attempt could not be started, or a live crypto transfer failed or stalledPopulatedUsually terminal; a stalled crypto attempt reopens and settles if its deposit lands late
PAYMENT_EXPIREDAn open attempt passes its payment windowPopulatedSee PAYMENT_EXPIRED
PAYMENT_CANCELLEDA payment attempt is cancelledPopulated
REFUND_PENDINGA refund starts and is awaiting completionnulldata.refund
REFUND_COMPLETEDA refund reaches completed statusnulldata.refund
PAYMENT_BRIDGE_PENDINGBridge transfer queued after settlementPopulateddata.paymentBridge
PAYMENT_BRIDGE_SUBMITTEDBridge transfer submitted to the providerPopulateddata.paymentBridge
PAYMENT_BRIDGE_COMPLETEDDestination transfer confirmedPopulateddata.paymentBridge
PAYMENT_BRIDGE_FAILEDDestination transfer failedPopulateddata.paymentBridge
PAYMENT_CHARGEBACKEDA settled payment is disputed and 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_CREATEDA payout is createdn/adata is a PayoutView; see payout events
PAYOUT_ORDER_CANCELLEDYou cancel a payout before any funding arrives, or the customer moves its money in the Peer app before any buyer paid part of itn/adata is a PayoutView; see payout events
PAYOUT_ORDER_PARTIALLY_FUNDEDUSDC arrived below the payout and you sent less than funding.amountn/aCan fire several times; see payout events
PAYOUT_ORDER_FUNDEDA payout is funded, or its funding deadline passed with part of it received, and its depositAmount is fixedn/adata is a PayoutView; see payout events
PAYOUT_ORDER_EXPIREDThe funding deadline passed and nothing arrivedn/adata is a PayoutView; see payout events
PAYOUT_ORDER_MATCHEDA buyer starts paying a listed payoutn/aOnce per buyer; see payout events
PAYOUT_ORDER_PARTIALLY_PAIDA buyer paid part of a listed payout; once per paymentn/apartialPayment beside data; see payout events
PAYOUT_ORDER_SETTLEDA 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 paymentn/adata.settlement says where the USDC went; see payout events
DISPUTE_OPENEDA seller reports a payment-app case on your order and no buyer stake covers it; you're asked to pay the sellern/adata is DisputeWebhookData; see dispute events
DISPUTE_PAIDYour USDC payment to the seller is confirmed on Basen/adata is DisputeWebhookData; see dispute events
DISPUTE_ESCALATEDThe 7-day deadline passed unpaid; the Peer team handles the disputen/adata 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:

FieldTypeMeaning
disputeIdstringPeer Pay dispute ID
orderIdstringMerchant's order ID
paymentIdstringSettled fiat payment ID
intentHashstringLower-case 0x intent hash
amountUsdcstringGross USDC release owed to the seller, in raw 6-decimal units: "12500000" means 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 | nullConfirmed 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:

  • id
  • merchantId
  • status
  • amountMode: "FIXED", or "OPEN" for an open-amount order
  • requestedUsdcAmount
  • remainingUsdcAmount, both null on an open-amount order until a payment saves its amount
  • openAmount and amountVersion, on open-amount orders only
  • destinationAddress
  • destinationToken
  • destinationChainId
  • metadata

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.

A resized order keeps its original fiat snapshot

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:

  • id
  • orderId
  • rail
  • paymentAmount and currency, what the customer actually paid, in their own currency. It is "0" until the payment settles
  • currencyPerUsdRate, the market rate snapshotted for that payment
  • netSettledUsdcAmount, what settled to you after fees
  • totalUsdcFeeAmount, onchain fees in USDC (gross released minus net settled), excluding the peer's spread
  • 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 ever increases, which happens only when another deposit to the same address also succeeded
  • penalties, always present and empty when nothing applied
  • fulfillTransaction, the settlement transaction hash: Base for payment app settlements, or the destination chain hash for Relay crypto payments (use the order's destinationChainId to 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:

AttemptOutcome
FiatExpires when the 1 hour payment window elapses, and emits PAYMENT_EXPIRED
Live Relay-backed cryptoDoes 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 fundedExpires about 6 hours 20 minutes after creation, emits PAYMENT_EXPIRED, and stays eligible for late settlement
Sandbox cryptoExpires 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_CHARGEBACKED is emitted once for a fully charged-back fiat payment. One protocol payment is never partially charged back.
  • ORDER_PARTIALLY_CHARGEBACKED is emitted when some, but not all, currently settled payments on an order have chargebacks.
  • ORDER_CHARGEBACKED is 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:

  1. Failures a retry cannot fix send PAYMENT_BRIDGE_FAILED at once, as before: no source wallet, a wallet the payout signer cannot use, a Relay failure, refund or refunded status, the 24-hour cap, or a stall that cannot be proven unsent.
  2. 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 PENDING and is resubmitted up to 3 times, about 10 minutes, 1 hour and 3 hours after the first failure, sending PAYMENT_BRIDGE_SUBMITTED again each time. It ends with PAYMENT_BRIDGE_COMPLETED, or with PAYMENT_BRIDGE_FAILED about 3 to 3.5 hours after the first failure, whose failure message ends with (after N automatic retries).
  3. Bridges already in flight at Relay keep today's behavior: they stay SUBMITTED until 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.

FieldMeaning
data.payoutCurrencySaved 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.buyerPaidFiatOnce 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.fiatOnly 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.

EventFires when
PAYOUT_ORDER_CREATEDA payout is created
PAYOUT_ORDER_CANCELLEDYou 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_FUNDEDUSDC 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_FUNDEDThe payout is funded; depositAmount and fundedAt are set. A partly funded payout is funded with what arrived when funding.quoteExpiresAt passes
PAYOUT_ORDER_EXPIREDfunding.quoteExpiresAt passed with nothing received; funding.depositAddress is null
PAYOUT_ORDER_MATCHEDA 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_PAIDA 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_SETTLEDThe 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"
}
}
}
  • partialPayment sits beside data and only on this event.
  • expectedAmount is the payout's depositAmount; paidAmount is what buyers have paid so far, this payment included; remainingAmount is the difference. fill is this payment's USDC amount, lower-case txHash and informational fiat.
  • data is the payout after Peer Pay applied this payment and anything else the same check found, normally LISTING. It can still be PAYING when the listing was already withdrawn, or already SETTLED when the same check closed the payout, because the rest was swept as dust or another buyer paid the rest. Read partialPayment for this payment's own numbers, not data.paidAmount.
  • When one check applies several payments, Peer Pay sends one event per payment, oldest first, then PAYOUT_ORDER_SETTLED if 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_FUNDED ends in the USDC received so far in base units (1 USDC = 1000000): partial funding of 40.5 USDC is cashout_<storedId>_CASHOUT_ORDER_PARTIALLY_FUNDED_40500000.
  • PAYOUT_ORDER_MATCHED ends in the buyer's intent hash: cashout_<storedId>_CASHOUT_ORDER_MATCHED_0x…, so each buyer gets one.
  • PAYOUT_ORDER_PARTIALLY_PAID ends 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_CREATED precedes the terminal payment events only when the attempt was successfully started. An attempt that fails while being started emits PAYMENT_FAILED with no preceding PAYMENT_CREATED, so a handler must tolerate a terminal event for a payment id it has 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, including PAYMENT_SETTLED and ORDER_FULFILLED, can be retried or arrive out of order. Reconcile the full snapshots and deduplicate by X-Webhook-Id (payout events: by payload.id).
  • To reconcile outside the event stream, read the order back through the Orders API and the Payments API.