Concepts

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

The contract's example payment, as recorded
Your customer paid10.0042 USDC network fee included
QBitFlow's fee, 0.99%−0.099 USDC
Network fee−0.0042 USDC
Reached your wallet9.901 USDC

From the contract · refund.approved

A 50% refund of it, from your wallet: the customer gets 5.0021 USDC back of the 10.0042 USDC they paid, and 4.8989 USDC stays with you. QBitFlow keeps its fee.
Reached your wallet9.901 USDC
Sent back · 50%−5.0021 USDC of 10.0042 USDC paid
Stays with you4.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), within refunds.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 gets 404.

A customer's request waits in GET /refunds (status=pending) until answered:

GET /refunds · the request and its answer

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"}
Its example: the refund.requested event's data, the same Refund, one of the page.
  1. initiatedBy: customer or merchant: who started it.
  2. paidMinUnits: The base: everything the customer's wallet sent.
  3. refundPercent: The share asked: 100 for a customer's request; you set the share when you approve.
  4. status: pending until 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.

  1. Customer wallet → QBitFlow API: Asks for a refund (a refund you start skips this)
  2. QBitFlow API → Your server: refund.requested
  3. Your wallet → QBitFlow API: Approves a share, and signs it
  4. QBitFlow API → Your server: refund.rejected, if you rejected it in the dashboard (no signature)
  5. Your wallet → Customer wallet, on-chain: Sends the share back, on-chain
  6. QBitFlow API → Your server: refund.approved
Blue mark: money, on-chain. The rest moves no funds.

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 refundPercent of 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

  1. your refund → approved (final): sent, confirmed
  2. pending → approved (final): approved, confirmed
  3. pending → rejected (final): rejected

A refund you start is recorded as approved once its transfer confirms; it is never pending.

StatusMeansWebhook
pendinga customer asked: not answered or not confirmed yetrefund.requested
approvedthe transfer confirmed on-chain, the share sent recorded (your own refund: initiatedBy: "merchant")refund.approved
rejectedyou rejected the request, with a message: finalrefund.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.