First, the rule that shapes everything: crypto has no chargeback
Before any steps, get the mental model straight, because it is the opposite of card payments. On a card, a refund and a chargeback are network actions: the acquirer can pull funds back, reverse a settled charge, and arbitrate a dispute weeks after the fact. None of that exists on-chain. A confirmed crypto payment is final — there is no protocol-level undo, no third party who can claw the transaction back, and no dispute window hanging over a sale you already fulfilled.
That finality is a feature on the way in and a responsibility on the way out. Because nobody can reverse a payment you received, you also cannot reverse one you sent: a refund is not an annulment of the original transaction, it is a new transaction you broadcast to return value. So the work is not 'press undo' — it is 'send money out, correctly, to the right place, for a defensible amount, with a record of why'. halfin's refunds primitive keeps you out of free-form transfers: a refund is created against the invoice the customer already paid, so you inherit who paid, how much, in which asset, and on which network instead of re-typing a wallet address from a support email. That binding is what the whole flow below is built on.
Step 1 — Decide your refund policy before you need it
Refunds go wrong most often when they are improvised. Decide the policy once, in calm conditions, so that when a request comes in the operator is applying a rule rather than inventing one. Three questions cover almost everything: who is allowed to approve a refund, what amounts are in policy, and how long after a sale you will honor a return.
Tie the 'who' to halfin's permissions. Moving funds — payouts and refunds alike — sits behind scoped API keys and operator permissions, so a read-only reporting key cannot trigger a refund and a support tool can be limited to exactly what it needs. Decide which operators carry the refund permission and keep that list small; refunding is a money-movement action and should be treated like one.
Decide the 'what' and the 'when' in plain terms your support team can apply without escalating every case, and write it down. The point of a policy is that the person processing the refund at 6pm on a Friday does not have to guess what the business would have wanted.
- Who can approve — grant the refund permission to a small set of operators; keep reporting and integration keys out of it.
- What amount is in policy — full, partial, or surplus-only, with a clear default for each common case.
- How long — a return window after the sale, after which refunds are an exception, not a default.
- Where it routes — define escalation for amounts or cases outside the standard policy.
Step 2 — Find the invoice the customer actually paid
Every halfin refund is anchored to a completed invoice, so the first operational step is always: locate that invoice. This is not bureaucracy — it is the single thing that turns a risky outbound transfer into a controlled one. The invoice carries the paid amount, the settlement asset, the network it arrived on, and the payer context. Once you have the invoice, you are no longer reasoning from a customer's email; you are reasoning from a confirmed payment record.
In practice you find the invoice the same way you find any other order: by your own reference. If you attached an order id or customer id when you created the invoice, look it up by that. Otherwise search by the customer, the amount, or the date in the dashboard, or read it back from the API. Confirm it is actually paid before you go further — you cannot refund an invoice that never settled, and 'the customer says they paid' is not the same as a confirmed, reorg-final payment on the invoice record.
The read below fetches an invoice by id so you can confirm its paid amount, asset, and network before issuing anything — the numbers you refund against come from this record, not from the support ticket.
# Read the invoice the customer paid, to confirm amount, asset, and network
# before issuing any refund against it. Use a key scoped to read invoices.
curl https://api.thehalfin.com/api/v1/invoices/00000000-0000-0000-0000-000000000000 \
-H "X-API-Key: $HALFIN_API_KEY"
# Confirm the invoice is actually paid and note the settled amount, the
# asset, and the network it arrived on. A refund reasons against THIS
# record, not against what the customer remembers paying. See
# docs.thehalfin.com for the full invoice response schema.Step 3 — Choose full, partial, or surplus, against the paid amount
A halfin refund is reasoned about relative to what was actually paid on that invoice, so the amount you return is bounded by a known, confirmed figure rather than invented. That gives you three everyday shapes, and your policy from Step 1 tells you which one applies. Pick deliberately — the amount is the part a customer and an auditor will both scrutinize.
A full refund returns the entire paid amount: the order was cancelled before fulfilment, or you are unwinding the sale completely. A partial refund returns a portion: one line item was unavailable, or you are issuing a goodwill credit while keeping the rest of the order. A surplus refund returns only the amount over what the invoice asked for: the customer overpaid — often because they covered a network fee out of the same amount — and you are handing back the excess while the sale itself stands.
Set one expectation up front, with the customer and internally: the value landing in the payer's wallet is what you refund minus the cost of sending it. Network fees and on-chain rates apply to the outbound transaction like any transfer, so a 'full' refund is full against the paid amount, not a guarantee of an identical token figure net of fees.
| Situation | Refund shape | Returned against the invoice |
|---|---|---|
| Order cancelled before fulfilment | Full | The entire paid amount |
| One line item unavailable | Partial | The portion for the missing item |
| Customer overpaid the invoice | Surplus | Only the excess over the invoiced amount |
| Goodwill credit, sale stands | Partial | An agreed portion of the paid amount |
Step 4 — Set the destination, and treat it as the highest-risk field
The single most expensive mistake in a manual refund is the destination address. An on-chain transfer is irreversible: send to the wrong wallet and the money is gone, with no support queue that can retrieve it. So the destination deserves more care than any other field, and the whole reason to bind a refund to an invoice is to take the guesswork out of it.
Because the refund references the invoice the customer paid, the asset and network the payer used are part of that record — you are not choosing a chain at random, you are returning value the way it came in. Send the refund on the network the original payment arrived on, in an asset that makes sense for that network. A refund of a Tron USDT payment goes back over Tron; refunding it to an Ethereum address is how funds get stranded.
Validate the destination against something you trust, not a string pasted into a chat thread. Prefer the payer context the paid invoice already carries; if the customer supplies a new address, confirm it on a channel you control and re-read it character by character, because a single transposed digit is a permanent loss with no recall.
- Match the network to the original payment — refund a Tron payment over Tron, a Base payment over Base.
- Prefer the payer context the paid invoice already carries over a freshly pasted address.
- When the customer supplies an address, confirm it on a channel you control and re-read it in full.
- Remember the transfer is irreversible — there is no recall once it is broadcast.
Step 5 — Approve it as an operator, so it is logged and attributable
Sending money out of a balance is the most sensitive action a payment system performs, and halfin routes refunds through operator control on purpose. A refund is something a person with the right permission initiates and approves — not something that fires automatically off a raw customer request. That deliberate human gate is where someone confirms the request is legitimate, the amount matches policy, and the destination is the payer you expect.
This is also where two recurring manual-refund failures get designed out. Double payment — two operators independently processing the same request — is contained when the refund is bound to one invoice and approval is a single explicit action against it. And the orphaned outbound transfer — a payment that leaves your balance with no link to a sale — cannot happen, because the refund references the invoice from the start. Every refund lands in the operation log with its actor, its amount, its destination, and its source invoice — and the reason that matters is not the easy case but the one that gets questioned weeks later by finance, an auditor, or a partner, where the record answers the obvious questions without anyone reconstructing the event from chat history.
Step 6 — Reconcile the refund back to the sale
A refund is not finished when the transaction broadcasts; it is finished when your books reflect it. Because each halfin refund is bound to the invoice it returns value against, the trail runs cleanly from the sale, to the payment that settled it, to the return that gave part or all of it back. Finance does not see a mysterious outbound transfer — they see a return tied to invoice X, which they can open and match to the original order.
Reconcile against the invoice the refund was issued against, not against a customer's word that they got the money. The bound invoice carries the paid amount, the asset, and the network, so it is the anchor your back office matches the return to, and the operation log from Step 5 is the audit trail finance reads it back from.
Keep your webhook discipline for the events halfin actually signs. halfin delivers HMAC-signed webhooks for invoice, balance, and payout state — for example invoice.paid, balance.credited, and payout.completed. As with every halfin webhook, verify the signature over the raw bytes with a constant-time compare before you act on the payload; an event is an instruction to mutate your ledger, so treat it with the same suspicion you treat a payment confirmation. The read below fetches the bound invoice by id so your back office reconciles the return against a confirmed sale rather than guessing. See docs.thehalfin.com for the full event catalog and response schemas.
# Reconcile a refund against the invoice it was issued against: re-read that
# invoice to match the return to a confirmed sale. Use a key scoped to read
# invoices.
curl https://api.thehalfin.com/api/v1/invoices/00000000-0000-0000-0000-000000000000 \
-H "X-API-Key: $HALFIN_API_KEY"
# The refund is bound to this invoice, so finance can match the return to
# the original sale. halfin signs webhooks for invoice, balance, and payout
# events (e.g. invoice.paid, balance.credited, payout.completed) — verify
# the HMAC signature over the raw bytes (constant-time compare) BEFORE you
# act on any of them. See docs.thehalfin.com for the full event catalog and
# response schemas.Refund, or something else? Pick the right primitive
Not every 'send money back' is a refund, and using the wrong primitive is how your books drift. A refund must point at a paid invoice and return value against it; reach for one only when you are returning money to someone who paid you on an invoice.
When there is no originating invoice, you want a payout instead. A single payout pays one destination that was never a customer sale — a supplier settlement, an affiliate commission, a withdrawal. A mass payout fans many such payments out in one idempotent batch. These are operating outflows, not revenue reversals, and keeping them distinct from refunds is what lets finance close the month without untangling which outbound transfers were returns and which were costs.
Settlement asset can differ from what the customer paid, and the refund still reasons from the original invoice. If your balance has moved into another asset through balance conversion or treasury rebalancing, the refund still draws the payer context — the asset and network the customer used — from the invoice's record, which is exactly the context it needs to send value back the way it came.
| You are… | Use | Anchored to an invoice? |
|---|---|---|
| Returning money to a customer who paid an invoice | Refund | Yes — a paid invoice |
| Paying a supplier, affiliate, or withdrawal | Single payout | No |
| Fanning out many payments at once | Mass payout | No — one idempotent batch |