Appearance
Refunds
A refund is its own record (ref_…), never a mutation of the payment. The payment stays the immutable fact of what was collected; a refund is a fresh outbound swap that reverses its value.
The custody model
0billing never holds funds — settlement went straight to your treasury, and refunds don't change that. A refund is funded by one transfer you sign from the treasury you control. There is no delegated spending authority, no hot wallet key in config, nothing that could drain a treasury if this server were compromised.
What 0billing does is everything around that one transfer:
- Quote the reverse swap — treasury asset in, the customer's original asset out, delivered to their address — and reserve a one-time deposit address for it (
status: awaiting_funds). - You fund it: send
source.amountofsource.assetfrom the treasury tosource.depositAddress. The dashboard shows this instruction on every awaiting refund; any wallet works. - Watch it settle exactly like an inbound payment (
detected→completed), and tell your app via webhook so entitlement reversal is driven the same way granting is.
An unfunded refund quote simply expires — nothing moved, nothing owed.
The currency model
Refunds are denominated in USD — the unit everything else in 0billing is priced in — and delivered in the asset and chain the customer paid from, at the rate at refund time.
- The customer paid $20 in BTC three weeks ago: a full refund delivers $20 worth of BTC today. Neither side is exposed to the price move between payment and refund; the refund is of the purchase value, not the coins.
- The swap overhead is the merchant's:
source.amountis the refund value plus routing costs, anddestination.amount— what the customer receives — is the full quoted value. Refunding should make the customer whole. - Delivery defaults to the
refundToaddress the payer supplied at checkout (the only customer address we hold), overridable per refund withtoAddress— e.g. when the customer's exchange deposit address rotated.
Partial refunds
amountUsd may be any part of the payment; refunds stack until the payment is fully returned. failed and expired refunds don't count against the cap — in both cases the treasury kept (or got back) its money.
Failure is safe
If the outbound swap fails, the router returns the funds to the treasury — the refund goes failed, the customer received nothing, and the amount is refundable again. The webhook (refund.failed) tells you to try again rather than to reverse anything.
API and dashboard
POST /v1/payments/:id/refund{ amountUsd?, toAddress? }— admin-only: refunds move treasury money.GET /v1/refunds?paymentId=…andGET /v1/refunds/:id(polling the latter drives it forward).- In the dashboard, expand a confirmed payment → Refund… — it shows every refund's state and the exact funding instruction while one is awaiting.
- Events:
refund.created,refund.completed,refund.failed,refund.expired— see webhooks.