When the hosted page isn't yours enough
Hosted checkout solves the common case: you create an invoice, redirect the customer to checkout.thehalfin.com, and the payment page — QR code, network picker, wallet buttons, countdown, paid state — is rendered and maintained by halfin. For most merchants that is exactly the right trade. You ship in an afternoon and never touch a block explorer.
But some teams can't redirect. A trading platform funding flow, a game's top-up screen, an embedded B2B portal — these have a checkout that is part of the product, not a detour from it. Sending the customer to a third-party domain mid-flow costs conversions and breaks the visual contract you've built. For those teams the constraint isn't 'we want crypto', it's 'we want crypto inside our own surface, with our own copy, our own layout, our own analytics'.
Self-hosted checkout is the same payment engine as the hosted page, exposed one layer lower. halfin still derives the deposit address, watches the chain, applies the confirmation threshold, locks the rate at activation, and credits your balance. You own everything the customer sees: the markup, the wallet UX, the loading states, the success screen. The boundary is the API, not the browser.
What you build, what halfin runs
The division of labor is deliberate. You are responsible for presentation and for reacting to state. halfin is responsible for everything that touches a blockchain or a price feed. Drawing the line here means a frontend developer can ship a crypto checkout without ever learning what a UTXO is or why Polygon reorgs differently than Bitcoin.
Concretely, you call the invoicing API to create a payment, render the deposit address and amount halfin returns, and listen for webhook events to advance your own UI. You never sign a transaction, never hold a private key, never poll an RPC node, and never decide how many confirmations a Tron deposit needs. That last decision — the per-chain confirmation threshold — is one of the easiest things to get dangerously wrong, and it stays on halfin's side of the line.
| Concern | You own it | halfin owns it |
|---|---|---|
| Checkout markup, branding, layout | yes | — |
| Wallet connect UX, QR rendering | yes | — |
| Network / asset picker presentation | yes | — |
| Order-to-invoice mapping in your DB | yes | — |
| Deposit address derivation | — | yes |
| On-chain monitoring & confirmations | — | yes |
| Reorg-aware crediting | — | yes |
| Rate lock at activation, expiry | — | yes |
| Underpaid / overpaid handling | — | yes |
The integration shape
A self-hosted checkout is three moving parts: create an invoice when the customer reaches your payment step, render what halfin returns, and react to webhook events as the payment lands. The first part is a single authenticated call against the invoicing API. You send the fiat-anchored amount and your own order reference; halfin returns a deposit address, the exact crypto amount for the selected network, and an expiry timestamp with the rate already locked.
Use an idempotency key on the create call so a retried request — a flaky network on your backend, a double-click on the customer's pay button — never spins up two invoices for one order. The key is yours to choose; deriving it from your order id is the usual move. Send the request from your server, never the browser: the API key is a secret and scoped permissions only protect you if the key never reaches a client.
What comes back is everything your UI needs to render the payment step. You decide how to present it — a QR code from the address, a copy button, a countdown to the expiry, a wallet-deeplink button if you want one. halfin doesn't impose a layout because at this tier the layout is the whole point of self-hosting.
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": "49.00",
"fiat_currency": "USD",
"currency": "USDT_TRC20",
"deferred": true,
"description": "Account funding — order_4821",
"idempotency_key": "order_4821"
}'
# Response carries the deposit address, the exact crypto amount,
# the locked rate, and an expiry timestamp. Render these in your
# own checkout UI; halfin watches the chain from here.
# See docs.thehalfin.com for the full request schema.Webhooks are how your UI learns the payment landed
Polling the API for invoice status works, but it's the wrong default. The right pattern is to treat the webhook as the source of truth: halfin posts a signed event to your endpoint when the invoice is detected, confirmed, paid, underpaid, overpaid, or expired, and your backend updates the order and pushes the new state to the customer's open checkout.
Every webhook carries an HMAC signature. Verify it before you take any business action — before you mark an order paid, release a download, or credit a balance. An unverified webhook endpoint is an open door: anyone who learns the URL can forge a 'paid' event. The signature check is a few lines and it is not optional. halfin retries delivery with backoff, so a brief outage on your side won't lose the event, but your handler must be idempotent — the same event can arrive more than once.
Because the customer is sitting on a checkout you rendered, you control the moment of truth. When the paid webhook verifies, you can flip the UI to a success state over your own websocket or a short poll against your own backend — not against halfin. The customer never leaves your domain, and the 'payment confirmed' moment looks like the rest of your product.
- Verify the HMAC signature before acting on any event — no exceptions.
- Make handlers idempotent; events can be redelivered after retries.
- Return 2xx fast and do slow work async, so retries aren't triggered by your latency.
- Drive your customer-facing UI from your own backend state, updated by the webhook — not by client-side polling of halfin.
Assets and networks you can present
Self-hosted checkout exposes the same rails as every other halfin surface. You choose which to show in your picker — a TRC-20 USDT funding flow for one market, an ERC-20 or Solana option for another — and halfin handles each chain's confirmation rules underneath. Stablecoins anchor the amount against the invoiced fiat; native-coin payments settle against the rate locked at activation.
halfin settles native coins on Bitcoin, Ethereum, the XRP Ledger, and Solana, alongside ERC-20 tokens on Ethereum and the EVM L2s Base, Arbitrum, and Polygon, plus BNB Smart Chain and Tron. For stablecoins, USDT settles on Tron (TRC-20), Ethereum (ERC-20), and Solana; USDC settles on Ethereum (ERC-20), Solana, and Base. The table below lists where each stablecoin is available.
You don't have to surface all of these. A lot of self-hosted checkouts ship with one stablecoin on one fast, low-fee chain and add networks as demand appears. The point of owning the UI is that the picker is yours to shape.
| Stablecoin | Networks |
|---|---|
| USDT | Tron (TRC-20), Ethereum (ERC-20), Solana |
| USDC | Ethereum (ERC-20), Solana, Base |
Edge cases the engine already handles
The reason self-hosting is safe is that the hard parts don't move to your side. A customer who sends slightly less than the invoiced amount produces an underpaid state, not a silent loss — halfin records exactly what arrived and surfaces it so you can decide whether to credit, top-up-request, or refund. An overpayment is captured the same way rather than rounded off. A deposit that lands in a block which later reorgs out is not credited until it clears the confirmation threshold, so you never mark an order paid on a transaction that disappears.
Expiry is the other quiet hazard. The rate is locked when the invoice activates and the invoice carries a time-to-live, so a customer who opens your checkout and pays an hour later either pays against the locked quote or hits an expired invoice — they never pay yesterday's price into a stale address. Your UI reads the expiry timestamp and renders the countdown; the enforcement lives in the engine.
If you need to give money back later, that's the refunds product, driven from the same API and dashboard. Self-hosted checkout is the intake path; the lifecycle after payment — conversion, payout, refund — runs through the rest of the platform you're already settling into.