@bitxpay/sdk
v0.1.0
Published
Official BitXPay server-side SDK for payment links, subscriptions and mass payouts.
Maintainers
Readme
BitXPay Node.js / TypeScript SDK
Official server-side SDK for the BitXPay API. Covers payment processing and subscriptions, plus webhook verification.
Mass payouts are not yet included in this SDK and will be added in a future release.
⚠️ Server-side only. This SDK signs requests with your Ed25519 private key. Never ship that key — or this SDK — to a browser, mobile app, or any client device. Run it on your backend only.
Contents
- Requirements
- Installation
- Get your API credentials
- Quickstart
- Configuring the client
- Payment processing
- Subscriptions
- Pagination
- Webhooks
- Error handling
- Advanced configuration
- Development
Requirements
- Node.js 18+ — the SDK uses the built-in
fetchandnode:crypto, so it has zero runtime dependencies. - Works with both ESM (
import) and CommonJS (require).
Installation
npm install @bitxpay/sdk
# or: pnpm add @bitxpay/sdk / yarn add @bitxpay/sdk// ESM / TypeScript
import { BitxpayClient } from "@bitxpay/sdk";// CommonJS
const { BitxpayClient } = require("@bitxpay/sdk");Get your API credentials
- Sign in to the BitXPay dashboard and open Developers → API Keys.
- Create a key. Choose test for sandbox or live for production.
- You'll be shown two values once:
- an API key —
btxm_test_…(sandbox) orbtxm_live_…(production) - an Ed25519 private key —
Ed25519:<base64>
- an API key —
Copy both immediately; the private key is never shown again. Store them as environment variables (or in your secrets manager) — never commit them.
# .env (load with your process manager or a dotenv loader; do NOT commit this file)
BITXPAY_API_KEY=btxm_test_xxxxxxxxxxxx
BITXPAY_PRIVATE_KEY=Ed25519:MC4CAQAwBQYDK2VwBCIEI...Each request is authenticated with three headers the SDK sets for you:
| Header | Value |
| --- | --- |
| X-API-Key | your API key |
| X-API-Timestamp | RFC3339 timestamp (must be within a 5-minute window) |
| X-API-Signature | base64 Ed25519 signature of METHOD + PATH + TIMESTAMP + BODY |
Quickstart
A complete program that creates a payment link and prints the checkout URL:
import { BitxpayClient } from "@bitxpay/sdk";
const bitxpay = new BitxpayClient({
apiKey: process.env.BITXPAY_API_KEY!,
privateKey: process.env.BITXPAY_PRIVATE_KEY!,
environment: "sandbox", // switch to "production" when you go live
});
async function main() {
const link = await bitxpay.payments.create({
payment_name: "Order #1234",
amount: 49.99,
currency: "USD",
customer_email: "[email protected]",
success_url: "https://example.com/thanks",
});
console.log("Send your customer to:", link.payment_link);
console.log("Status:", link.payment_status, "| id:", link.id);
}
main().catch((err) => {
console.error("BitXPay request failed:", err);
process.exit(1);
});Run it:
BITXPAY_API_KEY=btxm_test_xxx BITXPAY_PRIVATE_KEY=Ed25519:xxx npx tsx quickstart.tsConfiguring the client
const bitxpay = new BitxpayClient({
apiKey: process.env.BITXPAY_API_KEY!,
privateKey: process.env.BITXPAY_PRIVATE_KEY!,
environment: "production",
timeoutMs: 30_000,
maxRetries: 2,
});| Option | Default | Description |
| --- | --- | --- |
| apiKey | — | Your merchant API key (required). |
| privateKey | — | Ed25519 private key, Ed25519:<base64> (required). |
| environment | "sandbox" | "sandbox" → https://sandboxapi.bitxpay.com, "production" → https://api.bitxpay.com. |
| baseUrl | — | Override the API URL entirely (staging/self-hosted). Takes precedence over environment. |
| timeoutMs | 30000 | Per-request timeout in milliseconds. |
| maxRetries | 2 | Retries for 429/5xx/network errors (exponential backoff + jitter, honors Retry-After). |
| defaultHeaders | {} | Extra headers added to every request. |
| fetch | global fetch | Custom fetch implementation (for older runtimes or instrumentation). |
A single client is cheap and thread-safe — create one at startup and reuse it.
Payment processing
Payment links are hosted checkout pages your customers pay with crypto.
// Create
const link = await bitxpay.payments.create({
payment_name: "Pro plan — annual",
description: "12 months of Pro",
amount: 199.0,
currency: "USD",
customer_id: "cus_42",
customer_email: "[email protected]",
order_id: "ORD-2026-0042", // your reference, echoed back on webhooks
success_url: "https://example.com/thanks",
cancel_url: "https://example.com/pricing",
expires_at: "2026-12-31T23:59:59Z",
max_uses: 1,
webhook_metadata: { plan: "pro-annual" },
});
// Retrieve
const fresh = await bitxpay.payments.get(link.id);
console.log(fresh.payment_status); // e.g. "processing", "completed", "expired"
// Change lifecycle state: "activate" | "deactivate" | "expire"
await bitxpay.payments.updateStatus(link.id, "deactivate");
// Delete
await bitxpay.payments.delete(link.id);
// List (newest first) with filters
const page = await bitxpay.payments.list({
page: 1,
page_size: 20,
status: "completed",
search: "ORD-2026",
sort_by: "amount",
sort_order: "desc",
});
console.log(`${page.totalItems} links, showing ${page.data.length}`);
// Supported currencies for the amount/currency fields
const currencies = await bitxpay.payments.listCurrencies();Subscriptions
Plans are anchored to an on-chain contract: create the plan on-chain first, then register it with BitXPay so subscribers and recurring charges are tracked.
// Register a plan
const plan = await bitxpay.subscriptions.createPlan({
tx_hash: "0xabc...", // on-chain plan creation tx
plan_id: "1",
version: "1",
network_id: networkId,
currency_id: currencyId,
token: usdcAddress,
amount: "10000000", // smallest-unit string
formatted_amount: "10.00",
currencies: [usdcAddress],
interval: 2_592_000, // 30 days, in seconds
trial_period: 604_800, // 7 days
name: "Pro Monthly",
description: "Pro plan billed monthly",
});
await bitxpay.subscriptions.listPlans({ page: 1, page_size: 20 });
await bitxpay.subscriptions.getPlan(plan.id);
await bitxpay.subscriptions.updatePlan(plan.id, { status: "inactive" });
// Subscribers
const subscriber = await bitxpay.subscriptions.createSubscriber({
subscription_plan_id: plan.id,
customer_email: "[email protected]",
success_return_url: "https://example.com/welcome",
});
await bitxpay.subscriptions.listSubscribers({ plan_id: plan.id });
await bitxpay.subscriptions.getSubscriber(subscriber.id);
await bitxpay.subscriptions.updateSubscriber(subscriber.id, { status: "active" });
// Subscription links (the binding between a subscriber and a plan)
await bitxpay.subscriptions.listLinks({ is_active: true });
await bitxpay.subscriptions.getLink(linkId);Subscription amounts are decimal strings (e.g.
"10.00") to avoid floating-point precision loss.
Pagination
Every list method takes page / page_size and returns a normalized Page<T>,
regardless of the (varying) envelope each endpoint uses on the wire:
const page = await bitxpay.payments.list({ page: 1, page_size: 50 });
page.data; // T[] — the items on this page
page.page; // current page number
page.pageSize; // items per page
page.totalItems; // total across all pages
page.totalPages;
page.hasMore; // is there another page?Iterate every page:
async function* allPaymentLinks() {
let page = 1;
while (true) {
const result = await bitxpay.payments.list({ page, page_size: 100 });
yield* result.data;
if (!result.hasMore) break;
page += 1;
}
}
for await (const link of allPaymentLinks()) {
console.log(link.id, link.payment_status);
}Filters differ per endpoint (passed straight through to the API):
| List | Filters |
| --- | --- |
| payments.list | status, is_active, currency, min_amount, max_amount, created_from, created_to, search, sort_by, sort_order |
| subscriptions.listPlans | deprecated, start_date, end_date, search |
| subscriptions.listSubscribers | plan_id, start_date, end_date, search |
| subscriptions.listLinks | is_active, plan_id, start_date, end_date, search |
Sorting (
sort_by/sort_order) is supported only onpayments.list.
Webhooks
BitXPay signs every webhook with X-Webhook-Signature: sha256=<hex> — an
HMAC-SHA256 of the raw request body keyed by your webhook secret (set up under
Developers → Webhooks in the dashboard).
Verify against the raw body bytes. If your framework parses JSON before you verify, the re-serialized bytes won't match and verification will fail. Use a raw-body parser on the webhook route.
Express
import express from "express";
import { constructWebhookEvent, WebhookSignatureError } from "@bitxpay/sdk";
const app = express();
const secret = process.env.BITXPAY_WEBHOOK_SECRET!;
// express.raw gives you the unparsed body Buffer needed for verification.
app.post("/webhooks/bitxpay", express.raw({ type: "*/*" }), (req, res) => {
let event;
try {
event = constructWebhookEvent(req.body.toString("utf8"), req.headers, secret, {
toleranceSeconds: 300, // reject deliveries older than 5 minutes (optional)
});
} catch (err) {
if (err instanceof WebhookSignatureError) return res.status(400).send("invalid signature");
throw err;
}
switch (event.event_type) {
case "payment.succeeded":
// fulfil the order — event.data has the payment details
break;
case "subscription.payment":
// extend the subscription
break;
}
res.sendStatus(200); // ack fast; do heavy work asynchronously
});Next.js (App Router)
import { constructWebhookEvent, WebhookSignatureError } from "@bitxpay/sdk";
export async function POST(req: Request) {
const raw = await req.text(); // raw body, unparsed
try {
const event = constructWebhookEvent(raw, req.headers, process.env.BITXPAY_WEBHOOK_SECRET!);
// handle event.event_type ...
return new Response("ok", { status: 200 });
} catch (err) {
if (err instanceof WebhookSignatureError) return new Response("invalid", { status: 400 });
throw err;
}
}Just need a boolean check? Use verifyWebhookSignature(rawBody, signatureHeader, secret).
Event types: payment.succeeded, payment.failed, payment.pending,
subscription.created, subscription.payment, subscription.failed,
subscription.cancelled.
Error handling
Every non-2xx response throws a typed subclass of BitxpayAPIError:
| Class | Status |
| --- | --- |
| BitxpayBadRequestError | 400 |
| BitxpayAuthenticationError | 401 |
| BitxpayPermissionError | 403 |
| BitxpayNotFoundError | 404 |
| BitxpayConflictError | 409 |
| BitxpayValidationError | 422 |
| BitxpayRateLimitError | 429 (.retryAfter) |
| BitxpayServerError | 5xx |
import {
BitxpayAPIError,
BitxpayValidationError,
BitxpayRateLimitError,
} from "@bitxpay/sdk";
try {
await bitxpay.payments.create({ payment_name: "x", amount: -1, currency: "USD" });
} catch (err) {
if (err instanceof BitxpayValidationError) {
console.error("Bad input:", err.message, err.body);
} else if (err instanceof BitxpayRateLimitError) {
console.error(`Rate limited; retry after ${err.retryAfter}s`);
} else if (err instanceof BitxpayAPIError) {
// status, machine code, and a request id for support tickets
console.error(err.status, err.code, err.requestId, err.message);
} else {
throw err; // BitxpayConnectionError (network/timeout) or something unexpected
}
}BitxpayConnectionError— network failure or timeout (these are retried up tomaxRetriesfirst).BitxpayConfigError— thrown from the constructor for bad/missing options, before any request.
Advanced configuration
// Point at a staging API
const staging = new BitxpayClient({
apiKey,
privateKey,
baseUrl: "https://staging.example.com",
});
// Tighter timeout, no retries (e.g. inside a request handler with its own budget)
const strict = new BitxpayClient({ apiKey, privateKey, timeoutMs: 5_000, maxRetries: 0 });
// Bring your own fetch (proxy, logging, undici agent, ...)
import { fetch as undiciFetch } from "undici";
const instrumented = new BitxpayClient({ apiKey, privateKey, fetch: undiciFetch as typeof fetch });Development
npm install
npm run build # bundle to dist/ (ESM + CJS + .d.ts)
npm test # vitest
npm run typecheckLicense
MIT
