Zaptrain API
Build payments into your application.
The Zaptrain API provides a RESTful interface to create, manage, and track invoices paid over Bitcoin Lightning. All API requests use application/json for request and response bodies.
The base URL for all API endpoints is https://zaptrain.com.
This documentation covers the v1 API. For a machine-readable OpenAPI specification, visit /api/docs/openapi.json. For interactive exploration, try the Swagger UI.
Authentication
API Key (Bearer Token)
All v1 API endpoints accept Bearer token authentication using API keys. Generate an API key from Settings → Developer settings and include it in theAuthorization header.
Authorization: Bearer isvk_abc123def456...
API keys are prefixed with isvk_. They are shown only once when created. If you lose an API key, delete it and create a new one in your settings.
Session Cookie
Browser sessions authenticated via the Zaptrain web app automatically include session cookies, which is useful for reading the API from a browser console on zaptrain.com. Cookies are ambient credentials, so any request that changes something (POST, PATCH, DELETE) must come from zaptrain.com itself: a cross-site request carrying only cookies is rejected with 403. From your own server or another origin, use an isvk_ API key instead.
Error Responses
If authentication fails, the API returns a 401 Unauthorized status. State-rule violations (sending a paid invoice, reminding an unsent invoice, canceling an expired one) return 409 with a machine code in error (invoice_not_open or invoice_not_sendable) and a human-readable message. Invalid amounts return 400 with invalid_amount or invalid_tax.
{
"error": "Unauthorized"
}Hosted checkout
Add a “Pay with Lightning” button to your own site in about twenty lines of server code. Zaptrain hosts the payment page; your customer is sent back to you when they have paid. It works like Stripe Checkout: a redirect, not an embedded widget — there is no JavaScript library to load, and the hosted page cannot be placed in an iframe.
How it works
- Your server creates a session with
POST /api/v1/checkout/sessionsand gets back ahosted_url. - You redirect the customer's browser to that URL. They scan the QR with any Lightning wallet.
- Zaptrain sends them to your
return_urlwith?session_id=…&status=paid&ts=…&signature=…. - Your server checks the signature, then confirms with
GET /api/v1/checkout/sessions/{id}before fulfilling.
Before you start
In Settings → Developer settings create an API key (isvk_…) and reveal your checkout signing secret. Keep both on your server as environment variables — never in browser code.
1 · The button
<!-- Any page on your site. Plain form POST — no JavaScript required. -->
<form method="post" action="/buy">
<input type="hidden" name="orderId" value="order_abc123">
<button type="submit">Pay with Lightning ⚡</button>
</form>2 · Create the session and redirect
// server.js (Node 18+, Express) — your server, never the browser
app.post("/buy", async (req, res) => {
const r = await fetch("https://zaptrain.com/api/v1/checkout/sessions", {
method: "POST",
headers: { Authorization: `Bearer ${process.env.ZAPTRAIN_API_KEY}`, "Content-Type": "application/json" },
body: JSON.stringify({
line_items: [{ description: "Premium plan", quantity: 1, unit_price_sats: 50000 }],
customer_email: req.body.email,
client_reference_id: req.body.orderId,
return_url: "https://merchant.example/checkout/success",
cancel_url: "https://merchant.example/checkout/cancel",
}),
});
const session = await r.json();
res.redirect(303, session.hosted_url); // customer pays on zaptrain.com
});3 · Handle the return and fulfill
import crypto from "crypto";
// 1. Check the signature on the return redirect (cheap, immediate)
function verifyReturn({ session_id, status, ts, signature }) {
const canonical = `${session_id.replace(/^cs_/, "")}.${status}.${ts}`;
const expected = crypto
.createHmac("sha256", Buffer.from(process.env.ZAPTRAIN_SIGNING_SECRET, "hex"))
.update(canonical).digest("hex");
const a = Buffer.from(signature, "hex"), b = Buffer.from(expected, "hex");
const fresh = Math.abs(Date.now() / 1000 - Number(ts)) <= 600; // 10 min
return a.length === b.length && crypto.timingSafeEqual(a, b) && fresh;
}
// 2. Confirm with the API before you fulfill (authoritative)
app.get("/checkout/success", async (req, res) => {
if (!verifyReturn(req.query)) return res.status(400).send("Bad signature");
const r = await fetch(`https://zaptrain.com/api/v1/checkout/sessions/${req.query.session_id}`, {
headers: { Authorization: `Bearer ${process.env.ZAPTRAIN_API_KEY}` },
});
const session = await r.json();
if (session.payment_status !== "paid") return res.redirect("/checkout/pending");
await fulfillOrder(session.client_reference_id, session.amount_paid_sats);
res.render("thanks");
});GET proves the money actually arrived — it is the only thing to fulfill on. A customer can close the tab before the redirect, so also poll the session if your success page is never hit. Sessions are open → complete or expired;payment_status is unpaid or paid.A complete runnable example (Express, with the mock wallet for local testing) is available on request — email support@zaptrain.com and we'll send it over. Endpoint details are in the Checkout Sessions API section below.
Webhooks
Register a URL under Settings → Developer settings → Webhook and Zaptrain POSTs a signed event to it the moment an invoice is paid — whether it was created in the dashboard, through the API, by an AI assistant or through a hosted checkout. Your system learns about the payment without polling.
Events
id as the idempotency key.{
"id": "evt_V1StGXR8_Z5jdHi6B-myT",
"type": "invoice.paid",
"created_at": "2026-09-13T17:04:12.418Z",
"data": {
"invoice": {
"id": 67,
"public_id": "IxJx-Btnzc3NyWJbi0tQf",
"status": "paid",
"customer_name": "Alice",
"customer_email": "alice@example.com",
"amount_sats": 1500,
"currency": "SATS",
"memo": "Latte",
"due_date": "2026-09-19",
"expiry_date": null,
"paid_at": "2026-09-13T17:04:11.900Z",
"payment_url": "https://zaptrain.com/pay/IxJx-Btnzc3NyWJbi0tQf",
"created_at": "2026-09-12T06:57:03.101Z"
}
}
}Verifying the signature
Events follow the Standard Webhooks spec: three headers — webhook-id, webhook-timestamp (unix seconds) and webhook-signature (v1,<base64>) — and the signed string is id.timestamp.body with HMAC-SHA256 over the raw body, keyed by your secret's bytes (base64 after the whsec_ prefix). Any Standard Webhooks library verifies it; here is the check by hand:
import crypto from "crypto";
import express from "express";
const app = express();
const SECRET = process.env.ZAPTRAIN_WEBHOOK_SECRET; // whsec_… from Settings → Developer → Webhook
// Raw body: the signature covers the exact bytes we received.
app.post("/zaptrain/webhook", express.raw({ type: "application/json" }), (req, res) => {
const id = req.header("webhook-id");
const ts = req.header("webhook-timestamp");
const sigs = (req.header("webhook-signature") || "").split(" ");
const body = req.body.toString("utf8");
// Reject anything older than 5 minutes (replay protection).
if (Math.abs(Date.now() / 1000 - Number(ts)) > 300) return res.status(400).send("stale");
const key = Buffer.from(SECRET.replace(/^whsec_/, ""), "base64");
const expected = "v1," + crypto.createHmac("sha256", key).update(`${id}.${ts}.${body}`).digest("base64");
const ok = sigs.some((s) => s.length === expected.length && crypto.timingSafeEqual(Buffer.from(s), Buffer.from(expected)));
if (!ok) return res.status(400).send("bad signature");
const event = JSON.parse(body);
if (event.type === "invoice.paid") {
// Idempotent: you may see the same event.id more than once.
markOrderPaid(event.data.invoice.public_id, event.data.invoice.amount_sats);
}
res.sendStatus(200); // any 2xx within 5 s counts as delivered
});GET /api/v1/invoices/{id} or the checkout session before fulfilling, and keep the payment link as the customer's receipt./api/v1/users/meGet merchant profile
Returns the authenticated merchant's profile including branding settings and account info.
Examples
{
"id": 1,
"name": "Acme Corp",
"email": "hello@acme.com",
"brand_name": "Acme",
"brand_color": "#d26434",
"logo_url": "/api/logo/abc123.png",
"default_memo": "Thank you!",
"created_at": "2024-01-15T10:30:00Z"
}/api/v1/users/meUpdate merchant profile
Update mutable profile fields: brand name, brand color, and default memo. All other fields are ignored.
Request Body
| Parameter | In | Type | Required | Description |
|---|---|---|---|---|
| brand_name | string | — | Display name for invoices | |
| brand_color | string | — | Hex color (e.g., #d26434) | |
| default_memo | string | — | Default text on invoices |
Examples
{
"id": 1,
"name": "Acme Corp",
"email": "hello@acme.com",
"brand_name": "Acme Updated",
"brand_color": "#1e4d8c",
"logo_url": "/api/logo/abc123.png",
"default_memo": "Thanks for your business.",
"created_at": "2024-01-15T10:30:00Z"
}/api/v1/users/me/usageGet fee rate
Returns the current fee rate applied to all invoices created by this merchant.
Examples
{
"fee_rate": 0.01,
"fee_rate_pct": "1%",
"description": "1% of each paid invoice"
}/api/v1/invoicesList invoices
Retrieve a paginated list of invoices for the authenticated merchant, ordered by creation date (newest first).
Parameters
| Parameter | In | Type | Required | Description |
|---|---|---|---|---|
| limit | query | integer | — | Max results to return (1–100, default 20) |
| offset | query | integer | — | Number of records to skip (default 0) |
Examples
{
"invoices": [
{
"id": 42,
"public_id": "xK9mPqR3vN7w",
"merchant_id": 1,
"customer_name": "Jane Smith",
"customer_email": "jane@example.com",
"status": "sent",
"currency": "SATS",
"amount_cents": 50000,
"memo": "Web design work",
"due_date": "2024-04-01",
"bolt11": "lnbc500u1p0sample...",
"lexe_index": "abc123",
"lexe_status": "pending",
"lexe_status_msg": "invoice generated",
"fee_sats": 500,
"tax_percent": 10,
"tax_amount_sats": 5000,
"created_at": "2024-03-01T00:00:00Z",
"updated_at": "2024-03-01T00:00:00Z",
"sent_at": "2024-03-01T12:00:00Z"
}
],
"total": 23
}/api/v1/invoicesCreate invoice
Create a new invoice with status draft (shown as Unsent in the dashboard). Nothing is emailed; the payment link is minted when the customer opens the payment page or when you send it.
Request Body
| Parameter | In | Type | Required | Description |
|---|---|---|---|---|
| customer_name | string | — | Customer name (optional) | |
| customer_email | string | — | Customer email address (optional; validated if given). Needed only for send/remind — otherwise share the payment link yourself. | |
| memo | string | — | Invoice description/memo | |
| due_date | string | — | Due date (ISO 8601 format) | |
| items | array | Yes | Line items with description, quantity, unit_price_sats | |
| tax_percent | number | — | Tax percentage (default 0) | |
| tax_amount_sats | integer | — | Flat tax in satoshis (default 0) |
Examples
{
"id": 43,
"public_id": "mK2nQpX9wP5r",
"merchant_id": 1,
"customer_name": "Jane Smith",
"customer_email": "jane@example.com",
"status": "draft",
"currency": "SATS",
"amount_cents": 55000,
"memo": "Web design work",
"due_date": "2024-04-01",
"bolt11": null,
"lexe_index": null,
"lexe_status": null,
"fee_sats": 550,
"tax_percent": 10,
"tax_amount_sats": 5000,
"created_at": "2024-03-15T10:30:00Z",
"updated_at": "2024-03-15T10:30:00Z",
"sent_at": null
}/api/v1/invoices/{id}Get invoice
Retrieve a single invoice by its internal ID. Returns full invoice details including status, payment info, and line items.
Parameters
| Parameter | In | Type | Required | Description |
|---|---|---|---|---|
| id | path | integer | Yes | Invoice internal ID (not public_id) |
Examples
{
"id": 42,
"public_id": "xK9mPqR3vN7w",
"merchant_id": 1,
"customer_name": "Jane Smith",
"customer_email": "jane@example.com",
"status": "sent",
"currency": "SATS",
"amount_cents": 50000,
"memo": "Web design work",
"due_date": "2024-04-01",
"bolt11": "lnbc500u1p0sample...",
"lexe_index": "abc123",
"lexe_status": "pending",
"lexe_status_msg": "invoice generated",
"fee_sats": 500,
"tax_percent": 10,
"tax_amount_sats": 5000,
"created_at": "2024-03-01T00:00:00Z",
"updated_at": "2024-03-01T12:00:00Z",
"sent_at": "2024-03-01T12:00:00Z",
"items": [
{
"id": 101,
"invoice_id": 42,
"description": "Design work",
"quantity": 1,
"unit_price_sats": 50000
}
]
}/api/v1/invoices/{id}/sendSend invoice email
Send invoice to customer via email. Optionally customize the email subject and message. Updates invoice status to 'sent'; calling it again on a sent invoice re-sends. Returns 409 { error: 'invoice_not_sendable' } for paid, canceled or expired invoices.
Parameters
| Parameter | In | Type | Required | Description |
|---|---|---|---|---|
| id | path | integer | Yes | Invoice internal ID |
Request Body
| Parameter | In | Type | Required | Description |
|---|---|---|---|---|
| subject | string | — | Custom email subject | |
| message | string | — | Custom message to include in email body |
Examples
{
"ok": true,
"status": "sent"
}/api/v1/invoices/{id}/remindSend reminder email
Send a payment reminder to customer. Use mode='same' to resend the original email, or mode='new' to regenerate with optional custom message. Only sent, attempted or overdue invoices qualify — returns 409 { error: 'invoice_not_open' } otherwise (send an unsent invoice first).
Parameters
| Parameter | In | Type | Required | Description |
|---|---|---|---|---|
| id | path | integer | Yes | Invoice internal ID |
Request Body
| Parameter | In | Type | Required | Description |
|---|---|---|---|---|
| mode | string | — | 'same' to resend original, 'new' to regenerate (default 'new') | |
| subject | string | — | Custom email subject (mode='new' only) | |
| message | string | — | Custom message (mode='new' only) |
Examples
{
"ok": true,
"status": "sent"
}/api/v1/invoices/{id}/cancelCancel an invoice
Soft-cancel a sent (attempted / overdue) invoice. Preserves the invoice row and history and voids the open Lightning request. Paid and expired invoices cannot be canceled, and unsent invoices must be deleted instead — those return 409 { error: 'invoice_not_open' }. Canceling an already-canceled invoice is a no-op (idempotent).
Parameters
| Parameter | In | Type | Required | Description |
|---|---|---|---|---|
| id | path | integer | Yes | Invoice internal ID |
Examples
{
"ok": true,
"status": "cancelled",
"invoice": {
"id": 42,
"public_id": "xK9mPqR3vN7w",
"status": "cancelled",
"amount_cents": 50000,
"customer_name": "Jane Smith",
"customer_email": "jane@example.com"
}
}/api/v1/invoices/{id}/paymentGet payment status
Get payment and Lightning Network status for an invoice. Includes BOLT11 code, payment status, and finalization timestamp.
Parameters
| Parameter | In | Type | Required | Description |
|---|---|---|---|---|
| id | path | integer | Yes | Invoice internal ID |
Examples
{
"id": 42,
"public_id": "xK9mPqR3vN7w",
"status": "sent",
"bolt11": "lnbc500u1p0sample...",
"lexe_status": "completed",
"lexe_status_msg": "payment received",
"lexe_finalized_at": "2024-03-02T14:30:00Z",
"amount_sats": 50000,
"fee_sats": 500
}/api/v1/checkout/sessionsCreate a checkout session
Creates an invoice and a hosted payment page for it. Redirect the customer to hosted_url; after payment they are sent to your return_url with a signed query string.
Request Body
| Parameter | In | Type | Required | Description |
|---|---|---|---|---|
| line_items | array | Yes | 1–50 items: { description (≤500 chars), quantity (int ≥1), unit_price_sats (int ≥1) } | |
| return_url | string | Yes | https:// URL (http://localhost allowed) the customer returns to after paying; ≤2048 chars | |
| cancel_url | string | Yes | URL for the customer's Cancel link on the hosted page | |
| customer_email | string | — | Pre-fills the invoice's customer | |
| client_reference_id | string | — | Your order id (≤200 chars); echoed on the return redirect | |
| metadata | object | — | Up to 50 keys, ≤8 KB; returned on retrieve | |
| expires_in | integer | — | Seconds until the session expires: 1800–604800, default 86400 |
Examples
{
"id": "cs_V1StGXR8_Z5jdHi6B-myT",
"object": "checkout.session",
"status": "open",
"hosted_url": "https://zaptrain.com/checkout/V1StGXR8_Z5jdHi6B-myT",
"return_url": "https://merchant.example/checkout/success",
"cancel_url": "https://merchant.example/checkout/cancel",
"client_reference_id": "order_abc123",
"metadata": {
"order_id": "abc123"
},
"customer_email": "alice@example.com",
"expires_at": "2026-09-12T18:00:00.000Z",
"completed_at": null,
"cancelled_at": null,
"created_at": "2026-09-11T18:00:00.000Z",
"payment_status": "unpaid",
"amount_paid_sats": 0,
"invoice": {
"public_id": "k3Jd9sLq2",
"amount_sats": 50000
}
}/api/v1/checkout/sessions/{id}Retrieve a checkout session
The source of truth for fulfillment. Accepts cs_<id> or the bare id. Fulfill when payment_status is "paid" — never on the redirect alone. Another merchant's session returns 404.
Parameters
| Parameter | In | Type | Required | Description |
|---|---|---|---|---|
| id | string | Yes | Session id, e.g. cs_V1StGXR8_Z5jdHi6B-myT |
Examples
{
"id": "cs_V1StGXR8_Z5jdHi6B-myT",
"object": "checkout.session",
"status": "complete",
"return_url": "https://merchant.example/checkout/success",
"cancel_url": "https://merchant.example/checkout/cancel",
"client_reference_id": "order_abc123",
"metadata": {
"order_id": "abc123"
},
"customer_email": "alice@example.com",
"expires_at": "2026-09-12T18:00:00.000Z",
"completed_at": "2026-09-11T18:04:12.000Z",
"cancelled_at": null,
"created_at": "2026-09-11T18:00:00.000Z",
"payment_status": "paid",
"amount_paid_sats": 50000,
"invoice": {
"public_id": "k3Jd9sLq2",
"amount_sats": 50000
}
}Need help? Contact us.