Skip to main content

Payouts

A payout pays one of your customers out. You choose how much the customer should receive in USDC and which token you will fund it with. Peer Pay returns a deposit address and an exact funding amount.

A deposit address is a one-off crypto address created for this payout only. Whatever you send there is converted to USDC on Base and delivered to the customer's Peer app wallet. You never need the customer's wallet address; the customer is identified by email.

EndpointAuthUse it when
POST /api/v1/payoutsAPI keyA customer asks to withdraw
GET /api/v1/payouts/{id}API keyYou want funding progress and status for one payout
GET /api/v1/payoutsAPI keyYou want a filtered page of payouts
POST /api/v1/payouts/{id}/cancelAPI keyYou no longer want to fund a payout

Once you fund the deposit address, Peer Pay detects the funds on its own; see Funding.

Every route returns the same PayoutView shape, and so do the payout webhooks.

Every live merchant can create payouts: every platform is on by default. A sandbox merchant can't. A merchant whose account setup isn't finished gets 409 MERCHANT_CONFIG_MISSING; turning every platform off in Settings → Payouts gives 403 PAYOUTS_DISABLED. See the merchant payouts guide.

Money rules​

  • The payout is funded and owed in USDC on Base (chain 8453, token 0x833589fCD6eDb6E08f4c7C32D4f71b54bdA02913). The customer can take it as any supported coin and network.
  • The payout must be at least one payout step, a whole multiple of that step (100 USDC by default), and at most 1000 USDC.
  • Your merchant fee is X% of the amount funded, set in basis points (100 bps = 1%, at most 1000 bps). The Peer fee is set for your merchant by Peer (200 bps of the amount funded by default). Both fees come out of the deposit, so the funding amount is grossed up to cover them. Your fee goes to your merchant wallet and the Peer fee to Peer's fee wallet.
  • Customers can use only the payout apps you turned on that Peer also allows, and you can fund only with tokens Peer allows for your merchant. Peer can turn your payout apps on or off for your merchant, like your checkout payment methods, and switch an app or token off for every merchant. Tokens can also be limited for your merchant alone.
  • Crypto payouts use relay_<chainId> rails and near_intents_133701 for Zcash, grouped as one Crypto option for the customer. Only networks in the payout’s rails are offered. See the rail values.
  • Rails and fee wallets are copied when the payout is created. The payout apps the customer can pick, both fee wallets, the fees and the payout step are copied onto the payout, so a later settings change applies to new payouts only. Peer can also remove a network from an open payout; see crypto payouts.
  • A small buffer covers price movement while the deposit is converted: 1% of the payout, capped at 5 USDC for stablecoins; 2%, capped at 10 USDC for other tokens, BTC included. A Bitcoin deposit is converted only after the network confirms it, so its price can move for longer.

Payout currencies​

Players can receive the catalog currencies on PayPal and Revolut; you still fund and see USD. All payout amounts, fees, limits, refunds and settlement accounting stay in USDC on Base. Fiat fields are information only; never use them to decide how much to fund or refund.

  • USD is always fixed at 1.00 USD per USDC and never uses an oracle. Crypto payouts are unchanged.
  • Non-USD listings use the same oracle parameters as Peer web and mobile: the SDK's spread-oracle config with the per-environment Chainlink adapter, at 0% spread. The adapter, adapter config and maximum staleness are taken unchanged from the SDK; maxStaleness is 86,400 seconds today. Pay applies no staleness logic of its own; the contracts and Curator handle whether buyers can signal.
  • Before a buyer signals, the “≈” amount is display only, from batched oracle reads cached for 60 seconds with a two-second timeout. A failed read only removes the “≈” estimate: it never hides a currency or blocks saving a method or confirming. Once a buyer signals, their rate binds; each payment keeps its own bound rate, even if the market moves or the rest is paid later.
  • PayPal and Revolut offer every currency in their catalog by default. No admin or merchant currency setting is required. Changing a listed method or currency withdraws the listing, returns to FUNDED, and preserves paid parts in their original currencies.

The table below is the payout catalog exported as PAYOUT_RAIL_CURRENCIES and PAYOUT_CURRENCY_INFO from @zkp2p/pay-shared. Read the Rails column to get each rail's currencies; USD comes first, then table order. All entries have two minor units. Prefixes follow the SDK, except MX$, CN¥ and CHF disambiguate the display (CHF includes a trailing space).

CodeNamePrefixRails
USDUnited States Dollar$venmo, cashapp, paypal, zelle, revolut, chime
EUREuro€paypal, revolut
GBPBritish Pound£paypal, revolut
AUDAustralian DollarA$paypal, revolut
CADCanadian DollarC$paypal, revolut
CHFSwiss FrancCHF revolut
CNYChinese YuanCN¥revolut
MXNMexican PesoMX$revolut
NZDNew Zealand DollarNZ$paypal, revolut
SGDSingapore DollarS$paypal, revolut
TRYTurkish Lira₺revolut
ZARSouth African RandRrevolut

Limits stay USDC-denominated: the minimum payout (10 USDC in production) and 1,000 USDC per buyer payment. There are no per-currency fiat limits. USD is fixed 1:1; every other entry uses the SDK's Base oracle at 0% spread.

Peer web's packages/core/src/platforms/{paypal,revolut,venmo,cashapp,zelle,chime}.ts and mobile's src/helpers/paymentPlatforms/*.ts match the SDK 0.14.5/0.14.7 rail catalogs in production and staging (research verified 2026-10-08). Revolut also supports JPY, HKD, SAR, AED, THB, PLN, CZK, DKK, HUF, NOK, RON and SEK in Peer, but the SDK has no Base oracle feed for them, so payouts cannot price them under this rule. BRL, IDR, INR and PHP have feeds but are not supported by these payout rails.

PayPal buyer proof and SAR verify receipt currency; Curator forwards the intent's currency hash to attestation-service. Revolut buyer proof also verifies currency, and neither payee hash includes it. Wrong-currency risk: an otherwise valid buyer payment in another currency is converted by attestation with a 3% penalty (attestation-service/src/transformers/operations/crossCurrency.ts), potentially leaving a partial fill and relisting the remainder. This is existing protocol behavior.

Worked example​

You ask for a 100 USDC payout, fund it with USDC and charge a 100 bps (1%) merchant fee.

  1. Buffer: 1% of 100 = 1. The conversion must deliver 100 + 1 = 101 USDC.
  2. Fees take 100 + 200 = 300 bps (3%) of the deposit, so the deposit must be 101 ÷ 0.97 = 104.123712 USDC (rounded up to the last unit).
  3. Your merchant fee is 1% of 104.123712 = 1.041237 USDC (rounded down).

A 1000 USDC payout hits the 5 USDC buffer cap: 1005 ÷ 0.97 = 1036.082475 funded, with a 10.360824 merchant fee.

The response carries the final numbers. Fund exactly funding.amount; do not recompute it.

Idempotency-Key header​

POST /api/v1/payouts requires an Idempotency-Key header: 8–128 characters of letters, digits, _ or -. Use one key per withdrawal and reuse it on retries.

You sendYou get
A new key201 and a new payout, with checkoutUrl and idempotentReplay: false
The same key and the same body200 with the existing payout, the current checkoutUrl and idempotentReplay: true
The same key and a different body409 IDEMPOTENCY_KEY_CONFLICT
The same key while the first request is still running409 IDEMPOTENCY_REQUEST_IN_PROGRESS

checkoutUrl is the link you give the customer to follow their payout. A replay with the same key and body returns the current link, so a lost response can be retried safely. Owners and managers can also copy it from the payout's page in the dashboard while the payout is open. The customer can act through the link until the payout ends (settled, cancelled or expired); after that it only shows how the payout ended. If Peer rotates the key that signs these links, links handed out earlier stop working; a replay of the create request returns the new link, as does the dashboard while the payout is open.

Create a payout​

POST /api/v1/payouts
X-API-Key: <your key>
Idempotency-Key: <your key for this withdrawal>
Content-Type: application/json

Request body​

FieldTypeDescription
customerEmailstringThe customer's email, up to 254 characters. Lower-cased before use
payout.amountstringUSDC owed to the customer, a decimal with at most 6 decimals
payout.chainIdnumberMust be 8453
payout.tokenAddressstringMust be Base USDC
funding.chainIdnumberChain you fund from: Ethereum (1), Optimism (10), BNB Smart Chain (56), Polygon (137), World Chain (480), Hyperliquid (999), Arc (5042), Base (8453), Arbitrum (42161) or Bitcoin (8253038). Solana, Tron and Zcash can't fund a payout
funding.tokenAddressstringToken you fund with. Use 0x0000000000000000000000000000000000000000 for a chain's native token, and bc1qqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqmql8k8 for BTC on Bitcoin
funding.refundAddressstring?Where Relay sends the funding back if it can't be converted. Required for Bitcoin: a Bitcoin address you control, bech32 (bc1…) or legacy (1…, 3…); a bc1 address is stored lower-case. Optional on EVM chains: leave it out to refund the wallet that sent the funding, and set it when you fund from an exchange withdrawal, because Relay doesn't refund exchange wallets.
merchantReferencestring?Your own reference, 1–128 characters. List matches it exactly
returnUrlstring?An https:// URL the customer returns to when they are done
railsPayoutRailType[]?Non-empty list of unique payout rail ids. Limits the effective merchant/platform rails; never enables a rail either turned off. Unavailable requested rails are dropped. If none remain: 422 PAYOUT_REQUESTED_RAILS_UNAVAILABLE

Unknown fields are rejected. Unknown rail ids, the retired crypto value, an empty rails list or repeated rails return 400 (duplicates: “Each rail can be listed once”). Rail order is normalized, so reordered rails replay with the same idempotency key. Different rails conflict. A request without rails replays exactly as before (same key, same body).

Response​

{
"success": true,
"message": "Payout created",
"responseObject": {
"payoutId": "cmg1x7k2p0003s60h4f9z2q8d",
"merchantReference": "withdrawal-8841",
"status": "AWAITING_FUNDING",
"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": "0xe69f55d15f396f82e5da402a475d5fa72c3bd3db",
"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": null,
"returnUrl": "https://casino.example.com/cashier",
"expiresAt": "2026-09-25T18:06:36.911Z",
"fundedAt": null,
"settledAt": null,
"cancelledAt": null,
"createdAt": "2026-09-24T18:06:36.911Z",
"checkoutUrl": "https://<checkout host>/c/cmg1x7k2p0003s60h4f9z2q8d?t=…",
"idempotentReplay": false
},
"statusCode": 201
}

Send funding.amount of funding.tokenAddress on funding.chainId to funding.depositAddress (the bc1… address for BTC on Bitcoin), in one or more transfers, before funding.quoteExpiresAt; see Funding for the deadline and what happens next.

Never send funds after quoteExpiresAt. Create a new payout with a new idempotency key.

PayoutView fields​

FieldDescription
statusAWAITING_FUNDING, FUNDED, READY, LISTING, PAYING, SETTLED, CANCELLED, or EXPIRED
fundingStatusAWAITING_FUNDING, PARTIALLY_FUNDED, FUNDED, or FUNDING_EXPIRED
payoutId, merchantReference, payout, returnUrlThe payout id, your reference, the requested payout (amount, chainId, tokenAddress, decimals) and your return URL
merchantFeebps, and amount in decimal USDC estimated at quote time from the quoted deposit, rounded down. Relay charges the bps on what is actually deposited
funding.chainId, funding.tokenAddress, funding.decimalsThe funding chain, token address and token decimals
funding.amountWhat you send, in the funding token
funding.depositAddressWhere you send it. null once funding.quoteExpiresAt passes, once fundingStatus is FUNDED, or once the payout is CANCELLED or EXPIRED; never send when it is null
funding.receivedAmountUSDC that has arrived on Base so far, after conversion and fees
funding.sentAmountWhat you sent before the deadline, in the funding token. For Bitcoin, a transaction counts as sent once Relay records its sweep of the deposit, which can wait for the network to confirm it; until then it reads 0.
funding.missingAmountWhat you have not sent yet, in the funding token: funding.amount minus funding.sentAmount, never below 0. It stays above 0 on a payout that was funded short. For Bitcoin it stays at the full amount until Relay records the sweep.
funding.expectedAmountThe USDC the quote delivers on Base: the payout plus the buffer
depositAmountThe USDC the payout pays out, set when it is funded; null until then. A Peer-app withdraw after listing (CANCELLED) replaces it with the USDC the withdraw returned to the customer's Peer wallet. Later listings or payouts subtract paidAmount and refund losses reported in settlement.refundFeeAmount; see deposit size
paidAmountDecimal USDC buyers have paid the customer across every listing, partial payments included; "0.00" until a buyer pays. A crypto payout transfer never counts in it; payoutTransfer and settlement.sentAmount report it instead
payoutCurrencySaved fiat currency from the catalog above, or null for crypto or no method
partialFills[].fiat{ currency, amount } at that payment’s bound rate, with a two-decimal fiat amount; null if its currency or rate is unknown
settlement.buyerPaidFiatFiat totals across all buyer payments, one entry per currency in first-paid order. null if any payment’s fiat is unknown; [] if no buyer paid. This supplements the USDC breakdown
partialFillsOne entry per buyer payment that paid only part of what was listed, oldest first, with amount (decimal USDC), txHash (the payment on Base, lower-case) and paidAt (when Peer Pay recorded it, in ISO time). [] until a buyer pays part. The payment that pays the rest counts in paidAmount only. See partial payments
settlementOnly when SETTLED: decimal USDC buyerPaidAmount, sentAmount (USDC sent for a crypto payout or remainder), returnedAmount (left in or returned to the customer’s Peer wallet), dustAmount (swept by the escrow when it closed the deposit), and refundFeeAmount (USDC lost on bridge refunds, "0.00" when none). sentTo is the customer’s recipient, never a deposit address: EVM lower-case; Solana, Tron, Bitcoin and transparent Zcash canonical; null when nothing was sent
attemptLatest payout attempt (kind, rail, status), or null. kind is ESCROW for a buyer-paid listing and CRYPTO for a crypto payout; a CRYPTO attempt’s rail is its network id, such as relay_42161. rail can be null. status is ACTIVE, CLOSED, SETTLED or FAILED
payoutTransferSet only when SETTLED with a sent amount, including a send-to-address remainder. Destination chainId, tokenAddress, decimals, customer address, delivered amount in destination tokens, delivering txHash, Base usdcTxHash, and provider (RELAY, NEAR_INTENTS, or null for direct Base USDC). Otherwise null. address is canonical: EVM checksummed, bech32 lower-case, base58 as entered. Compare it with settlement.sentTo case-insensitively on EVM
cancelSourceOnce cancelled: MERCHANT (you, before funding) or PEER_APP (the customer withdrew the listing in the Peer app, or spent the funded USDC there so the wallet no longer covers it); otherwise null. The customer checkout never cancels a payout
funding.quoteExpiresAtThe funding deadline; see funding for how it moves
expiresAtAlways equals funding.quoteExpiresAt and moves with it
fundedAtWhen the payout became FUNDED; null until then
settledAtWhen the payout became SETTLED; null until then
createdAt, cancelledAtWhen the payout was created or cancelled, in ISO time; cancelledAt is null until cancellation

The view never includes the customer's email, their wallet address, their payee handle or the Peer fee.

Lifecycle​

statusMeaningWebhook when it starts
AWAITING_FUNDINGWaiting for your funds at the deposit addressPAYOUT_ORDER_CREATED
FUNDEDThe USDC is in the customer's wallet; they have not chosen a payout method that is ready yet (none chosen, or a Venmo, Cash App or PayPal account not connected yet)PAYOUT_ORDER_FUNDED
READYThe customer’s payout method is ready to confirm: a connected or skipped Venmo, Cash App or PayPal account; a Zelle, Revolut or Chime handle; or a crypto destination. A confirmed crypto payout may instead be bridging with no actionsnone
LISTINGListed for a buyer to pay what is left, or a send-to-address remainder is bridging with no actionsnone, or PAYOUT_ORDER_PARTIALLY_PAID when it comes back after a partial payment
PAYINGA buyer is paying. The customer cannot change the payout appPAYOUT_ORDER_MATCHED
SETTLEDThe payout ended: a buyer paid what was left, the crypto payout arrived at the customer’s address, the escrow swept a small rest as dust, or the customer withdrew the rest in the Peer app or sent it to an address after a partial payment. settledAt is set and settlement says where the USDC wentPAYOUT_ORDER_SETTLED
CANCELLEDEnded without a payout, before any buyer paid part of it; see cancelSourcePAYOUT_ORDER_CANCELLED
EXPIREDNo USDC arrived before the funding deadlinePAYOUT_ORDER_EXPIRED

A bridged payout stays READY (whole payout) or LISTING (remainder), with no customer actions until delivery. A verified refund returns it to READY with no merchant webhook; see crypto payouts.

If a buyer's payment expires or is released before it completes, the payout goes back from PAYING to LISTING with no webhook, and a later buyer sends a new PAYOUT_ORDER_MATCHED.

Peer Pay follows the listing on Base. Moves normally show within seconds, and within about a minute when the indexer is slow.

Partial payments​

A buyer may pay part of a listing. Peer Pay records it in paidAmount and partialFills, sends PAYOUT_ORDER_PARTIALLY_PAID, and lists what is left again (LISTING), even below the listing minimum, when the deposit accepts buyers and no exit is in progress. Another buyer can pay the rest; partial payments can repeat.

When the escrow sweeps a small rest as dust and closes the deposit, the payout settles with settlement.dustAmount. The payment that leaves only dust is included in partialFills.

After any partial payment, every way the payout ends is SETTLED, with the breakdown in settlement, never CANCELLED: a buyer pays the rest, the customer withdraws the rest to their Peer wallet or sends it to an address (see after a partial payment), or the customer takes the listing back or spends the rest in the Peer app.

Funding​

  1. You send the funding token to the deposit address.
  2. Relay, the conversion service behind the deposit address, converts it and delivers USDC on Base to the customer's Peer app wallet.
  3. Peer Pay checks Base itself for the USDC that reached the customer's wallet and updates the payout. Relay's notifications only tell Peer Pay when to look; the amount on Base is what counts.

Peer Pay checks when Relay reports activity on the deposit address, and on a schedule until the payout leaves AWAITING_FUNDING. You do not call anything. Read the payout back, or listen for the payout webhooks.

What has happenedfundingStatusstatusWebhook
You sent the full funding.amount and USDC arrived, or the USDC received covers the payoutFUNDEDFUNDEDPAYOUT_ORDER_FUNDED
USDC arrived below the payout and you sent less than funding.amountPARTIALLY_FUNDEDAWAITING_FUNDINGPAYOUT_ORDER_PARTIALLY_FUNDED, on becoming partial or when the received amount grows
The deadline passed with some USDC received, below the payout, and nothing still convertingFUNDEDFUNDEDPAYOUT_ORDER_FUNDED; the deposit is set from what arrived
As above, with a transfer still convertingPARTIALLY_FUNDEDAWAITING_FUNDINGNone until it lands
A transfer failed or was refunded to your refundAddress (on EVM chains, the sending wallet when omitted; Bitcoin funding always has one)UnchangedUnchangedNone. The payout keeps waiting for funds until the deadline, and each refunded Relay request is recorded as a route refund
funding.quoteExpiresAt passed, nothing arrived and nothing is still convertingFUNDING_EXPIREDEXPIREDPAYOUT_ORDER_EXPIRED

While an on-time transfer is still converting, the payout keeps waiting rather than expiring, even if you sent the full funding.amount. Only transfers that started at or before funding.quoteExpiresAt count towards funding. For Bitcoin, a transfer starts when Relay first sees it in the mempool, before it confirms, so a transaction broadcast before the deadline counts even if it confirms after.

The funding deadline is 24 hours after the payout is created, by default. Each time Peer Pay sees more USDC arrive on time, the deadline moves to 24 hours after that, unless it is already later, so a top-up always has a full day. The customer withdraws whatever arrived when the deadline passes; funds are never sent back to you.

Never send funds after quoteExpiresAt. Create a new payout with a new idempotency key.

Deposit size​

depositAmount is set when the payout becomes FUNDED, from the USDC received before the deadline:

  • If it covers the payout, the deposit is the payout. It never exceeds the payout.
  • If it is at most 1 USDC above a whole payout step (100 USDC by default), the deposit is rounded down to that step.
  • Otherwise the deposit is the amount received.

For a 200 USDC payout with a 100 USDC step:

USDC receiveddepositAmountWhy
200.30200.00Covers the payout, so the payout
199.96199.9699.96 above the 100 step, more than 1 USDC, so kept as received
100.80100.000.80 above the 100 step, so rounded down

The last two can happen when you sent the full funding.amount but the conversion delivered less than the payout, or when the deadline passed before the rest arrived.

depositAmount is set once at funding. If the customer withdraws the listing in the Peer app (CANCELLED), it is replaced with the USDC the withdraw returned to their Peer wallet. A later listing or payout holds depositAmount minus paidAmount minus any refund losses from earlier crypto payouts, reported in settlement.refundFeeAmount when the payout settles.

Funding issues: late funds, overpayment and route refunds​

Pay records money that reaches a payout outside the normal funding path as a funding issue:

KindWhen
OverpaymentYou sent more than funding.amount and more USDC than the quote arrived; the issue is the extra USDC. Relay delivering slightly over its own quote for an exact send is not an overpayment
Late fundsUSDC arrives after the payout left AWAITING_FUNDING (any later status), or comes from a transfer that started after the funding deadline; the issue is that amount
Route refundRelay could not route a payment and sent it back to your refund address (on EVM chains, the sending wallet if omitted), in the token you sent. Relay does not auto-refund exchange wallets. For Bitcoin, Relay sends BTC to your Bitcoin refund address, and the issue carries the Bitcoin txid

funding.receivedAmount keeps growing, but the payout is never reopened and the money is never moved onto another payout. Each issue shows on the payout's dashboard page and timeline. Peer support contacts you and settles it outside Pay, by returning the funds or by recording that they were claimed; Pay itself moves no money for an issue.

Pay only watches the quoted route. Funds sent in a different token or on a different chain than quoted are not detected by Pay; contact Peer support.

Get a payout​

GET /api/v1/payouts/{id}
X-API-Key: <your key>

Returns the PayoutView with message Payout found. A payout that belongs to another merchant returns 404 PAYOUT_NOT_FOUND.

List payouts​

GET /api/v1/payouts?customerEmail=player@example.com&status=AWAITING_FUNDING
X-API-Key: <your key>
ParameterTypeDescription
customerEmailstring?Exact match, case-insensitive
statusstring?One status value
merchantReferencestring?Exact match
pagenumberDefault 1
limitnumber1–100, default 20

Newest first. The response is { items: PayoutView[], page, limit, total }.

Cancel a payout​

POST /api/v1/payouts/{id}/cancel
X-API-Key: <your key>

Only a payout whose status and fundingStatus are both AWAITING_FUNDING can be cancelled. Before cancelling, Peer Pay checks Relay for any record at the deposit address, including failed or refunded transfers. Any record blocks cancellation; if no USDC arrives, the payout expires at the funding deadline once nothing is still converting. On success the view comes back with status: "CANCELLED" and cancelSource: "MERCHANT", and a PAYOUT_ORDER_CANCELLED webhook fires.

Customer checkout​

Send the customer to the checkoutUrl from create. You never call the customer API behind it; it is internal to the checkout.

The customer sees your name, logo and the amount: the payout until funding, then the deposit the customer actually receives, or what came back after a refunded bridge. The page uses your checkout theme from Settings → Checkout, like the payment checkout. It reads your theme and branding setting on each load. Peer branding removal is configured by the team: it hides "Powered by Peer" but keeps the sign-in flow and the "Privacy Policy" link.

They then:

  1. Sign in to their Peer account with a one-time code. The page emails one automatic code to the payout's email per page load and asks for it; the customer can resend it. A wrong account shows "This withdrawal belongs to a different Peer account" (403 CASHOUT_PLAYER_MISMATCH); an expired login shows "Your sign-in has expired" (401 CASHOUT_LOGIN_INVALID). Both offer Sign out. Right after sign-in, the page attaches Peer Pay's payout signer to the customer's Peer wallet, limited by a payout-only policy: it can approve and use the Peer escrow, relist and withdraw from it, add the Peer Pay merchant group to Venmo or PayPal listings, and send up to 1,000 USDC per transfer on Base. Peer Pay then sends the approve, deposit, group configuration, relist, withdraw or USDC transfer itself, with gas sponsored; the customer never sees a wallet prompt for these sends. A customer with wallet MFA confirms once when the signer is attached. The setup screen says "Getting your withdrawal ready". If setup fails or takes 30 seconds, it says "We couldn’t get your withdrawal ready" and offers "Try again", with a "Manage your withdrawal" section that sends the customer to the Peer app (see the next step).

  2. Choose how to get paid. On first arrival, a banner says {merchant} sent {amount} to your own wallet. When PayPal or Revolut is offered, a Currency selector appears above "Select withdrawal method". It defaults to the saved method's currency, then the last payout's currency if offered, then USD; there is no geolocation. Each currency shows only the fiat apps that pay it, with a display-only estimate such as “≈ €89.27”. Crypto stays available. Switching currency keeps a selected PayPal account or Revolut Revtag; a fiat app that cannot pay the new currency is deselected. Under "Select withdrawal method", there is one row per fiat app and a single Crypto row for every crypto network on the payout. The customer enters a Venmo username, a Cash App $Cashtag, a PayPal username and the email on that PayPal account, a Zelle email, a Revolut Revtag or a Chime $ChimeSign. The help line says "You receive your withdrawal at this account. Check it’s yours." PayPal works only for an account already set up in Peer (422 CASHOUT_PAYEE_NOT_REGISTERED). If a crypto network is on, the customer can instead take the payout as crypto. The checkout has no cancel: the money is already in the customer's own Peer account. The last row is Peer app, "Manage your withdrawal". Choosing it replaces Continue with Your {amount} is in your Peer account. Sign in to the Peer app with {masked email} to manage it. and App Store and Google Play buttons (a phone shows only its own store). The payout stays FUNDED or READY until the customer withdraws it here or moves the money in the Peer app.

  3. Connect the payout account where needed:

    RailHow the customer connectsContinue without verifying
    Venmo"Verify Venmo" opens a pop-up at app.peer.xyz/connect/venmo for Gmail, Outlook or iCloudEvery device
    Cash App, PayPal"Open the Peer App Clip" on iPhone/iPad; a QR code elsewhereAndroid and desktop; not iPhone/iPad
    Zelle, Revolut, ChimeNothing to connect; READY at once, and each buyer proves their paymentNo connect step

    The connect card is titled Verify your {Rail} and says Required to withdraw to {account}. The desktop QR line is Scan with your iPhone camera to open the Peer App Clip and verify {Rail}. On Android it is Have an iPhone? Scan this code with its camera to open the Peer App Clip and verify {Rail}. The page moves on by itself once connected. The skip button reads "Continue without verifying". Skipping makes the payout READY; once confirmed, it is listed without a connection.

  4. Confirm ("Start withdrawal"). Peer Pay lists the unpaid USDC from the customer's Peer wallet (the whole deposit the first time). For non-USD, the review shows “You receive ≈ €89.27” and explains that the exact amount is set when someone starts paying. If the estimate is unavailable it shows the currency name. The payout is LISTING once the deposit lands.

  5. Receive. A buyer pays: PAYING while they pay, then SETTLED once the listing is paid. A buyer who pays only part leaves the rest to be relisted when no exit is in progress; see after a partial payment. While waiting, non-USD amounts remain estimates. While a buyer pays, the known bound amount is exact (no “≈”), and Done and the player email use the known fiat paid. Unknown fiat falls back to the USDC amount. The page can be closed and reopened; once settled it shows what arrived and where.

The summary panel and the review and listing rows name where the customer receives the payout: "Receiving at Venmo @alice", for example. The label becomes "Received at" once settled.

A skipped listing still offers verification while it waits. The hint is Verify your {Rail} to get paid faster. Some payments only go to verified accounts., with Verify {Rail}. The same live listing becomes available to merchants that take only connected accounts as soon as Peer sees the connection. Nothing is relisted, and Peer Pay sends nothing. A checkout state read or the credential job clears the skip when the connection is active. While still skipped, the listing gets no reconnect email. If a connection later lapses, the page shows Verify {Rail} again to get paid and Verify {Rail} again; only Venmo, Cash App and PayPal have connections that can lapse.

Before the customer confirms, Peer Pay checks that their Peer wallet still holds this payout's USDC, with the same newest-first rule as the background wallet check (a payout funded in the last 10 minutes is left out until its USDC shows in the balance). If the customer spent it in the Peer app, the page says "Not enough USDC for this withdrawal", with rows "This withdrawal needs" and "Available for it", says "This withdrawal closes in a few minutes." and offers only your contact link. Confirming returns 409 CASHOUT_WALLET_SHORT. The background check, every 5 minutes, then cancels it with cancelSource: "PEER_APP" before any partial payment; after a partial payment it settles with settlement.returnedAmount. The page then says "Withdrawal closed" and You moved this money in the Peer app.

While Peer Pay is sending, every tab shows "Starting your withdrawal", "Resuming your withdrawal", "Updating your withdrawal" or "Sending your USDC". After a partial payment the latter screens name the rest: "Resuming your remaining $50.00", "Moving your $50.00" or "Sending your $50.00". Each says "This takes a few seconds." and "You can leave this page. It picks up where you left off." Nothing else is offered; confirming, saving or changing the method, relisting or sending to an address returns 409 CASHOUT_SEND_IN_PROGRESS. Peer Pay never sends a second listing while the first may still land, because the customer's Peer wallet also holds their other payouts. If the deposit fails or reverts, nothing is listed: the page says "That didn’t go through. Try again." and the customer can confirm again. The page can be closed at any point; Peer Pay finishes the send and lists the payout without it.

Venmo and PayPal listings. After the deposit, Peer Pay sends one more transaction that lets Peer Pay merchants take the listing without staking (the Peer Pay merchant group). If Peer Pay can't build that group transaction, confirming answers 503 CASHOUT_LISTING_UNAVAILABLE before anything is listed. The page says "Starting your withdrawal" until it lands. If it fails, Peer Pay withdraws the listing and the payout returns to FUNDED, keeping what buyers have already paid: the customer sees "That didn’t go through. Try again." and chooses the payout method again. If a buyer is already paying, the withdraw waits until their payment ends. If a buyer starts paying just as the withdraw lands, the listing takes no new buyers. Peer Pay withdraws again once that payment ends, accounting for every withdrawal on the attempt. A payment that completes the payout settles as usual; a partial payment is kept, and the group exit returns the unpaid rest to FUNDED instead of relisting it. A dust sweep settles with the settlement breakdown.

If the signer is missing, because it was never attached or the customer removed it in the Peer app, the page attaches it again before offering anything that sends, and those actions return 409 CASHOUT_SIGNER_NOT_ATTACHED until it is attached. Cancelling before the payout is listed moves no money and needs no signer.

The page header links to your customer support from Settings → Payouts. It reads Contact {merchant}, or "Help" on phones, once the customer is signed in and the state has loaded. The expired, out-of-limits and wallet-short screens repeat Contact {merchant}. Without a support link the page shows no contact line. It never sends customers to Peer support, because only you can act on their payout.

A buyer who starts paying but doesn't finish returns the payout to LISTING, waiting for a new buyer.

Each listing asks one buyer to pay its whole amount (min = max): the original deposit or the rest after a partial payment. If the buyer pays only part, Peer Pay records it and relists the rest when the deposit still accepts buyers and no exit is in progress. The deposit must be between $10 and $1,000 to be listed. A non-production environment may lower the minimum to as little as $1; the summary's rails[].minAmount shows it. Outside that range the page says "This amount is outside the withdrawal limits" and "Choose another withdrawal method.", with "Change withdrawal method", which opens the method list without that method's rail, and your contact link. Confirming returns 422 CASHOUT_AMOUNT_OUTSIDE_RAIL_LIMITS. What is left after a partial payment is listed at any size up to $1,000, even below that minimum.

Before any buyer has paid part of the payout, while no buyer payment is in progress, the customer can change the payout app. Peer Pay withdraws the listing, the payout returns to FUNDED, and the page opens the method list, with its Peer app row, again. The checkout offers no cancel. After a partial payment, see after a partial payment.

The customer can also leave outside Pay. If they withdraw the listing in the Peer app, the payout is CANCELLED with cancelSource: "PEER_APP". If they spend the funded USDC in the Peer app before listing, so their wallet no longer covers the payout, Peer Pay cancels it the same way, newest payout first. Before any partial payment, both send PAYOUT_ORDER_CANCELLED. After a partial payment, both end SETTLED with settlement.returnedAmount and send PAYOUT_ORDER_SETTLED instead.

A relist can also be needed without a partial payment, after a Peer-app withdraw left a range the deposit can't fill. The page shows "Your withdrawal is paused" and Resume {amount} so someone can pay you.

My withdrawals, in the page header, lists the signed-in customer's payouts from you only, five at a time: this payout first, then the rest newest first, with the total count. Each page is read from Peer Pay as the customer moves through the list. The same header toggle reads "Back to this withdrawal" while the list is open.

Crypto payouts​

When your payout rails include a supported relay_<chainId> or near_intents_133701, the customer can choose a coin, network and address instead of a fiat payout app. The picker shows only networks on that payout:

Rail idNetworkCoins
relay_1EthereumETH, USDC, USDT, PYUSD, WBTC
relay_10OptimismETH, USDC, USDT
relay_56BNB Smart ChainBNB, USDC, USDT, WBTC
relay_137PolygonUSDC, USDT, WBTC
relay_480World ChainETH, USDC
relay_999HyperliquidHYPE, USDC, USDH
relay_5042ArcUSDC
relay_8453BaseETH, USDC, USDT, SOL, WBTC
relay_42161ArbitrumETH, USDC, USDT, WBTC
relay_8253038BitcoinBTC
relay_728126428TronUSDT
relay_792703809SolanaSOL, USDC, USDT, PYUSD
near_intents_133701ZcashZEC

There is no buyer, LISTING, PAYING or PAYOUT_ORDER_MATCHED for a whole crypto payout; the $10–$1,000 single-buyer listing range does not apply. Base USDC (relay_8453) is a direct transfer with gas paid by Peer. Every other supported coin/network uses Relay, including same-chain swaps on Base, except ZEC on Zcash, which uses NEAR Intents.

  1. The customer enters an address for the chosen network. Pay validates EVM checksums, Solana, Tron, Bitcoin and transparent Zcash formats. It refuses zero addresses where the network has one, the customer’s Peer wallet and configured escrows on every EVM chain, Base USDC on EVM, and the selected token’s contract or mint. Smart-contract wallets are accepted. EVM recipients are checksummed, Bitcoin bech32 lower-cased, and base58 recipients retain their case. The method is READY.
  2. The customer reviews "You receive": the exact USDC amount on Base for a direct payout, or ≈ {amount} {SYMBOL} for a bridged coin, refreshed every 30 seconds. The note says "Network and bridge fees come out of what you receive, so the amount can change a little before it arrives. Price updates every 30 seconds." Fees come out of the payout; the USDC sent is fixed (no gross-up). The arrival row reads "About a minute" for Base USDC, "Within an hour" for Bitcoin, "Within 30 minutes" for Zcash, and "In a few minutes" for other payouts. Under "Check the network", they tick This address can receive {SYMBOL} on {Network}. Sending to the wrong network can’t be undone. This is required for every crypto payout, including Base USDC, and resets whenever the destination changes. On confirmation, Pay checks the network against the payout's current rails again, then sends the unpaid USDC (deposit minus buyer payments and refund losses).
  3. A direct payout settles only on the exact Base USDC Transfer from the customer's wallet to their address. A bridged payout sends that USDC to the provider's deposit address and stays READY (or LISTING for a remainder) with no actions until the provider proves delivery to the customer's destination. The Base transfer alone sends no merchant webhook.
  4. A whole crypto payout’s delivery emits PAYOUT_ORDER_SETTLED with a CRYPTO attempt on the network rail, for example { "kind": "CRYPTO", "rail": "relay_42161", "status": "SETTLED" }. payoutTransfer.amount is the delivered destination token; settlement.sentAmount is the USDC sent from Base. The customer's email links to the delivering transaction on its network.

While bridging, the page says Sending to {Network}. Its subtitle is "This usually takes a few minutes. You can close this page; we’ll email you when it arrives." For Bitcoin the first sentence is "This can take up to an hour."; for Zcash it is "This can take up to 30 minutes." The progress rows read {usdc} USDC left your wallet, Sending ≈ {amount} {SYMBOL} with Receiving at {short address}, and Arrives on {Network}.

While a direct send is in flight, customer changes return 409 CASHOUT_SEND_IN_PROGRESS. While a bridge is QUOTED or BRIDGING, actions are empty: method changes, confirm, relist, skip-connect and send-to-address return 409 CASHOUT_PAYOUT_BRIDGING; preview returns 409 CASHOUT_PAYOUT_QUOTE_NOT_ALLOWED. No second payout starts while the first may arrive. A failed Base transfer closes the attempt; a bridge already funded waits for evidence without a timeout and is never failed for missing evidence.

A verified Base refund returns the payout to READY, with a PAYOUT_REFUNDED timeline entry and no merchant webhook. The banner reads We couldn’t send {SYMBOL} on {Network}, so {X} USDC came back to your wallet. Try again, or choose another withdrawal method. The title is "Try again", with "Withdraw again" and "Choose another withdrawal method". The ACTION_REQUIRED email has subject Your withdrawal didn’t reach {Network}. USDC retained on refunds is deducted from the next payout and reported in settlement.refundFeeAmount when the payout eventually settles. There is no automatic retry.

Peer removes a network by editing the stored rails, including on open payouts. New selections, previews, confirms and send-to-address requests on it then return 422 CASHOUT_PAYOUT_NETWORK_UNAVAILABLE, and the checkout reloads the list. An in-flight bridge stays governed by send, delivery or refund evidence. You can only narrow the available networks with the create request's rails.

An unknown payout rail (including retired crypto) returns 400 validation. Relay quote refusals are mapped by its errorCode, never a status number in its message: AMOUNT_TOO_LOW becomes 422 CASHOUT_PAYOUT_AMOUNT_TOO_LOW; NO_SWAP_ROUTES_FOUND, NO_INTERNAL_SWAP_ROUTES_FOUND, NO_QUOTES, INSUFFICIENT_LIQUIDITY, CHAIN_DISABLED and INVALID_OUTPUT_CURRENCY become 422 CASHOUT_PAYOUT_ROUTE_UNAVAILABLE. INVALID_ADDRESS becomes 422 CASHOUT_ADDRESS_NOT_ALLOWED with { reason: "INVALID" }. Other Relay quote failures become 502 CASHOUT_PAYOUT_QUOTE_FAILED.

NEAR's amount-too-low refusal (HTTP 400 with a message containing too low and try at least followed by a minimum) becomes 422 CASHOUT_PAYOUT_AMOUNT_TOO_LOW. When NEAR_1CLICK_JWT is unset or blank, ZEC previews and binding quotes return 422 CASHOUT_PAYOUT_ROUTE_UNAVAILABLE before contacting NEAR or writing an attempt. Other NEAR request failures become 502 CASHOUT_PAYOUT_QUOTE_FAILED. In-flight bridges keep reading public status and settle or refund only on evidence.

ZEC on Zcash​

ZEC payouts use NEAR Intents on near_intents_133701, inside the customer’s one Crypto option. The destination is chain 133701, token nep141:zec.omft.near, with 8 decimals. Only mainnet transparent addresses (t1…, t3…) are accepted, trimmed without changing case. Shielded and unified addresses are refused: an address longer than 64 characters (after trimming) returns 400; a non-empty invalid address within that limit returns 422 CASHOUT_ADDRESS_NOT_ALLOWED with { "reason": "INVALID" }.

The arrival estimate is Within 30 minutes. The Base USDC transfer must start before a send-by time (internal; not in any response), 15 minutes after the binding quote request (5 minutes before NEAR’s 20-minute refund deadline). At or after that time, a transfer that has never started fails internally as SEND_BY_PASSED, with no USDC sent, and the bridge becomes NOT_SENT. A whole payout stays READY so the customer can confirm again; a send-the-rest exit settles with the unsent USDC returned to the customer’s wallet. A started send is never re-sent at or past send-by; it is resolved from chain evidence. With none it stays open and Peer is alerted; if no transfer was sent, the USDC stays in the wallet. It is never failed merely because time passed.

A verified refund returns USDC to the customer’s wallet, less NEAR’s refund fee. The payout returns to READY, and the next payout deducts that loss; settlement.refundFeeAmount reports it when the payout settles. Price movement affects the ZEC delivered, bounded by the quote’s minAmountOut.

ZEC is a payout token only; it cannot fund a payout.

After a partial payment​

If a buyer pays $150 of a $200 payout, once the rest is relisted, the page title reads "$150.00 paid, $50.00 still to come", with a Paid row showing "$150.00 of $200.00". When the deposit accepts buyers and no exit is in progress, Peer Pay lists the rest again on its own; while it sends, the page says "Resuming your remaining $50.00".

If a withdraw lands while a buyer holds the listing, it returns nothing and leaves the deposit listed but no longer taking new buyers: EscrowV2 withdrawDeposit clears acceptingIntents. This can be a send-to-address, change-method or group exit Pay sends, or a withdraw in the Peer app. If that buyer then pays only part, Peer Pay does not list the rest again. The page offers the choices below when no exit send remains in progress; sending to an address also needs a crypto network on the payout. A required group exit finishes automatically as described above; any earlier withdrawals remain accounted for. In the customer checkout state, partialPayment.acceptingBuyers is then false. It is true while the listed deposit takes new buyers, and false when nothing is listed.

While no buyer is paying and no send or required group exit is in progress, the customer has the choices below. Sending to an address is offered only if the payout includes a crypto network:

  • Send $50.00 to a crypto address. The customer chooses a coin, network and address using { rail, tokenAddress, address }, with the same checks as a crypto payout. The form warns This address must accept {SYMBOL} on {Network}. Crypto transfers can’t be reversed. They tick This address can receive {SYMBOL} on {Network}. Sending to the wrong network can’t be undone., review the estimate and confirm. Peer Pay withdraws the listing, then sends the remainder as direct Base USDC or through Relay or NEAR Intents. A bridged remainder stays LISTING with no actions until delivery, then settles with settlement.sentAmount, the customer’s settlement.sentTo, and payoutTransfer. A verified bridge refund returns to READY with that destination saved, so the customer can try again; the next payout subtracts refundFeeAmount. A removed network must be changed first. If the Base transfer fails after the withdraw lands, the USDC stays in the customer’s Peer wallet and the payout settles with settlement.returnedAmount. If the withdraw fails, nothing is sent and the rest stays listed.
  • Change withdrawal method. Peer Pay withdraws the listing and returns the payout to FUNDED, keeping depositAmount and paidAmount. Once the customer sets up the new method (connects or skips connecting Venmo, Cash App or PayPal, enters a Zelle, Revolut or Chime account, or chooses a crypto destination) and confirms, only the unpaid amount (depositAmount minus paidAmount and bridge refund losses) is listed or sent, below the listing minimum if needed. The method list's Peer app row names the rest: Your $50.00 is in your Peer account. If the customer moves it in the Peer app, the payout settles with settlement.returnedAmount.

While a buyer pays the rest, nothing is offered. If listing the rest again fails, the listing keeps its old range and no buyer can take it. The page says "We couldn’t resume the $50.00. Try again." and offers "Resume $50.00" alongside the two choices.

Embedding the customer checkout​

Pass checkoutUrl through ensureEmbedModeUrl, which adds embed=true. Embedded mode applies only inside a frame. It drops the outer padding, border and shadow, including on the loading and link-error screens.

The checkout posts events to the parent window on channel zkp2p_checkout_embed_v1:

EventPayloadMeaning
checkout.success{ payoutId, status: "LISTING" }First listing: listed, not paid
checkout.success{ payoutId, status: "SETTLED" }The payout settled; a whole crypto payout posts only this success
checkout.failed{ payoutId, reason }reason is EXPIRED or LINK_INVALID

A Peer-app close posts nothing. See the payment checkout's own events for its separate payloads. Embedded mode is in beta; use the hosted page for production.

Customer emails​

The customer gets these emails, each with its own link back to the checkout:

  • FUNDED: Your {amount} from {merchant} is ready, with the available payout methods.
  • CANCELLED: only after the customer moved the money in the Peer app (a cancel before funding sends no email): Your withdrawal from {merchant} is closed, "You moved the money in the Peer app. Sign in to the Peer app with this email to see what’s in your wallet."
  • PARTIALLY_PAID: You were paid part of your withdrawal from {merchant}. When the rest is relisted, it says "Someone paid $150.00 of your $200.00 withdrawal. Your remaining $50.00 is waiting for another payment." Otherwise it says "Someone paid $150.00 of your $200.00 withdrawal. $50.00 is left. Open your withdrawal to see what happens to it."
  • SETTLED: You received {amount} from {merchant}, or Your withdrawal from {merchant} is complete when there is a breakdown. It says Peer users paid you {amount}., {amount} went back to your own wallet., and The last {amount} was too small to pay out. for the parts that apply. A bridged remainder says You received {amount} {SYMBOL} at {short address} on {Network}.; a direct remainder says You received {amount} USDC at {short address} on Base. A direct crypto payout names the USDC actually sent, after earlier payout refund fees, rather than the original deposit. The settled email also covers withdrawing, keeping or sending the rest after a partial payment.
  • ACTION_REQUIRED: Verify {Rail} again to keep your withdrawal moving once when a connection lapses; Your {amount} withdrawal is still waiting once per dropped buyer; or Your withdrawal didn’t reach {Network} after a refunded crypto payout, with "Try again".

Every buyer payment sends exactly one email: a partial-payment email whether or not the rest is relisted, and only the settled email for the final payment, including a payment that leaves only dust. Once the payout is CANCELLED, SETTLED or EXPIRED, the merchant link and email links only show how it ended; nothing can be changed through them.

The one exception is a payment that leaves only dust but that Peer Pay first records as a partial payment against a deposit read made before the escrow swept the dust. It gets its partial-payment email and then the settled email. Peer Pay cannot tell that payment apart without the escrow's dust threshold, which it never reads.

Customer API reference​

For a funded payout whose Venmo, Cash App or PayPal method needs a connection, the customer actions include CONNECT_RAIL and SKIP_CONNECT. POST /api/v1/cashout-checkout/:id/skip-connect needs the link token and customer login, with no body. It returns the fresh checkout state with status READY and method.connectSkipped: true. Venmo, Cash App and PayPal methods always report connectSkipped as a boolean. Saving any payout method or restoring the saved crypto method after a refunded send-to-address exit clears the skip. Withdrawing a listing to change its method returns the payout to FUNDED with no method and clears the skip too; a new Venmo, Cash App or PayPal method must connect or skip again. A QUOTED or BRIDGING bridge returns 409 CASHOUT_PAYOUT_BRIDGING first. Otherwise, if SKIP_CONNECT is not offered, the route returns 409 CASHOUT_SKIP_NOT_ALLOWED with { status, actions }; final payout links reject POSTs with 401 CASHOUT_LINK_EXPIRED.

The fiat method route is PUT /api/v1/cashout-checkout/:id/payout-method, with the checkout link token and Peer-app login. PayPal and Revolut require an explicit currency, including USD:

{ "rail": "revolut", "payeeHandle": "alice", "currency": "EUR" }
{ "rail": "paypal", "payeeHandle": "alice", "paypalEmail": "alice@example.com", "currency": "EUR" }

currency must be in the rail's catalog above. Both bodies are strict: omitting it gives 400; an old PayPal or Revolut checkout tab must reload. Other fiat rails retain their existing bodies and reject a currency field. A currency outside the rail's catalog returns 400 Invalid request. Oracle estimate failures never block saving or confirming.

GET /api/v1/cashout-checkout/:id/state adds these informational fields:

FieldMeaning
method.currency, lastPayoutTarget.currencyThe currency on a fiat method; crypto methods have no currency
payoutCurrenciesOffered { currency, rails, rate, estimate } entries, USD first. Only rails on this payout count. rate and estimate are null for USD or a failed read; a missing unpaid amount also makes estimate null
fiatPricing{ currency, rate, unpaid, listed } for a saved non-USD method; null for USD, crypto or no method. The rate is { currency, rate } with six decimals in fiat per USD. Amounts are { currency, amount } with two decimals; missing estimates are null, and listed is null without a live listing
buyerPaymentExact { currency, amount } while PAYING with a known bound rate; otherwise null
partialPayment.paidFiatTotals in first-paid currency order; null if any payment's fiat is unknown, [] if no buyer paid. partialPayment itself is null until a partial payment
settlement.buyerPaidFiatThe same grouped buyer totals once SETTLED; all other settlement amounts remain USDC

These internal crypto routes require the checkout link token and Peer-app login:

RouteBody and result
PUT /api/v1/cashout-checkout/:id/payout-method{ rail, tokenAddress, address }; saves the canonical destination and makes the payout READY
POST /api/v1/cashout-checkout/:id/payout-quoteSame body; returns CashoutPayoutQuote without storing it
POST /api/v1/cashout-checkout/:id/confirmNo body; rechecks the saved destination and takes a fresh binding quote if bridged
POST /api/v1/cashout-checkout/:id/send-to-addressSame destination body; withdraws and sends the listed remainder

For example, Base USDC uses:

{
"rail": "relay_8453",
"tokenAddress": "0x833589fCD6eDb6E08f4c7C32D4f71b54bdA02913",
"address": "0x1234567890123456789012345678901234567890"
}

rail names the network; do not send a separate chainId. The strict schema accepts only crypto rails and trimmed tokenAddress and address strings of 1–64 characters. All three bodies reject unknown fields. The token must be in the payout catalog for that network.

The preview is available in READY with a crypto method and CONFIRM offered, or in LISTING with SEND_TO_ADDRESS offered. It prices the unpaid USDC in READY and the listed remainder in LISTING. It returns destination, usdcAmount (decimal USDC), estimatedAmount (decimal destination token) and ISO quotedAt, inside the standard envelope with message “Payout priced”. For Base USDC the two amounts are equal and Relay is not called. A zero output, including in a preview, returns 422 CASHOUT_PAYOUT_AMOUNT_TOO_LOW. An unavailable action returns 409 CASHOUT_PAYOUT_QUOTE_NOT_ALLOWED with { status, actions }.

The estimate is not a promise. Confirm and send-to-address each take a new, strict, exact-input Relay or NEAR Intents quote, with the customer’s Peer wallet as refundTo, and send it once. A refused or failed quote creates no attempt, transaction or bridge. No delayed binding quote is refreshed or reused; a later refund lets the customer confirm again for a fresh quote.

In CashoutCheckoutState:

  • method for crypto is { rail, destination }, with destination chainId, tokenAddress, symbol, decimals and canonical address.
  • lastPayoutTarget is PayoutMethodView | null: { rail, handle, currency } for fiat or { rail, destination } for crypto. It only prefills an available rail and never a bridge deposit address.
  • settleTx is { chainId, txHash } | null: the destination transaction for a delivered bridge, or the Base transaction for other settled payouts. It is null when the newest attempt’s newest bridge was refunded; the refunded Base send is not a settling transaction.
  • payoutBridge is the newest attempt’s newest bridge, or null when absent or NOT_SENT. It contains provider, status (QUOTED, BRIDGING, DELIVERED or REFUNDED), destination, usdcAmount, estimatedAmount, deliveredAmount, destinationTxHash, refundedAmount and refundTxHash. The delivered and estimated amounts use destination-token decimals; the USDC and refunded amounts use USDC decimals. Unresolved result fields are null.

Errors​

Failures use the standard envelope with success: false and a message; the merchant failures below also carry an errorCode. Validation failures (400, Invalid request, with details in responseObject.fieldErrors and responseObject.formErrors), authentication failures (401) and rate limits (429) carry no errorCode. An unexpected 500 (Internal server error) also has no errorCode.

StatuserrorCodeWhen
400INVALID_IDEMPOTENCY_KEYThe Idempotency-Key header is missing or malformed
400PAYOUT_TOKEN_UNSUPPORTEDThe merchant create request’s payout is not USDC on Base (independent of the customer’s destination)
400PAYOUT_NOT_STEP_MULTIPLEThe payout is below one payout step or not a whole multiple of it
400PAYOUT_ABOVE_MAXThe payout is above the payout maximum
400PAYOUT_FUNDING_TOKEN_UNSUPPORTEDThe funding token is unsupported, switched off by Peer, or not in your allowed funding tokens
403IP_NOT_ALLOWEDYour API-key IP allowlist refused the request; see IP allowlist
403PAYOUT_SANDBOX_UNSUPPORTEDThe API key belongs to a sandbox merchant
403PAYOUTS_DISABLEDNo payout methods are turned on in your payout settings
403PAYOUT_MERCHANT_WALLET_MISSINGYou charge a merchant fee but have no merchant wallet to receive it
404PAYOUT_NOT_FOUNDNo payout with this id for your merchant
409MERCHANT_CONFIG_MISSINGYour merchant account setup isn't finished (create)
409IDEMPOTENCY_KEY_CONFLICTThe key was already used with a different body
409IDEMPOTENCY_REQUEST_IN_PROGRESSA request with this key is still being processed
409PAYOUT_NOT_CANCELLABLEThe payout is no longer unfunded: status or fundingStatus is not AWAITING_FUNDING. responseObject is { status, fundingStatus }
409PAYOUT_FUNDING_DETECTEDRelay has a record for the deposit address, including a failed or refunded transfer. The payout can't be cancelled; if no USDC arrives, it expires at the funding deadline once nothing is still converting
422PAYOUT_REQUESTED_RAILS_UNAVAILABLENone of the request’s rails remains effective: “None of the requested payout methods are available right now”
422PAYOUT_NO_RAILS_AVAILABLEPeer has switched off every payout method you turned on, crypto networks included
422PAYOUT_ROUTE_UNQUOTABLEThis funding route cannot be priced for the requested payout
422PAYOUT_FUNDING_AMOUNT_TOO_LOWRelay refused the funding route with AMOUNT_TOO_LOW: “This payout is too small to route from this funding token. Choose a larger payout or another funding token.”
502PAYOUT_PRIVY_WALLET_FAILEDThe customer's wallet could not be prepared; retry with the same key
502PAYOUT_RELAY_QUOTE_FAILEDAny other Relay quote failure, including coded refusals a retry won't fix. Retry with the same key; if it keeps failing, use another funding token
502PAYOUT_RELAY_STATUS_FAILEDFunding could not be checked before cancelling; retry the cancel

A request body that fails validation returns 400 before the idempotency key is checked. For example, Bitcoin funding without refundAddress returns 400 with Bitcoin funding needs a refund address in responseObject.fieldErrors.funding. An invalid Bitcoin refund address gives Must be a Bitcoin address; a wrong Bitcoin token gives Must be the BTC token address. On other chains, a non-EVM funding.tokenAddress gives Must be an EVM address, also a 400 validation error, before the funding-token availability check.

The customer checkout behind checkoutUrl returns these codes. You never call it, but you may see them in support conversations. The messages below are the customer page’s copy, mapped from the API error codes; the raw API message can differ. Except for CASHOUT_WALLET_SHORT, customer 409 responses carry { status, actions } in responseObject.

StatuserrorCodeWhen
401CASHOUT_LINK_EXPIRED“This withdrawal has ended. Open the Peer app to see what’s in your wallet.” An action on a final payout; responseObject is { status }. GETs remain readable
401CASHOUT_LOGIN_INVALID“Your sign-in has expired. Sign in again to continue.” The Peer login token could not be verified
401CASHOUT_LOGIN_REQUIRED“Sign in to continue.” Missing or empty Bearer login token
401CASHOUT_TOKEN_INVALID“This link doesn’t work. Ask whoever sent it for a new one.” Missing or wrong link token, or unknown payout id
403CASHOUT_PLAYER_MISMATCH“This withdrawal belongs to a different Peer account.” The signed-in Peer account does not match the payout
404CASHOUT_NOT_FOUND“We couldn’t find this withdrawal.” The payout was not found after link authentication
409CASHOUT_ATTEMPT_NOT_LISTED“This withdrawal already moved on. The page now shows where it is.” There is no active escrow listing to withdraw
409CASHOUT_BUYER_PAYMENT_ACTIVE“Someone is paying you right now, so this can’t change.” A method change cannot withdraw while a buyer pays
409CASHOUT_CONFIRM_CONFLICT“This withdrawal already moved on. The page now shows where it is.” A concurrent change prevented confirmation or send-to-address; the page reloads the payout
409CASHOUT_METHOD_LOCKED“You can’t change the withdrawal method right now.” Saving or changing the method is not offered, or the payout changed before it could be saved
409CASHOUT_NOT_READY“This withdrawal already moved on. The page now shows where it is.” Confirmation has no eligible method, state or unpaid amount
409CASHOUT_PAYOUT_BRIDGING“Your withdrawal is on its way. Wait for it to arrive.” Customer actions blocked by a QUOTED or BRIDGING bridge
409CASHOUT_PAYOUT_QUOTE_NOT_ALLOWED“This withdrawal can’t be priced right now.” Preview has no eligible action or its amount is zero
409CASHOUT_RELIST_NOT_ALLOWED“You can’t resume this right now. The page now shows where your withdrawal is.” Relist is not offered: the range already matches what is left, a buyer is paying, the listing no longer takes new buyers, or the payout changed. Also returned when the remainder is below the method’s minimum
409CASHOUT_REMAINDER_CHANGED“What’s left changed. Check the amount and try again.” The listed remainder changed before send-to-address could start; the page reloads the payout
409CASHOUT_SEND_IN_PROGRESS“Your withdrawal is being sent.” A listing (including its group configuration), relist, withdraw or USDC transfer is in progress, including a remainder exit. Confirming, saving or changing the method, relisting and sending to an address are blocked
409CASHOUT_SEND_NOT_ALLOWED“This can’t be sent to an address right now.” Sending needs a partial payment, a listed remainder, no buyer payment or send in progress, and a crypto network on the payout
409CASHOUT_SIGNER_NOT_ATTACHED“Your withdrawal is still getting ready. Try again in a few seconds.” The payout signer is not attached; the page attaches it before retrying an action that sends
409CASHOUT_SKIP_NOT_ALLOWED“You can’t skip connecting right now.” SKIP_CONNECT is not offered; a QUOTED or BRIDGING bridge returns CASHOUT_PAYOUT_BRIDGING first
409CASHOUT_WALLET_SHORT“Your wallet no longer has enough USDC for this withdrawal.” The checkout offers only your contact link until the background check closes the payout. responseObject is the checkout state, including walletShortfall with decimal USDC requiredAmount and availableAmount
422CASHOUT_ADDRESS_NOT_ALLOWED“That address can’t receive withdrawals”: the crypto payout or remainder address is refused; responseObject.reason is INVALID, ZERO, OWN_WALLET, ESCROW or TOKEN. For INVALID, the page says “That isn’t a wallet address” or Enter a valid {Network} address when the network is known. For OWN_WALLET, it says “That’s the wallet the money is already in. Enter a different address.”
422CASHOUT_AMOUNT_OUTSIDE_RAIL_LIMITS“This amount is outside the limits for that withdrawal method. Choose another.” The unpaid amount cannot be listed on this method
422CASHOUT_CREDENTIAL_INACTIVE“Connect your account first.” Confirmation needs an active connection and the customer has not skipped connecting
422CASHOUT_PAYEE_HANDLE_INVALIDThe account does not fit the platform rules, or Peer refused it. Messages are “Enter a valid Venmo username”, “Enter a valid $Cashtag”, “Enter a valid PayPal username”, “Enter a valid Zelle email”, “Enter a valid Revtag” or “Enter a valid $ChimeSign”
422CASHOUT_PAYEE_NOT_FOUNDPeer found no such account. Messages begin “We couldn’t find that” and name the Venmo username, $Cashtag, PayPal username, Zelle email, Revtag or $ChimeSign; they ask the customer to check spelling and, for Cash App, public visibility, or for Revolut/Chime, discoverability
422CASHOUT_PAYEE_NOT_REGISTERED“Set up this PayPal account in Peer first, then try again.” The PayPal account is not registered in Peer
422CASHOUT_PAYOUT_AMOUNT_TOO_LOWThis amount is too small to send as {SYMBOL} on {Network}. Choose another coin or network. For Zcash: This amount is too small to send as ZEC on Zcash. Choose another coin or network. Relay or NEAR minimum refusal, or zero output, in preview or binding quote
422CASHOUT_PAYOUT_DESTINATION_UNSUPPORTED“This coin or network isn’t available for withdrawals.” The token is not supported on the selected network
422CASHOUT_PAYOUT_NETWORK_UNAVAILABLE{Network} isn’t available for this withdrawal. Choose another network. For Zcash: Zcash isn’t available for this withdrawal. Choose another network. The rail is absent from current payout rails, including after ops remove it. Without a known network, the page uses “That network”
422CASHOUT_PAYOUT_ROUTE_UNAVAILABLE{SYMBOL} on {Network} isn’t available right now. Choose another coin or network. The provider route is unavailable; see crypto payouts
422CASHOUT_PAYPAL_EMAIL_INVALID“Enter a valid PayPal email”. The supplied PayPal email cannot be normalized as an account email
422CASHOUT_PAYPAL_EMAIL_MISMATCH“That email isn’t the one on this PayPal account.” The email does not match the registered PayPal payee
422CASHOUT_RAIL_UNAVAILABLE“That withdrawal method isn’t available. Choose another.” The selected fiat rail is absent from the payout
502CASHOUT_PAYOUT_QUOTE_FAILED“We couldn’t price this withdrawal right now. Try again.” Any other quote failure
503CASHOUT_LISTING_UNAVAILABLE“Withdrawals to that app aren’t available right now. Try again later.” Peer Pay cannot prepare the Venmo or PayPal group transaction; nothing was sent
503CASHOUT_PAYEE_UNAVAILABLE“Something on our side isn’t responding. Try again shortly.” Peer could not check or register the payout account

See crypto payouts for provider refusals and network availability.