What a halfin payment link actually is
A payment link is the simplest way to get paid: a URL the customer opens, sees an amount and a way to pay, and pays. There is no dedicated 'payment link' object in halfin and you do not need one. When you create an invoice, the response carries a hosted checkout URL on checkout.thehalfin.com. That URL is your payment link. Sharing it by any channel — an email, a Telegram message, a button on a page you already own, a QR on a printed invoice — is the entire mechanism.
This matters because it keeps one model behind every way you collect money. The link you paste into a chat and the checkout a customer reaches from your storefront are the same invoice underneath, with the same lifecycle, the same rate lock, and the same signed webhook events. You are not learning a second system to support a one-off request; you are reusing the invoice you already understand.
It also means a link is single-purpose by design. Each invoice is one amount, for one payment, with one expiry. A payment link is not a reusable 'pay me anything' page — it is a specific request for a specific amount, which is exactly what you want when you are billing a named customer for a named thing.
- A payment link is an invoice's hosted checkout URL on checkout.thehalfin.com — there is no separate link object.
- Share it however you like: email, chat, a button's href, a QR code on a PDF.
- One link = one invoice = one amount and one expiry; it is a request, not a standing storefront.
- The customer pays on the hosted page; you never write wallet code to support the link.
Step 1 — Create the invoice that backs the link
Everything starts with one authenticated call to POST /api/v1/invoices. You send only two headers — X-API-Key for your scoped key and Content-Type: application/json — and a small JSON body. The response hands you back an invoice whose hosted checkout URL is the link you will share. Include an idempotency_key in the body so that a retried request (a timeout, a double-click on your own 'generate link' button) never creates a second invoice for the same payment.
You create the invoice one of two ways, depending on whether you price in crypto or in fiat. If you want to charge a fixed asset amount — exactly 25 USDT, say — send currency (the crypto, network included) plus amount. If you price in dollars or euros and want the customer to settle the equivalent in whatever asset they hold, send amount_fiat plus fiat_currency plus deferred: true; halfin locks the conversion rate when the invoice activates and pins the payable token amount for the life of that invoice. Add a description so the customer knows what they are paying for, and optionally a redirect_url so the page sends them back to you after payment.
The fiat-anchored variant is the more common one for a shared link, because the customer chooses the asset on the checkout page rather than you having to ask which coin they hold before you generate the URL. The fixed-asset variant is right when you are billing a price that is genuinely denominated in a token.
| Field | Required when | What goes in it |
|---|---|---|
| currency | Fixed-asset link | The crypto asset + network you bill in, e.g. USDT_TRC20. Pair with amount. |
| amount | Fixed-asset link | Decimal amount as a string, e.g. "25.00". Never a number. |
| amount_fiat | Fiat-anchored link | The fiat figure you bill, as a string, e.g. "49.00". Pair with fiat_currency + deferred. |
| fiat_currency | Fiat-anchored link | The fiat anchor, e.g. USD or EUR. The customer settles the equivalent in any supported asset. |
| deferred | Fiat-anchored link | true — defers asset selection to the customer on the hosted page. |
| description | Recommended | What the customer is paying for; shown on checkout, e.g. "Invoice 1042 — design retainer". |
| redirect_url | Optional | Where checkout returns the customer after they pay, e.g. your thank-you page. |
| idempotency_key | Always | Deterministic per payment so a retried create returns the same invoice, not a duplicate. |
# Create a fiat-anchored invoice; its hosted checkout URL is the payment link.
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": "Invoice 1042 — design retainer",
"redirect_url": "https://yourstore.example/thanks",
"idempotency_key": "invoice-1042"
}'
# The response carries a hosted checkout URL on checkout.thehalfin.com.
# That URL is the link you share. See docs.thehalfin.com for the full schema.Step 2 — Share the hosted checkout URL
Read the hosted checkout URL out of the create response and put it wherever the customer will see it. There is nothing to host, embed, or style on your side — the URL points at checkout.thehalfin.com, which renders the amount, a QR code and address, the networks you accept, and the live payment status. Your job ends at delivering the link.
How you deliver it is up to the channel. In an email or a chat message, paste the URL as plain text or behind 'Pay now'. On a page you control, set it as a button's href so a click opens the page in a full-page redirect or a new tab. On a printed or PDF invoice, encode the same URL as a QR code so a phone camera lands the customer straight on checkout. The link is just a string, so every one of these is the same underlying move.
Two practical notes. First, the invoice has an expiry — the rate is only locked for a bounded window — so a link is best generated close to when you send it, not weeks ahead. If a customer comes back after expiry, create a fresh invoice at the current rate and send the new URL. Second, treat the link as you would any payment request: it is specific to one customer and one amount, so do not post a single link to a public broadcast channel expecting many people to pay it.
- Email / chat: paste the URL, or hide it behind a 'Pay now' label.
- On your own page: set the URL as a button or anchor href — full-page redirect or new tab.
- Print / PDF: encode the URL as a QR code so a phone lands on checkout.
- Links expire with the invoice — generate close to send time; re-issue at the current rate if it lapses.
Step 3 — What the customer sees and does
When the customer opens the link, checkout.thehalfin.com shows them the request: the amount, a description if you set one, and the ways they can pay. On a fiat-anchored link they pick the asset and network they hold — USDT on Tron, USDC on Base, BTC, ETH, SOL, and the other supported rails — and the page shows the exact token amount to send, the receive address, a QR code, and a countdown to expiry. On a fixed-asset link the asset is already chosen; the page shows the address and amount for that one rail.
The customer sends the payment from their own wallet or exchange. The hosted page tracks it live: it reflects when a matching deposit appears on-chain, while confirmations accumulate toward the per-chain threshold, and when the payment is finally credited. You wrote none of that wallet logic — halfin's checkout owns the address display, the QR, the status, and the reorg-aware crediting behind it.
If you set a redirect_url, checkout sends the customer back to your page after they pay, which is good for the customer's experience — they land on your thank-you screen. But the redirect is a convenience for the human, not a source of truth for your backend. A customer can close the tab, lose signal, or never follow the redirect, and the payment is still real. That is why confirmation lives in the next step, not here.
Step 4 — Confirm on the webhook, not on the redirect
The link gets the customer to pay; the webhook tells your system it happened. Point a webhook endpoint at your server and act on the events the invoice emits — never on the fact that a browser hit your redirect_url, which you cannot trust and which may never fire. The event you most care about is invoice.paid: it fires when the invoice is fully settled under the chain's confirmation threshold, with reorg-aware crediting, meaning the money actually held. That is the signal to mark the order paid, send the receipt, or unlock whatever the customer bought.
Verify the HMAC signature over the raw request bytes with a constant-time compare before you act on any event. An unsigned or mismatched request is not a halfin event and must never touch your records. Once verified, look up the invoice by its identifier and update your own database. Keep the handler idempotent — a webhook can be redelivered, and a second invoice.paid for the same invoice must not fulfil the order or email the customer twice.
invoice.paid is the happy path, but a shared link meets the same real-world messiness as any invoice, and each case has its own event. The customer might send slightly too little or too much, pay after the window closed, or trigger a deposit that a chain reorganization later unwinds. Handle the events you care about and ignore the rest; the table below maps the common ones to what they mean for a link you sent.
| Event | What it means | What you do |
|---|---|---|
| invoice.confirming | A matching deposit appeared and confirmations are accumulating. | Optional: show 'payment in flight'. Do not fulfil yet. |
| invoice.paid | Fully settled under the per-chain threshold; the money held. | Fulfil the order, send the receipt, mark paid — once. |
| invoice.underpaid | The customer sent less than the amount due. | Record the shortfall; request a top-up or settle partially per your policy. |
| invoice.overpaid | The customer sent more than the amount due. | Record the excess so it is visible and accountable; handle per your policy. |
| invoice.expired | The payment window elapsed before a sufficient payment arrived. | Re-issue a fresh invoice at the current rate and send the new link. |
| invoice.deposit_reversed | A previously seen deposit was unwound by a chain reorganization. | Reverse anything you provisionally granted; the invoice is not paid. |
When a payment link is the right tool — and when it is not
A shared payment link is at its best for one-to-one billing: an invoice to a named client, a deposit request, a per-conversation 'pay here' in a support thread, a printed bill a customer scans. The customer needs nothing but the URL and a wallet, and you need nothing but the create call and a webhook handler. There is no storefront to build and no checkout page to maintain.
It is the wrong tool when payment is part of a flow you already render. If a customer is mid-checkout on your own site, you do not want to hand them a loose link — you want to create the invoice server-side and redirect into the hosted checkout as a step in your flow, which is the same primitive used a little more tightly. And if you need the payment UI to live inside your own pages with your own branding, that is self-hosted checkout against the same API, not a shared link.
The throughline across all three — a pasted link, a redirect from your storefront, a self-rendered page — is that they create the same invoice and confirm on the same signed webhook. Pick the lightest one that fits the moment. For 'I just need this person to pay me in crypto', the link is the lightest thing there is.