Products · API integration

Crypto payment API integration

You are not buying a button on a checkout page — you are wiring a payment layer into a product that already has its own orders, ledger, and back office. halfin exposes that layer as a spec-first REST API at api.thehalfin.com/api/v1, with idempotency keys so retries are safe, scoped API keys so a read-only accounting integration can never move money, and a typed @halfin/sdk-merchant package so your editor knows the shape of every request before you ship it.

01

The integration problem this solves

Adding crypto acceptance usually means stitching together a node, a fee estimator, an address manager, a confirmation watcher, and a reconciliation job per chain — then doing it again for the next chain. Each of those is a place where a dropped connection, a reorg, or a duplicated retry turns into a credited-twice invoice or a lost deposit. The cost is not the happy path; it is the long tail of failure modes you have to get right before you can take real money.

halfin moves that surface behind one HTTP contract. You create an invoice, read its state, register a deposit address, or queue a payout with a normal JSON request, and the platform owns node access, per-chain confirmation thresholds, and reorg-aware crediting. Your integration code stays small: build a request, send it, handle the response, and react to the webhook when settlement state changes.

The API is the same surface the merchant dashboard and the hosted checkout are built on — there is no second-class public API that lags behind an internal one. What you can do in the dashboard, you can do over the API, which means an integration never hits a wall where it has to fall back to clicking through a UI.

02

Spec-first REST at api.thehalfin.com/api/v1

The API is described by a public OpenAPI specification, and that spec is the source of truth — the server handlers, the generated SDK, and the documentation at docs.thehalfin.com all derive from it. When an operation changes, the change starts in the spec and propagates outward, so the contract you read is the contract the server enforces. There is no drift between a hand-written doc page and the live endpoint.

Operations are grouped by audience. Merchant-facing endpoints — invoices, deposit addresses, payouts, balances, conversions, refunds — authenticate with an API key. The customer-facing checkout endpoints use a short-lived token tied to a single invoice, so your server-side key never touches the browser. You integrate against the merchant surface; the hosted or self-hosted checkout handles the customer side for you.

Every monetary value crosses the wire as a decimal string, never a floating-point number, because a fraction of a satoshi lost to binary rounding is a real reconciliation defect. Treat amounts as strings end to end, and if you need arithmetic, use a decimal library rather than JavaScript's native number type.

  • Base URL: api.thehalfin.com/api/v1 (the dashboard origin dashboard.thehalfin.com/api resolves to the same API).
  • Authentication: X-API-Key header on every merchant request.
  • Money fields: decimal strings, per-currency precision — never parse them as floats.
  • Documentation and an interactive reference live at docs.thehalfin.com.
03

Idempotency keys make retries safe

Networks fail mid-request. A payout call times out, your client retries, and now you cannot tell whether the first request created a payout or died before the server saw it. Without protection, the safe-feeling retry is exactly what double-pays a supplier.

halfin takes an idempotency key as a field on the request body of state-changing operations — `idempotency_key` on invoice and payout creation. Set a unique value, typically derived from your own order or batch identifier, and the platform records the result against that key. If the same key arrives again with the same body, you get the original outcome back instead of a second invoice or a second payout. Send the same key with a different body and the request is rejected as an idempotency_key_mismatch rather than silently doing the wrong thing.

This is what makes the API safe to call from a queue worker, a cron job, or a serverless function with at-least-once delivery. Derive the key from a stable business identifier rather than a fresh random value per attempt, so the retry of a specific operation carries the same key the original did. Mass payouts extend the same guarantee to a whole batch, so resubmitting an interrupted batch settles only the payouts that did not already go out.

04

Scoped API keys and least privilege

Not every integration should be able to move money. An accounting export needs to read balances and list invoices; it has no business creating a payout. A storefront backend needs to create invoices and read their state, but does not need to approve withdrawals. Handing every integration one all-powerful key turns a single leaked credential into a treasury incident.

API keys carry scoped permissions, so you mint a key for what an integration actually does and nothing more. A read-only reporting key, a payments key for the storefront, and a separate payouts key for the finance service can coexist, each with its own blast radius. Rotate or revoke one without disturbing the others.

Pair scopes with the platform's own controls — payout approval, operation logging, and audit history live alongside the API — so that even an authorized key operates inside guardrails. halfin's custody posture is exactly this: signing, scoped permissions, and an audit trail, not a vault guarantee. Design your key layout the way you would design database roles: minimum rights for the job in front of each service.

IntegrationNeeds to readNeeds to writeKey scope
Accounting / reporting exportInvoices, balancesNothingRead-only
Storefront backendInvoice stateCreate invoicesInvoicing
Finance / treasury serviceBalances, payoutsCreate + approve payoutsPayouts
Reconciliation workerInvoices, payouts, eventsNothingRead-only
05

Create an invoice over the API

A typical first integration is server-side invoice creation: your backend quotes an order in fiat, asks halfin for an invoice, and hands the customer a payment link or renders your own checkout against it. The request below anchors the order to a fiat amount with `amount_fiat` and `fiat_currency`; halfin locks the crypto amount at the rate in force when the invoice activates and returns an invoice you can poll or, better, wait for over a webhook. (If you would rather quote a fixed crypto amount, send `amount` plus the crypto `currency` instead — the two are mutually exclusive.)

Put `idempotency_key` in the body, tied to your order, so a retried request returns the same invoice instead of creating a duplicate. The response carries the invoice identifier and its current state; from there you either redirect to the hosted checkout or drive your own self-hosted checkout against the same invoice.

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": "129.00",
    "fiat_currency": "USD",
    "description": "Pro plan — annual",
    "external_id": "order-8f3a2c",
    "idempotency_key": "order-8f3a2c-2026-06-10"
  }'
06

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.

The SDK 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 can read in the docs maps directly to a call. You build a client once with createHalfin, then pass it to the operation functions, which take the same body fields you would send over raw HTTP — idempotency_key included. When the spec gains a new operation or field, regenerating the SDK brings it into your types; a removed field or a renamed operation shows up as a type error rather than a runtime surprise.

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: "129.00",
    fiat_currency: "USD",
    description: "Pro plan — annual",
    external_id: "order-8f3a2c",
    idempotency_key: "order-8f3a2c-2026-06-10",
  },
});

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

React to settlement with webhooks, not polling

Polling an invoice until it flips to paid wastes requests and still lags real settlement. The platform pushes the change instead: when an invoice is detected, confirmed, paid, or expires — or when a payout completes — halfin sends an HMAC-signed event to your endpoint. You verify the signature, then act.

The order matters. Verify the signature first, before you read the body as a business fact, because an unsigned or wrongly signed request is not a halfin event and must not credit an order or release goods. Once verified, treat the event as the trigger for your own state transition, and keep handlers idempotent so a redelivered event does not double-process. Webhooks are the spine of a clean integration: invoices, payouts, and reconciliation all hang off the same signed event stream.

  • Events are HMAC-signed — verify the signature before taking any business action.
  • Make handlers idempotent; an event can be redelivered.
  • Return quickly and do slow work asynchronously so deliveries are not held open.
  • Use webhooks as the source of truth for state changes; poll only as a fallback.