PFlux DOCS
GitHub Integration enquiries Start integrating
PAYMENTS/REFUNDS

Refunds

A refund is a transfer from your wallet back to the payer. P2Flux works out who and how much, and verifies it afterwards — it never holds the money in between.

ENDPOINTS3
WHO SENDS ITThe merchant
PARTIALSupported
TOKEN LIFE15 minutes
P2FLUX NEVER HOLDS THE MONEY

A refund is a transfer from the merchant’s wallet to the payer’s. P2Flux works out who and how much from the original settlement, locks those terms so they cannot be edited in a browser, and verifies afterwards that the transfer happened. It never takes custody and never sends the transaction itself.

How a refund works

A completed blockchain payment cannot be reversed — that is true of the chain, not a P2Flux limitation. So a refund is a second, separate payment going the other way, and the endpoints here exist to make that safe rather than to move money.

STEPWHOWHAT HAPPENS
PrepareYour serverP2Flux reads the original settlement from the chain and returns who gets the money back, how much is allowed, and a short-lived token carrying those terms.
SendThe merchant’s walletAn ordinary token transfer to the payer. Your wallet, your gas, your signature.
VerifyYour serverP2Flux checks the transfer against the original settlement and confirms it is settled.
WHY THE PAYER IS NOT A PARAMETER

Neither endpoint accepts a destination address, a merchant or a maximum from the caller. All three are derived from the original settlement on chain. If a caller could name the destination, a refund button would be a withdrawal form.

1. Lock the terms

POST/v1/refunds/prepare

Work out the refund from the original settlement and lock it.

FIELDTYPEREQUIREDNOTES
tx_hashstringYesThe transaction that settled the original payment.
amountstringYesRefund amount in base units — micro-USDC, so "5000000" is 5 USDC. Decimals are refused: this is where partial refunds would otherwise acquire rounding bugs.
intentstringOne-timeThe intent of the payment being refunded.
subscriptionstringRecurringThe capability, instead of an intent.
period_indexintegerRecurringWhich renewal is being refunded.

Exactly one of intent or subscription identifies the settlement, and either way the transaction hash is required: the capability says what was authorized, the receipt says what actually happened.

The response returns refund_token, chain_id, token, merchant, payer, the original_amount and the refund_amount (each with a base-unit twin), and expires_at.

DO NOT STORE THE TOKEN

It lives fifteen minutes and exists only to carry the terms to a browser. Reconciliation later uses /v1/refunds/verify with the original settlement, which needs no token at all.

2. The merchant sends it

Open the hosted checkout at <checkout>/#/refund/<refund_token> and connect the merchant wallet. The checkout shows the locked terms and sends the transfer.

It reports back to the opening page by postMessage, twice: p2flux.refund.sent when the transaction is broadcast, and p2flux.refund.confirmed once it has settled. Both carry tx_hash.

You can also skip the checkout entirely. Nothing here is privileged: a refund is a plain token transfer of refund_amount to payer, so a treasury tool or a script that already holds the merchant key can send it and you verify the result the same way.

3. Verify it landed

POST/v1/refunds/verify

Confirm the refund happened, and that it is settled.

Takes the original settlement again — tx_hash plus intent or subscription and period_index — with refund_amount in base units and refund_tx_hash. Deliberately not the prepare token, so you can reconcile hours or days later, after a crash or a support ticket, without having kept a fifteen-minute token alive.

A settled refund returns status: "REFUNDED" with refund_tx_hash, refund_amount, original_amount, payer, merchant and block_number.

REFUNDEDVerified against the original settlement and confirmed on chain.
REFUND_CONFIRMINGThe transfer is not settled yet. Ask again with the same hash.
REFUND_TRANSACTION_MISMATCHThat transaction is not a refund of this payment — wrong amount, wrong destination or wrong token.
REFUND_WRONG_MERCHANTThe refund was not sent from the wallet that received the original payment.

Refunding a renewal

Refunds are per charge, never per subscription. Identify the renewal with the capability, the transaction that charged it, and its period_index. Refunding one renewal does nothing to the subscription itself: it stays active and the next renewal will still be due.

The refundable amount is what the customer was charged for that period, excluding the network cost they reimbursed. To stop future charges as well, the customer cancels — see Cancel and revoke.

Refund idempotency

YOUR INTEGRATION MUST ENFORCE REFUND IDEMPOTENCY

A refund is a real wallet-to-wallet USDC transfer. P2Flux verifies that a refund matches the original payment, but the stateless API does not track whether you have already issued another refund for that payment. Nothing here will stop a second one, and a second one moves real money out of your wallet. Your integration must enforce refund idempotency.

P2Flux does not keep a global refund ledger. That is the same statelessness that means we never hold your money — and it has the same consequence in both directions: no record for anyone to freeze, and no record to check against.

So refund limits are enforced by the merchant integration, which is the only part of the system that knows what an order is:

  • Through an official P2Flux integration, this is already handled. Each one keeps its own refund record and refuses a second refund for the same payment — Tipster Script does today, and the same holds for commerce plugins as they arrive.
  • Through the SDKs or the API directly, it is yours to implement. Persist refund state against your own order record and block duplicate sends before they reach a wallet. Neither SDK does this for you: they are thin clients over the API and keep no state of their own.
WHAT TO PERSIST

Record the refund against the order the moment you call /v1/refunds/prepare, not after the transfer confirms — the gap between sending and confirming is exactly where a retry or a double-click lands. Reconcile later with /v1/refunds/verify, which needs no token and works days afterwards.

What can go wrong

CODEMEANING
REFUND_AMOUNT_INVALIDNot a positive base-unit integer, or larger than the original payment.
REFUND_ORIGINAL_PAYMENT_INVALIDThe settlement being refunded does not check out on chain.
REFUND_WRONG_MERCHANTThe refund came from a wallet other than the one that was paid.
REFUND_TRANSACTION_MISMATCHThe transfer does not match the refund it claims to be.
REFUND_CONFIRMINGSent, not yet settled. Verify again with the same hash.
INVALID_REFUND_TOKEN / REFUND_TOKEN_EXPIREDThe prepare token is malformed or older than fifteen minutes. Prepare again.
Something more than a standard integration?
Marketplace flows, platform billing and custom settlement logic.
Discuss an integration