Products

TypeScript SDK reference: @halfin/sdk-merchant

If your backend is TypeScript, you should not be hand-typing request bodies and re-reading the spec for field names. @halfin/sdk-merchant is a thin, typed client over the same REST API your raw HTTP calls would hit — createInvoice, createPayout, and the rest take exactly the body fields the endpoint takes, idempotency_key included, and the types are generated from the same OpenAPI definition the server enforces. Your editor autocompletes the request, the compiler catches a wrong field before you ship, and the full reference lives at docs.thehalfin.com.

01

A typed client, not a second API

The SDK does not invent behavior on top of the API or hide it behind a friendlier abstraction. It is a transport-thin wrapper: every function maps one-to-one to a REST operation, takes the same JSON body that operation takes, and returns the same response shape. Anything you can read on the endpoint in the docs maps directly to a call here — there is no SDK-only feature and no endpoint the SDK papers over.

That matters because it keeps the SDK honest. When you debug a call, you are debugging the same request a curl command would send; the network tab shows the same POST to api.thehalfin.com/api/v1. The SDK's whole job is to put the request and response shapes into your type system, so the contract is checked at compile time instead of discovered at runtime.

If you do not work in TypeScript, you lose nothing by skipping it. The REST API is the product; the SDK is a convenience for TypeScript projects. Any language can call the endpoints directly with an X-API-Key header — the SDK just gives TS callers autocompletion and a red squiggle on a mistyped field.

02

Generated from the same spec the server runs

The types you import are not hand-maintained — they are generated from the same OpenAPI specification that produces the server handlers and the documentation. The spec is the source of truth, and the change flows outward from it: when an operation gains a field, the regenerated SDK gains it in the types; when an operation is renamed or a response field is removed, the regenerated SDK surfaces that as a type error on the lines that used it.

The practical payoff is that the SDK cannot quietly drift from the live endpoint. A field that exists in your editor's autocomplete exists on the wire, because both come from the same definition. You are never reconciling a hand-written client against a doc page that lagged behind a deploy — a class of integration bug that simply does not arise here.

Upgrading is therefore a typecheck, not an archaeology project. Bump the package, run tsc, and the compiler walks you to every call site a contract change touched. A breaking change shows up as a build failure in your code rather than a 4xx in production.

03

The shape of a call

Every operation follows the same calling convention. You build a client once with createHalfin, passing your API key, and then pass that client to the operation functions alongside a typed body. The functions return a result object with data on success and error on failure, so you branch on error rather than wrapping every call in try/catch — though throwing on error works fine too.

createInvoice and createPayout are the two you reach for first. createInvoice takes the same invoice fields as the REST endpoint — a fiat-anchored amount_fiat plus fiat_currency, or a fixed amount plus crypto currency, and an idempotency_key in the body. createPayout takes a destination, an amount and asset, and its own idempotency_key. Read operations — fetching an invoice, listing payouts, reading a balance — take the identifier or filter and return the typed resource.

The fields below are the ones you actually pass; the SDK types the rest. There is no Idempotency-Key header anywhere in this — idempotency is a snake_case body field on the create calls, exactly as it is over raw HTTP.

SDK functionREST operationKey body fieldsIdempotency
createInvoicePOST /v1/invoicesamount_fiat + fiat_currency (or amount + currency)idempotency_key in body
createPayoutPOST /v1/payoutsdestination, amount, currencyidempotency_key in body
getInvoiceGET /v1/invoices/{id}invoice idn/a (read)
listPayoutsGET /v1/payoutsstatus / paging filtersn/a (read)
04

Create an invoice with the SDK

Here is the canonical first integration: a server-side fiat-anchored invoice. You build the client with your API key from the environment, call createInvoice with the same body you would POST over HTTP, and branch on the returned error. amount_fiat is a string — monetary values are strings end to end across the API so they never pick up a floating-point rounding error — and idempotency_key goes in the body, tied to your own order identifier, so a retried call returns the original invoice instead of creating a duplicate.

The crypto amount is locked against your fiat anchor when the invoice activates, and the response carries the invoice id and current status. From there you redirect to hosted checkout or render your own page against the same invoice, and wait for the state change over a webhook rather than polling.

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",
    // snake_case body field, NOT an Idempotency-Key header —
    // a retry with the same key returns this same invoice
    idempotency_key: "order-8f3a2c-2026-06-10",
  },
});

if (error) throw error;
console.log(data.id, data.status); // typed from the OpenAPI spec
05

Queue a payout the same way

createPayout mirrors the invoice flow: same client, a typed body, and an idempotency_key derived from your batch or transfer identifier. The idempotency guarantee is what makes the SDK safe to call from a queue worker or a serverless function with at-least-once delivery — a retried createPayout carrying the same key returns the original payout rather than sending a second one to the destination.

A created payout does not move funds on its own. It enters a pending-approval state and is released from the dashboard by an operator with the right permission — the SDK queues the payout; a human (or a separately scoped service) approves it. Mass payouts are a fan-out over the same single-payout endpoint, each carrying its own idempotency_key, so resubmitting an interrupted run settles only the legs that did not already go out. There is no batch endpoint to learn; you loop createPayout with stable keys.

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

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

const { data, error } = await createPayout({
  client,
  body: {
    destination: "TXk...payeeAddress",
    amount: "250.00",
    currency: "usdt_tron",
    idempotency_key: "settlement-2026-06-10-supplier-42",
  },
});

if (error) throw error;
// payout is queued in pending-approval; it is released
// from the dashboard, not auto-sent by this call
console.log(data.id, data.status);
06

Webhooks: the SDK is one side of the loop

The SDK creates and reads resources; settlement state comes back to you over webhooks, not over the SDK. When an invoice or payout changes state, halfin sends an HMAC-signed event to your endpoint — invoice.confirming, invoice.paid, invoice.overpaid, invoice.underpaid, invoice.expired, balance.credited, payout.completed, payout.failed, among others. Verify the signature before you read the body as a business fact: an unsigned or wrongly signed request is not a halfin event and must never credit an order or release a payout.

Once a signature verifies, treat the event as the trigger for your own state transition and keep the handler idempotent so a redelivered event does not double-process. A clean integration is the SDK on the write side and verified webhooks on the read side: you createInvoice, and the invoice.paid event — not a polling loop — tells you to fulfill. The event names and payload schemas are documented alongside the SDK at docs.thehalfin.com.

  • The SDK does not deliver webhooks — your endpoint receives the signed event stream.
  • Verify the HMAC signature before acting; reject anything that does not verify.
  • Real events include invoice.confirming, invoice.paid, invoice.overpaid, invoice.underpaid, invoice.expired, payout.completed, payout.failed — there is no invoice.activated event.
  • Keep handlers idempotent; events can be redelivered.
07

The full reference lives in the docs

This page covers the shape and the two calls you start with; it is not the exhaustive method list. Because the SDK and the documentation are generated from the same OpenAPI spec, the complete reference — every operation, every field, every response shape, and the webhook event catalog — lives at docs.thehalfin.com and stays in lockstep with the package. When in doubt about a field name or a response type, the docs and your editor's autocomplete agree, because they read the same source.

Treat docs.thehalfin.com as the canonical method reference and this page as the orientation. The SDK is one way into the API integration surface; the underlying REST contract, idempotency model, and scoped API keys are the same whether you call them through @halfin/sdk-merchant or over raw HTTP.