# halfin API

Source: https://docs.halfin.xyz/
Markdown: https://docs.halfin.xyz/md

<div className="api-docs-shell">
  <nav className="api-docs-nav" aria-label="API documentation sections">
    <a href="#introduction">
      Introduction
    </a>

    <a href="#integration">
      Integration
    </a>

    <a href="#gates">
      Gates
    </a>

    <a href="#self-checkout">
      Self-Checkout
    </a>

    <a href="#limits-and-quotas">
      Limits and Quotas
    </a>

    <a href="#webhooks">
      Webhooks
    </a>

    <a href="#changelog">
      Changelog
    </a>

    <a href="#api-methods">
      API Methods
    </a>
  </nav>

  <div className="api-docs-content">
    ## Introduction

    halfin exposes a merchant API for payment invoices, hosted checkout, reusable deposit addresses, merchant balances, payouts, rates, supported currencies, and webhook event schemas.

    The public API base URL is:

    ```text
    https://dashboard.halfin.xyz/api
    ```

    Use `sk_test_...` keys for isolated test data and `sk_live_...` keys for live funds. The environment is part of the API key; merchant API requests do not switch environments with a different public base URL.

    ## Integration

    ### Getting access

    1. Create a merchant account in the dashboard.
    2. Create a test API key under **Settings -> API Keys**.
    3. Configure a webhook endpoint and save the webhook secret.
    4. Create a first invoice with an idempotency key.
    5. Fulfill only after a verified webhook or a server-side invoice status check.

    ### Making a request

    Merchant endpoints use JSON request bodies and JSON responses.

    ```bash
    curl -X POST https://dashboard.halfin.xyz/api/v1/invoices \
      -H "Content-Type: application/json" \
      -H "X-API-Key: $HALFIN_API_KEY" \
      -d '{
        "currency": "BTC",
        "amount": "0.01000000",
        "description": "Order #0001",
        "idempotency_key": "order-0001"
      }'
    ```

    The response envelope contains `data` on success and `meta.request_id` for support and logs.

    ```json
    {
      "data": {
        "id": "00000000-0000-0000-0000-000000000001",
        "currency": "BTC",
        "amount_requested": "0.01000000",
        "status": "pending",
        "checkout_url": "https://checkout.halfin.xyz/pay/00000000000000000000000000000000",
        "expires_at": "2026-06-01T12:30:00Z",
        "created_at": "2026-06-01T12:00:00Z"
      },
      "meta": {
        "request_id": "req_000000000001"
      }
    }
    ```

    ### Authentication

    | Surface               | Authentication                                       | Notes                                                                   |
    | --------------------- | ---------------------------------------------------- | ----------------------------------------------------------------------- |
    | Merchant endpoints    | `X-API-Key: sk_test_...` or `X-API-Key: sk_live_...` | Required for invoices, payouts, balances, ledger, and static addresses. |
    | Public data endpoints | None                                                 | Rates and currencies are public read endpoints.                         |
    | Webhook delivery      | `X-Halfin-Signature`                                 | Verify every received webhook with the raw request body.                |

    There is no merchant request HMAC scheme for ordinary API calls. HMAC is used for webhook verification only.

    ## Gates

    Gate identifiers are the values used by the API and webhook payloads. Limits are stored in the processor gate seeds and can still be restricted per merchant or environment by account settings.

    Call [GET /v1/currencies](/api/public/getCurrencies) for the current live availability.

    ### Production Gates

    | Gate ID         | Asset | Network  | Confirmation mode |    Min |    Max |
    | --------------- | ----: | -------- | ----------------: | -----: | -----: |
    | `bitcoin`       |   BTC | bitcoin  |          3 blocks | 0.0001 |     10 |
    | `ethereum`      |   ETH | ethereum |         12 blocks |  0.001 |    100 |
    | `solana`        |   SOL | solana   |        commitment |   0.01 |  10000 |
    | `ethereum_usdt` |  USDT | ethereum |         12 blocks |      1 | 100000 |
    | `ethereum_usdc` |  USDC | ethereum |         12 blocks |      1 | 100000 |
    | `bsc`           |   BNB | bsc      |         15 blocks |  0.001 |    100 |
    | `bsc_usdt`      |  USDT | bsc      |         15 blocks |      1 | 100000 |
    | `tron`          |   TRX | tron     |         19 blocks |      1 | 100000 |
    | `tron_usdt`     |  USDT | tron     |         19 blocks |      1 | 100000 |
    | `base`          |   ETH | base     |         20 blocks |  0.001 |    100 |
    | `arbitrum`      |   ETH | arbitrum |         20 blocks |  0.001 |    100 |
    | `arbitrum_usdt` |  USDT | arbitrum |         20 blocks |      1 | 100000 |
    | `arbitrum_usdc` |  USDC | arbitrum |         20 blocks |      1 | 100000 |
    | `polygon`       |   POL | polygon  |        128 blocks |    0.1 | 100000 |
    | `polygon_usdt`  |  USDT | polygon  |        128 blocks |      1 | 100000 |
    | `polygon_usdc`  |  USDC | polygon  |        128 blocks |      1 | 100000 |
    | `solana_usdc`   |  USDC | solana   |        commitment |      1 | 100000 |
    | `xrp`           |   XRP | xrp      |         immediate |    0.1 | 100000 |

    ### Test Environment Gates

    Test keys use the same API shape and gate identifiers while keeping invoices, balances, payouts, and webhook deliveries isolated from live funds.

    | Test key gate group | Gate IDs                                                                                                                                   |
    | ------------------- | ------------------------------------------------------------------------------------------------------------------------------------------ |
    | Native assets       | `bitcoin`, `ethereum`, `solana`, `bsc`, `tron`, `base`, `arbitrum`, `polygon`, `xrp`                                                       |
    | Stablecoins         | `ethereum_usdt`, `ethereum_usdc`, `bsc_usdt`, `tron_usdt`, `arbitrum_usdt`, `arbitrum_usdc`, `polygon_usdt`, `polygon_usdc`, `solana_usdc` |

    Call `GET /v1/currencies?environment=test` for the current test-key availability returned by the API.

    ## Self-Checkout

    Redirect customers to the halfin-hosted payment page with the `checkout_url` returned by `POST /v1/invoices`. The customer selects a payment method on the checkout page; no wallet or blockchain integration is required on the merchant side. See the [Hosted Checkout guide](/guides/hosted-checkout) for the full integration steps.

    ## Limits and Quotas

    | Area               | Current contract                                                                                                                                                                                      |
    | ------------------ | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
    | Request throttling | Public production throttling is enforced at the edge/reverse proxy, with service-level buckets where configured. A throttled request returns `429` and can include `Retry-After`.                     |
    | Idempotency        | Invoice creation returns the original invoice on same-key/same-body replay and rejects the same key with a different body. Payout creation with the same idempotency key returns the existing payout. |
    | Pagination         | List endpoints use `limit` and `offset`; the merchant maximum is 100.                                                                                                                                 |
    | API keys           | Test and live keys are isolated. Keep live keys server-side.                                                                                                                                          |
    | Webhooks           | Delivery is at least once; handlers must de-duplicate by stable payload identifiers.                                                                                                                  |
    | Timeouts           | Webhook endpoints should respond quickly with a `2xx`; slow or failing endpoints are retried.                                                                                                         |

    ## Webhooks

    halfin sends webhooks as JSON `POST` requests. Delivery is at least once. `balance.credited` includes a stable `event_id`. Invoice and payout events currently do not include a top-level `event_id`, so make handlers idempotent by the event type plus the invoice, payout, or payment IDs in `data`.

    ### Events

    | Event                      | When it is sent                                              |
    | -------------------------- | ------------------------------------------------------------ |
    | `invoice.confirming`       | A deposit is seen and is waiting for required confirmations. |
    | `invoice.paid`             | The invoice amount is met and confirmations are sufficient.  |
    | `invoice.overpaid`         | The confirmed payment is above the requested amount.         |
    | `invoice.underpaid`        | The confirmed payment is below the requested amount.         |
    | `invoice.expired`          | The payment window expired before payment completion.        |
    | `invoice.invalid`          | The invoice entered an invalid terminal state.               |
    | `invoice.late_deposit`     | Funds arrived after the invoice expired.                     |
    | `invoice.deposit_reversed` | A previously observed deposit was reversed.                  |
    | `invoice.activated`        | A draft/deferred invoice was activated.                      |
    | `invoice.draft_expired`    | A draft/deferred invoice expired before activation.          |
    | `invoice.sent`             | An invoice notification was sent.                            |
    | `balance.credited`         | Merchant balance was credited.                               |
    | `payout.completed`         | A payout completed.                                          |
    | `payout.failed`            | A payout failed.                                             |
    | `test`                     | Dashboard test delivery event.                               |

    For payments with settlement conversion, use `balance.credited` as the fulfillment signal - `invoice.paid` confirms customer payment but balance credit can still be pending.

    ### Signature Verification

    Webhook requests include:

    ```text
    X-Halfin-Signature: t={timestamp},v1={hmac}
    ```

    Compute `HMAC-SHA256(secret, "{timestamp}.{raw_body}")`, compare it to `v1`, and reject timestamps outside a 300 second window.

    ```ts
    import crypto from 'node:crypto';

    export function verifyHalfinWebhook(
      signatureHeader: string,
      rawBody: Buffer,
      secret: string,
      now = Math.floor(Date.now() / 1000),
    ): boolean {
      const parts = Object.fromEntries(
        signatureHeader.split(',').map((part) => part.split('=') as [string, string]),
      );
      const timestamp = Number(parts.t);
      if (!Number.isFinite(timestamp) || Math.abs(now - timestamp) > 300) return false;
      if (!/^[0-9a-f]{64}$/.test(parts.v1 ?? '')) return false;

      const payload = `${timestamp}.${rawBody.toString('utf8')}`;
      const expected = crypto.createHmac('sha256', secret).update(payload).digest('hex');
      const expectedBuffer = Buffer.from(expected, 'hex');
      const providedBuffer = Buffer.from(parts.v1, 'hex');
      if (expectedBuffer.length !== providedBuffer.length) return false;

      return crypto.timingSafeEqual(expectedBuffer, providedBuffer);
    }
    ```

    ### Example Payloads

    ```json
    {
      "event": "invoice.paid",
      "created_at": "2026-06-01T12:05:00Z",
      "data": {
        "invoice_id": "00000000-0000-0000-0000-000000000001",
        "external_id": "order-0001",
        "currency": "BTC",
        "environment": "live",
        "amount_requested": "0.01000000",
        "amount_paid": "0.01000000",
        "status": "paid",
        "paid_at": "2026-06-01T12:05:00Z"
      }
    }
    ```

    ```json
    {
      "event_id": "00000000-0000-0000-0000-000000000102",
      "event": "balance.credited",
      "created_at": "2026-06-01T12:06:00Z",
      "data": {
        "invoice_id": "00000000-0000-0000-0000-000000000001",
        "payment_id": "00000000-0000-0000-0000-000000000002",
        "external_id": "order-0001",
        "outcome": "target_credited",
        "source_amount": "0.01000000",
        "source_currency": "BTC",
        "source_network": "bitcoin",
        "credited_amount": "600.000000",
        "credited_currency": "USDC",
        "credited_network": "ethereum"
      }
    }
    ```

    ### Webhook Schemas

    <APIPage document={"./openapi/openapi.public.yaml"} operations={[]} webhooks={[{"name":"invoice.confirming","method":"post"}]} hasHead={false} />

    <APIPage document={"./openapi/openapi.public.yaml"} operations={[]} webhooks={[{"name":"invoice.paid","method":"post"}]} hasHead={false} />

    <APIPage document={"./openapi/openapi.public.yaml"} operations={[]} webhooks={[{"name":"invoice.overpaid","method":"post"}]} hasHead={false} />

    <APIPage document={"./openapi/openapi.public.yaml"} operations={[]} webhooks={[{"name":"invoice.underpaid","method":"post"}]} hasHead={false} />

    <APIPage document={"./openapi/openapi.public.yaml"} operations={[]} webhooks={[{"name":"invoice.expired","method":"post"}]} hasHead={false} />

    <APIPage document={"./openapi/openapi.public.yaml"} operations={[]} webhooks={[{"name":"invoice.invalid","method":"post"}]} hasHead={false} />

    <APIPage document={"./openapi/openapi.public.yaml"} operations={[]} webhooks={[{"name":"invoice.late_deposit","method":"post"}]} hasHead={false} />

    <APIPage document={"./openapi/openapi.public.yaml"} operations={[]} webhooks={[{"name":"invoice.deposit_reversed","method":"post"}]} hasHead={false} />

    <APIPage document={"./openapi/openapi.public.yaml"} operations={[]} webhooks={[{"name":"invoice.activated","method":"post"}]} hasHead={false} />

    <APIPage document={"./openapi/openapi.public.yaml"} operations={[]} webhooks={[{"name":"invoice.draft_expired","method":"post"}]} hasHead={false} />

    <APIPage document={"./openapi/openapi.public.yaml"} operations={[]} webhooks={[{"name":"invoice.sent","method":"post"}]} hasHead={false} />

    <APIPage document={"./openapi/openapi.public.yaml"} operations={[]} webhooks={[{"name":"balance.credited","method":"post"}]} hasHead={false} />

    <APIPage document={"./openapi/openapi.public.yaml"} operations={[]} webhooks={[{"name":"payout.completed","method":"post"}]} hasHead={false} />

    <APIPage document={"./openapi/openapi.public.yaml"} operations={[]} webhooks={[{"name":"payout.failed","method":"post"}]} hasHead={false} />

    <APIPage document={"./openapi/openapi.public.yaml"} operations={[]} webhooks={[{"name":"test","method":"post"}]} hasHead={false} />

    ## Changelog

    | Date       | Change                                                                                                      |
    | ---------- | ----------------------------------------------------------------------------------------------------------- |
    | 2026-05-27 | Documentation rebuilt as a single-page API reference. No API behavior changed in this documentation update. |
    | 2026-05-13 | Public OpenAPI reference published for merchant endpoints, public data endpoints, and webhook schemas.      |

    ## API Methods

    ### Method Overview

    | Group            | Method | Path                                 | Auth    |
    | ---------------- | ------ | ------------------------------------ | ------- |
    | Public Data      | GET    | `/v1/rates`                          | None    |
    | Public Data      | GET    | `/v1/currencies`                     | None    |
    | Balances         | GET    | `/v1/balances`                       | API key |
    | Balances         | GET    | `/v1/balances/{currency}/ledger`     | API key |
    | Invoices         | POST   | `/v1/invoices`                       | API key |
    | Invoices         | GET    | `/v1/invoices`                       | API key |
    | Invoices         | GET    | `/v1/invoices/{invoiceID}`           | API key |
    | Invoices         | POST   | `/v1/invoices/{invoiceID}/cancel`    | API key |
    | Static Addresses | POST   | `/v1/addresses`                      | API key |
    | Static Addresses | GET    | `/v1/addresses`                      | API key |
    | Static Addresses | GET    | `/v1/addresses/{addressID}`          | API key |
    | Static Addresses | GET    | `/v1/addresses/{addressID}/invoices` | API key |
    | Payouts          | POST   | `/v1/payouts`                        | API key |
    | Payouts          | GET    | `/v1/payouts`                        | API key |
    | Payouts          | GET    | `/v1/payouts/{payoutID}`             | API key |
    | Payouts          | POST   | `/v1/payouts/{payoutID}/cancel`      | API key |

    ### Public Data

    <APIPage document={"./openapi/openapi.public.yaml"} operations={[{"path":"/v1/rates","method":"get"}]} webhooks={[]} hasHead={false} />

    <APIPage document={"./openapi/openapi.public.yaml"} operations={[{"path":"/v1/currencies","method":"get"}]} webhooks={[]} hasHead={false} />

    ### Balances

    <APIPage document={"./openapi/openapi.public.yaml"} operations={[{"path":"/v1/balances","method":"get"}]} webhooks={[]} hasHead={false} />

    <APIPage document={"./openapi/openapi.public.yaml"} operations={[{"path":"/v1/balances/{currency}/ledger","method":"get"}]} webhooks={[]} hasHead={false} />

    ### Invoices

    <APIPage document={"./openapi/openapi.public.yaml"} operations={[{"path":"/v1/invoices","method":"post"}]} webhooks={[]} hasHead={false} />

    <APIPage document={"./openapi/openapi.public.yaml"} operations={[{"path":"/v1/invoices","method":"get"}]} webhooks={[]} hasHead={false} />

    <APIPage document={"./openapi/openapi.public.yaml"} operations={[{"path":"/v1/invoices/{invoiceID}","method":"get"}]} webhooks={[]} hasHead={false} />

    <APIPage document={"./openapi/openapi.public.yaml"} operations={[{"path":"/v1/invoices/{invoiceID}/cancel","method":"post"}]} webhooks={[]} hasHead={false} />

    ### Static Addresses

    <APIPage document={"./openapi/openapi.public.yaml"} operations={[{"path":"/v1/addresses","method":"post"}]} webhooks={[]} hasHead={false} />

    <APIPage document={"./openapi/openapi.public.yaml"} operations={[{"path":"/v1/addresses","method":"get"}]} webhooks={[]} hasHead={false} />

    <APIPage document={"./openapi/openapi.public.yaml"} operations={[{"path":"/v1/addresses/{addressID}","method":"get"}]} webhooks={[]} hasHead={false} />

    <APIPage document={"./openapi/openapi.public.yaml"} operations={[{"path":"/v1/addresses/{addressID}/invoices","method":"get"}]} webhooks={[]} hasHead={false} />

    ### Payouts

    <APIPage document={"./openapi/openapi.public.yaml"} operations={[{"path":"/v1/payouts","method":"post"}]} webhooks={[]} hasHead={false} />

    <APIPage document={"./openapi/openapi.public.yaml"} operations={[{"path":"/v1/payouts","method":"get"}]} webhooks={[]} hasHead={false} />

    <APIPage document={"./openapi/openapi.public.yaml"} operations={[{"path":"/v1/payouts/{payoutID}","method":"get"}]} webhooks={[]} hasHead={false} />

    <APIPage document={"./openapi/openapi.public.yaml"} operations={[{"path":"/v1/payouts/{payoutID}/cancel","method":"post"}]} webhooks={[]} hasHead={false} />
  </div>
</div>
