Billium
SDKs & tools

@billium/node

Official Node.js SDK for Billium — invoices, webhook management, and webhook signature verification.

@billium/node is the official Node.js SDK for Billium. It covers invoices, customers, products, wallets, webhook management, and webhook signature verification.

Coverage: the SDK exposes billium.invoices, billium.customers, billium.products, billium.wallets, and billium.webhooks. A few less-common REST resources (API keys, team management, reports) aren't wrapped yet — call those directly with your HTTP client of choice, using the same x-api-key header documented in Authentication.

Installation

npm install @billium/node
# or
pnpm add @billium/node
# or
yarn add @billium/node

Requirements: Node.js ≥ 18.


Initialization

import { Billium } from '@billium/node';

const billium = new Billium({
  apiKey: process.env.BILLIUM_API_KEY,          // sk_... or pk_...
  merchantId: process.env.BILLIUM_MERCHANT_ID,  // mer_...
  webhookSecret: process.env.BILLIUM_WEBHOOK_SECRET, // whsec_...
});

Billium is a named export only — there is no default export. Both ESM and CJS consumers must destructure it:

// ESM
import { Billium } from '@billium/node';

// CJS
const { Billium } = require('@billium/node');

Constructor options

OptionTypeDefaultDescription
apiKeystringBillium API key. Use a secret key (sk_*) for full access. Public keys (pk_*) only cover invoices.create, invoices.get, and read-only product/wallet access; calling anything else throws a BilliumError at the SDK layer without a round-trip.
merchantIdstringYour merchant ID (mer_...). Required together with apiKey to call the merchant resource methods (invoices, customers, products, wallets) and the webhook management methods.
webhookSecretstringYour webhook secret (whsec_...). When set, billium.webhooks.verify() can be called without passing the secret every time.
baseUrlstringhttps://api.billium.toOverride the API base URL (useful for self-hosted deployments or integration tests against a staging env).
maxRetriesnumber2Retry attempts on network errors, 5xx, and 429. Set to 0 to disable.
baseDelayMsnumber500Starting backoff delay. Subsequent retries use exponential backoff with full jitter.
maxDelayMsnumber30000Upper bound on the backoff delay. Retry-After headers are honored and clamped by this value.

Retry behavior

  • GET, PUT, PATCH, DELETE are always retried when maxRetries > 0.
  • POST is retried only when an Idempotency-Key header is set on the request — otherwise retrying could create a duplicate resource. invoices.create() sends this header automatically when you pass options.idempotencyKey.
  • Retryable statuses: 429, 500, 502, 503, 504. Everything else (400, 401, 403, 404, 409, 422…) throws BilliumApiError immediately.
  • Retry-After headers are honored — the SDK waits for the server-specified delay before retrying.

See Idempotency for the full rationale and recommended patterns.


Invoices

Requires apiKey and merchantId in the constructor.

billium.invoices.create(params, options?)

Creates a new invoice.

import { randomUUID } from 'crypto';

const invoice = await billium.invoices.create(
  {
    name: 'Order #8821',
    rawAmount: 49.99,
    currency: 'USD',
    customerEmail: 'customer@example.com',
    redirectUrl: 'https://yoursite.com/thank-you',
  },
  { idempotencyKey: randomUUID() },
);

console.log(invoice.id);     // 'inv_...'
console.log(invoice.status); // 'AWAITING_PAYMENT'

Params

FieldTypeRequiredDescription
namestringYesInvoice display name (max 200 chars).
rawAmountnumberYesExpected amount in the currency (min 0, max 1 000 000).
currencystringNoCurrency code. Defaults to USD.
customerEmailstringNoCustomer email.
customerNamestringNoCustomer name.
customerAddressstringNoCustomer billing address.
customerPhoneNumberstringNoCustomer phone number.
descriptionstringNoInvoice description (max 1000 chars).
redirectUrlstringNoPost-payment redirect URL.

Options

FieldTypeDescription
idempotencyKeystringSet this in production. Enables safe retries and protects against accidental duplicate invoices when your own code retries. See Idempotency.

billium.invoices.get(invoiceId)

const invoice = await billium.invoices.get('inv_...');

billium.invoices.list(params?)

const result = await billium.invoices.list({ page: 1, limit: 20, search: 'order' });
// result.data  → Invoice[]
// result.total → number
// result.page  → number
// result.limit → number

billium.invoices.cancel(invoiceId)

Cancels an invoice. Throws BilliumApiError if the invoice is already in a terminal state, and BilliumError immediately if the configured API key is a public key (pk_*).

const invoice = await billium.invoices.cancel('inv_...');
console.log(invoice.status); // 'CANCELLED'

Invoice shape

Decimal fields are serialized as strings to preserve precision — use a decimal library (decimal.js) for arithmetic.

interface Invoice {
  id: string;
  merchantId: string;
  customerId: string | null;
  productId: string | null;
  name: string;
  description: string | null;
  redirectUrl: string | null;
  rawAmount: string;   // e.g. "49.990000"
  endAmount: string;   // e.g. "49.990000" (rawAmount + fees, when applicable)
  currency: string;
  status: InvoiceStatus;
  expiresAt: string | null;
  createdAt: string;
  updatedAt: string;

  // Relations — always present on responses, never undefined
  customer: InvoiceCustomer | null;
  product: InvoiceProduct | null;
  payments: InvoicePayment[];
  invoiceTimeline: InvoiceTimelineEntry[];
}

Customers

Requires apiKey and merchantId, and the apiKey must be a secret key (sk_*) — every customer method is secret-only.

Customers aren't created through the SDK: a customer record is provisioned automatically the first time you generate an invoice for a given email under your merchant. This client lists, retrieves, and updates those records.

billium.customers.list(params?)

const result = await billium.customers.list({ page: 1, limit: 20, search: 'jane' });
// result.data → Customer[]

Accepts page (default 1), limit (max 100, default 10), and search (matches email, name, phone, address, or ID).

billium.customers.get(customerId)

Returns a single customer, including a derived location (approximate country/city from the customer's checkout activity; either field is null when it can't be determined). location is present only on get() — not on list() rows or the update() response.

const customer = await billium.customers.get('cus_...');

billium.customers.stats(customerId)

Aggregate spend and invoice stats for a customer.

const stats = await billium.customers.stats('cus_...');
// { revenue, totalInvoices, paidInvoices, paidRate }

revenue is the summed amount of the customer's paid invoices, returned as a string to preserve precision (or the number 0 when they have no paid invoices).

billium.customers.update(customerId, params)

Updates a customer's name, address, or phoneNumber — only the fields you pass change. Email isn't updatable: it's the key that ties a customer to their invoices.

await billium.customers.update('cus_...', { name: 'Jane Doe' });

Customer shape

interface Customer {
  id: string;
  merchantId: string;
  email: string;
  name: string | null;
  address: string | null;
  phoneNumber: string | null;
  metadata: unknown;     // arbitrary JSON attached by Billium, or null
  createdAt: string;     // ISO 8601
  updatedAt: string;     // ISO 8601
  location?: {           // only on get(); each field may be null
    country: string | null;
    city: string | null;
  };
}

Products

Reads (get, list) work with a public key (pk_*); create, update, and delete require a secret key (sk_*).

billium.products.create(params)

const product = await billium.products.create({
  name: 'Pro plan',
  price: 49.99,
  currency: 'USD',
});

Params

FieldTypeRequiredDescription
namestringYesProduct display name (max 200 chars).
pricenumberYesFiat price (0–1 000 000). Sent as a number; returned as a string.
currencystringNoUSD | EUR | GBP | CAD | AUD | JPY. Defaults to USD.
descriptionstringNoProduct description (max 1000 chars).
imagestringNoStorage key of a previously uploaded image.
isActivebooleanNoWhether the product is purchasable. Defaults to true.
askForNamebooleanNoCollect the customer's name at checkout. Defaults to false.
askForAddressbooleanNoCollect the customer's address at checkout. Defaults to false.
askForPhoneNumberbooleanNoCollect the customer's phone number at checkout. Defaults to false.

billium.products.get(productId) / billium.products.list(params?)

const product = await billium.products.get('prd_...');
const result = await billium.products.list({ page: 1, limit: 20, search: 'plan' });
// result.data → Product[]

billium.products.update(productId, params)

Accepts the same fields as create, all optional — only the keys you send change.

await billium.products.update('prd_...', { price: 59.99, isActive: false });

billium.products.delete(productId)

Soft-deletes a product so it can no longer be purchased.

await billium.products.delete('prd_...');

Product shape

interface Product {
  id: string;
  merchantId: string;
  name: string;
  description: string | null;
  price: string;            // decimal serialized as a string
  currency: 'USD' | 'EUR' | 'GBP' | 'CAD' | 'AUD' | 'JPY';
  isActive: boolean;
  askForName: boolean;
  askForAddress: boolean;
  askForPhoneNumber: boolean;
  image: string | null;        // raw storage key — treat as opaque
  signedImage?: string | null; // ready-to-render URL; prefer this for display
  createdAt: string;
  updatedAt: string;
}

Wallets

Reads (get, list) work with a public key (pk_*); create, update, and delete require a secret key (sk_*). The API only ever returns public wallet configuration — a direct wallet's receiving address or an xpub wallet's extended public key. No private keys or seed material are stored or returned.

billium.wallets.list()

Returns a plain array (not a PaginatedResult) — a merchant has at most one wallet per cryptocurrency/network pair, so the full set is returned unpaginated.

const wallets = await billium.wallets.list(); // Wallet[]

billium.wallets.get(walletId)

const wallet = await billium.wallets.get('wal_...');

billium.wallets.create(params)

Adds a wallet. Pass address for a DIRECT_WALLET (one static receiving address, reused across invoices) or xpub for an XPUB_WALLET (a fresh address derived per invoice; BTC and LTC only). Only one wallet may exist per cryptocurrency/network pair — a duplicate is rejected.

const wallet = await billium.wallets.create({
  cryptocurrency: 'BTC',
  network: 'BTC',
  walletType: 'DIRECT_WALLET',
  address: 'bc1q...',
});

Params

FieldTypeRequiredDescription
cryptocurrencystringYesAsset the wallet settles in (BTC, ETH, USDT, USDC, …).
networkstringYesChain the wallet belongs to. Must be a network the asset is supported on.
walletTypestringYesDIRECT_WALLET or XPUB_WALLET.
addressstringCond.Receiving address. Required for DIRECT_WALLET. Validated against network.
xpubstringCond.Extended public key. Required for XPUB_WALLET.
derivationPathstringNoDerivation path for xpub wallets (e.g. m/84'/0'/0'/0).
isEnabledbooleanNoWhether the wallet can receive new payments. Defaults to true.
requiredConfirmationsnumberNoConfirmation depth before a payment settles. Each asset enforces its own minimum.

billium.wallets.update(walletId, params)

Updates mutable configuration (address, xpub, isEnabled, requiredConfirmations, derivationPath). Identity fields (cryptocurrency, network, walletType) can't change — create a new wallet instead.

await billium.wallets.update('wal_...', { isEnabled: false });

billium.wallets.delete(walletId)

Deletes a wallet. Throws BilliumApiError if the wallet still has active (non-terminal) payments against it.

await billium.wallets.delete('wal_...');

Wallet shape

interface Wallet {
  id: string;
  merchantId: string;
  cryptocurrency: 'BTC' | 'ETH' | 'USDT' | 'USDC' | 'BNB' | 'SHIB' | 'POL' | 'LTC' | 'DAI' | 'CRO' | 'TRX' | 'UNI';
  network: 'BTC' | 'ETH' | 'BNB' | 'POL' | 'LTC' | 'CRO' | 'TRX';
  walletType: 'DIRECT_WALLET' | 'XPUB_WALLET';
  isEnabled: boolean;
  requiredConfirmations: number;
  address: string | null;        // set for DIRECT_WALLET
  xpub: string | null;           // set for XPUB_WALLET (public key)
  derivationPath: string | null;
  createdAt: string;
  updatedAt: string;
}

Webhook signature verification

billium.webhooks.verify(rawBody, signature, secret?, options?)

Parses and verifies an incoming webhook request. Throws if the signature is invalid or the timestamp is outside the tolerance window.

// With webhookSecret set in the constructor:
const event = billium.webhooks.verify(
  rawBody,                     // Buffer | string — raw request body
  req.headers['x-signature'],  // x-signature header value
);

// Or pass the secret explicitly (overrides the constructor value):
const event = billium.webhooks.verify(rawBody, signature, 'whsec_...');

Parameters

ParameterTypeDefaultDescription
rawBodyBuffer | stringRaw request body, not parsed JSON.
signaturestringValue of the x-signature header.
secretstringWebhook secret. Can be omitted if webhookSecret is set in the constructor.
options.tolerancenumber300Max allowed age of the timestamp in seconds. Set to 0 to disable.

Returns WebhookEvent

interface WebhookEvent {
  event: string;      // e.g. 'invoice.paid'
  id: string;         // unique event ID — use for deduplication
  data: unknown;      // event-specific payload
  timestamp: string;  // ISO 8601
}

Throws

ErrorWhen
BilliumErrorNo secret was provided (neither in constructor nor as argument)
BilliumWebhookSignatureErrorHeader is malformed or HMAC does not match
BilliumWebhookTimestampErrorTimestamp is outside the tolerance window

Webhook management

Requires apiKey and merchantId, and the apiKey must be a secret key (sk_*). Public keys throw BilliumError at the SDK layer before hitting the network.

billium.webhooks.create(params)

const webhook = await billium.webhooks.create({
  url: 'https://yoursite.com/webhooks/billium',
  events: ['invoice.paid', 'invoice.expired'], // or ['invoice.*']
  description: 'Production order fulfillment',
});

// Store this secret now — it's only shown once.
const secret = webhook.webhookSecrets[0].secretKeyPreview; // 'whsec_...'

Params

FieldTypeRequiredDescription
urlstringYesHTTPS URL Billium will POST events to.
eventsWebhookEventType[]YesSubscribed events. Use invoice.* or payment.* for wildcards.
descriptionstringNoFree-text description.
isActivebooleanNoDefaults to true.
retryCountnumberNo0–10. Defaults to 3.
timeoutnumberNoDelivery timeout in ms (1000–30000). Defaults to 30000.

billium.webhooks.list()

const webhooks = await billium.webhooks.list();

billium.webhooks.update(webhookId, params)

await billium.webhooks.update('wh_...', {
  events: ['invoice.*', 'payment.confirmed'],
});

billium.webhooks.delete(webhookId)

await billium.webhooks.delete('wh_...');

billium.webhooks.ping(webhookId)

Sends a test delivery to the endpoint so you can confirm your handler is reachable.

await billium.webhooks.ping('wh_...');

Error types

All errors extend BilliumError, which extends the native Error class.

import {
  BilliumError,
  BilliumApiError,
  BilliumWebhookSignatureError,
  BilliumWebhookTimestampError,
} from '@billium/node';

// API errors
try {
  const invoice = await billium.invoices.create({ /* ... */ });
} catch (err) {
  if (err instanceof BilliumApiError) {
    console.error(err.status);  // HTTP status code, e.g. 400, 401, 403
    console.error(err.code);    // optional error code from the API
    console.error(err.message); // human-readable message
  }
}

// Webhook errors
try {
  const event = billium.webhooks.verify(rawBody, signature);
} catch (err) {
  if (err instanceof BilliumWebhookTimestampError) {
    // Possible replay attack — reject
    return res.status(400).send();
  }
  if (err instanceof BilliumWebhookSignatureError) {
    // Invalid or tampered signature
    return res.status(400).send();
  }
  throw err;
}
ClassnameCause
BilliumError'BilliumError'Base class / configuration error (e.g. pk_* used for sk_*-only method)
BilliumApiError'BilliumApiError'Non-2xx HTTP response from the Billium API
BilliumWebhookSignatureError'BilliumWebhookSignatureError'Malformed header or HMAC mismatch
BilliumWebhookTimestampError'BilliumWebhookTimestampError'Timestamp outside tolerance window

See Errors for the REST error envelope.

On this page