API reference
21 integration endpoints, all POST with a JSON body, plus a GET liveness probe and a GET alias for capabilities. Nothing is authenticated in v1: the capability in the body is what authorizes the call.
Try these requests in the API explorer — the same endpoints, browsable, against the Base Sepolia test environment. Both pages come from openapi.json, the contract the API is tested against on every change.
Authentication
There is no API authentication in v1. Payments are secured by the customer’s signature and by the contract, not by knowing who is calling. There are no API keys, no bearer tokens and no accounts.
What protects each call instead is the capability it carries. Payment intents, setup tokens, subscription capabilities and cancel tokens are HMAC-signed by the deployment and verified on every request; a token minted for another deployment fails here even if a key were shared. The rate limits below protect the service itself.
A p2s2 subscription capability authorizes charges against a customer’s wallet. Keep it server-side. Use /v1/subscriptions/revoke/session when something has to reach a browser.
Capabilities never appear in a path or query string, which is what a server, a proxy and an access log actually record. The hosted checkout does carry one in the URL fragment — browsers do not send a fragment in the request or in the Referer header, so it stays on the client. That protects it from your infrastructure, not from everything running in the page.
Conventions
- The 21 integration endpoints are all POST with a JSON body — including the reads, so a capability travels in the body rather than in a path or query string a server would log. Two GETs sit outside that: GET /health, a liveness probe that takes no input, and GET /v1/capabilities, which answers the same body as its POST.
- Unknown fields are rejected, not ignored. A field the API does not recognise means the caller expects something it is not honouring.
- Amounts are decimal strings ("100.00"); base units appear alongside them as integer strings.
- Errors return { "error": CODE }, usually with an action telling your system what to do. See Errors.
- CORS is open and credentials are never allowed: there is no cookie or header authority for another origin to borrow.
- Request bodies are capped at 16 KB.
Payments
Mint a one-time payment intent.
Returns intent, reference, amount, expires_at, and a pay object with chain_id, splitter, token, recipient, amount_units and reference.
Authoritative terms for a checkout to display.
Returns the recipient, amount, base units, token, splitter, chain id, reference and expiry. Rejects a payment the contract has already settled.
Server-side proof that a payment landed.
Returns HTTP 200 either way. A proven payment returns valid: true with tx_hash, reference, amount, block_number and block_hash. Anything else returns { valid: false, code } — including PAYMENT_CONFIRMING, which is not a 409 on this endpoint. Branch on valid. Only transport failures use status codes: 400 for a malformed body, 429 for rate limiting, 500 for an unexpected fault. An expired intent is not a failure here: verification reads history and ignores expiry.
Find a settled payment when its transaction hash was lost.
Returns { found: true, … } with the ordinary verification result, { found: true, tx_hash, valid: false, code } when the transaction exists but does not verify yet, or { found: false, code: "PAYMENT_NOT_FOUND", as_of_block }. A miss is a statement about that block, never a verdict that nothing was paid. Runs at low priority and can be expensive — it is for a missing callback, not for polling.
Subscriptions
Create the technical terms of a subscription.
Returns setup_token, expires_at, chain_id, contract, amount and salt.
Terms plus the EIP-712 scaffold for a custom checkout.
Returns the full terms, max_gas_reimbursement, fee_bps, network_fee, typed_data, and a best-effort network_fee_estimate which may be null.
Exchange the customer’s signature for a capability.
Returns subscription (the p2s2 capability), subscription_id, amount, period and end.
Current state, read from the chain.
Returns the state fields documented in Recurring payments, including the echoed terms.
Mint a short-lived cancel token safe for a browser.
Returns cancel_token, expires_at (15 minutes by default), subscription_id and payer.
Unsigned calldata that cancels one subscription.
Returns chain_id, payer, to, data, subscription_id and a human-readable description. Only the payer’s wallet can send it.
Unsigned calldata that removes the token allowance entirely.
Returns chain_id, to, data and description. Stops every P2Flux subscription paid in this token from that wallet.
A narrow session for restoring one subscription’s allowance.
Returns approve_token (the p2approve1 session, 15 minutes), expires_at, payer and subscription_id. The token can approve and nothing else: it cannot charge, revoke, refund, or become a capability. Open <checkout>/#/approve/<approve_token>. See Restore an allowance.
Read an allowance-restore session back, for the browser holding it.
Returns chain_id, token, spender (the recurring contract — never a caller-supplied address), payer, subscription_id, required_units (the signed amount plus the gas reimbursement the next charge may add) and expires_at.
Refunds
All three take the original settlement: tx_hash plus either intent for a one-time payment, or subscription and period_index for a renewal. Neither the payer, the merchant nor a maximum is ever accepted from the caller — all three are derived from the chain.
Lock the terms of a refund the merchant is about to send.
Returns refund_token (15 minutes, for the browser only — do not store it), chain_id, token, merchant, payer, original_amount, refund_amount, their base-unit twins, and expires_at.
Read the terms behind a prepare token.
Returns the same terms for the browser holding the token. Touches no chain and changes nothing.
Confirm a refund happened and is settled.
Returns status: "REFUNDED" with refund_tx_hash, refund_amount, original_amount, payer, merchant and block_number. Takes the original settlement rather than the prepare token, so reconciliation works days later.
Charges
Attempt one recurring charge. Idempotent per billing period.
Returns status, ok, already_paid, action, subscription_id, period_index, period_start, period_end, next_period_at, and — when a transaction was sent — tx_hash and amount.
The transaction that charged one exact period, when the response was lost.
Returns { found: true, subscription_id, period_index, tx_hash, block_number, payer, recipient, net_units, fee_units, network_fee_units, amount_units } when the contract’s own SubscriptionCharged log names this subscription and this period and matches the signed terms; { found: false, code: "PAYMENT_NOT_FOUND", as_of_block } otherwise — ordinary for a skipped period, and a statement about one block rather than a verdict. 409 PAYMENT_CONFIRMING (with the hash) below finality, 503 RECOVERY_UNAVAILABLE when the bounded search cannot finish, 502 PAYMENT_RECOVERY_INCONSISTENT for a log that contradicts the terms. Full semantics in Recover a lost charge.
Service
Liveness only. Returns { "ok": true } and never grows a body.
/metrics and /ready exist but are bound to a loopback-only listener and are not reachable from outside the host. They are operational, not part of the integration surface.
Rate limits
Per minute, per client IP unless noted. These are a ceiling on abuse, not a merchant quota — there are no accounts, and many merchant sites share one outbound address. Defaults shown; an operator can change them.
Exceeding one returns RATE_LIMITED or CONCURRENCY_LIMIT with HTTP 429, a retry_after field and a matching Retry-After header. Nothing was charged.
There is no idempotency header, because the two write paths do not need one: a payment is settled once by the contract’s replay guard, and a charge is allowed once per billing period.