Developers

Developers: build crypto payments on halfin

You found this page because you have to wire crypto payments into something that already exists — an orders table, a ledger, a back office — and you want to know what the integration actually looks like before you commit. The short version: one spec-first REST API at api.thehalfin.com/api/v1, a typed @halfin/sdk-merchant package if you write TypeScript, idempotency keys so retries are safe, API keys scoped so a reporting job can't move money, HMAC-signed webhooks for settlement state, full reference at docs.thehalfin.com, and a sandbox you turn on in the dashboard. This page is the map; the docs are the territory.

01

The 30-second mental model

halfin sits between your application and a stack you would otherwise have to build per chain: nodes, fee estimation, address management, a confirmation watcher, and reorg handling. You talk to it over plain HTTP. You create an invoice, read its state, register a deposit address, or queue a payout, and the platform owns node access, per-chain confirmation thresholds, and reorg-aware crediting. Your code stays small — build a request, send it, react to the webhook when settlement state changes.

There is one public API, and it is the same surface the merchant dashboard and the hosted checkout are built on. You never hit a wall where an integration has to fall back to clicking through a UI because the API lagged behind an internal one. What the dashboard does, your code can do.

Customer-facing payment UI is not your concern unless you want it to be: hosted checkout renders the invoice for the payer with a short-lived per-invoice token, so your server-side API key never reaches the browser. If you do want to render the payment page yourself, self-hosted checkout drives against the same invoice object.

02

Start here: the path from zero to a paid invoice

A first integration is almost always server-side invoice creation. Your backend quotes an order, asks halfin for an invoice, hands the payer a link or your own page, and then waits for a signed webhook instead of polling. The five steps below are the spine of that flow; everything else is detail you add once it works end to end.

Do this against the sandbox first (see below), with a test API key, so a bug in step three doesn't move real money. Once the happy path is green, harden it: add idempotency keys to the create calls, verify the webhook signature before you act, and make your handler idempotent so a redelivered event is a no-op.

  • Get an API key from the dashboard and store it in a secret manager, never in source control.
  • Create an invoice over POST /api/v1/invoices with the X-API-Key header and your fiat amount.
  • Present the invoice — redirect to the hosted checkout, or render your own against the same object.
  • Receive the signed webhook when the invoice is paid; verify the HMAC signature first, then act.
  • Reconcile against the fiat amount you billed, and react to underpaid / overpaid / expired the same way.
03

Create an invoice: fiat-anchored or fixed-asset

The most common request is a fiat-anchored invoice: you price the order in a currency your accounting understands, and the payer settles in whatever supported asset they hold. Send amount_fiat and fiat_currency, mark it deferred so the payer picks the asset at activation, and halfin locks the crypto amount at the rate in force when the invoice goes live. The fiat figure is your source of truth; the payable token amount is derived from it and pinned for the life of the invoice.

If you would rather quote a fixed crypto amount — for example billing exactly 0.01 BTC — send amount plus the crypto currency code instead. The two shapes are mutually exclusive: amount_fiat + fiat_currency, or amount + a crypto currency such as BTC, ETH, or SOL. USD is never a value of the currency field; it is only ever a fiat_currency. Every monetary value crosses the wire as a string, never a float, because a fraction of a unit lost to binary rounding is a real reconciliation defect. The full request and response schema lives at docs.thehalfin.com — this is the minimal shape.

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": "Pro plan — March",
    "idempotency_key": "order-8f3a2c-2026-06-10"
  }'

# Fixed-asset variant — quote an exact crypto amount instead:
#   -d '{ "amount": "0.01", "currency": "BTC", "description": "Pro plan — March" }'
# The two shapes are mutually exclusive. See docs.thehalfin.com for the full schema.
04

The typed SDK: @halfin/sdk-merchant

If you work in TypeScript, the SDK saves you from hand-writing request shapes and re-reading the spec for field names. @halfin/sdk-merchant is generated from the same OpenAPI definition as the server, so the types you import are the types the API enforces. Your editor autocompletes the request body, flags a wrong field at compile time, and types the response so you are not casting JSON by hand.

It is a thin, typed client over the same REST endpoints — it does not hide the API or invent behavior on top of it, so anything you read in the docs maps directly to a call. Build a client once, then pass it to the operation functions, which take the same body fields you would send over raw HTTP. When the spec gains a new operation or field, regenerating the SDK brings it into your types; a removed field or a renamed operation surfaces as a type error rather than a runtime surprise. Any other language calls the REST API directly — the SDK is a convenience for TypeScript projects, not a requirement.

import { createHalfin, createInvoice } from "@halfin/sdk-merchant";

const client = createHalfin({ apiKey: process.env.HALFIN_API_KEY });

const { data, error } = await createInvoice({
  client,
  body: {
    amount_fiat: "49.00",
    fiat_currency: "USD",
    deferred: true,
    description: "Pro plan — March",
  },
});

if (error) throw error;
console.log(data.id, data.status);
05

Three primitives that keep the integration safe

Most of the correctness work in a payment integration is not the happy path — it is the failure modes around it. halfin gives you three primitives that handle the dangerous parts, and using all three is what separates a demo from something you can take real money on.

Idempotency keys make retries safe. A create call times out, your client retries, and without protection you cannot tell whether the first request created an invoice or died before the server saw it. Send an idempotency key derived from your own order or batch identifier; the same key with the same body returns the original outcome instead of a duplicate, and a replay with a different body is rejected rather than silently doing the wrong thing. That is what makes the API safe to call from a queue worker, a cron job, or a serverless function with at-least-once delivery.

Scoped API keys keep a leak small. A reporting export needs to read balances; it has no business creating a payout. Mint a key for exactly what an integration does and nothing more — a read-only reporting key, a payments key for the storefront, a separate payouts key for finance — each with its own blast radius, each rotatable without disturbing the others. Treat keys like database roles: minimum rights for the job in front of each service.

Signed webhooks tell you what changed. Polling an invoice until it flips to paid wastes requests and still lags real settlement. halfin pushes an HMAC-signed event instead; you verify the signature, then act. The order is absolute — verify first, treat the event as a business fact second — because an unsigned or wrongly signed request is not a halfin event and must never credit an order or release goods.

  • Idempotency keys — derive from a stable business identifier; the same key returns the same result, never a duplicate.
  • Scoped API keys — least privilege per integration, rotated and revoked independently.
  • Signed webhooks — verify the HMAC before any business action; make handlers idempotent against redelivery.
06

The webhook events you will handle

The events you receive are a fixed, documented set — six of them. Build your handler as a switch over the event type, verify the signature before the switch, and make each branch idempotent, because the same event may be delivered more than once and a correct handler treats a duplicate as a no-op. Return quickly and push slow work to a background job so a delivery is never held open.

Only one of these should release goods. invoice.paid fires after on-chain confirmations reach the per-chain threshold and crediting is reorg-aware, so it represents money that actually held. Earlier signals are useful for keeping the payer informed but are not a settled outcome. Hinge fulfillment on paid; treat the rest as state your own records should reflect.

EventWhat it meansWhat your handler typically does
invoice.confirmingInvoice went live; the rate is locked and the payment window is open.Show the payer the address, amount, network, and countdown; start a timer.
invoice.paidConfirmations met the per-chain threshold; the billed amount settled.Fulfill the order — this is the only event that should release goods.
invoice.underpaidA deposit arrived but fell short of the expected amount.Request a top-up or settle partially under your own policy.
invoice.overpaidA deposit exceeded the expected amount.Record the excess and reconcile or refund per your policy.
invoice.expiredThe payment window elapsed before a sufficient payment arrived.Re-issue at the current rate if the customer still wants to pay.
payout.completedAn approved payout settled on-chain to its destination.Mark the disbursement done in your own ledger.
07

Test in the sandbox before you touch live funds

You do not develop against real money. The dashboard gives you sandbox access with test API keys, so you can create invoices, drive them through their lifecycle, and exercise your webhook handler without a single on-chain transfer mattering. Wire your integration end to end there first — create, present, receive the signed event, reconcile — and only then point a separate set of live keys at production.

Keep the two worlds apart deliberately. Use distinct keys per environment so a test key can never reach live funds, and store both in a secret manager rather than in source control. When you flip to live, nothing about your code changes except which key it loads — the contract is identical, which is the point of testing against it.

Reach for the docs whenever the shape of a field matters: docs.thehalfin.com carries the full request and response schema, the interactive API reference, and the complete webhook envelope. This page tells you the order to do things in; the docs tell you the exact bytes.

  • Sandbox + test API keys come from the dashboard — develop the whole flow there first.
  • Separate keys per environment; a test key must never touch live funds.
  • Going live changes the key you load, not the contract — the API is the same in both.
  • Full schema, interactive reference, and webhook envelope live at docs.thehalfin.com.