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.
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
Mint a payment intent. Unknown fields are rejected outright.
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.
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.
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.
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:
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.
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.
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.
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.
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.