PFlux DOCS
GitHub Integration enquiries Start integrating
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.
The buyer has no ETH and cannot send the transaction
What happened — The default mode has the buyer send the payment themselves, which needs ETH for the Base network fee. A wallet holding only USDC cannot.
Did funds move? No. Nothing was signed or sent.
What to do — Create the payment with gas_payment_mode: 'payment_token' — the buyer signs instead, P2Flux submits the transaction, and the network fee is paid in USDC out of the same authorization. Ask GET /v1/capabilities first: it reports which networks, tokens and operations support it, and an unsupported request is refused with PAYMENT_TOKEN_GAS_UNSUPPORTED before the buyer sees anything. Subscription signup and allowance repair take the same path automatically in the hosted checkout.
RATE_LIMITED when the network fee is paid in USDC
What happened — Either this wallet's sponsored transactions kept failing simulation this hour, or it hit the emergency ceiling on sponsored broadcasts (cause: PAYER_EMERGENCY_LIMIT) - a sanity limit of hundreds an hour that ordinary use never reaches. Successful sponsored transactions are not limited.
Did funds move? No. A refused attempt costs nothing and consumes no allowance.
What to do — Honour retry_after. The hosted checkout already tells the buyer to try later, and offers the ETH path where the wallet can take it. Subscription collections are not sponsored transactions and are never counted against this.
INTENT_EXPIRED
What happened — the intent is older than its one-hour life. Expiry stops an intent being used to start a payment; it does not affect reading history.
Did funds move? No, and it cannot hide one either.
What to do — create a new payment and send the buyer to the new checkout URL. If you suspect a payment was made against the old intent, verify or recover it — neither call is blocked by expiry.
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 but no transaction hash, and verification normally needs one.
Did funds move? Possibly yes, and if so the money is already in your wallet.
What to do — call /v1/payments/recover with the intent alone. It finds the settling transaction and verifies it. A found: false answer is about that block only — a late payment can still land, so ask again rather than marking the order unpaid, and never create a second intent.

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 — one-time payments have a 0.01 USDC minimum, a recurring 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 — treat the period as collected. No transaction hash is returned because P2Flux stores nothing; to attribute, audit or refund the payment, /v1/charges/recover with the exact period_index finds the settlement from the contract’s own log. Do not charge again.
INSUFFICIENT_ALLOWANCE, but the customer wants to continue
What happened — the ERC-20 allowance to the recurring contract no longer covers the charge. The authorization the customer signed is intact.
Did funds move? No.
What to do — do not create a new subscription. /v1/allowances/restore/session issues a narrow session for #/approve/; the customer approves once, and the same capability charges again.
recoverCharge answers PAYMENT_NOT_FOUND after ALREADY_CHARGED
What happened — the contract’s period marker has advanced past the period you asked about, but no settlement log exists for that period. Most often the period was skipped and a later one collected - the marker is monotonic and says nothing about earlier periods. Occasionally a clock disagreement put the charge in the neighbouring period.
Did funds move? Not for this period. Check the neighbouring period index, then /v1/subscriptions/status.
What to do — never mark the order paid on the marker alone. Ask about the period the charge result actually named; if nothing matches, a person should look before anyone is told they paid.
RECOVERY_UNAVAILABLE on recoverCharge
What happened — the bounded search could not finish - typically an RPC provider without historical state and a billing period too long to scan within budget.
Did funds move? Unknown from this call.
What to do — retry later, and persist the time you attempted each charge so it can be passed as a hint: with one, the search starts where the settlement almost certainly is.
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.

Refunds

REFUND_CONFIRMING
What happened — the refund transfer was sent and has not settled yet.
Did funds move? Yes. It left your wallet.
What to do — verify again with the same refund transaction hash. Do not send a second refund.
REFUND_AMOUNT_INVALID
What happened — the amount is not a positive whole number of base units, or is larger than the original payment. Amounts here are micro-USDC — 5 USDC is "5000000", not "5.00".
Did funds move? No.
What to do — send the amount in base units and keep it at or below the original.
REFUND_WRONG_MERCHANT
What happened — the refund was sent from a wallet other than the one that received the original payment.
Did funds move? Something moved, but it does not count as a refund of this payment.
What to do — send it from the wallet that was paid. P2Flux derives the expected sender from the settlement and will not accept another.
The same payment was refunded twice
What happened — nothing in the API stopped it. P2Flux keeps no global refund ledger, so it cannot know a payment was already refunded — it only checks that each refund matches the original payment, which both of them did.
Did funds move? Yes, twice. Both were real wallet-to-wallet transfers out of your wallet.
What to do — enforce refund idempotency where your integration lives. An official P2Flux integration keeps its own refund record and already prevents this; anything built on the SDKs or the API directly must persist refund state and block duplicate sends itself. See Refund idempotency.

Service-side

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.
Discuss an integration