DOCS MENU+
REFERENCE/TROUBLESHOOTING
Troubleshooting
The failures you will actually meet, and the only question that matters first: did the money move?
Every entry answers the same three questions: what happened, whether money moved, and what to do about it.
How to read a failure
The action on a charge result already classifies the failure. This page is for the cases where you want to know what is actually going on underneath.
One-time payments
PAYMENT_CONFIRMING on verify
What happened — the payment cannot yet be proved. Either the transaction is mined but not three confirmations deep, or no receipt has been seen for that hash at all — which may mean it is still pending, that the node answering has not caught up, or that nothing was ever broadcast. The API cannot tell those apart.
Did funds move? Unknown. Do not assume either way. It is not evidence of payment and not evidence against it.
What to do — keep the same transaction hash and verify again until you get valid: true or a terminal code. Show “confirming” rather than “paid”. Do not fulfil, do not send the buyer back to checkout, and above all do not create a second intent — that is how someone pays twice.
INTENT_EXPIRED before the buyer paid
What happened — the intent is older than its one-hour life and nothing was sent against it.
Did funds move? No.
What to do — create a new payment and send the buyer to the new checkout URL.
INTENT_EXPIRED when a payment may already have gone through
What happened — expiry is checked against the intent, not against when the payment happened — so a transaction that confirmed legitimately can still return this if you verify after the hour is up.
Did funds move? Possibly yes. The intent expiring says nothing about the transaction.
What to do — do not create a new payment on the strength of this code alone. The settlement, if it happened, is on chain against the reference you stored, and can be read from the transaction directly. Prevent it next time by verifying as soon as you have a hash rather than in a later batch.
PAYMENT_ALREADY_PROCESSED
What happened — the contract has already settled this exact token, recipient, amount and reference.
Did funds move? Yes — once. This is the replay guard working.
What to do — find the original payment by its reference and reconcile against that. Do not create a second intent for the same order expecting a second payment.
INVALID_REFERENCE or TERMS_MISMATCH on verify
What happened — the receipt does not contain the payment this intent describes, or the recipient, amount or fee do not match what was signed.
Did funds move? Something may have moved, but not what this order asked for.
What to do — do not fulfil. Investigate the transaction hash by hand.
TRANSACTION_REVERTED
What happened — the transaction failed on chain.
Did funds move? No. A reverted transaction moves nothing (the buyer still paid its gas).
What to do — send the buyer back to checkout. Common causes are an insufficient USDC balance or an allowance that was changed mid-flow.
Subscription setup
SETUP_TOKEN_EXPIRED
What happened — the setup token is older than 24 hours.
Did funds move? No.
What to do — create the subscription terms again and send a fresh checkout link.
INVALID_SIGNATURE
What happened — the EIP-712 signature does not verify for that payer address.
Did funds move? No.
What to do — check that the connected wallet is the payer being submitted, and that the chain id and contract in the typed data match the deployment.
UNSUPPORTED_SIGNATURE_FORMAT
What happened — a smart-contract wallet signed before the account was deployed on chain.
Did funds move? No.
What to do — have the customer deploy their account first. A recurring authorization is replayed for months, so the account has to exist for the whole of that.
AMOUNT_OUT_OF_BOUNDS or PERIOD_OUT_OF_BOUNDS
What happened — the price or the interval is outside what this deployment accepts — the amount must exceed the fees, and the period sits between one hour and 366 days by default.
Did funds move? No.
What to do — fix the plan. This is configuration, not an outage: retrying identical terms returns the same answer forever.
Renewals
INSUFFICIENT_BALANCE
What happened — the customer’s wallet does not hold enough USDC.
Did funds move? No.
What to do — tell the customer. Charging again before they top up fails the same way. Your dunning schedule is your own policy.
INSUFFICIENT_ALLOWANCE
What happened — the ERC-20 allowance to the recurring contract was removed, or never granted.
Did funds move? No.
What to do — this is usually a deliberate cancellation by the customer. Ask them to re-authorize if they meant to continue.
PERMISSION_REVOKED
What happened — the authorization was revoked on chain, or the charge falls outside the signed window. Permanent.
Did funds move? No.
What to do — stop billing and close the subscription locally.
CONFIRMING
What happened — the renewal transaction was broadcast and has not settled yet.
Did funds move? Yes. The money has moved.
What to do — leave the period open, change nothing, and call again later. Do not treat it as a failure and do not start a second charge.
ALREADY_CHARGED
What happened — this period was already collected, usually by an earlier call of yours that timed out.
Did funds move? Yes — once.
What to do — mark the renewal paid. This is a success, not an error. No transaction hash is returned because P2Flux stores nothing; /v1/subscriptions/status is the reconciliation read.
NOT_DUE
What happened — you called before the billing period opened.
Did funds move? No.
What to do — use next_period_at from the response to schedule the next attempt.
The browser callback never arrived
What happened — the buyer confirmed, but p2flux.payment.completed never reached your page — the popup was closed, the tab crashed, the connection dropped. You have the intent and the reference but no transaction hash, and verification needs the hash.
Did funds move? Possibly yes, and if so the money is already in your wallet.
What to do — there is no P2Flux call that looks a payment up by reference, and no webhooks, so this cannot be recovered through the API. The settlement is public chain data: the splitter emits an event carrying an id derived from token, recipient, amount and reference, so you can match it to the order yourself. Never respond by creating a second intent — the replay guard protects the first reference, not a new one. See lost callbacks.
Service-side
RATE_LIMITED / CONCURRENCY_LIMIT / RPC_BUSY
What happened — the request was refused before anything was broadcast — per-caller limits, per-subscription pacing, or the service at capacity.
Did funds move? No. Nothing was spent and the subscription is untouched.
What to do — honour retry_after and try the identical call again. Note that /v1/charges is limited to six attempts per minute per subscription.
GAS_TOO_HIGH / GAS_QUOTE_UNAVAILABLE
What happened — the network cost of the renewal quoted above what the customer authorized — or above the 0.05 USDC cap the contract enforces — or the price feed could not be trusted, leaving no defensible amount to debit.
Did funds move? No. Nothing is broadcast, so nothing is spent and the billing period stays open.
What to do — retry later; GAS_TOO_HIGH suggests waiting for cheaper conditions rather than retrying immediately. Neither is the customer’s doing and neither is something they can fix.
RELAYER_* (TX_COST_TOO_HIGH, BUDGET_EXCEEDED, NOT_READY)
What happened — an operator-side limit stopped the transaction before it was signed.
Did funds move? No.
What to do — retry later. If it persists, the deployment operator needs to look at relayer funding and budgets.
Something more than a standard integration?
Marketplace flows, platform billing and custom settlement logic.