TillsafeDocstillsafe.comRequest early access

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

  1. You create the refund: POST /v1/invoices/{id}/refunds. You get a refund in awaiting_claim with a claim_url, a private link for the payer.
  2. 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.
  3. 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.claimed webhook, and the refund is ready_to_send, or in_review if the screening matched.
  4. You send the money from your own wallet to payout.address on payout.network, then record the transaction: POST /v1/refunds/{id}/mark_sent with its tx_hash. The refund is sent.

The network fee of step 4 is yours, like any transfer from your wallet.

Creating a refund

json
{ "amount": "5.00", "asset": "USDT@tron:nile", "reason": "other" }

All three fields are optional:

FieldDefault
amountWhat was received in that asset minus what was due: exactly the overpayment. For an asset the invoice never asked for, everything received in it.
assetThe asset that was received.
reasonoverpayment 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

StatusMeaning
awaiting_claimThe payer has not chosen an address yet. The link works for 30 days.
ready_to_sendClaimed and screened clear. Send it, then call mark_sent.
in_reviewThe payout address matched a screening list. Do not send. The payer is not told which list.
sentYou recorded the transaction. Final.
cancelledYou cancelled it (POST /v1/refunds/{id}/cancel); the link stops working. Allowed in any state except sent.
expiredNobody 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.claimed webhook carries the refund object, including its claim_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_sent accepts a 64-hex-digit transaction hash, and only from ready_to_send.