The problem: a payment address is awkward to type
A crypto deposit address is a long string of characters with no spaces and no forgiveness. Ask a customer to copy one from a desktop and retype it into a wallet on their phone and you've introduced the single easiest way to lose a payment: one transposed character and the funds go somewhere unrecoverable, or the wallet rejects the address and the customer gives up. The amount has the same fragility — the exact figure in the chosen asset has to match, down to the last decimal.
The QR code exists to remove that step entirely. A wallet that scans a payment QR reads the address and, where the chain's URI scheme supports it, the amount too — so the customer confirms a prefilled transaction instead of assembling one by hand. Nothing is typed, nothing is transposed, and the customer is paying within a couple of taps of landing on the page.
Building that yourself is more than calling a QR library. You'd encode a different payment URI per chain, render a scannable code at the right size and error-correction level, keep the on-screen address and the encoded address provably identical, and re-render the whole thing if the invoice's asset or network selection changes. halfin hosts that surface, so you never write any of it.
What the customer sees and does
When the customer reaches the hosted page and the invoice is open, the page shows the amount due in the asset and network they selected, the QR code, and the deposit address beneath it. On a phone, they open their wallet's scanner — or tap through to a wallet that registered the chain's URI scheme — point it at the code, and the wallet fills in the address and, where supported, the amount. They review and send.
On a desktop, the same QR and address serve a second device. The customer scans the on-screen code with the phone that holds their funds, or uses the copy-to-clipboard control to move the address into a wallet running elsewhere. Either way the address shown on the page and the address encoded in the QR are the same one halfin is watching, so it doesn't matter which the customer uses.
From the customer's side the whole interaction is: read the amount, scan, confirm, watch the page change. They never leave the page to find out whether it worked — the status on the page is the answer.
- Amount due, QR code, and deposit address shown together while the invoice is open.
- Mobile wallet scans the QR and prefills the address (and amount, where the chain's URI supports it).
- Copy-to-clipboard for desktop or a separate-device wallet.
- The on-screen address and the encoded address are the same watched address.
Live status: the page tracks the payment as it lands
Scanning is only half the experience. The other half is the customer knowing their money arrived, without refreshing or emailing support. While the invoice is open the page shows a waiting state with the QR, the address, and a countdown to expiry. The moment halfin sees a matching transaction on-chain, the page moves to a detected-and-confirming state, then to confirmed once the per-chain confirmation threshold is met. The customer watches the payment land in place.
That live status is driven by the same on-chain gates that back every halfin rail, not by the customer's wallet reporting success. Crediting is reorg-aware and each chain applies its own confirmation threshold, so a payment the page calls confirmed has actually settled under that chain's rules — it isn't an optimistic guess that a later reorg could undo.
If a wallet hiccups mid-payment, the QR and address are still on the page to re-scan, and the same address keeps accepting the deposit. A customer who closes the tab and comes back to the still-open invoice finds the same code waiting. The page reflects the truth on-chain rather than a one-shot state that breaks on a refresh.
| Page state | What it means | What the customer does |
|---|---|---|
| Waiting | Invoice open, no matching payment seen yet | Scan the QR or copy the address and send |
| Detected / confirming | A matching transaction is seen on-chain, gathering confirmations | Nothing — watch the page; the QR can be re-scanned if a wallet failed |
| Confirmed | The per-chain threshold is met; the payment has settled | Continue to the merchant's success page |
| Expired | The invoice window closed before a payment was confirmed | Start over from the merchant if a new invoice is issued |
Which networks the QR encodes
The customer scans a QR for whichever rail they chose from the ones you accept, and halfin encodes the payment in that chain's form. Every network on the page is backed by a real on-chain gate — there is no rail that renders a code but quietly never settles. Stablecoin payments scan exactly like native ones: the QR carries the address for the token's network, and the on-chain gate watches for the token transfer.
The set below is the full range a customer can be offered; you present the subset you've enabled. A network shown on the page is one halfin actually watches and confirms, with reorg-aware crediting under that chain's threshold.
| Network | Native asset | Stablecoins on this network |
|---|---|---|
| Bitcoin | BTC | — |
| Ethereum | ETH | USDT (ERC-20), USDC (ERC-20) |
| Base | ETH | USDC |
| Arbitrum | ETH | — |
| Polygon | native gas token | — |
| BNB Smart Chain | BNB | — |
| Tron | TRX | USDT (TRC-20) |
| XRP Ledger | XRP | — |
| Solana | SOL | USDT (SPL), USDC (SPL) |
You create the invoice; halfin renders the QR
There is nothing extra to call to get the QR. It is part of the hosted page that the invoice already produces. Create an invoice and redirect the customer to the hosted checkout URL in the response — the page shows the QR, the address, and the live status for whatever asset and network the customer picks. The example creates a deferred, fiat-anchored invoice so the payer chooses the rail on the page.
Pass idempotency_key in the request body — a snake_case field, not a header — so a retried create call returns the same invoice (and the same address and QR) instead of minting a second one. The only headers you send are X-API-Key and Content-Type. Treat the QR scan and the page status as the customer's experience, and the HMAC-signed webhook your server receives as the source of truth: verify its signature, then mark the order paid.
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",
"deferred": true,
"description": "Order 10472",
"idempotency_key": "order-10472"
}'
# The response carries a hosted checkout URL on checkout.thehalfin.com.
# That page renders the QR code, the deposit address, and the live
# payment status for the asset and network the payer selects.
# Redirect the customer there, then verify the signed invoice
# webhook on your server before marking the order paid.Where the QR fits, and where it doesn't
QR-code payments are the default experience on the hosted page because a human with a mobile wallet is the most common case. The QR, the address, and the live status are all part of the page halfin hosts — the parent hosted checkout product covers the redirect mechanics, the webhook, and the full invoice lifecycle around them.
If there's no human and no page in the loop — a system paying a system, or a recurring deposit handed out once — you don't need a QR at all. Static deposit addresses and the machine-to-machine settlement flow cover those without a checkout page. And if you need the QR and address to render inside your own product on your own domain, self-hosted checkout draws the same payment surface against the same API, at the cost of building and maintaining the page yourself.