Why a card chargeback can't happen on-chain
A card payment is a promise that can be unwound. The money you see in your processor is provisional: the issuing bank fronted it, and for months afterward the cardholder can dispute the charge, the issuer can side with them, and the funds are pulled back out of your account — often with a fee on top, and frequently for reasons unrelated to whether you delivered. That reversal power is structural: it lives with the bank, not with you, and you cannot opt out of it.
An on-chain payment has no such backstop. When a customer pays a halfin invoice, they broadcast a transaction that the network confirms over some number of blocks. Once it is confirmed to the depth that chain requires, the transfer is settled — there is no intermediary holding the funds in escrow who can later decide to send them back. Nobody, including halfin, can reach into a confirmed transaction and reverse it. The 'chargeback' as card merchants know it — a third party forcing money out of your account — has no mechanism to exist.
That is the single most important property to internalize: with crypto, the question is never 'will this settled payment be clawed back?' It is 'when is a payment actually settled?' Get the confirmation gating right and a paid order is permanently paid. The risk does not vanish — it relocates from after-the-fact reversal to before-the-fact confirmation, which is a risk you can fully control.
- Card payments are reversible by the issuer for months; that power is structural and not yours.
- An on-chain payment, once confirmed to the chain's threshold, is settled with no escrow to reverse.
- No party — not the payer, not halfin — can force a confirmed transaction back out.
- The real risk shifts from 'forced reversal' to 'did this actually confirm?' — which you control.
What 'finality' actually depends on
Finality is not instant, and treating an unconfirmed transaction as final is the one way to recreate a chargeback-shaped loss for yourself. A transaction that is merely seen in the mempool, or confirmed in a single block, can still be dropped or undone if the chain reorganizes. The protection crypto gives you only kicks in once the payment is confirmed to the right depth for the chain it arrived on.
halfin handles this for you, and it is worth knowing how. Crediting is reorg-aware and respects per-chain confirmation thresholds: the platform waits for the network-appropriate number of confirmations before marking an invoice paid, and if a chain reorganization unwinds a transaction that looked settled, that is reflected rather than ignored. So 'paid' from halfin means 'confirmed to a depth the chain stands behind' — not 'first seen'.
The practical rule that flows from this: fulfil on confirmed finality, never on first-seen. Show the customer that their payment is in flight once a deposit appears, but do not release goods, ship product, or grant access until the invoice reaches its paid state. The table below maps the invoice stages to the action each one warrants.
| Invoice stage | What it means | Safe action |
|---|---|---|
| Awaiting payment | Invoice is live, rate locked, no deposit seen yet. | Show address, amount, network, and countdown. Release nothing. |
| Confirming | A deposit appeared and confirmations are accumulating toward the threshold. | Tell the customer it's in flight; still hold the order — not yet final. |
| Paid | Threshold met; crediting is reorg-aware and settled to your balance. | Fulfil. This is the point at which the payment is final. |
| Underpaid / Overpaid | A real payment arrived but for less or more than due. | Decide per policy: request a top-up, settle partial, or refund the surplus. |
| Expired | The payment window closed before a sufficient payment confirmed. | Re-issue at the current rate if the customer still wants to pay. |
Where disputes still live — and how to defuse them
Removing forced reversals does not remove human disagreement. A customer can still claim the goods never arrived, that the item was not as described, or that they paid the wrong amount. What changes is the resolution path: there is no issuer to adjudicate and claw funds back over your objection. The dispute is between you and the customer, and you hold the funds while you sort it out — a far stronger position than the card model, where the money leaves first and you argue later.
The way you defuse these is operational, not financial. Because halfin invoices are fiat-anchored, every confirmed payment maps back to the exact amount you billed and the order it belongs to — keep that linkage and 'I paid but you have no record' evaporates. Because the rate locks when the invoice goes live and the invoice carries an expiry, the customer pays a concrete number they agreed to, so 'the price changed on me' does not arise. And because underpaid and overpaid amounts are recorded against the invoice rather than discarded, the 'I sent the wrong amount' case is visible and resolvable instead of becoming a support mystery.
When a legitimate dispute does resolve in the customer's favor — a genuine non-delivery, a defective item, a goodwill gesture — you settle it by sending money back on purpose. That is a refund, and unlike a chargeback it happens on your terms, after your review, for the amount you decide.
- No issuer adjudicates: a dispute is between you and the customer, and you hold the funds throughout.
- Fiat-anchored invoices map every payment to the billed amount and order — kills 'no record of my payment'.
- Rate lock + expiry means the customer agreed to a concrete number — kills 'the price changed'.
- Under/overpayment is recorded on the invoice — the wrong-amount case is resolvable, not lost.
Run refunds as a deliberate payout you control
A refund in crypto is the inverse of a payment, and it has none of the asymmetry of a card reversal. You initiate it, you choose the amount, and it does not move until it is approved. Operationally a refund is a payout: you send the agreed amount back to a destination address the customer controls, with a deterministic idempotency_key so a retry or a re-run never sends the money twice. The request shape — currency (asset plus network), amount as a string, destination, and the key — is the same one you would use for any payout.
Two safeguards matter here. First, the destination must be an address the customer actually controls on the network you are refunding — an on-chain send is itself final, so a refund to a wrong or unreceivable address is a real loss with no reversal to save you. Capture the refund address deliberately rather than assuming the address they paid from is refundable. Second, every payout you create enters a pending-approval state and is released from the dashboard before funds move; the refund you submit is a proposal, not an irreversible action. Use that gap to confirm the amount and the destination before a human signs it off.
Make the key reproducible from your own records — something like order_8841:refund — so that if your process retries, halfin returns the existing refund instead of creating a second one. The example below issues a single refund against the public payout API; the same call, fanned out in a loop, processes a batch of refunds, each made safe by its own key.
# A crypto refund is a deliberate payout you initiate and approve — not a
# forced reversal. Send the agreed amount back to an address the customer
# controls, with an idempotency_key so a retry never double-refunds.
curl -sS -X POST https://api.thehalfin.com/api/v1/payouts \
-H "Content-Type: application/json" \
-H "X-API-Key: $HALFIN_PAYOUTS_API_KEY" \
-d '{
"currency": "USDT_TRC20",
"amount": "49.00",
"destination": "TRefundAddressTheCustomerControls000000000",
"idempotency_key": "order_8841:refund"
}'
# The refund is created in a pending-approval state and released from the
# dashboard before funds move. Re-running this with the same idempotency_key
# returns the existing refund — it never sends twice.
# See docs.thehalfin.com for the full request and response schema.Reconcile refunds from signed webhooks
A released refund is not done the instant you approve it — like any on-chain send, it has to settle. The honest signal that it reached its terminal state is a webhook, not a polling loop. payout.completed fires when the refund settles under the chain's confirmation threshold; payout.failed fires when it cannot — for example, an address that turned out to be invalid at release. Drive your refund bookkeeping from those events so the status in your records means 'settled', not 'we think so'.
Verify before you act on any event. The HMAC signature on a webhook is what separates a real halfin event from a forged POST to your public endpoint, so recompute the HMAC over the exact raw request bytes and compare it in constant time before you mark anything refunded. Keep the handler idempotent — a redelivered payout.completed must not flip the same refund to settled twice or email the customer again. On payout.failed, investigate the reason, fix it, and re-run with the same idempotency_key if the money never moved.
- Reconcile refunds on payout.completed and payout.failed — do not poll the API for status.
- Verify the HMAC over the raw bytes (constant-time) before treating any POST as a halfin event.
- Dedupe on the event so a redelivered payout.completed settles a refund exactly once.
- On payout.failed, fix the cause and re-run with the same key if the money never moved.
A checklist for a chargeback-resistant crypto flow
None of this requires anything exotic. It is a small set of habits that, together, mean a paid order stays paid and a refund only ever happens because you decided it should — the operating discipline for accepting crypto without inheriting card-style reversal risk.
The throughline is that crypto does not eliminate the work of running a payments operation — it removes the part where a third party could overrule you after the fact. You still verify, gate on finality, and handle disputes. You just do it from a position where you hold the funds and make the decisions.
- Fulfil only on the paid state, never on a first-seen deposit — let confirmation thresholds do their job.
- Keep the fiat-anchored invoice linked to its order so every payment is provably matched.
- Treat refunds as deliberate, approved payouts — capture a correct refund address every time.
- Make refund idempotency_keys reproducible from your records so a retry can't double-pay.
- Reconcile both payments and refunds from signed, verified webhooks — not from polling.
- Record under/overpayments off the invoice so wrong-amount cases never become disputes.