Refunds
A share of what your customer paid, sent back from the wallet that was paid. You approve; your wallet signs.
From the contract · payment.completed
| Your customer paid | 10.0042 USDC network fee included |
|---|---|
| QBitFlow's fee, 0.99% | −0.099 USDC |
| Network fee | −0.0042 USDC |
| Reached your wallet | 9.901 USDC |
From the contract · refund.approved
| Reached your wallet | 9.901 USDC |
|---|---|
| Sent back · 50% | −5.0021 USDC of 10.0042 USDC paid |
| Stays with you | 4.8989 USDC |
QBitFlow keeps its fee: the whole refund comes out of your wallet, which also pays the network fee of the transfer back.
Who starts it
- Your customer asks from the refund page, if
refunds.enabled(the default), withinrefunds.windowDays(30 days by default). You approve a share or reject it with a message. - You send one from the dashboard (Dashboard/Payments/a payment/Refund), for a payment (
pay@…) or a bill (sub-hist@…), whatever those settings say. No route sends one: a key gets404.
A customer's request waits in GET /refunds (status=pending) until answered:
request.sh
curl https://api.qbitflow.app/v2/refunds \ -H "X-API-Key: $QBITFLOW_API_KEY"# 200: a page of Refund ({ items, hasMore, nextCursor? })200 · response.json
{ "amount": "10.0042", "amountMinUnits": "10004200", "amountUsd": "10.0042", "chain": "BASE", "createdAt": "2026-10-01T12:00:00Z", "currency": {…}, "held": false, "initiatedBy": "customer", "paid": "10.0042", "paidMinUnits": "10004200", "paidUsd": "10.0042", "reason": "I was charged twice for the same order", "refundPercent": "100", "status": "pending", "test": false, "txUuid": "pay@01a0f755-f200-7000-8000-000000000001", "uuid": "refund@01a0ec3b-3ba0-7000-8000-000000000006"}initiatedBy:customerormerchant: who started it.paidMinUnits: The base: everything the customer's wallet sent.refundPercent: The share asked: 100 for a customer's request; you set the share when you approve.status:pendinguntil answered: approved (the transfer confirms) or rejected.
Your wallet signs the transfer in the dashboard; the SDKs read refunds, they don't send them. Nothing is recorded until a refund you send confirms: a customer's request made meanwhile is answered by your transfer, and one made while it's being sent gets 409 refund_already_exists.
- Customer wallet → QBitFlow API: Asks for a refund (a refund you start skips this)
- QBitFlow API → Your server: refund.requested
- Your wallet → QBitFlow API: Approves a share, and signs it
- QBitFlow API → Your server: refund.rejected, if you rejected it in the dashboard (no signature)
- Your wallet → Customer wallet, on-chain: Sends the share back, on-chain
- QBitFlow API → Your server: refund.approved
What a refund pays back
- The base is everything your customer paid: the amount (QBitFlow's fee and any commission
included) plus the network fee on top,
paidMinUnits(paidUsd). - It sends
refundPercentof it, above 0 to 100, rounded down to a whole unit. At 100 %, your customer gets back exactly what their wallet sent. - You bear the fees (fees). QBitFlow keeps its fee, a marketplace its
commission; your wallet sends the whole share and pays the refund's network fee. It must hold the
share when you approve, or
422 insufficient_funds. - Partial refunds. Your customer asks for everything; you pick the share when you approve (the dashboard's slider), or when you send one from the dashboard.
- One refund per transaction, whatever it became: a partial refund or a rejection blocks another
(
409 refund_already_exists).
Statuses
- your refund → approved (final): sent, confirmed
- pending → approved (final): approved, confirmed
- pending → rejected (final): rejected
A refund you start is recorded as approved once its transfer confirms; it is never pending.
| Status | Means | Webhook |
|---|---|---|
pending | a customer asked: not answered or not confirmed yet | refund.requested |
approved | the transfer confirmed on-chain, the share sent recorded (your own refund: initiatedBy: "merchant") | refund.approved |
rejected | you rejected the request, with a message: final | refund.rejected |
When you approve a customer's request, it stays pending between your send and its confirmation; its approval says it's being
confirmed, or that the last attempt failed (send it again). Filter GET /refunds
by status (repeat it for several): pending, or approved and rejected for the answered ones. A
payment's refund and refundable say where it stands.
In a marketplace
A seller reads and answers their refunds while the members.refunds policy allows it; the team
always can. With it off, their customers can't ask either (403 refunds_disabled), except for a
payment you hold: you decide it.
A payment you hold for a seller is refunded from your wallet, and the seller bears it up to what
you owe them for it. Above their share (a full refund always is: the fees come back too), you
bear the rest. A released payment can't be refunded, nor asked for (409 held_funds_released). The organization decides and sends a held payment's refund: a seller gets 403 refund_decided_by_organization.