A donation is an invoice plus a checkout URL plus a webhook
Strip a donation gateway down to its parts and there is very little to build. You create an invoice for the tip amount, halfin returns a hosted checkout URL, you send the viewer there, and when the payment settles a signed webhook tells your server. That is the whole loop. Everything that feels hard about accepting crypto — generating a deposit address, drawing a QR code, picking a network, watching the chain for confirmations — lives inside the hosted page and the gate behind it, not in your code.
The reason to model a donation as an invoice rather than a bare address is attribution. A donation has to be tied to a creator, and often to a campaign or a stream session. An invoice carries that context: you stamp it with your own description and identifiers when you create it, and that context comes back to you on the webhook. A raw address gives you a payment with no idea who it was for; an invoice gives you a payment you can route to the right creator's balance automatically.
Because the fiat amount is the anchor, a 'tip 5 dollars' button means the viewer pays the live equivalent in whatever asset they hold, and your ledger still books a clean 5.00. The viewer chooses USDT on Tron or SOL or BTC; you reconcile the dollar figure you intended to collect, not a token amount you then have to value at month-end.
The hosted page is the part you do not build
Donations come from viewers, not developers. They are mid-stream, often on a phone, and they will abandon anything that asks them to think. The hosted checkout page is tuned for exactly that moment: it shows one amount, one address, a scannable QR, the networks you accept, and a live status that moves from waiting to confirmed on its own. You point the viewer at the URL and the page owns the rest of the interaction.
Crucially, you write no wallet-connect code and run no confirmation watcher. The page renders the deposit address, the customer sends from their own wallet or exchange, and crediting is reorg-aware with per-chain confirmation thresholds — so a tip your platform treats as received has actually settled under that chain's rules, not merely been broadcast. The difference between 'seen in the mempool' and 'safe to credit the creator' is handled for you.
If you would rather keep the donor inside your own interface, the same invoice object backs self-hosted checkout — you render the address, amount, and status yourself against the API. Most platforms start with the hosted page because it is the fastest path to a working tip jar, and move to self-hosted only when they want the donation flow pixel-matched to their brand.
- One amount, one address, one QR — designed for a viewer mid-stream on a phone.
- Supported networks and live payment status are rendered and updated by the page.
- Reorg-aware crediting with per-chain confirmation thresholds — no watcher to run.
- No wallet-connect code on your side; the donor pays from their own wallet.
The webhook tells you the tip landed — and whose it is
Treat the signed webhook as the source of truth, never the browser redirect. A viewer can pay a tip and close the tab before any success page loads; the redirect is a nicety, not a guarantee. The webhook is what actually arrives at your server, and it is what should credit the creator and write to your ledger. If you only listen to the redirect, you will silently lose the donations from everyone whose connection blipped at the wrong moment.
Every webhook is HMAC-signed. Verify the signature before you take any action — that check is what proves the event came from halfin and was not replayed or forged by someone who guessed your endpoint. Only after the signature verifies do you read the invoice, match it to the creator you stamped on it at creation, and move the money in your own books.
The events you care about for a donation gateway are a small, fixed set. invoice.confirming marks the tip as live and awaiting payment; invoice.paid is the one that credits the creator; invoice.expired tells you a started-but-abandoned donation timed out so you can clean it up. If a donor sends the wrong amount, invoice.underpaid and invoice.overpaid surface the gap as a first-class outcome rather than stranding the money — you decide your own policy for a tip that came in light or heavy.
| Webhook event | What happened | What your gateway does |
|---|---|---|
| invoice.confirming | The donation invoice is live and awaiting payment. | Show the donor the checkout; mark the tip as pending in your UI. |
| invoice.paid | Confirmations met the chain threshold; the tip settled. | Verify the signature, then credit the creator and log the donation. |
| invoice.underpaid | The donor sent less than the invoiced amount. | Record the shortfall; apply your own partial-tip policy. |
| invoice.overpaid | The donor sent more than the invoiced amount. | Record the excess so it is visible and accountable, not lost. |
| invoice.expired | The payment window elapsed before a sufficient payment arrived. | Clear the pending tip; nothing to credit. |
Create a donation invoice and read it back from one event
The income side is deliberately small to integrate. You create a fiat-anchored invoice for the tip and stamp it with the creator it belongs to, then send the donor to the checkout URL in the response. The amount is a string — monetary values are strings end to end, never floats — and the fiat currency is your anchor, so the donor settling in any supported asset still maps back to the figure you intended to collect.
Below is a minimal create call followed by the shape of the payload your server later receives. Notice that the creator identifier you put in the description on the way out is the same identifier you read on the way in — that round-trip is how the gateway attributes a donation without you maintaining a side table of pending addresses. The full request and response schemas live at docs.thehalfin.com.
# 1. Create a fiat-anchored donation invoice, stamped with the creator
# it belongs to. The response carries a hosted checkout URL.
curl -X POST https://api.thehalfin.com/api/v1/invoices \
-H "X-API-Key: $HALFIN_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"amount_fiat": "5.00",
"fiat_currency": "USD",
"deferred": true,
"description": "Tip for creator-8841",
"idempotency_key": "tip-creator-8841-2026-06-10T2014"
}'
# 2. Send the donor to the returned checkout URL. When the tip settles,
# your server receives an HMAC-signed webhook like this. VERIFY the
# signature header first, THEN credit the creator you read back from
# the invoice. See docs.thehalfin.com for the full event schema.
#
# {
# "event": "invoice.paid",
# "data": { "description": "Tip for creator-8841", ... }
# }Where a donation gateway stops, and what sits next to it
This page is only the income side: collecting a tip and knowing it landed. It deliberately says nothing about paying creators out — that is a separate job with its own primitive. When a creator wants their accumulated tips, you run that as a payout, and a whole-roster run is an idempotent mass payout. Keeping the two sides separate is the point: a donation gateway should be a tight, well-attributed intake, and the payout run should be its own auditable batch.
A few honest limits keep the gateway clean. There is no fiat on-ramp — the donor pays from crypto they already hold; halfin does not sell them the asset first. 'Recurring' support is invoice-per-cycle, not a saved card you auto-charge: a monthly supporter gets a fresh invoice and a fresh checkout each cycle, which is the honest shape of recurring crypto billing. And halfin is the payment rail, not your moderation or identity layer — who your creators are and whether a donation is allowed stays in your stack.
- Donation intake here; creator withdrawals are payouts, covered separately.
- No fiat on-ramp — the donor pays from crypto they already hold.
- Recurring support is invoice-per-cycle, not an auto-charged saved card.
- halfin processes the rail; creator identity and policy stay in your stack.