Skip to main content

CLI command reference

Run peer-pay-cli COMMAND --help for usage and an example. A command group such as peer-pay-cli webhooks lists its subcommands without contacting an API. Unsupported options and conflicting request options fail before authentication or network requests. On a terminal, missing required positionals and the required flags below are filled in after resolving the profile. Without a terminal, missing arguments retain their usage errors.

Filling in required arguments​

A fully specified command runs without argument prompts. For example:

peer-pay-cli webhooks test              # Select a webhook with Up/Down and Enter
peer-pay-cli webhooks test WEBHOOK_ID # Use this webhook immediately
peer-pay-cli payments create # Select ORDER_ID, then one of its enabled rails
peer-pay-cli payments cancel ORDER_ID # Select only a payment belonging to this order

Questions and selectors write to stderr, so --json on a terminal still allows prompts while stdout remains JSON. Piped stdin never prompts. Esc, Ctrl+C, or Ctrl+D cancels selectors. Headers show only the question label. Use Up/Down (or k/j, Ctrl+P/N) to move, then Enter or Space to confirm a single choice. In multiple selectors, Space toggles, a selects or clears all, and Enter confirms the set. Empty lists fail with a command for creating an item.

Pickers use the command's authentication scope, including --management where supported. Merchant order and merchant payment list pickers always use management authentication: the signed-in session on hosted profiles, or the local API key. Hosted management commands can therefore select IDs without a saved merchant API key.

Only required inputs are filled in. Optional filters (--status, --page, --limit, --cursor, --kind) and other optional flags remain unprompted, including crypto-simulate's optional --payment-id. The sole companion question is team invite --role: when email is asked interactively and role was omitted, the role selector defaults to MANAGER. Supplying email skips that selector too.

The registry uses these sources for every required positional:

CommandsMissing inputSource / labels
refunds get, refunds create, refunds cancel, orders get, orders cancel, orders resize, orders payments, payments create, payments cancel, payments recreate, payments extend, payments fulfill-sar, actions get, sandbox fail-payment, sandbox expire-payment, sandbox crypto-simulate, simulateORDER_IDMerchant order list, GET /api/v1/merchants/me/orders; ID, status, requested USDC amount, or Awaiting amount for an open-amount order with no saved amount. A local profile reads the local mock, including for simulate.
payments cancel, payments recreate, payments extend, payments fulfill-sarPAYMENT_IDAfter resolving ORDER_ID, GET /api/v1/merchants/me/orders/ORDER_ID/payments; ID, rail, status.
disputes getDISPUTE_IDDispute list, GET /api/v1/merchants/me/disputes; labels are DISPUTE_ID · STATUS · AMOUNT USDC · ORDER_ID.
payments bridgePAYMENT_IDMerchant payment list, GET /api/v1/merchants/me/payments; ID, rail, status.
webhooks update, webhooks delete, webhooks test, webhooks deliveriesWEBHOOK_IDGET /api/v1/webhooks; URL, active state, ID.
merchant switchMERCHANT_IDaccessibleMerchants from GET /api/v1/auth/me; merchant name and access role.
useNAMESaved profiles, with the active profile marked; no network request.
team resend, team revoke, local accept-inviteINVITE_IDPending invitations from GET /api/v1/merchants/me/invites; email and role.
team removeUSER_IDmerchantUsers from GET /api/v1/merchants/me; email (or ID when email is null) and role; excludes OWNER and locked (removalLocked) members.
onboarding ackSTEP_IDShared ONBOARDING_STEP_REGISTRY, with completed steps marked from GET /api/v1/merchants/dashboard/me/onboarding.
payouts get, payouts cancel, payouts timeline, payouts emails, payouts resolve-send, and every payouts simulate-* commandPAYOUT_IDPayout list, GET /api/v1/payouts; labels are PAYOUT_ID · STATUS · AMOUNT USDC.
payouts resolve-funding-issueFUNDING_ISSUE_IDOpen funding issues from GET /api/v1/admin/cashouts/support?status=open; ID, kind, payout ID. Local profiles only.
local retryDELIVERY_IDSelect a webhook first, then GET /api/v1/webhooks/WEBHOOK_ID/deliveries; delivery ID, status, event.
actions getACTION_IDText question; no action list endpoint exists.
apiMETHOD, PATHMethod selector (GET, POST, PUT, PATCH, DELETE), then a text path question.

Required flags use these sources:

CommandsMissing flagSource
payments create--railAfter resolving ORDER_ID, read GET /api/v1/orders/ORDER_ID and select from that order's enabledRails, labelled with each rail name. Falls back to a text question only if the order cannot be read.
quotes availability--quote-modeBackend QuoteModeSchema: exact-fiat or exact-token.
quotes availability--amount, --destination-chain-id, --destination-token, --destination-addressText questions.
webhooks create--urlText question. Omitted --events defaults to every event without prompting.
team redeem--tokenText question.
sandbox fail-payment, sandbox expire-payment--payment-idPayments belonging to the resolved order, as above.
sandbox crypto-simulate--outcomeBackend SimulateSandboxCryptoOutcomeSchema choices.
simulate--eventShared WebhookEventType, also used by local events.
merchant tier--tierBackend SelfServeTierSchema choices.
merchant ip-allowlist--entriesText question for comma-separated public IPs/CIDRs; skipped with --clear.
fees settlement-amounts--fee-payerShared FeePayer choices.
fees settlement-amounts--net-settled-usdc, --total-usdc-fee, --payment-amount, --currency-per-usd-rateText questions.
refunds create--seller-usernameText question.
orders resize--amount, --expected-amountText questions.
payments fulfill-sar--tx-idText question.
merchant bridge-signer-status--signer-idText question.
orders support-links--order-idsText question for comma-separated IDs.
merchant concierge--contact-email, --answersEmail text question, then nine intake questions with fixed-option selectors where available.
merchant concierge-inquiry--contact-email, --answersEmail text question, then nine intake questions with fixed-option selectors where available.
merchant logo--fileText question for the file path.
team invite--emailText question, then optional role selector from CreateMerchantInviteSchema.
credentials export--outputText question for the output path.
payouts create--idempotency-key, --email, --amount, --funding-chain-id, --funding-tokenText questions. With --data, supply --idempotency-key explicitly.
payouts configure-local--railsText question for comma-separated rail ids.
payouts simulate-funding, payouts simulate-payout-bridge--statusFunding status selector: success, failure, refund, pending.
payouts simulate-credential--statusCredential status selector: active, inactive, missing.
payouts simulate-payment--stepText question: signaled, replaced, expired, pruned or fulfilled.
payouts simulate-wallet-balance--amountText question for the wallet's USDC balance.
local settings, local admin, payouts resolve-funding-issue--dataMust be supplied explicitly; raw JSON is never prompted.

On commands that accept --data, supplying it skips the other required flag questions and validation requirements; missing positionals still prompt first. This includes payments create, team redeem, and quotes availability. local settings, local admin and payouts resolve-funding-issue still require explicit --data. orders create amount, fiat and open-amount inputs remain explicit. Optional --quote-limit and --nearby-quotes-count never prompt. merchant update preserves the server's current name when --name is omitted, without prompting. Choice fetches only use existing GET routes. Paginated lists show the API's default page; supply an ID directly to address an item outside that page.

Global options​

OptionMeaning
--profile NAMEExplicit saved profile; takes precedence over --local.
--localTarget the local mock profile.
--config FILECredentials file. Overrides PEER_PAY_CONFIG and the default ~/.config/peer-pay/config.json.
--jsonFull JSON output. Automatically enabled when stdout is piped or redirected.
--show-secretsReveal otherwise-redacted API keys, order tokens, and signing secrets; webhooks create and merchant rotate-key already show newly issued credentials in full. Does not apply to serve, profiles, or handoff. Prefer credentials export for application setup.
--helpShow help for the selected command. No login or running server is required.
--versionPrint the installed CLI version.

Options can appear before or after the command. Success exits with code 0; errors go to stderr and exit with code 1, including after interactive prompts. Both streams are flushed before exit. serve stays running until stopped; startup failures exit with code 1. Interrupted client commands release their profile lock before exiting with 130 (SIGINT/Ctrl+C) or 143 (SIGTERM). Machine-readable errors contain success: false and message. API failures also retain errorCode when present, responseObject, and the HTTP statusCode. API command JSON retains the backend's success, message, responseObject, and statusCode fields. webhooks create and merchant rotate-key show newly issued credentials in full without --show-secrets, save them in the profile, and print a one-time notice to stderr so JSON stdout stays clean. All other commands keep credentials redacted unless --show-secrets is supplied.

Profiles​

After you log in, commands target your Peer Pay account; add --local to target the mock, or peer-pay-cli use local. Selection is --profile NAME > --local > saved activeProfile > local if it exists. With no profile, run peer-pay-cli login or peer-pay-cli serve. Serve always owns local and never changes the active profile.

Sessions​

Email sessions refresh only for management requests when the saved access token is missing or expired, or once after a 401. API-key commands and credentials export do not contact Privy. A management request performs at most one refresh.

Request input​

--data accepts inline JSON or a file prefixed with @. Quote inline JSON so your shell passes it unchanged:

peer-pay-cli merchant settings --profile live --data '{"defaultPaymentCurrency":"USD"}'
peer-pay-cli merchant settings --profile live --data @settings.json

Use either --data or individual body options; combining them is an error. Amounts and chain IDs in JSON use decimal strings. --events takes comma-separated webhook event names. IDs in examples are placeholders for IDs returned by the API.

Interactive API errors report HTTP STATUS: MESSAGE [ERROR_CODE] (omitting the code suffix when absent), using the API message when available or HTTP status text for non-JSON responses (Request failed if the status text is empty). With --json or non-TTY output, stderr contains the failure envelope as JSON. Malformed --data JSON reports an input hint; unrelated JSON parse errors retain their own message. Successful bodyless HTTP 204 responses return responseObject: null. Local integration status identifies webhook deliveries with no HTTP response and asks you to check the webhook URL and delivery details.

Getting started​

With no command in a terminal, the CLI offers sign-in, merchant setup, coding-agent handoff, local sandbox, remote sandbox (item 5), status, and exit. Without a terminal it prints top-level help to stderr and exits 2.

Run peer-pay-cli serve to start the local sandbox. Local state is in memory unless --state FILE is set; Ctrl+C saves persistent state and stops the server. serve binds only to 127.0.0.1; --port defaults to 4020. --api-key, or the PEER_PAY_API_KEY environment variable, sets the local test key; otherwise a key is generated. With --state, a key that differs from the persisted one is rejected with --api-key differs from the persisted state key.

Login derives a profile name from the merchant and makes it active. Use --profile NAME to name it explicitly. It sends an email code unless --token-stdin is supplied. --api-url selects a backend origin (default https://api.pay.peer.xyz); --merchant-id selects an accessible merchant. The CLI gets the public Privy client ID and native app identifier from Peer Pay automatically when configured by the server. --client-id and --app-identifier <id> override their respective values. Both are saved with the profile and reused when refreshing the session. Do not place access tokens in command arguments.

Setup accepts --file setup.json, --dry-run to preview, or --yes to apply without confirmation. --dry-run and --yes cannot be combined. Destination chain IDs appear as strings in setup dry-run output. See Merchant setup. Logout removes local credentials only.

serve​

Start the local mock backend.

Open the server's root URL in a browser to see a landing page with the local ledger (merchant and counts), quickstart commands, endpoint links, documentation links, and the command reference. The page never includes credentials. Every other unknown path returns a JSON 404.

peer-pay-cli serve [options]

Options: --port, --state, --api-key.

Ctrl+C (SIGINT) or SIGTERM closes all HTTP connections, including idle keep-alive connections and unfinished requests, then saves state and releases the <state>.lock file. A hard kill such as SIGKILL bypasses cleanup; remove a stale lock only after confirming its server process has exited.

Example:

peer-pay-cli serve --state ./peer-pay.json

Local commands use the API key printed by serve. login --profile local is rejected by the CLI; direct POST /api/v1/auth/login returns an envelope with the local owner fixture profile with both wallet addresses already present; it does not authenticate with Privy. Public GET /api/v1/auth/cli-config returns {appId:"local-mock"} as a placeholder.

login​

Sign in or create a merchant account with email. The CLI checks API login configuration before requiring a terminal for email verification.

The result reports success: true, profile, and activeProfile after saving the authenticated profile. Without --profile, login reuses a profile with the same API URL and merchant ID, or derives a lowercase name using letters, numbers, underscores, and hyphens (up to 32 characters). An empty name uses merchant- plus the first eight merchant ID characters; name collisions receive a numeric suffix. The name local remains reserved for the mock. Owner wallets are provisioned automatically by the server on first sign-in, and later owner sign-ins retry missing wallets, regardless of which client is used. Existing wallet addresses are preserved. The CLI reports walletsProvisioned: true when both v1 addresses are present in the login response. If either is missing, sign-in still succeeds with walletsProvisioned: false and a message pointing to the manual retry command: peer-pay-cli merchant provision-wallets --profile NAME. For managers and cashiers, walletsProvisioned is null. The CLI does not make a separate provisioning request during login.

Login saves only non-empty API keys. A cashier-masked empty key is not stored, and the CLI clears that profile's cached API key, order tokens, and webhook secrets. After login, only merchant rotate-key replaces the saved API key. Generic API responses containing apiKey do not update credentials.

peer-pay-cli login [options]

Options: --email, --referral-code, --api-url, --merchant-id, --client-id, --app-identifier, --token-stdin, --transfer-token.

--transfer-token sends the token from a merchant-account-transfer email. For an account with no merchant yet, sign-in then returns a routing state instead of creating a merchant:

  • PENDING_OWNERSHIP_TRANSFER: login saves a profile without a merchant (named pending-transfer, or --profile NAME) and makes it active. Run peer-pay-cli transfer accept --profile NAME --token TRANSFER_TOKEN next. Without --transfer-token, an email with a pending transfer gets the same state, and the message asks you to open the link from the email.
  • OWNERSHIP_TRANSFER_EMAIL_MISMATCH: the transfer is for another email, shown masked (a•••@example.com). No profile is saved.
  • OWNERSHIP_TRANSFER_UNAVAILABLE: the transfer was withdrawn, expired, or the token is unknown. No profile is saved. Sign in again without --transfer-token.

An account that already has a merchant signs in normally; accept the transfer with transfer accept.

Example:

peer-pay-cli login --email owner@example.com

setup​

Configure your hosted merchant with a guided setup or JSON file. In a terminal, setup offers sign-in first when the profile is missing, local, or has no management session, then continues the guided questions. Without a terminal, sign in using peer-pay-cli login first; run peer-pay-cli setup --file setup.json --dry-run to preview or peer-pay-cli setup --file setup.json --yes to apply.

--dry-run validates the input before reading current state, makes no changes, and does not provision a sandbox. The preview omits the onboarding checklist; guided pricing-plan selection is deferred until you choose to apply. A setup file can include an explicit tier.

Setup checks your current role before applying changes. Managers can apply settings and enable the sandbox; owner-only onboarding reads and guided pricing selection are skipped. The successful result includes onboarding: 'skipped: requires owner', and a requested handoff still runs. Cashiers cannot run setup.

A logo is optional in both guided setup and setup JSON files. Saving the integration path completes the preceding profile, industry, and payment-method steps.

In a setup JSON file, omitting webhook.events defaults to every webhook event.

peer-pay-cli setup [options]

Options: --file, --yes, --dry-run, --harness, --project, --launch.

Use --harness codex|claude|opencode|gemini|prompt to prepare a handoff after setup is applied. Add --launch to start the installed agent in --project DIR (default: current directory). A cancelled setup does not create a handoff. Use --dry-run separately to preview settings.

Example:

peer-pay-cli setup --profile live

Launched harnesses receive PEER_PAY_CONFIG but do not inherit PEER_PAY_API_KEY from the CLI environment.

handoff​

Prepare integration instructions or launch your coding agent.

peer-pay-cli handoff [options]
OptionMeaning
--harness NAMEcodex, claude, opencode, gemini, or prompt. Prompts in a terminal; defaults to prompt in scripts.
--project DIRRepository to open in the agent. Defaults to the current directory.
--output FILEWrite instructions to a new file. Defaults to a unique file beside the CLI config, under handoffs/. Existing files are never overwritten.
--launchStart the selected installed agent and wait for it to exit. Requires a harness other than prompt.

Example:

peer-pay-cli handoff --harness codex --project ./my-store --launch

Supported agents are Codex, Claude Code (claude), OpenCode, and Gemini CLI. Install and sign into your chosen agent separately; its executable must be on PATH. The CLI starts an interactive session with the agent's normal permissions. In a terminal, missing --harness opens an arrow-key chooser (Up/Down or k/j, Enter or Space to choose, Escape, Ctrl+C, or Ctrl+D to cancel), missing --project prompts with the current directory as default, and omitting --launch asks whether to launch the selected agent. Prompt-only never launches. Without a terminal, no questions are asked: omitted harness defaults to prompt, the project defaults to the current directory, and launch requires --launch. Handoff uses either the selected hosted profile or local profile; with neither, run peer-pay-cli login or peer-pay-cli serve. Use --harness prompt to give the instructions to another coding tool yourself.

The instructions identify your selected profile and config path, describe the SDK and webhook integration, and outline local and hosted sandbox tests. They contain no API keys, tokens, or signing secrets. The agent can use the CLI with your selected profile by passing --profile NAME on every merchant command. Handoff does not change the active profile, including when invoked with --profile NAME. Local tests use peer-pay-cli serve --profile local and --profile local on client commands. --local is an alternative only without --profile, which takes precedence. The agent is instructed to export credentials directly into an ignored private file and ask before changing merchant settings or deploying.

peer-pay-cli handoff --profile live --harness claude --project ./my-store --launch
peer-pay-cli handoff --harness opencode --project ./my-store --launch
peer-pay-cli handoff --harness gemini --project ./my-store --launch
peer-pay-cli handoff --output ./peer-pay-instructions.md

A missing executable or nonzero agent exit makes the command fail; the error includes the saved prompt path so you can resume manually. The agent receives PEER_PAY_CONFIG for the selected credentials file. Its own terminal output is interactive even when the CLI uses --json; omit --launch for JSON-only scripts.

status​

Check integration verification for the selected profile.

peer-pay-cli status

Example:

peer-pay-cli status --profile live-sandbox

profiles​

List saved profiles without credentials.

peer-pay-cli profiles

Example:

peer-pay-cli profiles

logout​

Remove a saved profile from this computer.

peer-pay-cli logout

Example:

peer-pay-cli logout --profile live

Orders​

Split-fee pricing accepts principal drift in either direction up to the greater of SPLIT_FEE_TOLERANCE_USD (default 1) and the expected principal multiplied by SPLIT_FEE_TOLERANCE_BPS (default 50, or 0.5%). The percentage is of the order principal, not the fee. Fiat, crypto, and Apple Pay use the same policy. Settled split-fee orders complete with a residual up to the larger of that tolerance and the order completion threshold below; actual paid amounts, fees, and residuals remain recorded.

Every order completes once its remaining amount is at most its completion threshold: the greater of ORDER_COMPLETION_THRESHOLD_USDC (default 1 USDC) and requestedUsdcAmount × ORDER_COMPLETION_THRESHOLD_BPS / 10_000 (BPS default 100, or 1%). Both the floor and the percentage are current settings applied on each rollup; changing either applies to every order on its next rollup, including orders loaded from a saved state file. With the defaults, a 20 USDC order completes at 1 USDC or less remaining, and a 1,000 USDC order completes at 10 USDC or less remaining. Hosted environments can override these defaults.

The local simulator reads these environment variables when it starts, using the same defaults and validation as the hosted API. Hosted commands use the server's configuration. Dollar values must be nonnegative with at most six decimal places; basis points must be an integer from 0 to 10000. Set both split variables to 0 to require exact split pricing.

orders create requires --amount, --fiat-amount with --fiat-currency, --open-amount with --currency, or --data. The optional --idempotency-key belongs to your business order: retry with the same key and amount to return the same order. USDC retries compare the order's current requestedUsdcAmount: after resizing from 10 to 20, retrying 10 returns HTTP 409 IDEMPOTENCY_KEY_CONFLICT, while 20 replays. Fiat retries compare the original requestedFiatCurrency and requestedFiatAmount in metadata, even after resizing; a USDC retry for a fiat order conflicts. Decimal comparisons truncate to six places. A replay returns a null order token; the CLI retains the token from its original creation response. Whitespace around USDC and fiat decimal inputs is trimmed before conversion; fiat metadata stores the trimmed amount. Both order-creation endpoints return Order created for a new order and Order already created for an idempotent replay.

Open-amount orders (--open-amount, or openAmount in --data) have no amount when they are created: the buyer types it in checkout. --currency is the order's currency, which --min, --max and --presets are in (the buyer types in checkout's selected currency, and the limits convert to it); --min, --max and comma-separated --presets (1 to 6) are optional and take at most 2 decimals. The local simulator always accepts open amounts; hosted commands return the API's HTTP 400 OPEN_AMOUNT_DISABLED until open amounts are enabled for that environment. The order starts with amountMode: "OPEN", null requestedUsdcAmount and remainingUsdcAmount, and amountVersion: 0. It cannot be combined with an amount, inPersonCheckout: true or dynamicOrdersEnabled: true. Invalid input returns HTTP 400 OPEN_AMOUNT_INVALID with a message that names the field, for example openAmount.presets: 5.00 is outside the allowed range 10.00 to 100.00 USD. The effective range is the tightest of --min / --max and the local settings minOrderAmountUsdc and maxOrderAmountUsdc, converted at the local fixture rate. As for hosted sandbox merchants, the merchant's order-size limits do not apply. A --max that converts above maxOrderAmountUsdc returns openAmount.maxAmount: must convert to at most ${max} USDC, and a preset above the range is outside it. An idempotent retry must repeat the same normalized openAmount. A fixed request that reuses the key of an open order, or an open request that reuses the key of a fixed order, returns HTTP 409 IDEMPOTENCY_KEY_CONFLICT.

On an open-amount order, GET /api/v1/orders/ORDER_ID/platform-quotes returns platforms: [] with amountRequired: true until an amount is saved. While the amount can change, every response also carries openAmountDisplay: the effective range and presets in fiatCurrency at the local fixture rates, exact in the order's currency (presets outside the range left out) and in whole units in any other (minimum up, maximum down, presets to the nearest unit inside the range), with currencyPerOrderUnit. An empty range has no presets. Its bounds and currencyPerOrderUnit are null and its presets empty when either currency has no fixture rate. Add amount (in fiatCurrency) to price a draft: the response adds amountUsdc and applePay (chargeUsdcAmount, eligible), and nothing is saved. payments create --amount A --amount-currency C --expected-amount-version V, with --currency C on a fiat rail, saves a new amount, typed in C, with the payment. V must equal the order's amountVersion. The order's amounts and fiat metadata (in C) are updated, amountVersion goes up by one, and ORDER_AMOUNT_SET is sent before PAYMENT_CREATED. Repeating the saved amount in the same currency takes the normal path and sends no event; the same number in another currency is a different amount. A different amount returns:

  • HTTP 409 AMOUNT_LOCKED with { reason } while a payment can still complete: a CREATED or EXPIRED payment, a cancelled Zcash payment, a failed Relay payment the hosted API can still reopen, or any settled payment. A draft quote with any amount returns the same while the order is locked;
  • HTTP 409 AMOUNT_CONFLICT when the version is stale;
  • HTTP 400 AMOUNT_OUT_OF_RANGE with { effectiveMin, effectiveMax, currency } outside the effective range converted to the typed currency (the body's range is in that currency), including an amount above maxOrderAmountUsdc on an order without --max;
  • HTTP 400 INTENT_ABOVE_MAX on a fiat rail when the amount plus buyer-paid fees would sign more than maxOrderAmountUsdc, even inside the range (see payments create). Draft quotes leave such rails out.
  • HTTP 400 when amount, amountCurrency and expectedAmountVersion are not sent together, or when amountCurrency is not a fiat code. A draft quote that sends amount with a token fiatCurrency, such as USDC, returns the same.
  • HTTP 400 on a fiat rail when fiatCurrency is not amountCurrency. An omitted fiatCurrency means USD, so a fiat payment with a non-USD amount must send fiatCurrency in that currency. Crypto and Apple Pay payments are not checked.

amount on a fixed order returns HTTP 400 AMOUNT_NOT_ALLOWED, and a payment on an open order with no saved amount and no amount returns HTTP 400 AMOUNT_REQUIRED. Resizing an open-amount order returns HTTP 400 RESIZE_NOT_SUPPORTED. The hosted order read adds openAmount.effectiveMin, effectiveMax and savedInput, and amountLock. Like the hosted API, the local read returns effectiveMin and effectiveMax as null once a payment locks the amount or the order is fulfilled or cancelled. The local server does not rate-limit draft quotes; the hosted API does, for sandbox merchants too. Like the hosted API for sandbox merchants, it offers no nearby amounts and skips the monthly volume cap.

The local platform minimum defaults to 1 USDC to match production and applies after USDC truncation or fiat conversion. Set minOrderAmountUsdc through local settings to simulate stricter environments. Smaller amounts return HTTP 400 AMOUNT_BELOW_MIN with Requested USDC amount must be at least ${min} USDC, where ${min} is the configured local minimum. The local platform maximum defaults to 10,000 USDC, the hosted API's per-order limit, and is checked on the same amount right after the minimum. Set maxOrderAmountUsdc through local settings to change it. Larger amounts return HTTP 400 AMOUNT_ABOVE_MAX with Requested USDC amount must be at most ${max} USDC, where ${max} is the configured local maximum, so a fiat amount is refused when its converted USDC amount is above it. Both checks precede DYNAMIC_ORDERS_DISABLED. Fiat amounts that round to 0.00 USDC are rejected first with HTTP 400 AMOUNT_NON_POSITIVE and Converted fiat amount must be greater than 0 USDC.

Merchant order-size limits (minOrderSizeUsdc and maxOrderSizeUsdc) are accepted and stored through the admin config endpoint but are not enforced on order creation. The simulated merchant is always a sandbox merchant, matching the hosted API's sandbox behavior. When either limit is supplied, admin config writes reject a minimum greater than the maximum with Minimum order size cannot exceed maximum order size. A minimum above the local settings maxOrderAmountUsdc is rejected with Minimum order size cannot exceed the 10000 USDC platform maximum, naming the current setting; a maximum above it is accepted. Unrelated admin PATCHes do not revalidate stored limits. The platform-wide minimum and maximum above still apply.

Per-order enabledRails overrides replace the merchant selection, so an override such as cashapp can be used even when absent from the merchant list. EXCLUSIVE_SAR and in-person orders remove non-SAR fiat rails such as Revolut, while retaining SAR and crypto rails. Runtime-disabled rails are filtered from REST responses, webhook orders, and admin recent orders but retained in the stored selection for re-enablement. If no eligible rails remain, creation returns HTTP 400 NO_ELIGIBLE_PAYMENT_RAILS with No eligible payment rails for this merchant. This check precedes in-person rail eligibility, amount resolution, dynamic-order eligibility, and payout validation. Apple Pay counts as eligible only when enabled and available and the destination resolves with a valid address, for any payout token. With Apple Pay as the only available rail (including when accompanying fiat rails are runtime-disabled), an invalid destination address returns NO_ELIGIBLE_PAYMENT_RAILS before any below-minimum amount or in-person rail error. This matches the backend, including its rails error for an invalid address. A valid destination passes this gate; an in-person order then returns IN_PERSON_NO_ELIGIBLE_RAIL, or a below-minimum amount returns AMOUNT_BELOW_MIN. In-person orders (POST /api/v1/merchants/me/orders with inPersonCheckout: true) also require an offerable fiat rail eligible for seller automated release after SAR and runtime filtering. Crypto-only selections return HTTP 400 IN_PERSON_NO_ELIGIBLE_RAIL with In-person checkout requires at least one seller automated release eligible fiat rail.

Payout overrides in --data validate destinationAddress against destinationChainId: EVM, Solana, and Tron use the backend's address validators. After the earlier eligibility and amount checks pass, invalid addresses return HTTP 400 DESTINATION_INVALID with Destination address format does not match destination chain. The local merchant has provisioned EVM and Solana wallets and derives its Tron address from the EVM wallet. Orders use the backend merchant wallet resolver to select the wallet for the destination chain when no explicit address is supplied, including for Solana chain overrides and merchant payout defaults. After address validation, payout configuration failures return HTTP 400 PAYOUT_CONFIG_INVALID: Unsupported payout destination chain, Unsupported payout destination token for chain, or Merchant payout configuration requires sweep support for non-Base destinations. Solana USDC uses the provisioned Solana wallet by default and requires sweepEnabled: true. Tron creation requires USDT and sweep support, including when using the derived Tron address; Tron USDC is rejected. Base USDC does not require sweep support.

Setting dynamicOrdersEnabled: true cannot override a disabled merchant setting: creation returns HTTP 400 DYNAMIC_ORDERS_DISABLED with dynamicOrdersEnabled cannot be enabled: dynamic orders are disabled for this merchant. Omitting the field inherits the merchant setting; false disables it for the order.

Resize requires a dynamic order and both --amount (new total) and --expected-amount (current total). Resize and payment cancellation need an order token saved by this CLI profile. Hosted order cancellation requires the signed-in merchant's management permissions. List commands use backend pagination defaults; use their filter flags and --page / --limit for subsequent pages.

orders create​

The local minimum defaults to 1 USDC and the local maximum to 10,000 USDC; set minOrderAmountUsdc and maxOrderAmountUsdc with local settings to change them. Decimal inputs are trimmed. Order rails override merchant rails, subject to SAR and runtime eligibility; in-person orders need an eligible fiat rail. Payout chains and tokens must be supported; non-Base-USDC payouts require sweep support. Dynamic orders require merchant enablement. See the rules above for validation details.

Create an order with --amount (requestedUsdcAmount), --fiat-amount plus --fiat-currency, or --open-amount with --currency and optional --min, --max and --presets (openAmount). The three amount modes are exclusive. --data is an exclusive raw JSON body alternative for all commands with body flags.

--notes accepts a JSON object string or @file. --enabled-rails is a comma list. --dynamic-orders-enabled takes true or false. Use --management for POST /api/v1/merchants/me/orders and optionally --in-person true|false; otherwise creation uses POST /api/v1/orders. Cashiers must use in-person checkout and cannot override payout destinations, rails, or fee settings.

--fee-payer accepts MERCHANT, PAYEE, or SPLIT. An explicit SPLIT override requires --buyer-fee-share-bps, an integer from 0 to 10000 in steps of 1000 (5000 = 50%). Omit both flags to inherit the merchant settings. In split mode the amount is the order principal; checkout adds the buyer share of fees, and settlement deducts all fees. The order retains its original buyer share after settings change. See split payment fees.

Hosted creation returns HTTP 409 MERCHANT_OWNERSHIP_CHANGED when the merchant account changed owners while the order was being created. Nothing was saved: retry the same request, with the same --idempotency-key, and the order uses the new owner's payout wallet. The local simulator never returns this code, because it has no concurrent ownership transfer.

peer-pay-cli orders create [options]

Options: --amount, --fiat-amount, --fiat-currency, --open-amount, --currency, --min, --max, --presets, --idempotency-key, --destination-address, --destination-token, --destination-chain-id, --fee-payer, --buyer-fee-share-bps, --enabled-rails, --dynamic-orders-enabled, --success-url, --cancel-url, --notes, --management, --in-person, --data.

Example:

peer-pay-cli orders create --amount 10 --idempotency-key order-123
peer-pay-cli orders create --amount 100 --fee-payer SPLIT --buyer-fee-share-bps 400
peer-pay-cli orders create --open-amount --currency USD --min 10 --max 2000 --presets 25,50,100

orders get​

Use --management for GET /api/v1/merchants/me/orders/:orderId with the signed-in session.

Read an order and its payments. The hosted aggregate includes order.applePayChargeUsdcAmount: the current remaining USDC balance grossed up for Apple Pay fees in PAYEE mode or increased by the buyer share of fees in SPLIT mode, or null when Apple Pay is not enabled or pricing cannot be resolved. Pricing failures log a warning without preventing the order read; payment creation still rejects invalid pricing. Local simulation uses the same two-pass gross-up as the API and the order's frozen fee configuration. Checkout hides Apple Pay when this charge or requestedUsdcAmount is above 2,500 USDC, and payment creation rejects Apple Pay orders and funding quotes above that cap with a 422.

peer-pay-cli orders get ORDER_ID

Options: --management.

Example:

peer-pay-cli orders get ORDER_ID

orders list​

List rows include settledPaymentCount (all settled payments, including repeated rails), settledPaymentRails (distinct settled rails, oldest payment first), and netSettledUsdcAmount (post-fee USDC total; null when nothing has settled or any settled payment has no recorded net). bridgeInfo describes the latest conversion attempt of the newest settled payment with an attempt, or is null when absent. These fields match the merchant order list in local mode.

List merchant orders with first-class filter and pagination flags:

peer-pay-cli orders list --order-id ORDER_ID
peer-pay-cli orders list --display-status ACTIVE --chargeback-status NONE --limit 10 --page 1
peer-pay-cli orders list --search "Cash App"

orderId is a case-insensitive substring match. search (at least three characters) takes precedence over orderId and matches order IDs, notes, metadata, payment rails/display names, payment IDs, recipient handles/addresses (payTo), payment-app transaction IDs (railTransactionId), settlement hashes (fulfillTransaction), and rail identifiers without treating % or _ as wildcards. Crypto token/chain aliases match crypto payments. Plain non-negative decimals, optionally prefixed with $ (for example $20 or 20.00), also match the requested fiat amount in order metadata or requested USDC amount by exact numeric equality. Amount searches follow the same three-character minimum, so use $20 or 20.00, not 20. Hosted commands use the API search. The local simulator applies the same rules, except payment-app transaction IDs, which the simulator never records. Search considers the newest 5,000 matching orders before other filters.

status accepts CREATED, PARTIALLY_FULFILLED, FULFILLED, or CANCELLED. displayStatus overrides status and selects CREATED orders with: no CREATED or EXPIRED payments (CREATED), any CREATED payment (ACTIVE), or an EXPIRED payment and no CREATED payment (EXPIRED). Failed/cancelled attempts do not make an order active or expired.

chargebackStatus is NONE, PARTIALLY_CHARGEBACKED, or CHARGEBACKED, derived from settled payments: none charged back, a mix of charged and uncharged, or all charged back. It combines with display/status filters. All filters apply before totals and pagination; results are newest first. Page defaults to 1; limit defaults to 20 and has a maximum of 100.

peer-pay-cli orders list

Options: --status, --chargeback-status, --display-status, --order-id, --search, --page, --limit.

Example:

peer-pay-cli orders list

orders cancel​

Cancel a CREATED order that has never had a payment. Success returns HTTP 200 with message Order cancelled and the mapped order directly in responseObject (no nested order field). Repeated cancellation returns 409.

peer-pay-cli orders cancel ORDER_ID

Example:

peer-pay-cli orders cancel ORDER_ID

orders resize​

Resize an unpaid CREATED dynamic order with no CREATED or EXPIRED payments, using its cached order token.

peer-pay-cli orders resize ORDER_ID [options]

Options: --amount, --expected-amount.

The expected amount must match the current total, and the new amount must differ. Amounts round to six decimals. Invalid amounts return 400; ineligibility returns 409 with RESIZE_CONFLICT and Order cannot be resized. The local sandbox uses the local settings minOrderAmountUsdc floor (default 1 USDC to match production) and ignores merchant-specific sandbox size limits. A total above the local settings maxOrderAmountUsdc (default 10,000 USDC, like the hosted API) returns 400 AMOUNT_ABOVE_MAX with Requested USDC amount must be at most ${max} USDC before any eligibility check. FAILED and CANCELLED payments do not block resizing; any payment row blocks order cancellation. Cancelling a non-CREATED order returns 400, except an already CANCELLED order returns 409.

Example:

peer-pay-cli orders resize ORDER_ID --amount 15 --expected-amount 10

Payouts​

Payouts are owed in USDC on Base. You fund them through a Relay deposit address with a supported EVM token or Bitcoin; Solana, Tron and Zcash cannot fund payouts. Customers can take any supported payout coin and network. See the Payouts API for the money rules, statuses, and error codes. These commands use the API key.

payouts create​

Create a payout for a customer email and get its funding deposit address.

peer-pay-cli payouts create [options]

Options: --email, --amount, --funding-chain-id, --funding-token, --refund-address, --merchant-reference, --return-url, --idempotency-key, --rails RAIL,RAIL, --data.

--amount is the USDC owed to the customer, before their chosen crypto conversion. It must be a whole multiple of the merchant's payout step (local default 100) and at most 1000 USDC. A step mismatch returns 400 PAYOUT_NOT_STEP_MULTIPLE (Payout amount must be a multiple of {step} USDC); an amount above the maximum that passes the step check returns 400 PAYOUT_ABOVE_MAX ("Payout amount is above the payout maximum"). Lower the local step with payouts configure-local --payout-step.

Use --data or individual request flags, except --idempotency-key, which works with either. Mixing them stops with "Use --data or individual request options, not both."

--funding-token is the token address you fund with on --funding-chain-id, and the optional --refund-address receives refunds if the deposit cannot be converted. Omit it to refund the sending wallet. Set it for exchange withdrawals: Relay does not automatically refund exchange wallets. --idempotency-key is sent as the Idempotency-Key header, 8–128 letters, digits, _ or -, and is required even with --data. The first response has idempotentReplay: false. Rerunning with the same key and body returns the existing payout with status 200, the current checkoutUrl, and idempotentReplay: true; a different body returns 409 IDEMPOTENCY_KEY_CONFLICT. The link token is derived from the payout id and the current signing secret, and checked against it on every request. The local server uses a fixed secret, so a local link stays the same. Never send funds after funding.quoteExpiresAt; create a new payout with a new key.

For Bitcoin funding, pass --funding-chain-id 8253038 --funding-token bc1qqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqmql8k8. --refund-address is then required and must be a Bitcoin address you control (bc1…, 1… or 3…); a missing or invalid one fails before any request, with the API's message. The local server prices BTC at a fixed $100,000 per BTC with the 2% volatile buffer, capped at 10 USDC, and gives the payout a random bc1q… deposit address. With the default 0% Peer fee and zero merchant fee, the example below asks for 0.00102 BTC: the buffer takes 100 USDC to 102, with no fee gross-up.

peer-pay-cli payouts create --local --email customer@example.com --amount 100 --funding-chain-id 8253038 --funding-token bc1qqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqmql8k8 --refund-address bc1qw508d6qejxtdg4y5r3zarvary0c5xw7kv8f3t4 --idempotency-key payout-btc-001

Example:

peer-pay-cli payouts create --email customer@example.com --amount 100 --funding-chain-id 8453 --funding-token 0x833589fCD6eDb6E08f4c7C32D4f71b54bdA02913 --refund-address WALLET_ADDRESS --idempotency-key payout-001

--rails optionally limits this payout to a non-empty comma-separated list such as venmo,relay_8453,near_intents_133701. It never adds a rail the merchant or Peer turned off. Unavailable requested rails are dropped; if none remain, create returns 422 PAYOUT_REQUESTED_RAILS_UNAVAILABLE (“None of the requested payout methods are available right now”). An empty merchant rail list returns 403 PAYOUTS_DISABLED ("Payouts are not enabled for this merchant"); if Peer has disabled every merchant rail, create returns 422 PAYOUT_NO_RAILS_AVAILABLE (“None of this merchant’s payout methods are available right now”). Unknown ids, crypto, duplicates and an empty rails array fail before any request with the API’s validation message (the API itself answers 400); an empty --rails flag stops with “--rails is required”. The API normalizes rail order: a reordered list replays with the same key; different rails conflict. A request without rails replays exactly as before (same key, same body). --data can supply the same rails array.

payouts get​

Show one payout with funding progress and status.

peer-pay-cli payouts get PAYOUT_ID

Another merchant's payout returns 404 PAYOUT_NOT_FOUND.

Example:

peer-pay-cli payouts get PAYOUT_ID

payouts list​

List payouts, newest first, filtered by email, status or reference.

peer-pay-cli payouts list [options]

Options: --email, --status, --merchant-reference, --page, --limit.

--status accepts AWAITING_FUNDING, FUNDED, READY, LISTING, PAYING, SETTLED, CANCELLED, or EXPIRED. --merchant-reference matches exactly. --limit is 1–100 (default 20). The response is { items, page, limit, total }.

Example:

peer-pay-cli payouts list --email customer@example.com

payouts cancel​

Cancel a payout that is still awaiting funding.

peer-pay-cli payouts cancel PAYOUT_ID

Only an AWAITING_FUNDING payout with nothing received can be cancelled; otherwise the command returns 409 PAYOUT_NOT_CANCELLABLE with the current status and fundingStatus. Hosted cancels also return 409 PAYOUT_FUNDING_DETECTED when funds are already on the way.

Example:

peer-pay-cli payouts cancel PAYOUT_ID

payouts configure-local​

Change the local simulator merchant's payout settings; every platform is on by default.

peer-pay-cli payouts configure-local [options]

Options: --rails, --fee-bps, --payout-step, --support-url.

Local profiles only. It runs the same settings schema as the dashboard's Settings → Payouts tab (PUT /api/v1/merchants/me/payout-settings), which the CLI can't call: those routes need a dashboard sign-in. --rails is required: a comma-separated list of any of venmo, cashapp, paypal, zelle, revolut, chime, the supported relay_<chainId> network ids and near_intents_133701, stored in rail order. For example --rails venmo,relay_8453,near_intents_133701. Opening an existing state file does not append ZEC to its saved rails. Use configure-local --rails with the full desired list, including near_intents_133701, to enable ZEC for new local payouts. Existing payouts keep their stored rails.

The local NEAR simulator prices 1 ZEC = 100 USD, enforces a 1.58 USDC minimum, and uses a 20-minute refund deadline, with sendBy 5 minutes before it. These are local fixtures; local settings rates does not change them. Live minimums come from NEAR. Zcash recipients must be transparent t1… or t3… addresses.

The retired crypto id is refused with 400. The local merchant starts with every platform on, so payouts work before this command ever runs. The local Peer fee defaults to peerFeeBps: 0 (0%), with Peer fee wallet 0x0000000000000000000000000000000000000002. Use local admin payout to set the Peer fee and local admin payout-settings to set its wallet. --fee-bps is the merchant fee, 0–1000 (default 0). A nonzero merchant fee requires an enabled Master Merchant Account; otherwise the save returns 403 PAYOUT_MERCHANT_FEE_REQUIRES_MASTER_ACCOUNT without changing settings. New payouts use zero merchant fees whenever that permission is off; existing payouts keep their fee snapshots. Peer fees are separate and only admins can set them (nonzero requires Concierge). --payout-step is the whole USDC every payout must be a multiple of, PAYOUT_MIN_USDC–1000 (default 100). --support-url is optional: an https:// link or an email, stored as mailto:. Each run saves every setting, so leaving it out saves no support link, and the customer checkout then shows no contact line. A bare --support-url is a parser usage error; an empty value (--support-url "") stops with "--support-url is required". A rail Peer has switched off (see local admin payout-settings) can still be saved; new payouts leave it out. The response is the saved settings plus availableRails and availableFundingTokens, what Peer currently allows. Each payout keeps the step and rails it was created with; the customer support link is read from current merchant settings.

Example:

peer-pay-cli payouts configure-local --local --rails venmo,cashapp --fee-bps 0 --payout-step 50 --support-url help@example.com

payouts simulate-funding​

Add a simulated Relay record to a local payout's deposit address and check its funding.

peer-pay-cli payouts simulate-funding PAYOUT_ID [options]

Options: --status, --received, --sent, --relay-request-id, --superseded-by, --refund-tx-hash.

Local profiles only; hosted profiles reject it before any request. Each call adds one record, as if Relay had reported one payment to the deposit address, then runs the same funding check the API runs. --status is the Relay status: success, failure, refund, or pending. --received is the USDC that reached the customer's wallet on Base for that record; it is required with success and not allowed with any other status. --sent is the funding-token amount the merchant sent (default 0), and is not allowed with failure or refund. The local simulator has no Relay or Base reads, so these amounts stand in for them.

The payout moves exactly as a hosted one would. It becomes FUNDED and sends PAYOUT_ORDER_FUNDED when the USDC received covers the payout, or when the merchant sent at least the quoted funding amount, no record is pending and some USDC arrived. If USDC arrived below the payout and you sent less than funding.amount, the payout stays AWAITING_FUNDING with fundingStatus: PARTIALLY_FUNDED and sends PAYOUT_ORDER_PARTIALLY_FUNDED on becoming partial or when received grows, before the quote expires. If you sent the full funding.amount and part is still converting, the payout keeps waiting. A failure or refund record counts as nothing received: the payout keeps waiting until its deadline. Each on-time record that adds USDC moves funding.quoteExpiresAt to 24 hours after it, unless it is already later. When the deadline passes, a partly funded payout with nothing pending becomes FUNDED with a deposit set from what arrived and sends PAYOUT_ORDER_FUNDED. funding.sentAmount and funding.missingAmount are in the funding token; funding.expectedAmount and funding.receivedAmount are in USDC. Funds that arrive after a payout left AWAITING_FUNDING only raise its received amount.

Money outside the normal funding path is recorded as a funding issue, as the API does, with a FUNDING_ISSUE timeline entry: USDC received above the quote when the payout is funded and --sent totals more than the quoted funding.amount (OVERPAYMENT), USDC that arrives after funding closed or on an expired payout (LATE_FUNDS), and each refund record (ROUTE_REFUND, keyed on --relay-request-id, or a generated id when it is omitted). Running the check again records nothing new. See payouts funding-issues.

--relay-request-id names the record's Relay request for any status; it is generated when omitted, and an ID the payout already has returns 409 PAYOUT_FUNDING_RECORD_EXISTS. --superseded-by (only with --status pending) links a quote-time record to the request Relay moved its deposit to, as Relay does for Bitcoin. While that request is among the payout's records, the linked record is dropped and the request keeps the earlier time; a record can't supersede itself. This is the same superseded-record rule the API applies to Relay's records. --refund-tx-hash (only with --status refund) records the refund transaction on the route-refund issue: a Bitcoin txid for a BTC payout, a 0x hash otherwise; a hash in the other format is recorded as no hash, as the API records it.

A Bitcoin "phantom" record keeps the payout waiting until its fill arrives:

peer-pay-cli payouts simulate-funding PAYOUT_ID --local --status pending --relay-request-id q1 --superseded-by f1
peer-pay-cli payouts simulate-funding PAYOUT_ID --local --status success --relay-request-id f1 --received 102 --sent 0.00102

Example:

peer-pay-cli payouts simulate-funding PAYOUT_ID --local --status success --received 106.4 --sent 110

payouts sweep-local​

Run the local payout jobs: funding, settlement, credentials, wallet balances.

peer-pay-cli payouts sweep-local [options]

Options: --now.

Local profiles only; hosted profiles reject it before any request. --now is the ISO time the sweep runs at (default: the current time). Pass a time after a payout's funding.quoteExpiresAt to reach the deadline without waiting: a payout with nothing received becomes EXPIRED and sends PAYOUT_ORDER_EXPIRED, and a partly funded one becomes FUNDED with what arrived and sends PAYOUT_ORDER_FUNDED. A pending record keeps it waiting until that record resolves. The response reports the number checked and the resulting payout views in items.

The sweep then runs the jobs the API runs in the background:

  • Settlement reconcile refreshes every LISTING and PAYING payout from its simulated deposit and escrow events, with the same decision as the API. A buyer who drops (their intent expires or is pruned) moves the payout back to LISTING with one ACTION_REQUIRED customer email per buyer. A buyer who pays part of what is left records a partial fill as payouts simulate-payment describes. A rest at or below 0.10 USDC is swept as dust and settles. A withdrawn deposit's exit waits while a withdraw or USDC transfer for the attempt is still in flight; a relist or Peer Pay group send does not hold it. settlement reports { checked, moved }.
  • Credential check reads the simulated credential of every LISTING Venmo, Cash App or PayPal payout (Zelle, Revolut and Chime have nothing to connect). One that is not active gets one ACTION_REQUIRED reconnect email per listing, unless it is still skipped. The job clears the skip when it sees an active connection; later lapses get the same reconnect email as any connected listing. credentials reports { checked, emailed, skipped }: checked counts all examined listings, including skipped ones. An active skipped listing is cleared and counted only as checked, with no email; an inactive skipped listing also counts under skipped, with no email.
  • Wallet check compares each wallet that has a payouts simulate-wallet-balance record with its FUNDED and READY payouts funded at least 10 minutes before --now. When the balance no longer covers their unpaid amounts (the deposit less what buyers paid), it handles the newest first. An unpaid payout becomes CANCELLED with cancelSource: "PEER_APP", a PAYOUT_ORDER_CANCELLED webhook and a cancelled email. A partly paid payout becomes SETTLED with settlement.returnedAmount, PAYOUT_ORDER_SETTLED and the settled email. Both count in cancelled. A payout whose listing or USDC transfer is being sent is not cancelled. walletBalances reports { wallets, cancelled }. wallets counts distinct wallets with a FUNDED or READY payout past the grace period, including wallets with no simulated balance.

There is no local send dispatcher (the API's dispatch-payout-transactions job): a send Pay queued from the customer wallet waits for payouts resolve-send.

Example:

peer-pay-cli payouts sweep-local --local --now 2026-09-26T12:00:01Z

payouts simulate-credential​

Set the simulated Curator credential of a local payout's fiat account.

peer-pay-cli payouts simulate-credential PAYOUT_ID [options]

Options: --status.

Local profiles only; hosted profiles reject it before any request. The customer must have saved a payout method first (PUT /api/v1/cashout-checkout/:id/payout-method), otherwise it returns 409 PAYOUT_METHOD_MISSING. A crypto, Zelle, Revolut or Chime payout method has nothing to connect, so it returns 409 PAYOUT_METHOD_NO_CREDENTIAL. --status is active, inactive, or missing, and applies to every local payout that pays the same rail and handle, like a Curator seller credential. The next checkout state read moves an unlisted payout from FUNDED to READY when it is active, and back when it is not, unless the customer skipped connecting. A state read or credential check that sees an active connection clears the skip. While a payout is LISTING, inactive or missing adds CONNECT_RAIL to the checkout actions, even if it is skipped. The next payouts sweep-local sends one reconnect email for the listing unless it is still skipped. The response has the payee hash and the status.

Pass the credential status explicitly: the interactive prompt offers funding statuses, which this command refuses. See Local customer checkout for the customer routes.

Example:

peer-pay-cli payouts simulate-credential PAYOUT_ID --local --status active

payouts resolve-send​

Confirm or fail what Pay is sending from a local payout's customer wallet.

peer-pay-cli payouts resolve-send PAYOUT_ID [options]

Options: --result, --count.

Local profiles only. Checkout's confirm, change-method, relist and send-to-address queue sends from the customer's wallet and return the checkout state with sending set to LISTING, TRANSFER, WITHDRAW or RELISTING and no actions. The hosted API sends them with the Peer Pay signer; locally nothing is sent and they wait for this command, which stands in for the API's dispatch-payout-transactions job and Base and returns the checkout state.

Only sends QUEUED or SENT when the call starts can land in it. One queued while it runs waits for the next resolve-send, as a separate Base transaction would. So a Venmo or PayPal listing takes a second resolve-send with the default count: the first lands the approve and deposit, the second the Peer Pay group. --result failed on the group row queues a withdraw of the listing; the next resolve-send lands it and the payout returns to FUNDED. While a buyer pays, a failed group send waits. The first resolve-send after the buyer's payment lapses queues the withdraw, and the one after that lands it.

Each call first re-applies waiting failed sends and confirmed group-exit withdraws held by a buyer. This includes exits waiting at their retry cap (3 withdraws). When only such waiting rows exist, the call answers 200 with the checkout state, lands nothing, and ignores --result failed.

  • --result confirmed (default) lands every still-pending send queued when the call started, in order. Any fiat (Venmo, Cash App, PayPal, Zelle, Revolut or Chime) confirm queues the USDC approve then createDeposit, and the payout becomes LISTING with its deposit linked. A direct Base USDC confirm queues one transfer and the payout becomes SETTLED, with PAYOUT_ORDER_SETTLED and the settled email. An unpaid method change's withdraw returns the payout to FUNDED with the method cleared. If a buyer payment opened after a method-change withdraw was queued, the withdraw returns only what is free, the deposit stays listed, and the payout settles if the buyer pays.
  • A bridged TRANSFER to a Relay or NEAR Intents deposit address becomes BRIDGING when confirmed, keeping the attempt ACTIVE and the payout READY (crypto) or LISTING (remainder). It emits no settled webhook or email yet. Use payouts simulate-payout-bridge to provide delivery or refund evidence. A failed Base TRANSFER marks its bridge NOT_SENT and follows the existing failure rules.
  • --result failed reverts the first send queued when the call started. A failed listing or crypto payout Base transfer closes the attempt and returns the payout to READY so the customer can confirm again. A failed withdraw leaves the listing in place. The state has lastSendFailed: true until the next send starts.

With confirmed, --count N lands only the first N sends queued when the call started, in order (default: all). A send to an address queues a withdraw then a transfer, so --count 1 lands the withdraw alone and the state shows sending: "TRANSFER".

A confirmed SET_INTENT_RANGE send sets the deposit's range to what is left, records a RELISTED timeline row and lets buyers signal again. A failed send leaves the old range and the checkout offers RELIST.

After a partial payment, a method change returns to FUNDED with the method cleared, keeping depositAmount and what was paid. The next confirm lists only the unpaid amount, below PAYOUT_MIN_USDC if necessary. A direct Base USDC send to an address ends SETTLED with settlement.sentAmount, settlement.sentTo and payoutTransfer. A bridged remainder stays LISTING until delivery, then reports the destination payout. A verified refund returns READY with the destination saved and refund losses deducted from the next payout. If its transfer fails after the withdraw lands, it settles returned instead. If a buyer holds the deposit when the withdraw lands, the withdraw returns only what is free, the queued transfer fails and the listing stays open. If the withdraw returns another amount, the queued transfer fails and the payout settles returned. A withdraw landing while a buyer holds the deposit returns only the free part and stops the deposit taking new buyers, so a later partial payment is not relisted.

A held group-exit withdraw stays pending (sending: WITHDRAW). After the buyer's payment lapses or partially completes, the first resolve-send queues another withdraw and the next lands it, returning to FUNDED with lastSendFailed: true, keeping depositAmount and what buyers paid. A buyer who pays in full settles the payout. A failed group-exit withdraw while LISTING or PAYING queues one retry for the next call, up to 3 withdraws total on the attempt. lastSendFailed also stays true while a payout whose group send failed is back in FUNDED or READY. If the deposit is gone, settlement refresh closes the attempt using every recorded withdrawal.

While a send is in flight, confirm, change-method, relist, send-to-address and the payout-method update return 409 CASHOUT_SEND_IN_PROGRESS with the payout's status and actions. While a bridge is QUOTED or BRIDGING, those customer mutations and skip-connect return 409 CASHOUT_PAYOUT_BRIDGING (“Your cashout is on its way; wait for it to arrive”), with { status, actions }; preview returns 409 CASHOUT_PAYOUT_QUOTE_NOT_ALLOWED. Confirm returns 409 CASHOUT_CONFIRM_CONFLICT if the payout already has an active attempt. Relist returns 409 CASHOUT_RELIST_NOT_ALLOWED when it is not available. Send-to-address returns 409 CASHOUT_SEND_NOT_ALLOWED when it is not available, 409 CASHOUT_REMAINDER_CHANGED when what is left changed, or 422 CASHOUT_ADDRESS_NOT_ALLOWED for a disallowed payout address. With nothing queued and nothing waiting, this command returns 409 PAYOUT_SEND_MISSING.

Example:

peer-pay-cli payouts resolve-send PAYOUT_ID --local
peer-pay-cli payouts resolve-send PAYOUT_ID --local --result failed

For a NEAR payout, a QUEUED TRANSFER at or after sendBy fails SEND_BY_PASSED whichever --result is given to resolve-send. No USDC moves: the bridge takes NOT_SENT, a whole payout stays READY, and a send-the-rest exit settles returned. An already SENT row is not failed by this deadline.

payouts simulate-payout-quote​

Set the next local payout price, or make it refuse.

peer-pay-cli payouts simulate-payout-quote PAYOUT_ID [options]

Local profiles only; hosted profiles reject the command before any request.

OptionMeaning
--localSelect the local simulator profile
--amount DECIMALDecimal destination-token output for the next Relay or NEAR preview or binding quote; zero is allowed as a fixture and makes that quote fail as too small. Excess destination-token decimals make the consuming quote return 400 CASHOUT_PAYOUT_BRIDGE_SIMULATION_INVALID; amounts are never rounded
--refuseAMOUNT_TOO_LOW, ROUTE_UNAVAILABLE or FAILED; mutually exclusive with --amount

Supply exactly one of --amount or --refuse. The next Relay or NEAR preview or binding quote for this payout consumes the override once; direct Base USDC does not consume it. AMOUNT_TOO_LOW (or a zero output) returns 422 CASHOUT_PAYOUT_AMOUNT_TOO_LOW; ROUTE_UNAVAILABLE returns 422 CASHOUT_PAYOUT_ROUTE_UNAVAILABLE for Relay or NEAR; FAILED returns 502 CASHOUT_PAYOUT_QUOTE_FAILED, with the API’s exact messages. Use ROUTE_UNAVAILABLE to simulate unavailable ZEC payouts, including a hosted API without a partner JWT. The next preview, confirm or send-to-address consumes it once, returns 422 and writes no attempt, bridge or send. Use FAILED to simulate a NEAR request failure. The local-only route is POST /local/payouts/:id/payout-quote.

peer-pay-cli payouts simulate-payout-quote PAYOUT_ID --local --refuse ROUTE_UNAVAILABLE

payouts simulate-payout-bridge​

Deliver, refund or hold a local bridged payout; NEAR bridges use NEAR's statuses.

peer-pay-cli payouts simulate-payout-bridge PAYOUT_ID [options]

Local profiles only; hosted profiles reject the command before any request. A payout bridge must already be BRIDGING after payouts resolve-send. Otherwise this answers 409 PAYOUT_BRIDGE_NOT_SENDING, “Nothing is on its way to another network”. The local-only route is POST /local/payouts/:id/payout-bridge.

OptionMeaning
--localSelect the local simulator profile.
--statusRequired: success, refund, failure or pending.
--delivered DECIMALRequired with success; positive destination-token amount. More decimals than the token supports returns 400 PAYOUT_BRIDGE_SIMULATION_INVALID, including trailing zeros. Delivery settles the payout.
--refunded DECIMALPositive USDC amount no greater than sent. Refund defaults to the full send; failure without this flag keeps the bridge BRIDGING.

A refund, or failure with --refunded, returns the payout to READY; any difference between sent and refunded USDC is a refund fee deducted from the next payout. The attempt ends (FAILED for a whole crypto payout, CLOSED for a send-the-rest exit), and a send-the-rest exit restores the destination as the saved payout method. It records a PAYOUT_REFUNDED timeline entry and one ACTION_REQUIRED customer email. The fee appears as settlement.refundFeeAmount when the payout settles. Pending leaves it BRIDGING. Success requires a delivered amount greater than zero, in destination tokens. --delivered is only valid with success; --refunded is only valid with refund or failure. Invalid status/amount combinations return 400 PAYOUT_BRIDGE_SIMULATION_INVALID. --refunded uses USDC decimals (at most six), must be greater than zero and no greater than the sent USDC. With refund, omitting it refunds the whole send; with failure, omitting it means no refund and leaves BRIDGING. sweep-local never advances bridges: only simulated outcomes do. For a NEAR bridge, success evidence passes the hosted NEAR status checks. --delivered must be positive and at least the stored quote’s minAmountOut converted to ZEC. Zero or a delivery below that minimum returns 400 PAYOUT_BRIDGE_SIMULATION_INVALID, with the minimum in the message. The bridge remains BRIDGING: no state is written and no webhook is emitted. For a NEAR bridge, --status maps through NEAR’s statuses:

CLI statusNEAR status
successSUCCESS
refundREFUNDED
failureFAILED
pendingPROCESSING

The Zcash delivery hash is 64 hex digits without 0x. A failure without --refunded stays BRIDGING, just as it does for Relay. These commands simulate evidence locally and never call Relay, NEAR or send funds.

peer-pay-cli payouts simulate-payout-bridge PAYOUT_ID --local --status success --delivered 0.033

payouts simulate-payment​

Simulate a buyer payment step on a listed local payout and refresh its settlement.

peer-pay-cli payouts simulate-payment PAYOUT_ID [options]

Options: --step, --amount.

Local profiles only. This stands in for a buyer's intent on the escrow deposit, then runs the settlement refresh an indexer update triggers. --amount pays part of what is left (default: all of it). --step is one of:

  • signaled: a buyer opens a payment for everything left. The payout moves LISTING → PAYING and sends PAYOUT_ORDER_MATCHED; change-method returns 409 CASHOUT_BUYER_PAYMENT_ACTIVE. A second open payment returns 409 PAYOUT_BUYER_PAYMENT_ACTIVE.
  • replaced: the open payment expires and a new buyer signals before settlement refreshes. The payout stays PAYING, records the new intent and sends one PAYOUT_ORDER_MATCHED per buyer, without a buyer-dropped email.
  • expired: the open payment expires and the payout returns to LISTING. Its locked funds remain reclaimable: send-to-address withdraws them, and the next signal prunes the expired intent before locking those funds for the new buyer.
  • pruned: the open payment is released. The payout returns to LISTING.
  • fulfilled: the buyer completes the payment for --amount (default: everything left). Paying everything left moves the payout to SETTLED, sets settledAt, sends PAYOUT_ORDER_SETTLED and records the settled customer email. A smaller amount records a partial fill, sends PAYOUT_ORDER_PARTIALLY_PAID, records the partially-paid customer email and moves PAYING → LISTING. If the deposit still exists and accepts buyers, and no send for the attempt is in flight, it also queues a relist for payouts resolve-send. The email's restRelisted is true only when this relist is queued now, or a relist is already in flight on a deposit that accepts buyers. Otherwise it is false. A rest at or below 0.10 USDC is swept as dust and the payout settles with settlement.dustAmount; the settled email is the only email for that payment.

Local non-USD payout estimates and new buyer intents use local settings' existing rates fixture, in currency units per USD: EUR 0.92, GBP 0.79, AUD 1.52, CAD 1.37, CHF 0.88, CNY 7.25, MXN 18.5, NZD 1.68, SGD 1.35, TRY 34, ZAR 18.2 by default. The bound 18-decimal rate is that fixture value × 10^18; USD is always fixed at 1.00 and ignores the fixture. There is no local staleness logic and no live oracle call. A missing fixture rate leaves the currency available without a display estimate; the state GET still succeeds. Binding a new signal or replacement requires the rate and returns 400 if it is missing. For example:

peer-pay-cli local settings --data '{"rates":{"EUR":"0.92","GBP":"0.79"}}'

signaled and replaced bind the current local rate; fulfilled keeps that rate even if rates changes afterward. A 40 USDC EUR payment at the default rate reports { "currency": "EUR", "amount": "36.80" }. Checkout buyerPayment, partialPayment.paidFiat, merchant partialFills[].fiat, settlement.buyerPaidFiat and webhook partialPayment.fill.fiat mirror the hosted fields. All payment controls, paid amounts, fees and accounting remain USDC. Older local state files load with USD for fiat methods and escrow attempts, fixed USD fill rates, matching the hosted migration. All catalog currencies are available by default.

A zero amount returns 400 PAYOUT_PAYMENT_AMOUNT_INVALID; an amount above what is left returns 400 PAYOUT_PAYMENT_ABOVE_REMAINING. A new signal or replacement before the relist lands returns 409 PAYOUT_LISTING_NOT_RELISTED. Once a withdraw landed while a buyer held the deposit, signaled and replaced return 409 PAYOUT_LISTING_NOT_ACCEPTING: the escrow takes no new buyer.

replaced, expired, pruned and fulfilled return 409 PAYOUT_BUYER_PAYMENT_MISSING without an open payment. Every step returns 409 PAYOUT_ATTEMPT_NOT_LISTED when the payout is not listed. The response is the customer's checkout state. The open-payment checks precede the accepting-buyers check.

Example:

peer-pay-cli payouts simulate-payment PAYOUT_ID --local --step signaled
peer-pay-cli payouts simulate-payment PAYOUT_ID --local --step fulfilled

payouts simulate-peer-withdraw​

Simulate the customer withdrawing a listed local payout in the Peer app, outside Pay.

peer-pay-cli payouts simulate-peer-withdraw PAYOUT_ID

Local profiles only. This stands in for a withdraw the customer makes in the Peer app, then runs the settlement refresh. An unpaid payout ends CANCELLED with cancelSource: "PEER_APP", sends PAYOUT_ORDER_CANCELLED and records the cancelled email; depositAmount becomes what the withdraw returned. After a partial payment it ends SETTLED with settlement.returnedAmount, keeping depositAmount and what buyers paid, and sends the settled webhook and email. If the customer had asked Pay to change method, the payout returns to FUNDED instead. While a buyer payment holds the deposit, the withdraw returns nothing, the deposit stays listed but takes no new buyer, and the payout keeps its status; the buyer's payment still completes. Returns 409 PAYOUT_ATTEMPT_NOT_LISTED when the payout is not listed.

The exit waits only for a withdraw or USDC transfer from the wallet that is still in flight, as in the API; a queued relist does not hold it.

Example:

peer-pay-cli payouts simulate-peer-withdraw PAYOUT_ID --local

payouts simulate-wallet-balance​

Set the simulated USDC balance of a local payout's customer wallet for the wallet check.

peer-pay-cli payouts simulate-wallet-balance PAYOUT_ID [options]

Options: --amount.

Local profiles only. --amount is the wallet's USDC on Base, such as 50. It applies to every local payout of the same customer. payouts sweep-local then runs the wallet check. A wallet with no simulated balance covers all its payouts. The local checkout state uses this balance too, with the same 10-minute funding grace and newest-first rule: a short payout shows walletShortfall, offers no actions, and returns 409 CASHOUT_WALLET_SHORT on confirm; the wallet check in payouts sweep-local closes it.

Example:

peer-pay-cli payouts simulate-wallet-balance PAYOUT_ID --local --amount 50

payouts emails​

List a local payout's customer emails with their resume links.

peer-pay-cli payouts emails PAYOUT_ID

Local profiles only. The local simulator records email metadata and resume links; it neither renders email bodies nor sends email. The simulator records every customer email the API would send: FUNDED, ACTION_REQUIRED, PARTIALLY_PAID, CANCELLED and SETTLED. Each entry has restRelisted: a boolean for PARTIALLY_PAID, null otherwise. A state file with a PARTIALLY_PAID email missing this flag is refused on load; older emails of other kinds load with restRelisted: null. ACTION_REQUIRED covers a dropped buyer, a reconnect, and a refunded crypto payout. The refunded payout's email adds no timeline row of its own; the refund has a PAYOUT_REFUNDED row. Each email has a resume link on the local server whose t token opens the customer checkout API like the merchant's checkout link. Resume tokens derive from each email id with a separate fixed local secret; the local server re-derives them on every request.

Example:

peer-pay-cli payouts emails PAYOUT_ID --local

payouts funding-issues​

List local payout funding issues.

peer-pay-cli payouts funding-issues [options]

Options: --status.

The output retains the admin field cashoutId. Local profiles only: the hosted route, GET /api/v1/admin/cashouts/support, needs the admin key, which the CLI never holds. Returns { fundingIssues } as the admin Payouts page shows them, newest first by detectedAt, at most 200. --status open (the default) lists unresolved issues; --status all includes resolved ones. Each issue has its kind (LATE_FUNDS, OVERPAYMENT or ROUTE_REFUND), amountUsdc (null for a route refund, which Relay pays back in the funding token), the payout, and how it was resolved.

Example:

peer-pay-cli payouts funding-issues --local --status all

payouts resolve-funding-issue​

Record how Peer returned or claimed a local funding issue's money.

peer-pay-cli payouts resolve-funding-issue FUNDING_ISSUE_ID [options]

Options: --data.

Local profiles only, for the same reason as payouts funding-issues. It records the outcome and moves no money. The body is one of:

{ "resolution": "RETURNED", "txHash": "0x…64 hex", "note": "Sent back to the merchant's refund address" }
{ "resolution": "CLAIMED", "note": "Merchant asked Peer to keep it for the next payout" }

note is 1–500 characters and a return needs the transaction hash: a 0x… hash, or a Bitcoin txid (64 hex digits, no prefix). Bitcoin txids are stored lower-case; 0x hashes keep their supplied case. The CLI adds "actor": "peer-pay-cli" when the body names no actor. An issue is resolved once: a second resolve returns 409 CASHOUT_FUNDING_ISSUE_RESOLVED, and an unknown ID returns 404 CASHOUT_FUNDING_ISSUE_NOT_FOUND. Each resolve is written to the admin change history.

Example:

peer-pay-cli payouts resolve-funding-issue FUNDING_ISSUE_ID --local --data @resolve.json

payouts timeline​

Show a local payout's dashboard timeline and next step.

peer-pay-cli payouts timeline PAYOUT_ID

Local profiles only. Returns { timeline, nextStep } as the dashboard's payout detail shows them: one entry per moment, oldest first, with its type, text, txHash, txChainId and occurredAt. txChainId identifies the transaction’s network (null when no link is available). A bridged payout's SETTLED row links to the destination network; TRANSFER_SENT and PAYOUT_REFUNDED link to Base. The moments are:

  • Funding and settlement: CREATED, PARTIALLY_FUNDED, FUNDED, FUNDING_EXPIRED, MATCHED, PARTIALLY_PAID, SETTLED, CANCELLED, and FUNDING_ISSUE.
  • Customer steps: SIGNED_IN, METHOD_READY, LISTED, RELISTED, LISTING_WITHDRAWN, TRANSFER_SENT, and PAYOUT_REFUNDED.
  • Action-required emails: BUYER_DROPPED and RECONNECT_NEEDED; reconnect covers Venmo, Cash App and PayPal.

nextStep says what happens next, such as "Waiting for a buyer" or Sending {SYMBOL} on {Network} to the customer while bridging, and is null once the payout is final.

Example:

peer-pay-cli payouts timeline PAYOUT_ID --local

Local customer checkout​

The simulator serves the API's customer router under /api/v1/cashout-checkout. Every route needs x-cashout-token: TOKEN, using the t query parameter from checkoutUrl or an email resume link. Except for the summary, also send Authorization: Bearer local-player:<email> with the payout's customer email.

MethodPath (under /api/v1/cashout-checkout)Authentication
GET/:idLink token only.
GET/:id/stateLink token and customer login.
PUT/:id/payout-methodLink token and customer login.
POST/:id/payout-quoteLink token and customer login.
POST/:id/confirmLink token and customer login.
POST/:id/skip-connectLink token and customer login.
POST/:id/relistLink token and customer login.
POST/:id/send-to-addressLink token and customer login.
POST/:id/change-methodLink token and customer login.
GET/:id/activityLink token and customer login.

A missing login returns 401 CASHOUT_LOGIN_REQUIRED; an invalid local login returns 401 CASHOUT_LOGIN_INVALID, and another email returns 403 CASHOUT_PLAYER_MISMATCH. An unknown payout id returns the same 401 CASHOUT_TOKEN_INVALID as a wrong link token. The customer routes also return 422 CASHOUT_RAIL_UNAVAILABLE or CASHOUT_CREDENTIAL_INACTIVE, and 409 CASHOUT_METHOD_LOCKED or CASHOUT_NOT_READY when the action is unavailable. Relisting below the method minimum returns 409 CASHOUT_RELIST_NOT_ALLOWED (“What’s left is below the payout method minimum”). See the API's customer errors for the full list.

The smallest payout step and the smallest single buyer payment are PAYOUT_MIN_USDC, a whole USDC amount from 1 to 10 read from the environment when serve starts, 10 when unset. This variable configures the local simulator only; the minimum payout is 10 USDC in production. The retired CASHOUT_MIN_USDC variable is ignored by the CLI; use PAYOUT_MIN_USDC instead. The payout settings responses return it as payoutStepRange, and a listing below it returns 422 CASHOUT_AMOUNT_OUTSIDE_RAIL_LIMITS.

The local POST /api/v1/cashout-checkout/:id/skip-connect route takes no body and requires both the link token and customer login. When a funded Venmo, Cash App or PayPal method needs connecting, its actions include CONNECT_RAIL and SKIP_CONNECT. Skipping returns 200 with the fresh READY state and method.connectSkipped: true; it records METHOD_READY and lets confirm list without an active credential. Buyers prove their payment, as on Zelle. State reads keep it READY while the connection is inactive. A skipped listing still offers CONNECT_RAIL, so the customer can connect while it waits. Connecting helps find a buyer faster: in Peer Pay, 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. The simulator mirrors clearing the skip when a state read or credential check sees an active connection, with no status change or new send or timeline event. It then treats the listing like any connected one, including a reconnect email if the connection later lapses. A skipped listing gets no reconnect email until Peer Pay sees it connect (payouts sweep-local counts an inactive skipped listing under skipped). Every payout-method save and the method restore 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 connectSkippedAt: null, so a new Venmo, Cash App or PayPal method must connect or skip again. State files saved before connectSkippedAt existed load that field as null. When the action is unavailable, this route returns 409 CASHOUT_SKIP_NOT_ALLOWED (“This cashout can’t skip connecting now”) with { status, actions }. A QUOTED or BRIDGING payout returns 409 CASHOUT_PAYOUT_BRIDGING first. Final payout links reject POSTs with 401 CASHOUT_LINK_EXPIRED.

The local payout-method route applies the backend's account rules. The body is { "rail": "venmo" | "cashapp", "payeeHandle" } or { "rail": "zelle" | "chime", "payeeHandle" }. Multi-currency rails require:

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

PayPal and Revolut accept their payout catalog currencies. Missing currency or a currency outside the rail's catalog is a 400; other rails reject the extra field. Old PayPal/Revolut tabs must reload. An oracle estimate never gates saving or confirming. Changing a listed method or currency withdraws it and returns to FUNDED, preserving paid parts in their original currencies. For crypto the strict body is { "rail": "relay_8453", "tokenAddress": "0x833589fCD6eDb6E08f4c7C32D4f71b54bdA02913", "address": "0x1234567890123456789012345678901234567890" }. The rail selects the network; another token/network uses its own catalog token and rail. The same { rail, tokenAddress, address } body serves payout-quote and send-to-address. Send-to-address is offered after a partial payment only while the rest is listed, no buyer is paying and the payout includes a crypto network; without one it returns 409 CASHOUT_SEND_NOT_ALLOWED. Local checkout state uses the API’s settleTx: it is null when the newest attempt’s newest bridge was refunded, even if the payout later settles with the remainder returned. The backend’s destination and current-rails checks apply, including 422 CASHOUT_PAYOUT_NETWORK_UNAVAILABLE for a network missing from the payout's stored rails. Hosted, ops can remove a network from open payouts; locally, create the payout with the rails you want. POST /api/v1/cashout-checkout/:id/payout-quote is available in READY with crypto CONFIRM, or LISTING with SEND_TO_ADDRESS. It returns destination, usdcAmount, estimatedAmount and quotedAt. Local Relay estimates assume a fake 1 USD per token, rescaling USDC’s six decimals to the destination token; it never calls Relay. Base USDC needs no Relay quote. A Venmo username is 5–30 letters, digits, - or _ after one leading @. A Cash App cashtag is lowercased after one leading $, and must be 1–20 letters or digits. A PayPal username is lowercased after a paypal.me/ link or one leading @, and may use letters, digits, ., _ and -. A Zelle email is trimmed and lowercased, with one @, no whitespace, a dotted domain and up to 254 characters. A Revtag drops one leading @, a leading revolut.me/ link with or without https:// or http:// and any trailing /, is lowercased, and may use only letters, digits, ., _ and -. A $ChimeSign is lowercased with exactly one leading $ (added when missing) and no whitespace or further $. Anything else returns 422 CASHOUT_PAYEE_HANDLE_INVALID. An invalid PayPal email returns 422 CASHOUT_PAYPAL_EMAIL_INVALID. The saved PayPal email appears only in the customer's checkout state, as method.paypalEmail. A Zelle, Revolut or Chime method is READY at once, with a METHOD_READY timeline entry and no CONNECT_RAIL.

The local Curator stand-in knows every valid handle, so four Curator refusals never happen locally: CASHOUT_PAYEE_HANDLE_INVALID (a Zelle, Revolut or Chime handle Curator refuses; the local handle rules above still apply), CASHOUT_PAYEE_NOT_FOUND (no such account), CASHOUT_PAYEE_NOT_REGISTERED (a PayPal account not set up in Peer) and CASHOUT_PAYPAL_EMAIL_MISMATCH (an email that isn't the PayPal account's).

Payments​

Create requires an order ID and either --rail or --data. --currency sets fiatCurrency for fiat rails (for example, revolut + EUR) and a crypto rail’s currency (for example, relay_8453 + USDC). When omitted, fiat defaults to USD and crypto defaults to USDC; crypto rails ignore fiatCurrency, and fiat rails ignore currency in a --data body. Common rails include venmo, revolut, and relay_8453; see Payment platforms. Use --currency, --refund-to, and --onramp-provider for crypto-specific fields, or pass the canonical create-payment body with exclusive --data. List returns merchant payments; cancel takes both the order ID and payment ID.

payments create​

Use --payment-method-id for paymentMethodId, --quote-limit (1 to 20) for quoteLimit, and repeat --excluded-pay-to VALUE to build excludedPayToValues. --currency still maps to fiatCurrency for fiat rails and a crypto rail’s currency.

Create a payment attempt. Local sandbox permits multiple CREATED attempts; replacementPaymentId leaves prior attempts unchanged.

Hosted crypto route creation can return HTTP 502 with error code CRYPTO_PAYMENT_ROUTE_FAILED. The CLI preserves that code and the provider's diagnostic message. This is a generic route failure, not a minimum-amount or fee-specific error. The local simulator does not call the route provider.

peer-pay-cli payments create ORDER_ID [options]

Options: --rail, --currency, --refund-to, --onramp-provider, --replacement-payment-id, --payment-method-id, --quote-limit, --excluded-pay-to, --amount, --amount-currency, --expected-amount-version, --data.

On an open-amount order, --amount with --amount-currency (the fiat currency it is typed in) and --expected-amount-version (the amountVersion you read) saves a new amount with the payment and sends ORDER_AMOUNT_SET; see Orders for the rules and error codes. Without them, the payment uses the saved amount. On a fiat rail, also pass --currency with the same code as --amount-currency. Without --currency the payment is in USD, so a non-USD amount is refused before the request is sent:

peer-pay-cli payments create ORDER_ID --rail revolut --currency EUR \
--amount 92 --amount-currency EUR --expected-amount-version 0

A fiat payment is also checked against the local settings maxOrderAmountUsdc (default 10,000 USDC), like the hosted API, on the amount it would sign on chain: the order amount plus any buyer-paid fees (PAYEE, or the buyer's share under SPLIT). Above it, local creation returns HTTP 400 INTENT_ABOVE_MAX with This payment would be ${intent} USDC with fees, above the ${max} USDC limit per payment (the intent rounded up and the maximum rounded down to the cent). The check runs before anything is stored: no payment, no PAYMENT_CREATED, and an open-amount --amount is not saved. Exactly the maximum is allowed. For example, a PAYEE 10,000 USDC order with a 150 bps fee signs 10,152.284265 USDC and is refused on venmo. Crypto, Zcash and Apple Pay payments sign no escrow intent and are not checked.

Payment creation attaches the order token saved in the current profile when available. --onramp-provider and --replacement-payment-id (or their canonical fields in --data) require that token; create the order with orders create to save it. Ordinary payment creation works without a saved token. Local privileged creation returns HTTP 401 with Missing order token or Invalid order token, matching the hosted API.

An order created before the merchant account changed owners takes no new payments. Hosted and local creation return HTTP 409 ORDER_FROM_PREVIOUS_OWNER with This order was created before the merchant account changed owners. Ask the merchant for a new payment link. Payments already on the order still settle to the previous owner, and payments recreate still recovers them.

Example:

peer-pay-cli payments create ORDER_ID --rail venmo --currency USD

payments list​

List merchant payments with --status, --chargeback-status, --page, and --limit:

peer-pay-cli payments list --status SETTLED --chargeback-status CHARGEBACKED --limit 10 --page 1

status accepts CREATED, SETTLED, FAILED, EXPIRED, or CANCELLED. chargebackStatus=NONE selects payments without a chargeback fact; CHARGEBACKED selects payments with one. Filters combine before totals and pagination; results are newest first. Page defaults to 1; limit defaults to 20 and has a maximum of 100. The payment-list schema has no orderId or search filter; retrieve an order's payments through orders get ORDER_ID.

peer-pay-cli payments list

Options: --status, --chargeback-status, --page, --limit.

Example:

peer-pay-cli payments list

payments cancel​

Cancel an open payment using its cached order token.

peer-pay-cli payments cancel ORDER_ID PAYMENT_ID

Example:

peer-pay-cli payments cancel ORDER_ID PAYMENT_ID

Webhooks​

Create requires --url; --events defaults to all webhook events. The signing secret is returned only on creation and saved in the selected profile. Update accepts --url, --events, --active, and custom-header flags. Omitting --events on update preserves existing subscriptions. Create and update accept flags only; --data is not supported.

Hosted destinations require public HTTPS. Local destinations can use localhost HTTP. test is a connectivity check with null business entities; use test-order or simulate for business payloads. Deliveries shows attempt history, status, and response details. Local failures retry after 60 seconds, 5 minutes, 30 minutes, 2 hours, 8 hours, and 24 hours.

Every event uses the {id, type, timestamp, data} envelope. The JSON id equals the signed X-Webhook-Id and stays the same across delivery retries. data includes order, payment, refund, and paymentBridge, with explicit null values for absent entities. IDs, ISO timestamps, decimal amounts, and chain IDs are strings.

Bridge events include the attempt's status, attempt number, amounts, transaction fields, and timestamps; transaction arrays can be empty before broadcast. Refund event statuses are PENDING or COMPLETED, distinct from the merchant refund execution entity. See events and payloads for refund fields, resize amounts, and chargeback snapshots and triggers.

webhooks list​

List webhook destinations and subscriptions.

peer-pay-cli webhooks list

Example:

peer-pay-cli webhooks list

Hosted profiles require HTTPS webhook URLs for both webhooks create and webhooks update. Local profiles may use HTTP for localhost handlers. Updates that omit the URL keep it unchanged.

webhooks create​

Register a webhook, show its signing secret in full once, and save it in the profile. No --show-secrets flag is needed. The API never returns this secret from webhooks list; export the saved value later with peer-pay-cli credentials export --output FILE --webhook-id <id>.

peer-pay-cli webhooks create [options]

Options: --url, --events, repeatable --header NAME=VALUE.

With --url, omitting --events subscribes to every webhook event; pass comma-separated event names to select a subset.

Example:

peer-pay-cli webhooks create --local --url http://localhost:3000/webhooks/peer-pay --header X-Auth=secret

Repeat --header to add custom headers, for example --header X-Auth=secret --header X-Merchant=shop. Each entry splits at the first =: the name is trimmed and lowercased, while the value is kept verbatim (including additional = and spaces). Empty values are allowed; a missing = or empty name is rejected.

The backend WebhookHeadersSchema allows at most 10 headers, with no duplicate names after normalization. Names may contain letters, digits, and these HTTP header token characters: !#$%&'*+-.^_`|~. Values cannot contain CR, LF, or NUL; the schema imposes no other value length limit. Forbidden names are connection, content-length, content-type, expect, forwarded, host, proxy-authenticate, proxy-authorization, te, trailer, transfer-encoding, and upgrade; all proxy- and x-forwarded- prefixes are forbidden too. x-webhook-id, x-webhook-timestamp, and x-webhook-signature are reserved.

Webhook create and update accept flags only; --data is not supported.

webhooks update​

Update a webhook URL, events, headers, or active state.

peer-pay-cli webhooks update WEBHOOK_ID [options]

Options: --url, --events, --active true|false, repeatable --header NAME=VALUE, --clear-headers.

Only provided flags appear in the PATCH body. Omitted fields stay unchanged. --events takes comma-separated event names and must contain at least one event; --active false disables deliveries and --active true enables them. --header replaces the entire custom header collection, matching the backend PATCH; it does not merge with existing headers. Use --clear-headers to send customHeaders: [] and remove all headers. --header and --clear-headers cannot be combined. The same header rules above apply to updates.

Example:

peer-pay-cli webhooks update WEBHOOK_ID --active false --header X-Auth=secret

webhooks delete​

Delete a webhook and its locally saved secret (HTTP 204 with no body).

peer-pay-cli webhooks delete WEBHOOK_ID

Example:

peer-pay-cli webhooks delete WEBHOOK_ID

webhooks test​

Send a connectivity test with empty business entities.

peer-pay-cli webhooks test WEBHOOK_ID

Example:

peer-pay-cli webhooks test WEBHOOK_ID

webhooks deliveries​

Inspect the latest 50 webhook deliveries; pagination options are not supported.

peer-pay-cli webhooks deliveries WEBHOOK_ID

Example:

peer-pay-cli webhooks deliveries WEBHOOK_ID

Merchant​

Hosted updates use the signed-in session and the backend's owner/manager checks. update requires a JSON body with name; it can also set industryType, industryOtherText, and integrationPath. settings applies a partial merchant-config update, including payment rails, currency, fee payer, settlement, checkout theme, and dashboard preferences. Unspecified config fields are retained; a supplied merchantCheckoutTheme replaces the whole theme object. Local config PUT validates the replacement and PATCH validates the merged result before saving any fields. Invalid payouts return HTTP 400 without an errorCode, with Unsupported payout destination chain, Unsupported payout destination token for chain, or Non-Base payout destinations require sweep support. For example, Ethereum USDC requires sweepEnabled: true; disabling sweep on that existing configuration is rejected without changing the saved settings. Base USDC permits sweep to be disabled.

logo requires --file; supported formats are PNG, JPEG, WebP, and GIF, up to 2 MB. tier requires --tier BASE or --tier PRO. Pricing and availability are enforced by the backend. Key rotation invalidates the previous key and saves the replacement; update any applications using the old key. provision-wallets retries missing owner wallet setup without replacing existing addresses. Concierge and Shopify commands submit the corresponding setup requests; they do not complete those workflows locally.

merchant get​

Use --management for the dashboard role-masked GET /api/v1/merchants/me view.

Read your merchant profile and settings.

peer-pay-cli merchant get

Options: --management.

Example:

peer-pay-cli merchant get --profile live

merchant update​

Flags map to name, industryType, industryOtherText, and integrationPath. If --name is omitted, the CLI fetches GET /api/v1/merchants/dashboard/me and preserves its name before PUT. Empty industry strings clear those fields. Raw --data must include name and does not trigger the fetch.

Update business name, industry, or integration path.

peer-pay-cli merchant update [options]

Options: --name, --industry-type, --industry-other-text, --integration-path, --data.

Example:

peer-pay-cli merchant update --profile live --name "Example Store" --industry-type ecommerce

merchant settings​

Local updates validate merged payout settings before saving; non-Base-USDC payouts require sweep support. Both PATCH and PUT validate payout settings before Apple Pay availability. PATCH filters EXCLUSIVE_SAR rails before payout validation; PUT filters them afterward. Successful merchant PATCH/PUT and admin config PATCH responses use the message Merchant config updated.

PATCH merchant config with first-class flags or an exclusive --data JSON body. Theme and dashboard flags first fetch GET /merchants/me and merge existing nested fields, because each object is replaced as a whole by the backend. Avoid concurrent edits to those objects between the read and write. Use --replace for PUT: fetch current writable fields, overlay flags, and require every required config field. --replace --data @settings.json instead validates and sends that full object directly, without a merge. Raw bodies are never combined with individual request flags.

The MANAGER role cannot change sweepEnabled, destinationChainId, or destinationToken; the backend rejects changes to these values. --replace resends the current destination and sweep values as part of the complete config, which is allowed when those values are unchanged.

Options:

  • --fee-payer: MERCHANT, PAYEE, or SPLIT.
  • --buyer-fee-share-bps: Buyer share of total fees from 0 to 10000 in steps of 1000 (5000 = 50%) in SPLIT mode. Required when enabling SPLIT; subsequent PATCH updates can retain the saved rate. A full SPLIT replacement body must include buyerFeeShareBps.
  • --enabled-rails: Comma-separated supported rails.
  • --default-payment-currency: Default fiat currency; an empty string sends null.
  • --clear-default-payment-currency: Send null for defaultPaymentCurrency.
  • --destination-chain-id: Decimal integer chain ID.
  • --destination-token: Settlement token.
  • --sweep-enabled: true or false; MANAGER role cannot change sweep or destination values; --replace may resend unchanged values.
  • --dynamic-orders-enabled: true or false.
  • --theme-preset: Checkout preset: default, dark, light, or custom.
  • --theme-page-background-color: Checkout page background color (#rgb or #rrggbb).
  • --theme-panel-background-color: Checkout panel background color (#rgb or #rrggbb).
  • --theme-panel-text-color: Checkout panel text color (#rgb or #rrggbb).
  • --theme-panel-text-muted-color: Checkout panel text muted color (#rgb or #rrggbb).
  • --theme-panel-accent-color: Checkout panel accent color (#rgb or #rrggbb).
  • --theme-panel-border-color: Checkout panel border color (#rgb or #rrggbb).
  • --theme-button-background-color: Checkout button background color (#rgb or #rrggbb).
  • --theme-button-text-color: Checkout button text color (#rgb or #rrggbb).
  • --theme-currency-selector-enabled: true or false; show the checkout currency selector.
  • --theme-pay-with-crypto-enabled: true or false; show pay with crypto.
  • --theme-default-max-fee-percentage: Numeric checkout default maximum fee percentage.
  • --quick-amounts-enabled: true or false; enable dashboard quick amounts.
  • --quick-amounts-count: Dashboard quick amount count: 4, 6, or 8.
  • --quick-amounts: Comma-separated positive amounts (at most eight).
  • --dashboard-theme-preset: Dashboard theme preset.
  • --dashboard-theme-accent-hex: Dashboard custom accent color.
  • --max-fee-mode: flat or tranches; requires the matching percentage or tranches option. Currency is USD.
  • --max-fee-percentage: Flat maximum fee percentage, 0 to 100; requires --max-fee-mode flat.
  • --max-fee-tranches: JSON array inline or @filename with min, max (number or null), and value (0 to 100); requires --max-fee-mode tranches.
  • --clear-dashboard-ui: Send null for dashboardUiConfig; excludes dashboard flags.
  • --clear-max-fee: Send null for maxFeeConfig; excludes max-fee flags.
  • --replace: PUT the full writable config: fetch current settings, overlay flags, and validate all required fields. With --data, send that complete object directly.
  • --data: JSON body inline or @filename.

When a dashboard subobject does not exist, provide all its required fields: quick amounts need enabled, count, and amounts; theme needs preset and accent. Use --data to explicitly set nullable individual theme colors or dashboard theme values to null. Clear the whole dashboard or max-fee object with --clear-dashboard-ui or --clear-max-fee. --default-payment-currency "" and --clear-default-payment-currency both send null.

Examples:

peer-pay-cli merchant settings --profile live --data @settings.json
peer-pay-cli merchant settings --profile live --fee-payer PAYEE --enabled-rails venmo,revolut
peer-pay-cli merchant settings --profile live --fee-payer SPLIT --buyer-fee-share-bps 400
peer-pay-cli merchant settings --profile live --theme-preset dark --theme-pay-with-crypto-enabled true
peer-pay-cli merchant settings --profile live --quick-amounts-enabled true --quick-amounts-count 4 --quick-amounts 10,20,50,100
peer-pay-cli merchant settings --profile live --max-fee-mode flat --max-fee-percentage 2
peer-pay-cli merchant settings --profile live --max-fee-mode tranches --max-fee-tranches '[{"min":0,"max":null,"value":2}]'
peer-pay-cli merchant settings --profile live --replace --fee-payer MERCHANT

Upload a PNG, JPEG, WebP, or GIF logo, up to 2 MB.

peer-pay-cli merchant logo [options]

Options: --file.

Example:

peer-pay-cli merchant logo --profile live --file ./logo.png

merchant tier​

Select the BASE or PRO pricing plan.

peer-pay-cli merchant tier [options]

Options: --tier.

Example:

peer-pay-cli merchant tier --profile live --tier BASE

merchant sandbox​

This is a plain API call and does not save or activate a profile. Use peer-pay-cli sandbox enable for the complete flow.

Enable the hosted sandbox for your merchant.

peer-pay-cli merchant sandbox

Example:

peer-pay-cli merchant sandbox --profile live

merchant ip-allowlist​

Replace or clear the API key IP allowlist using owner management access.

peer-pay-cli merchant ip-allowlist --profile live --entries 8.8.8.8,1.1.1.0/24
peer-pay-cli merchant ip-allowlist --profile live --clear
peer-pay-cli merchant get --profile live --management

--entries accepts comma-separated public IPv4/IPv6 addresses or CIDRs (at most 50 entries before deduplication). The backend's UpdateApiKeyIpAllowlistSchema validates locally before sending, canonicalizes addresses and networks, and removes duplicates. Invalid entries report their index and the backend message, such as entries.1: Address is not public: "127.0.0.1". IPv4 prefixes must be /8 to /32 and IPv6 prefixes /32 to /128. Use --clear to send an empty list and remove restrictions; it cannot be combined with --entries. A missing --entries prompts on a terminal unless --clear is present; without a terminal it errors with --entries is required. Run peer-pay-cli merchant ip-allowlist --help.

The request is PUT /api/v1/merchants/me/api-key/ip-allowlist with {"entries":["8.8.8.8/32","1.1.1.0/24"]} (or {"entries":[]} to clear), using the management session and selected merchant context. Hosted access requires a Privy OWNER session. Success returns HTTP 200, message API key IP allowlist updated, and the merchant profile including apiKeyIpAllowlist. merchant get reads that field; --management lets the owner inspect it even when API-key access is blocked.

The local route validates with the same schema and returns the hosted HTTP 400 Invalid request envelope with formErrors and fieldErrors on invalid bodies. The list persists with serve --state FILE and appears in subsequent profile reads. Each local merchant has its own API key, and a local key also stands in for the local session identity on management operations; the mock has no Privy sessions or member role authentication. Invalid local keys still return HTTP 401.

The mock deliberately does not enforce the list against caller IPs. It binds to loopback, which cannot be allowlisted, and uses the same key for API and management access. Hosted API-key requests denied by the list return HTTP 403 with errorCode: "IP_NOT_ALLOWED", message IP address <ip> is not in this merchant's API key IP allowlist, and responseObject: { ip, reason: "IP_NOT_ALLOWED" }. An unresolved client IP instead returns message Client IP could not be resolved for API key IP allowlist enforcement and responseObject: { ip, reason: "CLIENT_IP_UNRESOLVED" } (ip is the observed address or null), with the same status and error code. Railway proxy resolution and internal service-token bypass are hosted behavior, not local simulation.

merchant rotate-key​

Replace your API key, show the new key in full once, and save it in the profile. No --show-secrets flag is needed. Export the saved key later with peer-pay-cli credentials export --output FILE.

peer-pay-cli merchant rotate-key

Example:

peer-pay-cli merchant rotate-key --profile live

merchant provision-wallets​

Retry provisioning missing owner wallets.

peer-pay-cli merchant provision-wallets

Example:

peer-pay-cli merchant provision-wallets --profile live

merchant concierge​

Submit a Concierge plan request with the intake questionnaire.

peer-pay-cli merchant concierge [options]

Options: --contact-email (required email), --business-name (optional, trimmed 1 to 200 characters), --telegram (optional, up to 64 characters; maps to telegramUsername), --message (optional, up to 1000 characters), --answers (required JSON object, inline or @filename), --data (whole body, inline or @filename, instead of individual request flags). Omitting businessName uses the merchant's name on the backend.

On a terminal, omitted --contact-email prompts for the email, then omitted --answers starts the same nine intake questions documented under merchant concierge-inquiry, with text input and fixed-option selectors. All nine answers are required, each a trimmed 1 to 500 character string. Optional flags do not prompt. Supplying both required flags or a complete --data body runs without prompts, including outside a terminal.

Example (using the answers.json questionnaire below):

peer-pay-cli merchant concierge --profile live --contact-email owner@example.com --answers @answers.json

This sends POST /api/v1/merchants/me/concierge-request using management authentication; the hosted route requires the merchant owner and records a pending tier request for admin approval. Hosted requests are limited to 5 per 3600 seconds per merchant; the local mock does not enforce this limit. A pending tier request already open returns HTTP 409 CONCIERGE_REQUEST_ALREADY_OPEN. Sandbox requests, including the local mock, validate the questionnaire first: invalid bodies return HTTP 400; valid bodies return HTTP 403 CONCIERGE_REQUEST_SANDBOX_FORBIDDEN without creating a request.

merchant concierge-inquiry​

Submit a Concierge intake inquiry.

Hosted inquiries are limited to 5 requests per 3600 seconds per merchant; the local mock does not enforce this limit.

peer-pay-cli merchant concierge-inquiry [options]

Options: --contact-email (required email), --business-name (optional, trimmed 1 to 200 characters), --telegram (optional, up to 64 characters; maps to telegramUsername), --message (optional, up to 1000 characters), --answers (required JSON object, inline or @filename), --data (whole body, inline or @filename, instead of individual request flags). Omitting businessName uses the merchant's name on the backend.

On a terminal, omitted --contact-email prompts for the email. When both --answers and --data are absent, the CLI walks these nine questions in order, showing each label and hint. Text questions use plain text input; crypto and feePayer use arrow-key selectors. Every answer is required and must be a trimmed 1 to 500 character string. Optional flags do not prompt. Supplying the required flags or a complete --data body runs without prompts, including outside a terminal.

Answer keyQuestionHint / fixed options
businessWhat do you sell, and what industry are you in?Your products or services and industry.
storefrontWhere do you sell?Your website, plus what it runs on: Shopify, WooCommerce, custom site, Telegram, or in person.
volumeHow much volume do you process today?Daily, weekly, or monthly volume, plus your average order size. Include the currency.
customersWhere are your customers?US, UK, EU, or elsewhere. This decides which payment apps we enable.
appsWhich payment apps do you want your customers paying with?Venmo, Cash App, Zelle, PayPal, Revolut, Wise, or others.
paymentsWhat do you use to accept payments today?Your current payment providers or methods.
cryptoDo you also want to accept crypto directly?Customers can pay with any coin on any network and you still settle in USDC. Options: Yes, No, Not sure yet.
feePayerWho pays the Peer Pay fee?Your customer, you, or a split between both. Options: Customer, Merchant, Split.
integratorWho is doing the integration on your side, you or a developer?We will send the right guide and set up an API key if needed.

Example:

answers.json
{
"business": "Clothing",
"storefront": "Shopify",
"volume": "USD 50,000 monthly; USD 100 average order",
"customers": "US",
"apps": "Venmo",
"payments": "Stripe",
"crypto": "Not sure yet",
"feePayer": "Merchant",
"integrator": "Our developer"
}
peer-pay-cli merchant concierge-inquiry --contact-email owner@example.com --answers @answers.json

This sends POST /api/v1/merchants/me/concierge-inquiry using management authentication; the hosted route requires the merchant owner. Unlike merchant concierge, this route is not sandbox-restricted. The local mock validates the same body and returns HTTP 201 with a generated inquiry ID:

{
"success": true,
"message": "Concierge inquiry submitted",
"responseObject": { "id": "550e8400-e29b-41d4-a716-446655440000" },
"statusCode": 201
}

merchant shopify​

Request Shopify setup for your merchant.

peer-pay-cli merchant shopify --store-domain DOMAIN --collaborator-code CODE --checkout-currency CURRENCY

All three flags are required; interactive terminals prompt for missing values.

  • --store-domain: Your store's myshopify.com domain, such as acme.myshopify.com.
  • --collaborator-code: The 4-digit collaborator request code from Shopify admin: Settings > Users > Security.
  • --checkout-currency: The 3-letter currency code your Shopify checkout uses, such as USD.

The CLI and the local sandbox use the backend's validation. The store domain is trimmed and lowercased, and any leading http:// or https://, path, query, hash and trailing dot are removed; it must then be a <store>.myshopify.com domain. The collaborator code must be exactly four digits. The currency is uppercased and must be an ISO 4217 code.

Invalid fields return these exact messages, in field order:

  • Enter your store's myshopify.com domain
  • Collaborator request code must be 4 digits
  • Enter a valid 3-letter currency code

Example:

peer-pay-cli merchant shopify --profile live --store-domain acme.myshopify.com --collaborator-code 1234 --checkout-currency USD

Team​

Hosted invitations require merchant management access. Invite requires --email; --role is MANAGER (default) or CASHIER. List returns invitations; use merchant get to inspect current members. Revoke takes an invitation ID; remove takes a member's user ID. Local invitations send no email. New local state includes an OWNER in merchant get's merchantUsers list; removing the owner is rejected with HTTP 403.

Each member row reports removalLocked. A locked member cannot be removed: team remove returns HTTP 403 with errorCode MEMBER_LOCKED, and only an admin can clear the lock. On hosted merchants, removing a member from the live merchant also removes their sandbox membership.

Redeem accepts --data @invite.json containing {"token":"INVITE_TOKEN"}. Keep this file private. After redemption, sign in with --merchant-id to select the newly accessible merchant. It does not replace the selected profile implicitly.

team list​

List merchant invitations.

peer-pay-cli team list

Example:

peer-pay-cli team list --profile live

team invite​

Invite a manager or cashier. The default role is MANAGER.

peer-pay-cli team invite [options]

Options: --email, --role.

Example:

peer-pay-cli team invite --profile live --email teammate@example.com --role MANAGER

team resend​

Resend an invitation.

peer-pay-cli team resend INVITE_ID

Example:

peer-pay-cli team resend INVITE_ID --profile live

team revoke​

Revoke an invitation.

peer-pay-cli team revoke INVITE_ID

Example:

peer-pay-cli team revoke INVITE_ID --profile live

team remove​

Remove a team member.

peer-pay-cli team remove USER_ID

Example:

peer-pay-cli team remove USER_ID --profile live

The local invite URL embeds the invite ID as its token: /local/invites/INVITE_ID/accept. Use that ID with team redeem --token INVITE_ID to accept the same record as local accept-invite. Local acceptance sends no email and requires only the API key. Redemption returns {merchantId,role,redeemed}, where role is the member's resulting role: redeeming consumes the invitation but never changes the role of an OWNER or a locked member. A retry by an existing member returns redeemed:false with that member's current role. Unknown/revoked tokens return 404, expired tokens return 410, and a redeemed token whose membership was removed returns 409.

team redeem​

Accept an invitation with --token or an exclusive JSON body. For local mock invitations, use the invitation ID as the token.

peer-pay-cli team redeem [options]

Options: --token, --data.

Example:

peer-pay-cli team redeem --profile live --token INVITE_TOKEN

Sub merchant transfers​

A Master Merchant Account owner can transfer a sub-merchant it created to a new owner by email. The caller must be OWNER of the sub-merchant and of the Master Merchant Account merchant, and the Master Merchant Account must have Master Merchant Account access enabled. Select the sub-merchant with --merchant-id, or with the profile's merchant.

A transfer link is valid for 7 days, and only one transfer can be pending per sub merchant. While a transfer is pending, sub-merchants set-fee on that sub merchant fails with MASTER_MERCHANT_FEE_LOCKED; cancel the transfer first. When the recipient accepts:

  • they become OWNER, and the sub merchant's payout wallets become their wallets;
  • the Master Merchant Account keeps a MANAGER seat the new owner cannot remove, with checkout settings and team access; refunds are OWNER-only;
  • unpaid orders created before the transfer are cancelled;
  • older orders with payment attempts stop accepting new payments.

The link is shown only once, in the transfer send output, and is emailed when sent. Output redacts transferUrl unless --show-secrets is set. To get a new link, cancel the pending transfer (transfer cancel) and send a new one (transfer send). Status never returns the link.

transfer send​

Send a sub merchant you own as its Master Merchant Account to a new owner by email. Save the returned transferUrl; it cannot be retrieved again.

peer-pay-cli transfer send [options]

Options: --email, --merchant-id.

Example:

peer-pay-cli transfer send --profile master-merchant --merchant-id SUB_MERCHANT_ID --email buyer@example.com --show-secrets

transfer cancel​

Withdraw the pending sub merchant transfer.

peer-pay-cli transfer cancel [options]

Options: --merchant-id.

Example:

peer-pay-cli transfer cancel --profile master-merchant --merchant-id SUB_MERCHANT_ID

transfer status​

Read the pending transfer and its open-order impact.

peer-pay-cli transfer status [options]

Options: --merchant-id.

Example:

peer-pay-cli transfer status --profile master-merchant --merchant-id SUB_MERCHANT_ID

transfer accept​

Accept a merchant account transferred to your email.

peer-pay-cli transfer accept [options]

Options: --token, --data.

Example:

peer-pay-cli transfer accept --profile pending-transfer --token TRANSFER_TOKEN

Previews the transfer first. Hosted and local previews list exactly CHECKOUT_CONFIG and TEAM in masterMerchantPermissions; the Master Merchant Account manager seat cannot refund after the transfer. If the payout wallet is not delegated, prints the dashboard /transfer link and exits with an error: the CLI cannot set up an embedded-wallet signer. Errors use TRANSFER_NOT_FOUND, TRANSFER_UNAVAILABLE, TRANSFER_EMAIL_MISMATCH, WALLET_SETUP_REQUIRED and TERMS_CHANGED.

Sub merchants​

A merchant that an admin enabled as a Master Merchant Account can create sub merchants. A sub merchant copies the Master Merchant Account's commercial terms and pays out to the Master Merchant Account's wallet until it is transferred; after that, payouts go to the new owner's wallet. It pays the Master Merchant Account a master merchant fee on top of Peer's fee, before and after a transfer. Creating a sub merchant and changing a fee need the Master Merchant Account owner; listing and previewing also work for managers. sub-merchants create does not select the sub merchant: run merchant switch SUB_MERCHANT_ID to act on it.

Errors: MASTER_MERCHANT_NOT_ENABLED (403) when the current merchant is not an enabled, LIVE Master Merchant Account; MASTER_MERCHANT_FEE_EXCEEDS_CAP (422) above your Master Merchant Account cap, when an admin set one; MASTER_MERCHANT_FEE_EXCEEDS_MAX_FEE (422) when a payment method's configured fees for an amount band would exceed the sub merchant's max fee; MASTER_MERCHANT_FEE_LOCKED (409) while a transfer of the sub merchant is pending (cancel it first) or once the sub merchant belongs to someone else.

Locally, turn on simulatedLive and enable the Master Merchant Account flag first:

peer-pay-cli local settings --data '{"simulatedLive":true}'
peer-pay-cli local admin --data '{"masterMerchantEnabled":true}'
# Optional cap: add masterMerchantMaxFeeBps (0–5000); null means no Master Merchant Account-specific cap.

The local session identity owns every sub merchant it creates. The local mock derives a sub merchant's master merchant fee from its referral split entry for the Master Merchant Account wallet.

sub-merchants list​

List the sub merchants you created as a Master Merchant Account, with fee, owner, and transfer state.

peer-pay-cli sub-merchants list

Example:

peer-pay-cli sub-merchants list --profile live

sub-merchants create​

Create a sub merchant that pays you a master merchant fee, as the Master Merchant Account owner.

peer-pay-cli sub-merchants create [options]

Options: --sub-merchant-name, --master-merchant-fee-bps, --data.

Example:

peer-pay-cli sub-merchants create --profile live --sub-merchant-name Outlet --master-merchant-fee-bps 100

sub-merchants set-fee​

Change the master merchant fee on a sub merchant you still own. The change applies to new orders.

peer-pay-cli sub-merchants set-fee SUB_MERCHANT_ID [options]

Options: --master-merchant-fee-bps, --data.

Example:

peer-pay-cli sub-merchants set-fee SUB_MERCHANT_ID --profile live --master-merchant-fee-bps 150

sub-merchants fee-preview​

Preview configured fees per payment method for a master merchant fee. These are configured fees before exchange-rate spread, not Peer's net revenue.

peer-pay-cli sub-merchants fee-preview [options]

Options: --master-merchant-fee-bps.

Example:

peer-pay-cli sub-merchants fee-preview --profile live --master-merchant-fee-bps 100

Onboarding​

get reads the backend checklist. ack acknowledges a manual step by its returned ID; automatically verified steps remain subject to backend evidence. skip dismisses onboarding without proving an integration is complete. ack and skip print the updated checklist, the same object get returns.

onboarding get​

Read your merchant onboarding checklist.

peer-pay-cli onboarding get

Example:

peer-pay-cli onboarding get --profile live

onboarding ack​

Acknowledge an onboarding step. onboarding ack rails confirms payment methods without completing profile or industry. onboarding ack profile completes the profile when a business name is saved; a logo is optional. Saving an integration path also completes the preceding profile, industry, and rails steps.

peer-pay-cli onboarding ack STEP_ID

Example:

peer-pay-cli onboarding ack STEP_ID --profile live

onboarding skip​

Dismiss the onboarding checklist.

peer-pay-cli onboarding skip

Example:

peer-pay-cli onboarding skip --profile live

Credentials​

Export requires --output FILE. It writes PEER_PAY_API_URL and PEER_PAY_API_KEY, plus PEER_PAY_WEBHOOK_SECRET when --webhook-id is given. The signing secret must have been saved by this profile at webhook creation. Files use mode 0600; existing files are never overwritten.

credentials export​

Write credentials to a new private environment file.

peer-pay-cli credentials export [options]

Options: --output, --webhook-id.

Example:

peer-pay-cli credentials export --output .env.peer-pay --webhook-id WEBHOOK_ID

Simulation​

simulate and local commands require a local profile. test-order also works with a hosted sandbox profile, where delivery requires a public HTTPS handler. It is rejected for live merchants. None of these commands grants hosted admin access or performs a real payment locally.

Simulation requires ORDER_ID and --event. Specify --payment-id to select an attempt, --amount for partial fiat settlement/refund/chargeback scenarios, and --rail when a scenario creates a payment. Non-Apple-Pay crypto settlement rejects --amount (amountUsdc) with HTTP 400; omit it to settle the full remaining amount. Apple Pay partial deposits use --deposit-amount. List supported event names with local events.

Direct refund event injection requires settled funds and progresses from REFUND_PENDING to REFUND_COMPLETED. Refunds created with refunds create require a fully fulfilled fiat order and complete with simulate --event REFUND_COMPLETED; their amount is fixed by the simulated deposit. Bridge events progress pending → submitted → completed or failed. A failed bridge can restart. Quoted, minimum, and actual bridge outputs use destination token units: 10 USDC on BSC is "10000000000000000000". Literal destination token addresses are accepted; unlisted tokens use six decimals in local simulation, matching local quotes. Output amounts are calculated only for submitted and completed bridge events. Bridge status flags retain attempt history: after failure → retry → completion, hasCompletedBridge and hasFailedBridge are both true; canRetryManually is false because the latest attempt completed. automaticRetry reports scheduledCount, maxRetries and nextRetryAt like the hosted API; maxRetries is 3 only when serve runs with PAYMENT_BRIDGE_AUTO_RETRY_ENABLED=true, and 0 otherwise. With it on, simulate --event PAYMENT_BRIDGE_FAILED --automatic-retry true on a submitted bridge keeps the same attempt PENDING, sets nextRetryAt from the hosted schedule (10 minutes, 1 hour and 3 hours after the first failure) and sends no webhook. Inject PAYMENT_BRIDGE_SUBMITTED to resubmit it: the simulator does not wait for nextRetryAt. After three scheduled retries, or with automatic retry off, the failure is terminal: PAYMENT_BRIDGE_FAILED is sent and its message ends with (after N automatic retries) when retries happened. Like the hosted API, failure fields appear only on failed attempts: a waiting attempt shows failureCode and failureMessage as null in latestAttempt, attempts and bridgeInfo. Webhook timing is in bridge failures and automatic retries. Chargebacks require settled payments. Invalid transitions fail before changing state. After a failed, expired, or cancelled attempt, create a new payment to retry the order. Local and hosted sandbox also permit a new attempt while another is CREATED. For Zcash, they return the existing deposit without a new PAYMENT_CREATED while it is CREATED, unexpired and quoted for the current remaining amount. An expired or differently priced deposit is cancelled without a webhook and replaced. A --replacement-payment-id (or replacementPaymentId in payments create --data) leaves the previous sandbox attempt unchanged; the live API validates and cancels the previous fiat attempt. The local hosted order read returns the newest payment, except when it was cancelled, expired or failed and an older fiat attempt still has an open, unexpired on-chain intent; then it returns the newest such attempt within the ten newest payments. If the newest attempt is a cancelled, expired or failed fiat attempt and no older fiat intent is open, it returns the newest settled attempt when one exists in that history; a cancelled, expired or failed crypto attempt stays current. A funded Zcash deposit takes priority while converting. A newest active or settled attempt stays current over older fiat intents. Coinbase Apple Pay considers only the latest payment by creation time, regardless of status, and replays it only when it is CREATED, unexpired, has the same Apple Pay provider, rail, currency, and remaining order amount. Thus Venmo → Apple Pay → Apple Pay creates two payments, while Apple Pay → Venmo → Apple Pay creates three.

Settlement marks an order FULFILLED when its remaining amount is at most its completion threshold (see Orders), preserving the residual amount. Successful crypto sandbox simulation returns actualDestinationAmount as the persisted net settlement after fees, in destination token units, on both the first response and replay. For a 10 USDC payment with a 5% merchant-paid fee on Base, this is "9500000" (9.5 USDC); expectedDestinationAmount remains the quote amount.

Fiat platform quotes exclude all crypto rails, including Zcash, and disabled rails. They also leave out a fiat rail whose payment would sign more than maxOrderAmountUsdc with buyer-paid fees, for fixed orders and open-amount drafts alike, so a payment start on it would return INTENT_ABOVE_MAX (see payments create). Platform and payment quotes both derive sellerAutomatedReleaseAvailable from the backend sandbox SAR eligibility rule (isSarEligibleFiatRail), using the local mock's empty SAR-disabled rail list. In-person Venmo orders therefore expose a selectable platform. Ordinary orders retain their rails and quote amounts. test-order --data '{"rail":"VENMO","requestedUsdcAmount":"10"}' normalizes the rail to lowercase. Test orders consider only supported, runtime-enabled merchant rails. Without an explicit rail, they prefer the first fiat rail, then the first crypto rail; [near_intents_133701, venmo] selects Venmo. No usable rail returns HTTP 400 with Merchant has no enabled rail to create a test payment on. An unavailable explicit rail returns HTTP 400 with Requested rail is not supported and enabled for this merchant.

Delivery attempts against an inactive webhook increment attempts and set lastAttemptAt before failing. Transport failures retain the prior HTTP response code and store a local failure reason in responseBody in persisted state; like the backend, delivery-list responses omit that internal field. The webhook test response returns that reason as error only when no HTTP response code exists; an inactive webhook returns error: null.

local settings accepts stakingFixture (boolean; seed/reset deterministic staking rows), rates (currency-to-USD fixture rates expressed as currency units per USD), quoteTtlSeconds, logoUrl (a URL, or null to clear the mock merchant logo), minOrderAmountUsdc and maxOrderAmountUsdc (positive decimal strings with up to six decimal places). The default rates cover every currency the checkout currency selector offers, plus USDC, USDT and ZEC. A state file saved earlier keeps its own rates. The local minimum defaults to 1 USDC to match production; raise it to simulate stricter environments. The local maximum defaults to 10,000 USDC, the hosted per-order limit. The maximum must stay above the minimum and below 1 trillion USDC; otherwise the request returns 400 and changes nothing. State files saved before this setting existed load with the 10,000 USDC default. These persisted settings apply to new orders, resizes and resize simulations. local admin accepts the merchant admin config body, including fee overrides, branding, and order-size limits. local retry restarts a finished delivery's retry budget; local accept-invite accepts an invitation without sending email and, like team redeem, never changes the role of an OWNER or a locked member.

simulate​

--event DISPUTE_OPENED, DISPUTE_ESCALATED and DISPUTE_PAID need a settled fiat --payment-id. DISPUTE_OPENED takes an optional --amount (the gross USDC release; defaults to the payment's net plus fees). The local mock enforces the hosted transitions: one dispute per payment; only OPEN escalates; PAID is final.

simulate injects local mock events. For the hosted sandbox API, also served locally, use sandbox crypto-simulate, sandbox fail-payment, or sandbox expire-payment.

Apply an event to a local order. No money moves. Non-Apple-Pay crypto rejects --amount; Apple Pay deposits use --deposit-amount. Bridge outputs use the destination token decimals, and status flags retain attempt history.

peer-pay-cli simulate ORDER_ID [options]

Options: --event, --payment-id, --amount, --amount-currency, --deposit-amount, --rail, --automatic-retry.

--event ORDER_AMOUNT_SET needs --amount, --amount-currency and --rail; a fiat rail is quoted in --amount-currency.

ORDER_RESIZED uses the local settings minOrderAmountUsdc floor, which defaults to 1 USDC to match production, and the maxOrderAmountUsdc ceiling, which defaults to 10,000 USDC; an amount above it returns HTTP 400 AMOUNT_ABOVE_MAX. On an open-amount order it returns HTTP 400 RESIZE_NOT_SUPPORTED.

ORDER_AMOUNT_SET takes --amount, --amount-currency (the currency the amount is typed in) and --rail. Like the hosted API, it fires only when a payment starts with a new amount, so the simulation starts that payment exactly as payments create --amount --amount-currency does: it saves the amount, sends ORDER_AMOUNT_SET, then PAYMENT_CREATED, and returns the order and the payment. The amount or its currency must differ from the saved one, or the simulation returns HTTP 409; locked, stale and out-of-range amounts return the codes listed under Orders, and a fiat rail whose fee-inclusive amount is above maxOrderAmountUsdc returns INTENT_ABOVE_MAX without saving the amount.

Crypto settlement accepts payments in CREATED or EXPIRED, including expired Apple Pay attempts. Zcash also accepts CANCELLED payments for late deposits. Repeated settlement of SETTLED is idempotent. FAILED crypto payments and CANCELLED non-Zcash payments return HTTP 409 with Payment is not open for sandbox simulation and no error code. Successful late settlement clears the payment error and updates the order's remaining amount and fulfillment status, including a previously cancelled order. Non-Apple-Pay crypto settles the full remaining amount and emits PAYMENT_SETTLED and ORDER_FULFILLED once; Apple Pay uses its quoted deposit contribution and may leave the order partially fulfilled.

Example:

peer-pay-cli simulate ORDER_ID --event PAYMENT_SETTLED --payment-id PAYMENT_ID

test-order​

Use --order-id to fulfill an existing sandbox order, --amount for requestedUsdcAmount, and --rail to select a rail. Omit all flags for backend defaults.

Create and fulfill a test order in local or hosted sandbox mode. The default test amount is 1 USDC. The local minimum defaults to 1 USDC to match production. If you raise minOrderAmountUsdc through local settings to simulate a stricter environment, supply requestedUsdcAmount at or above that minimum. An existing orderId does not create a new order. Without an explicit rail, the command chooses the first supported, runtime-enabled fiat rail, then the first crypto rail.

peer-pay-cli test-order [options]

Options: --order-id, --amount, --rail, --data.

Example:

peer-pay-cli test-order --profile live-sandbox

local reset​

Reset the selected local simulator state file without parsing it. This works for incompatible files from an older CLI version. Stop the server using that file first: a held state lock refuses the reset. The command renames the existing file to PATH.backup-<uuid> and prints the state and backup paths; it does not delete the backup or contact the hosted API. No legacy data is converted.

peer-pay-cli local reset --state PATH

--state is required and must name an existing file. Restart peer-pay-cli serve with the same --state path to initialize fresh state. Orders, payouts, settings and webhook history from the old file remain only in the backup.

Example:

peer-pay-cli local reset --state ./local-state.json

local events​

List all supported simulation events.

peer-pay-cli local events

Example:

peer-pay-cli local events

local settings​

simulatedLive (true or false) defaults to on for new local merchants and makes the selected merchant count as non-Sandbox for payout creation and the Master Merchant Account checks. Turn it off to get the hosted Sandbox refusals, such as 403 PAYOUT_SANDBOX_UNSUPPORTED. Responses still report SANDBOX, and every other path behaves as sandbox. Existing state files keep their stored value.

peer-pay-cli local settings --data '{"simulatedLive":true}'

Set mock staking fixtures (stakingFixture), exchange rates, payment expiry, logo (logoUrl), and the persisted local minimum and maximum. minOrderAmountUsdc accepts a positive decimal string with up to six decimal places and defaults to 1 USDC to match production. Raise it to simulate stricter environments for order creation and resizing. maxOrderAmountUsdc takes the same format and defaults to 10,000 USDC, the hosted per-order limit. It caps fixed orders, resizes, the open-amount range and the fee-inclusive amount a fiat payment signs (INTENT_ABOVE_MAX), must stay above minOrderAmountUsdc and below 1 trillion USDC, and is checked against the pair after the patch, so both can change in one request.

peer-pay-cli local settings [options]

Options: --data.

Example:

peer-pay-cli local settings --data '{"quoteTtlSeconds":30}'
peer-pay-cli local settings --data '{"minOrderAmountUsdc":"10"}'
peer-pay-cli local settings --data '{"maxOrderAmountUsdc":"500"}'

local admin payout​

Show or set Peer's payout fee, step, rails and tokens for the local merchant.

peer-pay-cli local admin payout [options]

Options: --data.

Local profiles only. Without --data it reads, and with it saves, the local merchant through the same routes the admin app uses: GET and PATCH /api/v1/admin/cashouts/merchants/MERCHANT_ID/config. The response has the Peer fee (peerFeeBps), the payout step (payoutStepUsdc) and its range (payoutStepRange), the merchant's rails (rails), customer support link (supportUrl), funding-token limit (fundingTokens), the rails customers can use (effectiveRails), and the merchant fee and wallet (merchantFeeBps, merchantFeeRecipient). rails is the same list the merchant edits in its dashboard; effectiveRails removes Peer’s global and runtime disables. Both lists hold the per-network rail ids in rail order. A body sets any of:

{ "peerFeeBps": 150, "payoutStepUsdc": 50, "rails": ["relay_8453"], "fundingTokens": ["8453:0x833589fcd6edb6e08f4c7c32d4f71b54bda02913"] }

peerFeeBps is 0–1000; nonzero rates require a Concierge merchant. Base, Pro, and merchants without a selected plan must use 0. A nonzero rate on a self-serve merchant returns 400 PAYOUT_PEER_FEE_REQUIRES_CONCIERGE.

payoutStepUsdc is a whole PAYOUT_MIN_USDC–1000, rails any of venmo, cashapp, paypal, zelle, revolut, chime, a supported relay_<chainId> network id or near_intents_133701 (the same list the merchant edits; crypto is refused), and fundingTokens "chainId:lowercaseAddress" keys Pay supports (empty means every token Peer allows). The CLI adds "actor": "peer-pay-cli" when the body names no actor. A save that changes nothing writes no history. A body with no editable field (including one with only actor) returns 400 "Nothing to change". Changes apply to new payouts only.

Any rail can be stored, including one Peer has disabled globally; effectiveRails leaves a disabled rail out, and new payouts don't offer it. A missing customer support link never refuses a save.

Example:

peer-pay-cli local admin payout --data @merchant-payout.json

local admin payout-settings​

Show or set the global payout rails, funding tokens and Peer fee wallet.

peer-pay-cli local admin payout-settings [options]

Options: --data.

Local profiles only; GET and PATCH /api/v1/admin/cashouts/settings. These apply to every merchant: a rail or token switched off here is off for all of them, and the Peer fee on new payouts goes to peerFeeRecipient (orders still use DEFAULT_FEE_RECIPIENT). The response also lists supportedFundingTokens, every token Pay can route. disabledRails uses the same fiat and per-network rail ids as rails and effectiveRails; crypto is refused. Adding relay_42161 turns Arbitrum off for new payouts. Existing payouts keep their stored rails. A body with no editable field returns 400 "Nothing to change".

Currencies come from the rail catalog: PayPal and Revolut pay all their listed currencies; other fiat rails pay USD. A settings body sets any of:

{ "disabledRails": ["paypal"], "disabledFundingTokens": [], "peerFeeRecipient": "0x…40 hex" }

Example:

peer-pay-cli local admin payout-settings --data @global.json

local admin​

masterMerchantEnabled and masterMerchantMaxFeeBps (0–5000) follow the hosted rules: enabling needs a simulated LIVE merchant; the cap is optional and null means no Master Merchant Account-specific cap. A sub merchant cannot be a Master Merchant Account (MASTER_MERCHANT_INELIGIBLE), and a sub merchant's Master Merchant Account split entry changes only through the master merchant fee (MASTER_MERCHANT_FEE_MANAGED).

Set mock merchant admin controls, including fee overrides.

peer-pay-cli local admin [options]

Options: --data.

Example:

peer-pay-cli local admin --data @admin.json

local retry​

Retry a completed local webhook delivery.

peer-pay-cli local retry DELIVERY_ID

Example:

peer-pay-cli local retry DELIVERY_ID

local accept-invite​

Accept a simulated invitation without sending email.

peer-pay-cli local accept-invite INVITE_ID

Example:

peer-pay-cli local accept-invite INVITE_ID

local session​

Act as another local identity, such as a transfer recipient. The identity is created on first use with a deterministic simulated wallet. --wallet-delegated false simulates a recipient whose payout wallet is not delegated yet. The local API key keeps authenticating; identity-scoped routes (/auth/*, transfer preview and accept) act as the session identity. Local transfer links point at http://localhost:5175/transfer.

peer-pay-cli local session [options]

Options: --email, --wallet-delegated.

Example:

peer-pay-cli local session --email new-owner@example.com

API requests​

Use api for endpoints or query filters without a dedicated command. METHOD must be GET, POST, PUT, PATCH, or DELETE. PATH must start with /api/v1/ (or /local/ for a local profile); requests cannot change the profile's origin and do not follow redirects. The command expects a JSON API response. --management uses the signed-in session instead of the API key.

api​

Call an API endpoint. Use --management for owner or manager authentication.

peer-pay-cli api METHOD PATH [options]

Options: --data, --management.

Example:

peer-pay-cli api GET '/api/v1/merchants/me/orders?limit=20'

Recent merchant APIs​

orders payments​

List all payment attempts for an order.

peer-pay-cli orders payments ORDER_ID [options]

Options: --management.

Example:

peer-pay-cli orders payments ORDER_ID --profile live

payments recreate​

Request a replacement for a failed, expired, or cancelled fiat payment.

peer-pay-cli payments recreate ORDER_ID PAYMENT_ID [options]

Options: --idempotency-key (8 to 200 characters), --management.

Example:

peer-pay-cli payments recreate ORDER_ID PAYMENT_ID --profile live --idempotency-key retry-001

payments extend​

Request an extension for an eligible fiat payment window.

peer-pay-cli payments extend ORDER_ID PAYMENT_ID [options]

Options: --idempotency-key (8 to 200 characters), --management.

Example:

peer-pay-cli payments extend ORDER_ID PAYMENT_ID --profile live --idempotency-key extend-001

payments fulfill-sar​

Request seller-verified fulfillment using a transaction ID.

peer-pay-cli payments fulfill-sar ORDER_ID PAYMENT_ID [options]

Options: --tx-id, --idempotency-key (8 to 200 characters), --management.

Example:

peer-pay-cli payments fulfill-sar ORDER_ID PAYMENT_ID --profile live --tx-id TRANSACTION_ID --idempotency-key fulfill-001

actions get​

Read a remediation action and its current payment snapshot.

peer-pay-cli actions get ORDER_ID ACTION_ID [options]

Options: --management.

Example:

peer-pay-cli actions get ORDER_ID ACTION_ID --profile live

quotes availability​

First-class flags map to amount, quoteMode, enabledRails, fiatCurrency, destinationChainId, destinationToken, destinationAddress, and nearbyQuotesCount (1 to 10, default 3). Amount, quote mode, and all three destination fields are required.

peer-pay-cli quotes availability --amount 10 --quote-mode exact-token --destination-chain-id 8453 --destination-token USDC --destination-address WALLET_ADDRESS

Check fiat quote availability for the requested rails and amount.

peer-pay-cli quotes availability [options]

Options: --amount, --quote-mode, --enabled-rails, --fiat-currency, --destination-chain-id, --destination-token, --destination-address, --nearby-quotes-count, --data.

Example:

peer-pay-cli quotes availability --amount 10 --quote-mode exact-token --destination-chain-id 8453 --destination-token USDC --destination-address WALLET_ADDRESS

merchant referral​

Read your referral code, reward recipient, and referred merchants.

peer-pay-cli merchant referral [options]

Example:

peer-pay-cli merchant referral --profile live

Remediation commands use your merchant API key by default. Pass --management to use an owner or manager session. Create requests return an action ID and status; a 202 response means accepted, not fulfilled. Poll with actions get until the status is SUCCEEDED, BLOCKED, or FAILED, and inspect the returned payment. Reuse the same --idempotency-key (8 to 200 characters) when retrying the same action. --tx-id is the payment platform transaction ID, not an onchain hash.

Recreate applies to eligible failed, expired, or cancelled fiat payments. Extend requires an open intent within six hours of expiry. Fulfill SAR requires an eligible seller-verifiable rail; the backend checks eligibility and approvals. Local and hosted sandbox remediation mutations return SANDBOX_NOT_SUPPORTED. Use simulate for local lifecycle tests. See remediation API for statuses, eligibility, rate limits, and errors.

merchant referral requires an owner or manager session and a live merchant with a payout wallet. It returns the code, referralFeeBpsByTier ({ "BASE": 30, "PRO": 50, "CONCIERGE": 100 }), recipient, total referred count, and the newest 50 referred merchants. Rates follow the referred merchant's tier, regardless of the referrer's tier. Local and hosted sandbox profiles return REFERRAL_SANDBOX_FORBIDDEN. Apply someone else's code when creating your account with login --referral-code CODE; existing merchants are not retroactively attributed.

For quotes availability, use a JSON body such as:

{
"amount": "10",
"quoteMode": "exact-token",
"enabledRails": ["venmo"],
"destinationChainId": "8453",
"destinationToken": "USDC",
"destinationAddress": "0x0000000000000000000000000000000000000001"
}

enabledRails scopes the check to the checkout's rails; omit it to use merchant settings. Crypto-only requests report no fiat availability. Local results are synthetic and do not represent live liquidity. As hosted, an amount above maxOrderAmountUsdc (converted at the fixture rate in exact-fiat mode) returns HTTP 400 AMOUNT_ABOVE_MAX, and a rail whose local signal amount, buyer-paid fees included, is above maxOrderAmountUsdc is not counted. See quote availability.

Known local simulator limitations: it does not reproduce dashboard roles, admin authorization, API-key IP allowlist enforcement, backend serviceability, live liquidity, fee-sensitive quoting, or nearby quote suggestions.

Local fiat-denominated orders divide the requested fiat amount by the fixture currency-per-USD rate, then round half-up to two USDC decimal places. USD inputs are rounded directly. Order fields omit trailing fractional zeros ("10", "10.5"); metadata.requestedUsdcAmountAtCreation retains two decimal places ("10.00", "10.50"). For example, 10 EUR at 0.92 EUR/USD becomes 10.87 USDC; payment quotes and webhook order amounts use that rounded value. A fee-free EUR settlement records 10.0004 EUR (10.87 × 0.92).

Local crypto quotes use shared per-chain token decimals for both origin and destination amounts. A 10 USDC payment from BSC to Base quotes originAmount: "10000000000000000000" and destinationAmount: "10000000". Arc USDC deposits (relay_5042) and payouts (destinationChainId: "5042") use the six-decimal ERC-20 interface, so 10 USDC is "10000000" in either direction. Enable relay_5042 for deposits and payout conversion for Arc payouts. Conversion uses fixture token units per USD; configure missing tokens with local settings --data '{"rates":{"ETH":"0.0004","BTC":"0.000013"}}'. For example, a 10 USDC quote costs 0.004 ETH at that fixture rate. As in hosted sandbox quotes, origin amounts round to token precision and then truncate to six decimal places, except ZEC, which retains eight. Base USDC and the default ZEC fixture quotes keep their existing values. These are deterministic test rates, not market prices.

Crypto settlement records paymentAmount from the persisted quote's originAmount, formatted in the origin token's decimals (eight for ZEC), even if fixture rates change afterward. For example, a 10 USDT destination at a 0.8 USDT/USD fixture rate quotes 12.5 USDC from Base and settles with paymentAmount: "12.5". Fiat settlement continues to use the gross USDC amount multiplied by the payment's currency-per-USD rate. Apple Pay continues to use the actual USDC deposit amount.

Test Coinbase Apple Pay funding​

Apple Pay is an ordinary payment method in enabledRails, separate from fiat and crypto rails. The admin-only applePayAvailable flag defaults to false and allows a merchant to enable apple_pay. Orders copy the resolved enabled rails; changing merchant enablement later does not enable Apple Pay on an existing order. Apple Pay uses relay_8453 USDC for funding and pays out in the order's destination token, like any other Relay crypto payment.

Pricing uses the apple_pay entry in railPlatformReferralFeeConfig, falling back to platformReferralFeeConfig, including tranches. { "mode": "exempt" } sets no platform fee without disabling Apple Pay. The local simulator snapshots these configs and referral splits when the order is created.

Fiat quote availability excludes apple_pay; its fee override never changes fiat platform quotes or their counts. Apple Pay settles through the Relay funding path.

A literal rail: apple_pay payment is rejected as a rail that is not enabled. Use rail: relay_8453, currency: USDC, and onrampProvider: coinbase_apple_pay.

Sandbox platform lists exclude Apple Pay. Automatic sandbox test orders choose a fiat rail first, then a crypto rail; an Apple Pay-only configuration has no rail for this automatic payment test.

Apple Pay tranche fees resolve twice, including after PAYEE gross-up, just as fiat fees do. For example, 95 USDC at 100 bps can cross into a 1000 bps tranche; the simulator then requests 105.555556 USDC to deliver 95 USDC.

When Apple Pay is unavailable, new orders omit it from their resolved rails; other payment methods remain usable. An order left with no payable method returns NO_ELIGIBLE_PAYMENT_RAILS. Admin rail updates strip Apple Pay unless availability is granted. Merchant settings reject a new enable attempt without availability, but accept an unchanged stored selection and clear it during the save. When the stored quote preference is EXCLUSIVE_SAR, merchant config PATCH and PUT remove non-SAR fiat rails (for example, ["venmo","revolut"] becomes ["venmo"]) before checking Apple Pay availability and saving. Available apple_pay is preserved. PATCH uses the stored selection when enabledRails is omitted; supplied rails replace the selection in both verbs. For an effective quote preference other than EXCLUSIVE_SAR, an admin PATCH that touches neither availability nor the rail list preserves the stored selection, including apple_pay when availability is false. With EXCLUSIVE_SAR (including when set in that PATCH), admin updates first filter non-SAR fiat rails from the supplied or stored selection, then strip Apple Pay if availability is false. Available Apple Pay is preserved. Explicitly setting applePayAvailable: false strips Apple Pay from the resulting selection in every mode.

peer-pay-cli local admin --data '{"applePayAvailable":true,"railPlatformReferralFeeConfig":{"apple_pay":{"mode":"flat","currency":"USDC","valueBps":500}}}'
peer-pay-cli merchant settings --data '{"enabledRails":["venmo","relay_8453","apple_pay"]}'
peer-pay-cli orders create --amount 10
peer-pay-cli payments create ORDER_ID --rail relay_8453 --currency USDC --onramp-provider coinbase_apple_pay
peer-pay-cli simulate ORDER_ID --event PAYMENT_SETTLED --payment-id PAYMENT_ID --deposit-amount 5
peer-pay-cli orders get ORDER_ID

--deposit-amount is a USDC funding amount for a successful local Apple Pay simulation. Omit it to fund the full quote. It cannot be combined with --amount. Net Apple Pay settlement uses destination token decimals. A 10 USDC deposit with a 500 bps fee to BSC USDC records netSettledUsdcAmount: "9.5" and returns actualDestinationAmount: "9500000000000000000"; order accounting remains in USDC. Partial deposits leave an order balance for another payment. Replaying settlement does not credit a deposit twice. Local testing models the payment and fee contract; it does not run Coinbase, card authorization, or an Apple Pay sheet.

The API form accepts paymentId, outcome, and optional depositAmountUsdc at POST /api/v1/orders/ORDER_ID/crypto/sandbox/simulate. Supplying paymentId rejects simulation if that payment has been replaced. Deposits must be positive and no larger than the persisted quote, and partial deposits require Apple Pay success.

Payment objects and webhooks now include penalties, an array of applied payment reductions. Local payments without an attestation return []. Live penalty entries contain kind, penaltyBps, originalAmount, and attestedAmount; see the payment API.

Local state uses format version 3, which holds several merchants and identities. serve --state FILE validates the saved state before starting. Invalid JSON and invalid version 3 contents report the state file path; repair the file or start with a new --state path. A version 2 file is rejected with uses state format version 2; this peer-pay-cli uses version 3: start with a new --state path and pass --api-key to keep the same local API key. A file written by a newer peer-pay-cli asks you to upgrade. Saved hosted profiles are unaffected. Loading an older state file rewrites stored crypto: rails and disabledRails expand to the twelve relay_* rails, payoutRail and attempt crypto become relay_8453 with Base USDC, and stored webhook payloads are rewritten too. Old webhook snapshots backfill settlement.refundFeeAmount as "0.00"; a payoutTransfer missing usdcTxHash gets decimals: 6, usdcTxHash from its txHash, and provider: null. Retired NEEDS_SUPPORT timeline rows are dropped on load.

Local JSON requests over 100 KB return HTTP 413; malformed or non-local Host headers return HTTP 403. Background delivery and payment-expiry errors are logged to stderr. A failed delivery save clears the in-flight marker so later attempts can run. Shutdown waits for active webhook deliveries, including connectivity tests, before saving state and releasing the state-file lock.

whoami​

Read the signed-in profile, accessible merchants, defaultMerchantId, and staking. Locally, lists the merchants the local session identity belongs to; serve seeds one, with that identity as OWNER.

peer-pay-cli whoami --profile live

merchant switch​

Select an accessible merchant and refresh its API key. Hosted profiles only; changing merchants clears cached order tokens and webhook secrets.

peer-pay-cli merchant switch MERCHANT_ID --profile live

merchant stats​

Read merchant dashboard statistics.

peer-pay-cli merchant stats --profile live

merchant tier-usage​

Read merchant tier usage.

peer-pay-cli merchant tier-usage --profile live

merchant bridge-signer-status​

Read bridge signer status using the management session.

peer-pay-cli merchant bridge-signer-status --profile live --signer-id SIGNER_ID --chain-type ethereum

Options: --signer-id, --policy-id, --chain-type.

Read support links for a comma-separated list of orders.

peer-pay-cli orders support-links --profile live --order-ids order_a,order_b

Options: --order-ids.

integration runbook​

Print the public integration runbook as Markdown text.

peer-pay-cli integration runbook --profile live

The runbook command prints plain text, including when piped; --json encodes that text as a JSON string.

whoami uses a hosted management session or the local API key. merchant switch requires a hosted profile. Switch checks accessibleMerchants using the target merchant on both /auth/me and login, so revoked access to the current merchant does not prevent switching. A target that returns 403/404 or is absent from accessibleMerchants reports You do not have access to merchant <id>. Switch saves the selected merchant and refreshed API key together. Failed selection or login leaves the profile unchanged. Roles whose login response masks the API key cannot use API-key commands after switching; they can use management commands. No wallet provisioning occurs during switch.

The local simulator supports merchant stats, tier usage, bridge signer status, order support links, and sandbox enablement. Its state holds merchants keyed by ID and identities keyed by Privy user ID and email; serve seeds one merchant whose OWNER is the local session identity owner@example.com. Each merchant's API key selects that merchant. X-Merchant-Id naming another merchant selects it when the session identity is a member and otherwise returns HTTP 403 Merchant access not found, like the hosted dashboard; /auth/me returns HTTP 404 Merchant not found for such an ID and /auth/login falls back to the oldest membership. Without X-Merchant-Id, both auth routes select the oldest membership. The response's accessibleMerchants list is sorted by role (OWNER, MANAGER, CASHIER), then by merchant name within each role; that order does not determine default selection. Orders, payments, refunds, webhooks, invites, members, config and idempotency keys are isolated per merchant. Privy login and role restrictions are not simulated.

Local merchants report SANDBOX and the fixture CONCIERGE tier. Consequently, merchant tier returns HTTP 403 TIER_CHANGE_SANDBOX_FORBIDDEN, and merchant concierge returns HTTP 403 CONCIERGE_REQUEST_SANDBOX_FORBIDDEN. Neither rejection changes state. Wallet provisioning returns the existing fixture addresses; PUT /api/v1/merchants/me/users reports Merchant user updated and returns the merchant profile; like the hosted route it never creates a membership or changes a role, and it returns HTTP 404 Merchant user not found when the session identity has no membership. merchant sandbox persists an idempotent pair using the same local merchant ID.

Onboarding acknowledgements, skip/completion timestamps, and Shopify setup requests persist in the state file. Unknown step IDs are rejected. requestSetup directs you to peer-pay-cli merchant shopify; acknowledging an automatic step does not satisfy its facts. Shopify setup records the normalized store domain, collaborator code and checkout currency in a local request and acknowledges requestSetup; it does not contact the Peer team. Invalid values are rejected with the backend's messages and change nothing. Requests saved before details were collected load with all three fields null. Completion is derived and saved whenever the checklist is returned (get, ack, skip); skipping only dismisses the checklist. Local orders never satisfy live-order steps. The onboarding sandboxApiKey is non-null only for CUSTOM_API, WOOCOMMERCE, and TELEGRAM integration paths.

Dashboard stats count fulfilled and partially fulfilled orders separately; volume and net receipts include only fulfilled orders (receipts sum their settled payments). Monthly tier usage counts fulfilled and partially fulfilled orders only when they have at least one SETTLED fiat-rail payment (including bank variants such as zelle-chase). Each qualifying order counts once at its full requested USDC amount, including mixed fiat/crypto orders, by order creation time in the current UTC calendar month (inclusive start, exclusive next-month start). Crypto-only orders and orders with no settled fiat payment do not count. The response shape remains { tier, limits, usage: { orderCount, volumeUsdc }, windowStart, windowEnd }. Sandbox usage has no tier caps. Order and payment lists include total, page, and limit alongside their rows.

Bridge signer status validates the hosted query and reports no attached signer for the EVM fixture (walletId: "", attached: false, additionalSigners: []). The fixture has no attached Solana bridge signer; Solana signer queries return HTTP 400 even though a payout wallet is provisioned. Support links return a map keyed by owned order IDs, with deterministic local_<orderId> thread IDs, hasMessages: false, and status: "AI_HANDLING"; no real support thread is created. Repeated and comma-separated order IDs are accepted, deduplicated, and filtered to existing local merchant orders. The public integration runbook works locally.

Staking, refunds, sandbox controls, and settlement amounts​

Staking and refunds use the hosted management session (Bearer and merchant ID), or the local API key. Hosted refunds need the owner's session; managers and cashiers get 403. The local mock deliberately provides simulated staking and refunds even though the hosted sandbox disables them. Bridge inspection uses the management session on hosted profiles and the API key locally. Sandbox controls use the API key and attach a cached order token if present, but do not require one.

simulate is local mock event injection. sandbox ... calls the hosted sandbox API, whose outcome routes are also served by the local mock. Sandbox commands require sandbox orders; use a sandbox profile for hosted testing.

Local staking starts with zero stake and empty lock/activity pages. Seed one active 10 USDC lock and two activity entries (a 100 USDC deposit and its lock):

peer-pay-cli local settings --data '{"stakingFixture":true}'
peer-pay-cli staking overview
peer-pay-cli staking locks --status ACTIVE
peer-pay-cli staking activity --limit 1

The staking taker, effective stake owner, and every lock and activity owner are the selected merchant's checksummed v1 EVM wallet, as hosted.

While seeded, the lock page also lists one ACTIVE lock for each of the selected merchant's local fiat payments that is CREATED, EXPIRED, or SETTLED. Its lockId is the payment's intent hash, its amount is the intent amount, and it is linked to the payment as a PAY_PAYMENT lock, as hosted does. Unsettled payment locks show maturesAt 0 (awaiting settlement). Cancelled and failed payments hold no lock. The overview's lock totals and locked stake count these locks, while free stake stays at 90 USDC.

Use stakingFixture:false to reset the fixtures. Amounts use USDC base units. Pages match hosted {items,nextCursor,latestUpdateBlock}; there is no hasMore field. Pass the returned cursor with the same filter to read the next page; nextCursor:null marks the end. Fixture data is deterministic and persisted. These addresses, balances, and access flags do not represent onchain stake.

staking overview​

Read merchant staking balances and access using the management session, or deterministic local fixtures.

peer-pay-cli staking overview [options]

Example:

peer-pay-cli staking overview --profile live

staking locks​

List merchant stake locks using the management session, or local fixtures.

peer-pay-cli staking locks [options]

Options: --status, --cursor, --limit.

--status is ACTIVE, UNLOCKED, or RESOLVED. --cursor is an opaque cursor from the previous page. --limit is 1 to 100, default 50.

Each lock's association is either {"kind":"EXTERNAL"} or a PAY_PAYMENT link with paymentId, orderId, and releasesIfUnpaidAt. releasesIfUnpaidAt is the ISO 8601 UTC time the lock frees itself if the customer never pays: the payment's quoteExpiresAt plus 30 minutes, when Peer cancels the expired intent. CREATED payments, and EXPIRED payments whose intent has not been cancelled yet, get that time. It is null once the payment is SETTLED, CANCELLED, or FAILED, once the intent has been cancelled, while automatic cancellation is switched off, or when the payment window closed more than 24 hours ago. The local mock never cancels intents, so a local EXPIRED payment keeps its time until that 24-hour limit.

Example:

peer-pay-cli staking locks --profile live --status ACTIVE --cursor CURSOR --limit 20

staking activity​

List merchant stake activity using the management session, or local fixtures.

peer-pay-cli staking activity [options]

Options: --kind, --cursor, --limit.

--kind is DEPOSITED, WITHDRAWN, TAKER_AUTHORIZATION_UPDATED, STAKE_OWNER_SELECTED, LOCK_FUNDED, STAKE_LOCKED, STAKE_LOCK_INCREASED, STAKE_LOCK_RESIZED, STAKE_UNLOCKED, STAKE_LOCK_RESOLVED, CLAIM_CREATED, or CLAIM_WITHDRAWN. --cursor is opaque; --limit is 1 to 100, default 50.

Example:

peer-pay-cli staking activity --profile live --kind DEPOSITED --limit 20

Local merchant refunds require a FULFILLED order with one settled, supported fiat payment, a Base USDC destination, and no recorded chargeback. Refund pricing uses the local currency rate discounted by 1%, rounded as in the hosted service. The merchant must fund that refund amount in the hosted flow; locally no funds move.

peer-pay-cli refunds create ORDER_ID --seller-username seller123
peer-pay-cli refunds get ORDER_ID
peer-pay-cli simulate ORDER_ID --event REFUND_COMPLETED

Creation returns HTTP 201 and a merchant refund entity with status SUBMITTED, while the order and webhook report PENDING. A repeated create returns HTTP 200 with the existing refund, preserving its seller and amount and emitting no new webhook. Completing with simulate changes the entity to REFUNDED, mirrors COMPLETED onto the order, and emits REFUND_COMPLETED. The deposit fixes the amount; a different --amount is rejected. Creating after completion returns 409.

refunds cancel ORDER_ID cancels only a pending local deposit, returns a CANCELLED entity, and clears the order refund fields so a new refund can be created. Cancellation emits no webhook, matching hosted behavior. Cancelling a completed or already cancelled refund returns 404 Refund not found. refunds get keeps returning the latest entity, including cancelled refunds. Refund events created through simulate without a merchant refund keep their existing behavior.

refunds get​

Read an order refund using the owner's management session, or the local API key.

peer-pay-cli refunds get ORDER_ID [options]

Example:

peer-pay-cli refunds get ORDER_ID --profile live

refunds create​

Create a refund using the owner's management session, or simulate a deposit locally.

peer-pay-cli refunds create ORDER_ID [options]

Options: --seller-username.

--seller-username is required, trimmed, and must contain 1 to 256 characters.

An order created before the merchant account changed owners cannot be refunded from the new owner's account: hosted and local creation return HTTP 409 ORDER_FROM_PREVIOUS_OWNER. A refund created before the transfer keeps its original wallet for retry and cancel.

Example:

peer-pay-cli refunds create ORDER_ID --profile live --seller-username seller123

refunds cancel​

Cancel a pending order refund using the owner's management session, or the local API key.

peer-pay-cli refunds cancel ORDER_ID [options]

Example:

peer-pay-cli refunds cancel ORDER_ID --profile live

payments bridge​

Read bridge status and attempt history using the management session, or the local API key.

peer-pay-cli payments bridge PAYMENT_ID [options]

Example:

peer-pay-cli payments bridge PAYMENT_ID

sandbox crypto-simulate​

Simulate a crypto outcome through the hosted sandbox API, also served locally. Public endpoint; no API key or order token is required. For local mock event injection, use simulate.

peer-pay-cli sandbox crypto-simulate ORDER_ID [options]

Options: --outcome, --payment-id, --deposit-amount.

--outcome is required: success or failure. --payment-id optionally pins the current payment. --deposit-amount is a positive USDC decimal with at most 6 decimal places, valid only for a successful Apple Pay payment.

Example:

peer-pay-cli sandbox crypto-simulate ORDER_ID --profile live-sandbox --outcome success --payment-id PAYMENT_ID --deposit-amount 5

sandbox fail-payment​

Fail a payment through the hosted sandbox API, also served locally. Public endpoint; no API key or order token is required. For local mock event injection, use simulate.

peer-pay-cli sandbox fail-payment ORDER_ID [options]

Options: --payment-id.

--payment-id is required.

Example:

peer-pay-cli sandbox fail-payment ORDER_ID --profile live-sandbox --payment-id PAYMENT_ID

sandbox expire-payment​

Expire a payment through the hosted sandbox API, also served locally. Public endpoint; no API key or order token is required. For local mock event injection, use simulate.

peer-pay-cli sandbox expire-payment ORDER_ID [options]

Options: --payment-id.

--payment-id is required.

Example:

peer-pay-cli sandbox expire-payment ORDER_ID --profile live-sandbox --payment-id PAYMENT_ID

fees settlement-amounts​

Calculate settlement and order contribution amounts. Public API; no API key or management session is required. Also served locally.

peer-pay-cli fees settlement-amounts [options]

Options: --fee-payer, --buyer-fee-share-bps, --quoted-bridge-spread-bps, --net-settled-usdc, --total-usdc-fee, --payment-amount, --currency-per-usd-rate.

--fee-payer, --net-settled-usdc, --total-usdc-fee, --payment-amount, and --currency-per-usd-rate are required. --fee-payer is MERCHANT, PAYEE, or SPLIT. SPLIT also requires --buyer-fee-share-bps, an integer from 0 to 10000 in steps of 1000. Amounts and rates are decimal strings: net USDC received, total USDC fees, fiat paid, and local currency per USD. MERCHANT contribution is fiat paid divided by the rate; PAYEE contribution is net USDC; SPLIT contribution is (1 - s) * (fiat paid / rate) + s * net USDC, where s = buyerFeeShareBps / 10000. If a payout bridge was quoted, pass --quoted-bridge-spread-bps as a nonnegative decimal string; the calculation first adjusts net USDC by 1 / (1 + quotedBridgeSpreadBps / 10000) to include that quoted cost. Amounts truncate to 6 decimals. The example returns net 97.5, fee 2.5, and contribution 100. A saved profile selects the API origin but needs no credentials.

Example:

peer-pay-cli fees settlement-amounts --fee-payer MERCHANT --net-settled-usdc 97.5 --total-usdc-fee 2.5 --payment-amount 92 --currency-per-usd-rate 0.92
# A $105 payment and $95 net settlement credit $100 of principal at a 50:50 split.
peer-pay-cli fees settlement-amounts --fee-payer SPLIT --buyer-fee-share-bps 5000 --net-settled-usdc 95 --total-usdc-fee 10 --payment-amount 105 --currency-per-usd-rate 1

Intentional local command restrictions​

The help-command route smoke matrix covers every command's primary route. Commands without HTTP routes (serve, profiles, logout, handoff, use, and credentials export) operate on the local process or filesystem.

  • merchant referral: 403 REFERRAL_SANDBOX_FORBIDDEN.
  • payments recreate, payments extend, payments fulfill-sar: 403 SANDBOX_NOT_SUPPORTED.
  • merchant tier: 403 TIER_CHANGE_SANDBOX_FORBIDDEN.
  • merchant concierge: 403 CONCIERGE_REQUEST_SANDBOX_FORBIDDEN.
  • actions get: 404 Action not found, because sandbox remediation creates no actions.
  • login --profile local: rejected by the CLI; the direct local POST /api/v1/auth/login route returns a 200 envelope with the merchant profile selected from the local session identity's memberships, falling back to the oldest membership when X-Merchant-Id is absent or inaccessible.
  • merchant switch and sandbox enable: hosted-profile operations; serve seeds one local merchant and saves only its API key in the local profile.
  • Local payout commands only run for the seeded local merchant. Any other merchant (including a sub-merchant or X-Merchant-Id selection) gets 403 PAYOUT_LOCAL_MERCHANT_ONLY.
  • payouts create: the seeded merchant starts with simulatedLive on, so it may create payouts; with it off, create returns the hosted 403 PAYOUT_SANDBOX_UNSUPPORTED. Only stablecoin and BTC funding is priced: BTC at a fixed $100,000 per BTC with the volatile buffer; any other funding token returns 422 PAYOUT_ROUTE_UNQUOTABLE. There is no Relay quote, so 422 PAYOUT_FUNDING_AMOUNT_TOO_LOW can't occur locally. Amounts use the no-Relay-cost maths, the merchant wallet check is skipped, and the funding deadline is the API's fixed 24 hours (local settings quoteTtlSeconds does not affect payouts), and the /c/ checkout page is not served locally; its customer API is.
  • payouts cancel: there is no Relay deposit check, so 409 PAYOUT_FUNDING_DETECTED cannot occur locally.
  • payouts configure-local: stands in for the dashboard's settings routes, which need a dashboard sign-in; payouts start with every platform on.
  • payouts simulate-payment and payouts simulate-peer-withdraw: there is no indexer or escrow; each step records the deposit change and runs the settlement refresh an indexer update would trigger. The local escrow stand-in's dust threshold is fixed at 0.10 USDC. The hosted API reads no threshold; it settles from the escrow's DustCollected event.
  • payouts simulate-funding and payouts sweep-local: funding is simulated. There are no Relay or chain reads; each simulated record stands in for one Relay request and its Base receipt, and sweep-local --now stands in for the clock the funding sweeper runs on. Hosted profiles reject both commands before any request. Each simulated record carries a Relay request ID, so --superseded-by can link records as Relay does.
  • payouts simulate-payout-quote and payouts simulate-payout-bridge: no Relay or NEAR calls. Relay estimates use 1 USD per token. NEAR uses fixed fixtures: 1 ZEC = 100 USD, a 1.58 USDC minimum, a 20-minute refund deadline, sendBy 5 minutes before it, and an address live for 10 more minutes. local settings rates does not change these fixtures. Bridges move only on simulated evidence; sweep-local never advances them.
  • Customer checkout (/api/v1/cashout-checkout): the API's own router, link-token auth and controller run over local state. GET /api/v1/cashout-checkout/:id returns the local merchant's merchantCheckoutTheme as merchant.checkoutTheme and its disableBranding as merchant.disableBranding, as the hosted API reads the merchant's current config. The Peer-app login stands in as Authorization: Bearer local-player:<email>; any other token returns 401 CASHOUT_LOGIN_INVALID, and another email returns 403 CASHOUT_PLAYER_MISMATCH. An unknown payout id returns the same 401 CASHOUT_TOKEN_INVALID as a wrong token. Curator credentials, the escrow deposit and its events, the indexer updates and the wallet balance are the payouts simulate-* records. See Local customer checkout for every route and both auth headers. The activity route's response includes the current payout's full recipient email and a checkoutUrl on every item, using the same deterministic checkout token as payout creation.
  • Pay's sends from the customer wallet: there is no Privy signer, dispatcher or Base. The local signer (local-cashout-signer, policy local-cashout-policy) is always attached and there is no attach step, so 409 CASHOUT_SIGNER_NOT_ATTACHED never occurs locally. Queued sends wait until payouts resolve-send confirms or fails them; a fiat confirm always queues the USDC approve, and the calldata is a local stand-in. A state file saved before server-side sends loads a pending listing or USDC transfer that never reported a transaction hash as FAILED, so the customer can confirm again. One whose hash was reported loads unchanged, since its transaction may have gone out.

Other commands require their normal inputs and state (for example a fulfilled order before creating a refund).

use​

Set the active profile. The name must already exist; local is allowed.

peer-pay-cli use NAME
peer-pay-cli use local

profiles marks it with active: true. Logging out of the active profile clears that selection. status includes profile and mode in its response object.

sandbox enable​

In a terminal, this command offers sign-in if the profile has no hosted management session. After activating NAME-sandbox, run peer-pay-cli use NAME to return to the hosted profile for merchant management or setup. Non-interactive setup errors include this recovery command.

Enable the remote sandbox using the hosted management session, read its key from the sandbox merchant profile, save <profile>-sandbox, and make it active. This works for every integration path, including an unset path, and requires an OWNER or MANAGER role. If the name belongs to a different API URL or sandbox merchant ID (including a profile with no merchant ID), the CLI tries <profile>-sandbox-2, -3, and so on until it finds an unused name or a matching profile. Re-running with --profile NAME reuses only a matching API URL and sandbox merchant ID, preserving its session, cached order tokens, and webhook secrets even when its API key rotates. Setup follows the same naming rule and returns the chosen sandboxProfile. A missing key fails without changing the saved profile or active selection.

peer-pay-cli sandbox enable

The result includes sandboxProfile and activeProfile. To return to your merchant account, run peer-pay-cli use NAME with the original profile name.

Seller disputes​

disputes list​

List seller disputes using the owner's management session, or the local mock.

peer-pay-cli disputes list [options]

Options: --status (comma-separated OPEN,ESCALATED or PAID), --order-id, --page (default 1), --limit (1–100, default 20). --order-id filters one order's disputes. Amounts in responses are raw 6-decimal USDC units. Payment attempts are dashboard-only: the USDC transfer is signed by the merchant's Peer Pay wallet in the browser.

Example:

peer-pay-cli disputes list --status OPEN,ESCALATED

disputes get​

Read one seller dispute using the owner's management session, or the local mock.

peer-pay-cli disputes get DISPUTE_ID [options]

Options: global profile and output options only. Payment attempts are dashboard-only: the USDC transfer is signed by the merchant's Peer Pay wallet in the browser.

Example:

peer-pay-cli disputes get DISPUTE_ID --profile live