Refunds
Non-custodial refunds - the payer chooses where the money goes, the address is screened, and you send it from your own wallet.
Tillsafe never holds your money, so it cannot send a refund for you. What it does is the hard part around it: find out where the payer wants the money, check that address, and keep the record straight.
The flow
- You create the refund:
POST /v1/invoices/{id}/refunds. You get a refund inawaiting_claimwith aclaim_url, a private link for the payer. - You give the payer the link (by email or on your support page). It is never emailed by the platform: whoever holds it chooses where the refund goes, so it must come from you.
- The payer claims it: they pick a network and enter an address. The address is screened against
sanctions lists and the token issuer's blacklist. You get a
refund.claimedwebhook, and the refund isready_to_send, orin_reviewif the screening matched. - You send the money from your own wallet to
payout.addressonpayout.network, then record the transaction:POST /v1/refunds/{id}/mark_sentwith itstx_hash. The refund issent.
The network fee of step 4 is yours, like any transfer from your wallet.
Creating a refund
{ "amount": "5.00", "asset": "USDT@tron:nile", "reason": "other" }All three fields are optional:
| Field | Default |
|---|---|
amount | What was received in that asset minus what was due: exactly the overpayment. For an asset the invoice never asked for, everything received in it. |
asset | The asset that was received. |
reason | overpayment without an amount, other with one. Also wrong_asset and late_payment. |
So: to return an overpayment, send {}. For a wrong token, pass its
asset and "reason": "wrong_asset". For a late payment or a cancelled order, pass the amount:
without one, a refund with no excess to return is refused with 400 amount_required.
You can refund in parts, several times, up to what was received and not already refunded
(409 refund_exceeds_received beyond it).
Statuses
| Status | Meaning |
|---|---|
awaiting_claim | The payer has not chosen an address yet. The link works for 30 days. |
ready_to_send | Claimed and screened clear. Send it, then call mark_sent. |
in_review | The payout address matched a screening list. Do not send. The payer is not told which list. |
sent | You recorded the transaction. Final. |
cancelled | You cancelled it (POST /v1/refunds/{id}/cancel); the link stops working. Allowed in any state except sent. |
expired | Nobody claimed it within 30 days. |
Each screening check and its result is in payout.screening, so you can see why a refund is in review.
Security notes
- The
refund.claimedwebhook carries the refund object, including itsclaim_token. Treat webhook bodies as confidential. - A claim can be submitted once: the same address again is a no-op, a different one is
409 claim_already_submitted. Cancel and create a new refund if the payer made a mistake. mark_sentaccepts a 64-hex-digit transaction hash, and only fromready_to_send.