Skip to content

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:

  1. 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).
  2. You fund it: send source.amount of source.asset from the treasury to source.depositAddress. The dashboard shows this instruction on every awaiting refund; any wallet works.
  3. 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.amount is the refund value plus routing costs, and destination.amount — what the customer receives — is the full quoted value. Refunding should make the customer whole.
  • Delivery defaults to the refundTo address the payer supplied at checkout (the only customer address we hold), overridable per refund with toAddress — 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=… and GET /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.

0billing — crypto billing for your app · pay from any chain, settle in one