Recurring payments
The customer signs an authorization once. Your system decides when a renewal is due and asks P2Flux to execute it; the contract enforces one charge per period.
P2Flux has no scheduler and no database. It does not know when a renewal is due, who the customer is, or what happens after a failure. It executes one charge when you ask, and the contract enforces one charge per period.
How it works
The customer signs an EIP-712 authorization once, with an ordinary EVM wallet, naming exactly what may be taken: payer, recipient, token, amount, period, start, end and a salt. The contract enforces those terms. The amount cannot be raised, the period cannot be shortened, and the recipient cannot be changed after signing. A charge outside the signed window, or a second charge inside one period, reverts.
The customer also grants an ordinary ERC-20 allowance to the recurring contract — that is the switch they can flip to stop everything, from their own wallet, without asking anyone. Unlimited by default, because a subscription with no end and a bounded allowance is one that stops one day; you can bound it at setup with allowance, and the resolve response then states the exact number the checkout asks the wallet to approve (approve_units).
1. Create the terms
Technical terms in, setup token out. No customer or product fields exist here.
The response returns the setup_token, its expires_at (24 hours by default), the chain_id, the recurring contract, the formatted amount, and a salt.
Store the salt with the pending order. It is what distinguishes two setups whose price and period are identical — so when a capability comes back, you can prove it belongs to this order and not to someone else’s cheaper plan.
2. Customer authorizes
Send the customer to <checkout>/#/subscribe/<setup_token>. The checkout resolves the token server-side, shows the terms, takes the token approval if one is needed, and collects the EIP-712 signature.
Those are two different things. The approval is an on-chain transaction from the customer’s wallet, so it costs network gas in ETH; it is the only point in the subscription where they need native currency. The signature is not a transaction — it is signed in the wallet, costs nothing and touches no chain.
The signature is exchanged for a capability — the p2s2 token — which is what your system stores and charges against later. The checkout posts it to the opening page as p2flux.subscription.created with the capability in subscription.
Call status and compare the echoed terms — payer, recipient, token, amount, period, start, end, salt — against what you actually sold. A capability can be cryptographically valid and still be the wrong one.
If you build your own page instead of using the hosted checkout, the same two calls are public: /v1/subscriptions/resolve returns the terms and the EIP-712 scaffold, and /v1/subscriptions/finalize exchanges setup_token, payer and signature for the capability.
Answer the checkout: the first charge verdict
After p2flux.subscription.created your page attempts the first charge server-side. The popup is still open, telling the customer the seller is collecting - and it stays honest only if you answer it. Two messages, both sent with postMessage to the checkout origin:
Send p2flux.activation_failed only for failures the customer or you must act on — the charge() result’s action already makes the split: CUSTOMER_ACTION_REQUIRED (INSUFFICIENT_BALANCE, INSUFFICIENT_ALLOWANCE) and STOP_SUBSCRIPTION (PERMISSION_REVOKED, SUBSCRIPTION_EXPIRED) are worth reporting, plus your own configuration refusals. For RETRY_LATER and WAIT send nothing: your renewal job finishes the activation, and the checkout keeps saying so.
Send the CODE, never a sentence. The checkout composes what the customer reads from a fixed set of identifiers and degrades unknown ones to a safe generic - a merchant page can name a failure, but it cannot write on pay.p2flux.com. Codes it distinguishes: INSUFFICIENT_BALANCE, INSUFFICIENT_ALLOWANCE, PERMISSION_REVOKED, SUBSCRIPTION_EXPIRED, and your configuration refusals as a seller-issue message.
If your page dies after the handshake and the capability never reaches you, nothing chargeable is orphaned - nobody holds it. Send the customer back to the same subscribe link: every term the on-chain id derives from, start and salt included, is fixed in the setup token, so signing it again reproduces the same subscription rather than a second one. While it waits, the checkout offers the customer Copy subscription ID - the public on-chain id, a support reference that can charge nothing. It never offers the capability.
3. Charge each period
Attempt this period’s payment. Safe to retry.
Call it from your existing renewal job. There is no amount and no recipient in the request — both come from the permission the customer signed.
P2Flux submits the transaction and pays its ETH gas up front. That cost is converted to USDC and added on top of the amount, so the contract debits amount + gas reimbursement — the customer reimburses it in the stablecoin they already hold and needs no ETH at renewal time. Two ceilings apply and the lower wins: what the customer signed for, and a hard 0.05 USDC cap in the contract.
The two P2Flux fees are separate money and come out of the amount instead, so the merchant funds them. Full arithmetic in Fees & gas.
Charge results
A charge answers with a status and an action. The action is what your system should do about it, so you never have to hard-code the code table yourself.
The contract allows one charge per billing period, so repeating a call after a timeout or a crash returns ALREADY_CHARGED rather than charging twice. There is no idempotency key to manage — the period is the key.
The retry schedule is yours. P2Flux reports the technical result; how long you wait, and whether you dun the customer, is business policy.
Recover a lost charge
The transaction that charged one exact period.
ALREADY_CHARGED proves a period was collected and names no transaction: P2Flux stores nothing, so the hash lives only in the contract’s log. Without it a paid period cannot be attributed to an order, audited, or refunded - both refund calls start from the original settlement. This finds it.
A settlement is returned only when the contract’s own SubscriptionCharged log names this subscription AND this period, and its payer, recipient and amount match the signed authorization. The contract’s period marker is a gate, never evidence: it is monotonic, so a marker of 7 says period 6 was collected and says nothing about period 5. Skipped periods are ordinary - there is no catch-up billing - and a skipped period answers PAYMENT_NOT_FOUND, never a later period’s transaction. A hint can never turn a miss into a hit.
Long periods. A charge can land anywhere inside its period, and a period can be 366 days - about 15.8 million blocks on Base, far more than one request may scan. The API bisects the contract’s marker over historical state and reads one log range at the crossing block, so a yearly period costs about the same as an hourly one. On an RPC provider without historical state it falls back to scanning the period window under a bounded budget, and when even that cannot finish it answers RECOVERY_UNAVAILABLE - never a wrong found: true.
Reconcile with status
Current state, read straight from the chain.
No stored state is consulted, because there is none. Use it to reconcile after downtime and to check the signed terms before activating anything.
Restore an allowance
A narrow session for repairing one subscription’s allowance.
INSUFFICIENT_ALLOWANCE is not a dead subscription. The authorization the customer signed is intact and you can still collect; what ran short is the ERC-20 allowance to the recurring contract, and the fix is one approve() from the customer’s own wallet - no new signature, no new subscription.
Exchange the capability for an approve_token and open <checkout>/#/approve/<approve_token>. The checkout connects the payer’s wallet (and refuses any other), reads the allowance, asks for exactly one approval against the server-named spender, and posts p2flux.allowance.restored with the tx_hash - or already_sufficient: true when nothing needed doing. Then charge the same capability again.
A customer whose wallet holds no ETH cannot send that approval — and the customer whose subscription needs repairing is exactly the person least likely to go and buy some first. The same screen handles it: it reads their native balance, and where there is none it asks P2Flux to price the transaction, shows that price, and takes two signatures instead. P2Flux sends one call that collects the quoted fee and sets the allowance together — either both happen or neither does — and the customer pays no additional fee for it, because a subscription already pays its fixed network fee on every collection. Nothing changes on your side: you post the same session and charge the same capability afterwards.
It names the payer, the spender, the token and how much the next charge pulls, and carries no authorization struct and no signature. It cannot charge a subscription, cannot revoke an authorization, cannot prepare a refund, and cannot become a p2s2 capability. It lives fifteen minutes. /v1/allowances/restore/resolve is the browser-side read the checkout uses to show the terms.
Cancel and revoke
Cancellation belongs to the customer. P2Flux cannot revoke a wallet’s authority and does not pretend to — the API returns unsigned calldata, and the customer’s own wallet sends it.
The cancel token exists so the capability never has to reach a browser: it carries the fields needed to build revoke() and not the customer’s signature, so it cannot be charged with. Open <checkout>/#/cancel/<cancel_token> to give customers self-service cancellation.
Failed renewals
Nothing retries on its own. A failed charge spent no money and changed nothing on chain, so the correct response is entirely yours to choose — the action field tells you which kind of failure it was.
Refunds
Supported, per charge rather than per subscription. You identify the renewal with the capability, the transaction that charged it and its period_index, and P2Flux derives who to pay back and how much from the settlement itself.
Refunding a renewal does not cancel the subscription: it stays active and the next renewal is still due. Stopping future charges is the customer’s action, described above. Full detail in Refunds.