openapi: 3.2.0 info: title: P2Flux One-time payments API version: 1.0.0 summary: Non-custodial USDC payments, subscriptions and refunds on Base. description: Programmable, non-custodial payments on Base. contact: name: P2Flux url: https://p2flux.com/docs/ license: name: Documentation for the hosted P2Flux API url: https://p2flux.com/terms.html servers: - url: https://api.p2flux.com description: 'Production - Base Mainnet (8453). Real money: every settlement moves real USDC and cannot be reversed by P2Flux. Point your integration here; use the test server for experiments.' - url: https://api-test.p2flux.com description: Test - Base Sepolia (84532). Identical API against the test deployment; value moved here is faucet USDC, not real money. The interactive explorer is restricted to this server by design. security: [] tags: - name: One-Time Payments description: A single payment to a single recipient. paths: /v1/payments: post: operationId: createPayment summary: Create a one-time payment intent tags: - One-Time Payments description: 'Signs an intent for one payment to one recipient. The intent is the whole record - P2Flux stores nothing and learns nothing later that the token itself does not carry. The `reference` is generated here and never accepted from the caller, deliberately: 32 random bytes carry no meaning, so a shop cannot push an order id, a customer id or an email into P2Flux by putting it in the reference. Keep your own `order -> reference` mapping. **Store the intent.** It is what verifies the payment later, and what recovers it if the callback is lost. An intent whose expiry has passed still verifies and still refunds - see `/v1/payments/verify`. Minimum amount: 0.01 USDC. Smaller amounts are refused as AMOUNT_OUT_OF_BOUNDS before an intent is created - the hosted verification pipeline cannot be run profitably below one cent. The splitter contract itself has no such floor; the minimum is a hosted-service boundary, applied only when an intent is issued.' requestBody: required: true content: application/json: schema: type: object additionalProperties: false properties: recipient: $ref: '#/components/schemas/Address' amount: $ref: '#/components/schemas/Amount' gas_payment_mode: type: string enum: - native - payment_token default: native description: 'How the buyer pays the chain''s network fee. Omit for `native`: the buyer sends the transaction and pays gas in the chain''s own currency, exactly as every integration written before this field existed. With `payment_token` the buyer needs none of that currency - P2Flux sends the transaction and the buyer reimburses the quoted network cost in the payment token, plus a flat gas-service fee (0.10 USDC). Ask `/v1/capabilities` first: an unsupported network or token is refused here with PAYMENT_TOKEN_GAS_UNSUPPORTED rather than after a customer has been sent to a checkout that cannot work.' required: - recipient - amount responses: '200': description: The signed intent and everything the checkout needs to build the transaction. content: application/json: schema: type: object required: - intent - reference - amount - expires_at - pay properties: intent: $ref: '#/components/schemas/Token' reference: $ref: '#/components/schemas/Bytes32' amount: $ref: '#/components/schemas/Amount' expires_at: type: integer description: Unix seconds. After this the intent cannot START a payment; it can still verify and refund one. pay: type: object description: Nothing secret - what a checkout needs to call the splitter. properties: chain_id: type: integer splitter: $ref: '#/components/schemas/Address' token: $ref: '#/components/schemas/Address' recipient: $ref: '#/components/schemas/Address' amount_units: $ref: '#/components/schemas/AmountUnits' reference: $ref: '#/components/schemas/Bytes32' fees: type: object description: What the merchant funds out of the amount, stated rather than implied. The buyer is debited the amount plus the quoted network fee in payment_token mode, and the amount alone natively. properties: payment_fee_units: type: string description: The percentage fee (1%), to the fee wallet. fixed_network_fee_units: type: string description: The fixed network fee, to the gas treasury. Zero outside payment_token mode. merchant_net_units: type: string description: amount - payment_fee - fixed_network_fee. examples: - intent: p2f1.k1.eyJ2IjoxfQ.c2lnbmF0dXJl reference: '0x4c2958875c223c9880b5b6262b0c069ad6727461c6443475ca2690b872e411ad' amount: '10.000000' expires_at: 1787139462 pay: chain_id: 8453 splitter: '0x5a3bd0945cd0c80b124870881de49a717d20e0d0' token: '0x833589fcd6edb6e08f4c7c32d4f71b54bda02913' recipient: '0xb4e43f3fBa5Add75395adAD366627E7d74141Fa9' amount_units: '10000000' reference: '0x4c2958875c223c9880b5b6262b0c069ad6727461c6443475ca2690b872e411ad' '400': description: 'Refused. `error` names which of: `AMOUNT_OUT_OF_BOUNDS`, `INVALID_REQUEST`' content: application/json: schema: $ref: '#/components/schemas/Error' '429': description: Too many requests from this IP. `retry-after` says when. content: application/json: schema: $ref: '#/components/schemas/Error' '500': description: 'Refused. `error` names which of: `INTERNAL_ERROR`' content: application/json: schema: $ref: '#/components/schemas/Error' /v1/payments/recover: post: operationId: recoverPayment summary: Find a payment whose transaction hash was lost tags: - One-Time Payments description: 'For when the checkout window dies between the wallet returning a hash and your server recording it: the money moved, the order looks unpaid, and there is nothing to reconcile against. Give this the intent and it finds the settling transaction on chain. You supply no hash and no hint. The match is bound to the exact payment the intent describes, so it can never return somebody else''s transaction. Pure reads and idempotent - safe to call on a schedule for any order in doubt, and it works long after the intent expired. **`found: false` with `PAYMENT_NOT_FOUND` is a statement about one block height, never a permanent verdict.** The contract does not enforce your intent''s expiry, so a slow wallet can still settle afterwards and a later call will find it. Stop polling on your own business rules - never on the strength of one not-found.' requestBody: required: true content: application/json: schema: type: object additionalProperties: false properties: intent: $ref: '#/components/schemas/Token' required: - intent responses: '200': description: Either the located transaction, or a not-found that names the block it was true at. content: application/json: schema: type: object required: - found properties: found: type: boolean valid: type: boolean tx_hash: $ref: '#/components/schemas/Bytes32' code: $ref: '#/components/schemas/ErrorCode' reference: $ref: '#/components/schemas/Bytes32' amount: $ref: '#/components/schemas/Amount' as_of_block: type: string description: Only on a miss. The head this answer was computed at. settled_outside_intent_contract: $ref: '#/components/schemas/Address' description: Only on a miss, for a `payment_token` intent whose terms were paid by hand through the original splitter. Not a settlement of this intent - verification holds an intent to the contract it names - but the merchant may hold the money, so the address is named for support. block_number: type: string description: 'On a found, valid payment: as `/v1/payments/verify` returns it.' block_hash: $ref: '#/components/schemas/Bytes32' description: On a found, valid payment. gas_payment_mode: type: string enum: - native - payment_token description: 'On a found, valid payment: how its network fee was paid.' accounting: $ref: '#/components/schemas/PaymentAccounting' description: 'On a found, valid payment: the same accounting `/v1/payments/verify` returns.' settlement_receipt: type: string description: 'On a found, valid payment: the same sealed receipt `/v1/payments/verify` issues, redeemable there.' examples: recovered: summary: Found and settled value: found: true valid: true tx_hash: '0xf9081f1db5f230d3eb481ec68bc8069b33a8409be4f0ea32942aee8d933188c6' amount: '0.250000' notFound: summary: Nothing settled AS OF this block - not a permanent answer value: found: false code: PAYMENT_NOT_FOUND as_of_block: '45689099' '400': description: 'Refused. `error` names which of: `INVALID_INTENT`' content: application/json: schema: $ref: '#/components/schemas/Error' '429': description: Limited per IP and per intent - recovery is a background job, not a poll loop. content: application/json: schema: $ref: '#/components/schemas/Error' '502': description: '`PAYMENT_RECOVERY_INCONSISTENT`: the contract says this settled and a search of every block the contract has existed for cannot find it. Abnormal - retry, and alert.' content: application/json: schema: $ref: '#/components/schemas/Error' '503': description: 'Refused. `error` names which of: `RECOVERY_UNAVAILABLE`, `RPC_BUSY`' content: application/json: schema: $ref: '#/components/schemas/Error' /v1/payments/resolve: post: operationId: resolvePayment summary: Authoritative terms for a checkout to display tags: - One-Time Payments description: 'What the buyer is about to pay, stated by the server rather than decoded in the browser. A tampered intent fails here, before anyone is shown - or asked to sign - anything. **This is the only endpoint that enforces expiry.** It also asks the contract whether the payment already settled, so a buyer is never sent to their wallet for a transaction that would revert.' requestBody: required: true content: application/json: schema: type: object additionalProperties: false properties: intent: $ref: '#/components/schemas/Token' required: - intent responses: '200': description: Terms to display and pay against. content: application/json: schema: type: object properties: recipient: $ref: '#/components/schemas/Address' amount: $ref: '#/components/schemas/Amount' amount_units: $ref: '#/components/schemas/AmountUnits' token: $ref: '#/components/schemas/Address' splitter: $ref: '#/components/schemas/Address' chain_id: type: integer reference: $ref: '#/components/schemas/Bytes32' expires_at: type: integer confirmations_required: type: - integer - 'null' description: 'How many confirmations the server''s finality policy requires before /v1/payments/verify answers valid. Advisory: a client may wait this depth out on its own RPC and then verify once, instead of polling verify while blocks accumulate. Null when the policy is tag-based (safe) - then poll verify as before. The server always applies the policy itself regardless of what the client does with this number.' '400': description: 'Refused. `error` names which of: `INVALID_INTENT`, `INTENT_EXPIRED`, `AMOUNT_OUT_OF_BOUNDS`' content: application/json: schema: $ref: '#/components/schemas/Error' '409': description: This reference has already settled on chain. Nothing is owed. content: application/json: schema: $ref: '#/components/schemas/Error' '502': description: 'Refused. `error` names which of: `RPC_ERROR`' content: application/json: schema: $ref: '#/components/schemas/Error' '503': description: 'Refused. `error` names which of: `RPC_BUSY`' content: application/json: schema: $ref: '#/components/schemas/Error' /v1/payments/verify: post: operationId: verifyPayment summary: Verify a payment against the chain tags: - One-Time Payments description: 'The trust boundary. The browser saying it paid is a claim; this makes it a fact by re-reading the receipt and checking every field against the signed intent. **Never grant access on a browser message alone.** A rejected payment is HTTP 200 with `valid: false` and a `code`, not an error status - so check `valid` explicitly. **`PAYMENT_CONFIRMING` means the customer has probably paid.** The transaction is on chain and not yet settled to the required depth. Poll the same hash; never ask for a second payment. **Expiry does not apply here.** An intent whose `expires_at` has passed still verifies: expiry stops a payment being started, not one that already happened. An integration that discards expired intents loses the ability to verify - or refund - a real payment, permanently.' requestBody: required: true content: application/json: schema: type: object additionalProperties: false properties: intent: $ref: '#/components/schemas/Token' tx_hash: $ref: '#/components/schemas/Bytes32' settlement_receipt: type: string maxLength: 2048 description: Optional. The sealed settlement receipt a previous CONFIRMED verify of this same payment returned. Presenting a valid one answers immediately without re-reading the chain; presenting a missing, expired, malformed or mismatched one silently falls back to the full verification. It is only ever honoured together with the exact intent and tx_hash it was issued for. required: - intent - tx_hash responses: '200': description: 'A verdict. `valid: true` settles the order; `valid: false` carries the reason.' content: application/json: schema: oneOf: - type: object required: - valid - tx_hash - reference - amount - block_number - block_hash - gas_payment_mode - accounting - settlement_receipt properties: valid: const: true tx_hash: $ref: '#/components/schemas/Bytes32' reference: $ref: '#/components/schemas/Bytes32' amount: $ref: '#/components/schemas/Amount' block_number: type: string block_hash: $ref: '#/components/schemas/Bytes32' gas_payment_mode: type: string enum: - native - payment_token description: 'How the network fee of this payment was paid: `native` by the buyer''s own ETH, `payment_token` out of the buyer''s USDC through a P2Flux-sponsored transaction. Always present on a valid verdict, on both the full verification and the settlement-receipt fast path.' accounting: $ref: '#/components/schemas/PaymentAccounting' description: Every unit of the payment as it settled on chain. Always present on a valid verdict - the settlement-receipt fast path returns the accounting sealed at the original verification, so the two paths answer identically. settlement_receipt: type: string description: Sealed, short-lived proof of this CONFIRMED verdict (token prefix p2paid1). Hand it back on a later verify of the same intent + tx_hash to get the same answer without another chain verification - e.g. from the buyer's browser to the merchant's server. Issued only with valid=true; a confirming or failed verification never carries one. - type: object required: - valid - code properties: valid: const: false code: $ref: '#/components/schemas/ErrorCode' examples: settled: summary: Paid and settled value: valid: true tx_hash: '0x2d6bbc112885a6976289e599f71003f7d310e7152ebd662c6221dffd3e0da708' reference: '0x4c2958875c223c9880b5b6262b0c069ad6727461c6443475ca2690b872e411ad' amount: '10.000000' block_number: '45688490' confirming: summary: Paid, still settling - poll the same hash, do not re-charge value: valid: false code: PAYMENT_CONFIRMING headers: Retry-After: description: 'Present on PAYMENT_CONFIRMING answers: seconds to wait before asking again. The server also remembers its last verdict per (intent, tx_hash) for a short window, so re-asking sooner returns the same answer from memory.' schema: type: string '429': description: 'Refused. `error` names which of: `RATE_LIMITED`' content: application/json: schema: $ref: '#/components/schemas/Error' /v1/capabilities: get: operationId: capabilities summary: What this deployment supports tags: - One-Time Payments description: 'Per network, token and operation: whether a buyer can pay without holding the chain''s native currency. Read it before offering the option. Architectural possibility is not support. A token that implements the right standards on a chain P2Flux has not deployed contracts to and tested is reported `false` here, and every request for it is refused deterministically. Also answers POST with an empty body, because an SDK transport may be POST-only.' responses: '200': description: Capabilities content: application/json: schema: $ref: '#/components/schemas/Capabilities' '429': description: '`RATE_LIMITED`. The answer changes only when the deployment does, so read it at start-up rather than per checkout.' content: application/json: schema: $ref: '#/components/schemas/Error' post: operationId: capabilitiesPost summary: What this deployment supports (POST form) tags: - One-Time Payments description: Identical to the GET. Exists so a host whose HTTP client only makes POST requests - the WooCommerce plugin injects `wp_remote_post` - can still ask. requestBody: required: false content: application/json: schema: type: object additionalProperties: false responses: '200': description: Capabilities content: application/json: schema: $ref: '#/components/schemas/Capabilities' '429': description: '`RATE_LIMITED`. The answer changes only when the deployment does, so read it at start-up rather than per checkout.' content: application/json: schema: $ref: '#/components/schemas/Error' /v1/payments/sponsor: post: operationId: sponsorPayment summary: Settle a payment the buyer funded with a signature tags: - One-Time Payments description: 'The buyer holds no native currency and has signed the token authorization the checkout showed them. P2Flux sends the transaction and takes the quoted network fee and the gas-service fee out of that same authorization - nothing is fronted on credit, and a transaction that fails moves no money at all. `status` of `CONFIRMING` means it is in flight. Ask `/v1/payments/verify` about the hash; do NOT call this again, because the buyer''s authorization may already be spent.' requestBody: required: true content: application/json: schema: type: object additionalProperties: false required: - intent - quote - payer - signature properties: intent: type: string quote: type: string description: The `p2gas1` token from `/v1/payments/resolve`. payer: type: string description: The wallet that signed. Must match the signature; the contract enforces it. signature: type: string description: 65-byte token authorization signature. responses: '200': description: Submitted or confirming content: application/json: schema: type: object properties: status: type: string enum: - SUBMITTED - CONFIRMING tx_hash: type: string reference: type: string network_fee_units: type: string fixed_network_fee_units: type: string buyer_total_units: type: string block_number: type: string native_gas_spent_wei: type: string description: What the transaction actually cost P2Flux, for reconciliation. The buyer paid the quote, not this. '400': description: 'Refused before anything was spent. `error` names which of: `INVALID_INTENT`, `INTENT_EXPIRED`, `INVALID_GAS_QUOTE`, `PAYMENT_TOKEN_GAS_UNSUPPORTED`, `INSUFFICIENT_PAYMENT_TOKEN_FOR_GAS`, `INSUFFICIENT_BALANCE`, `PAYMENT_TOKEN_GAS_UNAVAILABLE` (`cause: SPONSORSHIP_PAUSED` - too many sponsored transactions reverted on chain in the last hour; offer native gas and retry after `retry_after`).' content: application/json: schema: $ref: '#/components/schemas/Error' '409': description: '`PAYMENT_ALREADY_PROCESSED`, or `PAYMENT_TOKEN_GAS_QUOTE_EXPIRED` - requote and ask the buyer to sign again. A quote is also refused inside its last 20 seconds, because the token would reject it in the block that includes it.' content: application/json: schema: $ref: '#/components/schemas/Error' '429': description: '`RATE_LIMITED`: this payer has reached the day''s broadcasts for this operation (`SPONSORED_ATTEMPTS_PER_PAYER_24H`, default 5). Counted only when a transaction is actually sent; refusals before that are free.' content: application/json: schema: $ref: '#/components/schemas/Error' '502': description: '`SPONSORED_TRANSACTION_FAILED`: broadcast and refused on chain. Nothing moved; retry with a fresh quote.' content: application/json: schema: $ref: '#/components/schemas/Error' components: schemas: Error: type: object description: The uniform error envelope. `error` is the P2Flux code; `action` is what a merchant system should do about it, so integrations never hard-code that table themselves. Extra keys carry detail specific to the code (for example `retry_after`, `confirmations`, `as_of_block`). required: - error properties: error: $ref: '#/components/schemas/ErrorCode' action: $ref: '#/components/schemas/MerchantAction' additionalProperties: true examples: - error: INSUFFICIENT_BALANCE action: CUSTOMER_ACTION_REQUIRED AmountUnits: type: string pattern: ^\d{1,20}$ description: An integer count of micro-USDC (6 decimals), as a string. 2500000 is 2.50 USDC. Used wherever a decimal would invite a rounding error - notably refund amounts. examples: - '2500000' Address: type: string pattern: ^0x[0-9a-fA-F]{40}$ description: An EVM address. examples: - '0xb4e43f3fBa5Add75395adAD366627E7d74141Fa9' MerchantAction: type: string enum: - SUCCESS - WAIT - RETRY_LATER - CUSTOMER_ACTION_REQUIRED - STOP_SUBSCRIPTION - INVALID_REQUEST description: 'What to do about a result. - `SUCCESS` - done, nothing owed. - `WAIT` - **the money may already have moved.** The transaction exists and has not settled to the required depth. Ask again about the SAME transaction; never start another one. This is not a failure, and showing the customer an error here tells someone who has paid that they have not. - `RETRY_LATER` - nothing happened; the identical call is safe to repeat on your own schedule. - `CUSTOMER_ACTION_REQUIRED` - the customer must top up or re-approve. - `STOP_SUBSCRIPTION` - terminal; stop charging this subscription. - `INVALID_REQUEST` - permanent. Retrying returns the same answer forever; fix the request.' Bytes32: type: string pattern: ^0x[0-9a-f]{64}$ description: A 32-byte hex value, lowercase. Transaction hashes and references. examples: - '0x2d6bbc112885a6976289e599f71003f7d310e7152ebd662c6221dffd3e0da708' Capabilities: type: object properties: chain_id: type: integer network: type: string native_currency: type: string supported: type: boolean tokens: type: array items: type: object properties: address: type: string symbol: type: string decimals: type: integer gas_payment_modes: type: array items: type: string enum: - native - payment_token fixed_network_fee_units: type: string operations: type: object additionalProperties: type: boolean description: one_time_payment, subscription_signup, allowance_restore, allowance_removal. sponsor_contracts: type: object description: The contract that carries each sponsored operation, or null where the operation is not offered. A checkout compares the fee recipient of any sponsorship offer against this before asking the buyer to sign. additionalProperties: oneOf: - $ref: '#/components/schemas/Address' - type: 'null' zero_native_revoke: type: boolean description: 'Always false: revoking a recurring authorization is the payer''s own transaction, by the contract''s design. Removing an allowance stops collection and is a different act.' ErrorCode: type: string enum: - INVALID_INTENT - INTENT_EXPIRED - INVALID_REFERENCE - INVALID_SETUP_TOKEN - SETUP_TOKEN_EXPIRED - INVALID_CANCEL_TOKEN - CANCEL_TOKEN_EXPIRED - TERMS_MISMATCH - AMOUNT_OUT_OF_BOUNDS - PERIOD_OUT_OF_BOUNDS - PERMISSION_NOT_FOUND - TRANSACTION_NOT_FOUND - PERMISSION_REVOKED - ALREADY_CHARGED - NOT_DUE - SUBSCRIPTION_EXPIRED - INVALID_SIGNATURE - REFUND_CONFIRMING - INVALID_REFUND_TOKEN - REFUND_TOKEN_EXPIRED - REFUND_AMOUNT_INVALID - REFUND_WRONG_MERCHANT - REFUND_TRANSACTION_MISMATCH - REFUND_ORIGINAL_PAYMENT_INVALID - SIGNATURE_VALIDATION_TOO_EXPENSIVE - UNSUPPORTED_SIGNATURE_FORMAT - PAYMENT_ALREADY_PROCESSED - PAYMENT_NOT_FOUND - PAYMENT_RECOVERY_INCONSISTENT - RECOVERY_UNAVAILABLE - WRONG_SPENDER - WRONG_TOKEN - INVALID_EXTRA_DATA - GAS_FEE_TOO_HIGH - INSUFFICIENT_ALLOWANCE - INSUFFICIENT_BALANCE - INVALID_SUBSCRIPTION - RPC_ERROR - RELAYER_ERROR - INTERNAL_ERROR - TRANSACTION_REVERTED - INVALID_REQUEST - RATE_LIMITED - CONCURRENCY_LIMIT - GAS_TOO_HIGH - GAS_QUOTE_UNAVAILABLE - PAYMENT_CONFIRMING - RELAYER_TX_COST_TOO_HIGH - RELAYER_BUDGET_EXCEEDED - RELAYER_NOT_READY - RPC_BUSY - PAYMENT_TOKEN_GAS_UNSUPPORTED - PAYMENT_TOKEN_GAS_UNAVAILABLE - PAYMENT_TOKEN_GAS_QUOTE_EXPIRED - PAYMENT_TOKEN_GAS_LIMIT_EXCEEDED - INVALID_GAS_QUOTE - INSUFFICIENT_PAYMENT_TOKEN_FOR_GAS - SPONSORED_TRANSACTION_FAILED - SPONSORED_PERMIT_FAILED - SPONSORSHIP_CONFIRMING description: Every code this API can return. Stable identifiers - branch on these, never on the human-readable text or the HTTP status alone. PaymentAccounting: type: object description: Every unit of a settled payment, read from the chain rather than recomputed from what someone said they would charge. properties: payment_units: type: string payment_fee_units: type: string description: The P2Flux percentage fee, taken out of the amount. The merchant's net is smaller by this. network_fee_units: type: string description: The quoted network fee the buyer accepted. Zero in native mode. fixed_network_fee_units: type: string description: 'The fixed network fee on a one-time payment. MERCHANT-funded: it comes out of the amount, like the percentage fee, and is paid to the gas treasury - the same arrangement a subscription has. It is never added to what the buyer is debited.' merchant_net_units: type: string buyer_total_units: type: string payer: type: string Amount: type: string pattern: ^\d{1,12}(\.\d{1,6})?$ description: 'A USDC amount as a decimal string, up to 6 decimal places. Never a JSON number: binary floating point cannot represent most decimal prices exactly, and this is money.' examples: - '10.00' - '0.250000' Token: type: string maxLength: 8192 description: 'A signed P2Flux capability: payment intent (p2f1.), setup token (p2setup2.), subscription capability (p2s2.), cancel token (p2cancel1.) or refund token (p2refund1.). Opaque to the caller and unforgeable - the signature is what authorises the call. Treat it as a bearer secret: keep it server-side, never in a URL query or a log.' externalDocs: description: Guides, flows and worked examples url: https://p2flux.com/docs/