PFlux DOCS
GitHub Integration enquiries Start integrating
PAYMENTS/ONE-TIME PAYMENTS

One-time payments

A signed intent, a transaction the buyer sends from their own wallet, and a server-side verification that decides whether you fulfil.

ENDPOINTS3
ASSETUSDC
FEE1%
INTENT TTL1 hour

A one-time payment is a signed intent, a transaction the buyer sends, and a server-side verification. P2Flux stores none of it: the intent carries its own contents, and verification re-reads the chain.

Create a payment

POST/v1/payments

Mint a payment intent. Unknown fields are rejected outright.

FIELDTYPEREQUIREDNOTES
recipientstringYesAddress that receives the payment. 0x + 40 hex characters.
amountstringYesDecimal USDC, e.g. "100.00". Up to 6 decimal places and 12 integer digits.

There is deliberately no reference field. P2Flux generates it, so order ids, customer ids and emails cannot be pushed into the payment layer even by accident.

RESPONSEWHAT IT IS
intentSigned token. Everything else is derived from it; this is what the checkout needs.
reference32 random bytes. Your handle on this payment — store it against the order.
amountThe amount, formatted to six decimals.
expires_atUnix seconds. One hour after creation by default.
payChain id, splitter, token, recipient, amount in base units and the reference — the parameters of the transaction.

Resolve

POST/v1/payments/resolve

Authoritative terms for a checkout to display.

The hosted checkout calls this; you only need it if you are building your own payment page. It exists because a browser must never trust what it can decode out of a token itself — a tampered intent has to fail here, before anyone is shown or asked to sign anything.

It also asks the contract whether the payment is still payable, so a buyer is never sent to their wallet for a transaction that would revert.

Present the payment

Open the hosted checkout at <checkout>/#/pay/<intent>. The intent travels in the URL fragment — the part after the #. Browsers do not put a fragment in the HTTP request or in the Referer header, so it does not reach the server or its access logs, and the checkout clears it from the address bar once it has read it. It is still a value present in the page: treat it as a secret with respect to anything else running there, and keep it out of client-side analytics and error reporting.

The checkout reports back to the opening page by postMessage. Check the origin, and treat the message as a prompt to verify rather than as proof of payment.

MESSAGEDIRECTIONPAYLOAD
p2flux.readycheckout → your page
p2flux.helloyour page → checkoutAnswer to p2flux.ready.
p2flux.payment.completedcheckout → your pagetx_hash, reference

Verify

POST/v1/payments/verify

Server-side proof that a payment landed. Never skip it.

Takes the intent and the tx_hash. Everything is checked against the receipt: the settlement id derived from token, recipient, amount and reference; the recipient and token actually paid; that net plus fee equals the signed amount; that the fee is exactly 1%; that the USDC transfers happened; and that the transaction is three confirmations deep.

ONE RESPONSE SHAPE

This endpoint does not signal outcomes with HTTP status codes. Every answer it reaches — success or not — is HTTP 200, carrying either the success object or { "valid": false, "code": … }. Branch on valid, not on the status code. Only transport-level failures differ: a malformed body is 400, too many requests is 429, and an unexpected server fault is 500.

A successful verification returns valid: true with tx_hash, reference, amount, block_number and block_hash. Anything else returns valid: false and one of these codes:

PAYMENT_CONFIRMINGNot yet provable. Either the transaction is not deep enough, or no receipt has been seen for that hash at all. Keep the same hash and verify again — see below.
TRANSACTION_REVERTEDThe transaction was mined and failed. Nothing moved.
INVALID_REFERENCEThe receipt does not contain this payment.
TERMS_MISMATCHRecipient, amount or fee in the receipt does not match the intent.
WRONG_TOKENThe expected USDC movements are not in the receipt.
PAYMENT_ALREADY_PROCESSEDThe contract has already settled this exact payment.
INTENT_EXPIREDThe intent is past its expiry and verification is refused — read the warning under Expiry before letting this happen.
INVALID_INTENTThe intent is malformed, forged or for another deployment.

What PAYMENT_CONFIRMING means

It means the payment cannot yet be proved — not that it succeeded. One code covers two situations the API cannot tell apart from the outside:

  • A transaction exists and is mined, but is not yet three confirmations deep.
  • No receipt has been seen for that hash at all — it may be pending, the node answering may not have caught up, or no such transaction may ever have been broadcast.

Absence of a receipt is deliberately treated as “keep waiting” rather than “nothing happened”, because telling someone who has just paid that their transaction does not exist is the worse of the two mistakes. That is precisely why the code must not be read as confirmation of payment.

THE RULE

Do not fulfil until you get valid: true. While the answer is PAYMENT_CONFIRMING, keep the same tx_hash and verify again — do not send the buyer back to checkout, and do not create a second intent. A new intent carries a new reference, which the contract will settle a second time.

Expiry

An intent expires one hour after creation by default. After that, resolve and verify both reject it with INTENT_EXPIRED. Create the intent when the buyer is ready to pay, not when the cart is built.

VERIFY BEFORE THE INTENT EXPIRES

Expiry is checked against the intent, not against when the payment happened. A transaction that confirms legitimately but is only verified after the hour is up comes back INTENT_EXPIRED — even though the money moved. Verify as soon as you have a transaction hash and keep retrying inside the window; do not queue verification for a nightly job. If you are left holding an expired intent for a payment you believe succeeded, the settlement is still on chain and can be read from the transaction directly, but no P2Flux call will verify it after expiry.

Replay protection

The contract records each settlement by an id derived from the token, recipient, amount and reference, and refuses a second one. Because the reference is fresh per intent, the same buyer can re-order the same item at the same price as often as they like — but one receipt can never be used to verify two orders.

If the browser callback never arrives

The p2flux.payment.completed message is how you normally learn the transaction hash. It is a browser event, so it can be lost: the buyer closes the popup, the tab crashes, the phone locks, the connection drops between signing and the message being delivered.

There is no P2Flux call that looks a payment up by its reference, and there are no webhooks. The API is stateless by design and holds nothing to look up. So the hash is the one piece of the flow you cannot ask us to recover.

WHAT THIS MEANS IN PRACTICE

A payment whose callback is lost is not lost money — it settled on chain, to your wallet, and it is visible there. What is lost is the automatic link between that settlement and your order.

Two things reduce the exposure, and both are your side of the integration:

  • Persist the reference and the intent against the order the moment you create the payment, before the buyer is sent anywhere. Without them there is nothing to reconcile with.
  • Watch the chain for the settlement yourself if you need certainty. The splitter emits a settlement event carrying an id derived from token, recipient, amount and reference, so a payment can be matched to an order from public chain data alone. This is ordinary blockchain reading, not a P2Flux feature.

Do not respond to a missing callback by creating a second intent. The replay guard below protects the first reference, not a new one, so a second intent is a second payment the buyer can genuinely be charged for.

Refunds

Not currently available. There is no refund endpoint, no refund path in the contracts, and no refund method in either SDK.

Two different things are worth separating here.

WHAT IT WOULD BEAVAILABLE?
Reversing the paymentUndoing a settled blockchain transaction so the money returns automatically.No — and not something P2Flux could add. A confirmed transaction is final, and the funds are in your wallet rather than in any P2Flux account we could debit.
Sending the money backYou making an ordinary transfer from the wallet that received the payment, back to the customer.Yes, but it is your transaction, not a P2Flux feature. You decide when, you pay its gas, and P2Flux is not involved.

So a merchant can absolutely refund a customer. What does not exist is a P2Flux call that does it for you, or any record on our side that a refund happened.

Something more than a standard integration?
Marketplace flows, platform billing and custom settlement logic.
Discuss an integration