Zaptrain API
Build payments into your application.
The Zaptrain API provides a RESTful interface to create, manage, and track invoices and subscriptions paid over Bitcoin Lightning. Every payment is an invoice: a one-time invoice, or one payment of a subscription (those carry a subscription object). Plans, subscribers and invites have their own endpoints under Subscriptions API. 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. Canceling a subscription payment returns 409 subscription_invoice: cancel the subscription instead.
The Subscriptions API (plans, subscriptions, invites) answers errors as { error: <code>, message, field? }, where field is the snake_case request field at fault (e.g. amount_sats, customer_email). Codes: invalid_request and invalid_json (400), not_found (404), not_allowed, already_subscribed, lexe_required and payment_failed (409; a failed retry), rate_limited (429), email_failed (502) and save_failed (500). An unknown id in the path returns 404 with { "error": "Not found" }.
{
"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=…(status=no_payment_requiredfor a subscription to a plan that starts later). - 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 (or no_payment_required in subscription mode, below).Subscriptions
The same flow sells a subscription to one of your plans. Create the session with "mode": "subscription" and plan (the plan id) instead of line_items; the plan sets the price. client_reference_id becomes the subscription's, so it comes back in the subscription.* webhooks; in this mode it is up to 128 letters, digits and _ . : @ -. An archived or ended plan returns 409 plan_unavailable; an unknown plan returns 404.
curl -X POST https://zaptrain.com/api/v1/checkout/sessions \
-H "Authorization: Bearer isvk_abc123..." \
-H "Content-Type: application/json" \
-d '{
"mode": "subscription",
"plan": "O9yN2uK0xjoYgLQXUYBj",
"customer_email": "ada@example.com",
"client_reference_id": "user_42",
"return_url": "https://merchant.example/welcome",
"cancel_url": "https://merchant.example/pricing"
}'{
"id": "cs_Q2wE4rT6yU8iO0pA1sD3f",
"object": "checkout.session",
"mode": "subscription",
"status": "open",
"client_reference_id": "user_42",
"customer_email": "ada@example.com",
"payment_status": "unpaid",
"amount_paid_sats": 0,
"invoice": null,
"plan": { "id": "O9yN2uK0xjoYgLQXUYBj", "name": "Daily Beans" },
"subscription": null,
"hosted_url": "https://zaptrain.com/checkout/Q2wE4rT6yU8iO0pA1sD3f",
...
}The hosted page shows the plan's subscribe form. Once the customer subscribes and the first payment goes through (or right away, for a plan that starts later) they are sent to your return_url with the same signed query (session_id, status, ts, signature, client_reference_id). The session then carries subscription (id, status, payment_method: automatic or pay_each_time), and invoice is the first payment's invoice (null until they subscribe). payment_status is paid once the first payment arrives, or no_payment_required when the plan starts later and the session completed with nothing charged. Grant access from the GET on the session or from the subscription.created webhook.
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 — and when a subscription starts, falls past due or is canceled. Your system learns about payments and subscribers without polling.
Events
id as the idempotency key.periods_paid is 0 until the start). Carries data.subscription with its id, your client_reference_id (from ?ref= on the subscribe link), payment_method (automatic or pay_each_time), amount_sats (tax included; tax_percent), max_payments / ends_at when the plan ends by itself, and current_period_end. Grant access here.cancel_reason: ended). Revoke access here. Each successful renewal arrives as invoice.paid with an invoice.subscription object.{
"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). Every payment is an invoice: one-time invoices, and each subscription payment (with a subscription object; null on one-time invoices). Filter with type, the same as the Invoices screen.
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) |
| type | query | string | — | all (default), one_time, or recurring |
| subscription | query | string | — | A subscription id: only its payments |
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",
"subscription": null
}
],
"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. subscription is null for a one-time invoice; on a subscription payment it is { id, plan: { id, name }, period, payment_number, max_payments } (period counts from 0, payment_number from 1).
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",
"subscription": null,
"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' }. A subscription payment (subscription is not null) can't be canceled either: it returns 409 { error: 'subscription_invoice' } — cancel the subscription instead (POST /subscriptions/{id}/cancel). 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
mode "payment" (the default) creates an invoice for line_items and a hosted payment page for it. mode "subscription" creates a hosted page with a plan's subscribe form instead; the session has no invoice until the customer subscribes. Redirect the customer to hosted_url; afterwards they are sent to your return_url with a signed query string. In subscription mode the response also carries plan and subscription (see Hosted checkout above). An archived or ended plan returns 409 { error: 'plan_unavailable' }; an unknown plan returns 404.
Request Body
| Parameter | In | Type | Required | Description |
|---|---|---|---|---|
| mode | string | — | "payment" (default) or "subscription" | |
| line_items | array | — | Required in payment mode, not allowed in subscription mode. 1–50 items: { description (≤500 chars), quantity (int ≥1), unit_price_sats (int ≥1) } | |
| plan | string | — | Required in subscription mode: the plan id. The plan sets the price | |
| 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. In subscription mode it becomes the subscription's client_reference_id (echoed in its webhooks) and must be up to 128 letters, digits and _ . : @ - | |
| 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",
"mode": "payment",
"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. In subscription mode, "no_payment_required" means the customer subscribed to a plan that starts later and nothing was charged yet; subscription.id is the subscription to grant access for. 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",
"mode": "payment",
"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
},
"hosted_url": "https://zaptrain.com/checkout/V1StGXR8_Z5jdHi6B-myT"
}/api/v1/plansList subscription plans
Your plans, active and archived, with price, tax, schedule, start and end, payment methods, subscribe link and active subscriber count.
Examples
{
"plans": [
{
"id": "O9yN2uK0xjoYgLQXUYBj",
"name": "Daily Beans",
"description": null,
"amount_sats": 1000,
"tax_percent": 5,
"total_sats": 1050,
"interval": "daily",
"starts_on": null,
"ends_on": null,
"max_payments": 30,
"payment_methods": {
"automatic": true,
"pay_each_time": true
},
"status": "active",
"subscribe_url": "https://zaptrain.com/subscribe/O9yN2uK0xjoYgLQXUYBj",
"active_subscribers": 4,
"created_at": "2026-10-16T22:44:10.000Z"
}
]
}/api/v1/plans/{id}Get a plan
One plan, in the same shape as GET /plans (without active_subscribers). An unknown id returns 404.
Parameters
| Parameter | In | Type | Required | Description |
|---|---|---|---|---|
| id | path | string | Yes | Plan id |
Examples
{
"id": "O9yN2uK0xjoYgLQXUYBj",
"name": "Daily Beans",
"description": null,
"amount_sats": 1000,
"tax_percent": 5,
"total_sats": 1050,
"interval": "daily",
"starts_on": null,
"ends_on": null,
"max_payments": 30,
"payment_methods": {
"automatic": true,
"pay_each_time": true
},
"status": "active",
"subscribe_url": "https://zaptrain.com/subscribe/O9yN2uK0xjoYgLQXUYBj",
"created_at": "2026-10-16T22:44:10.000Z"
}/api/v1/plansCreate a plan
Same fields and rules as the Create plan screen. Set max_payments or ends_on, not both.
Request Body
| Parameter | In | Type | Required | Description |
|---|---|---|---|---|
| name | string | Yes | Up to 80 characters | |
| amount_sats | integer | Yes | Price per period, before tax | |
| interval | string | Yes | daily, weekly, monthly or yearly | |
| description | string | — | Up to 280 characters; shown to subscribers | |
| tax_percent | number | — | Added to the price; subscribers pay the total | |
| starts_on | string | — | YYYY-MM-DD (your timezone): joining earlier pays from this day | |
| max_payments | integer | — | Ends after this many payments | |
| ends_on | string | — | YYYY-MM-DD: no payments after this day | |
| payment_methods | object | — | { automatic, pay_each_time } (both true by default) |
Examples
{
"id": "O9yN2uK0xjoYgLQXUYBj",
"name": "Daily Beans",
"description": null,
"amount_sats": 1000,
"tax_percent": 5,
"total_sats": 1050,
"interval": "daily",
"starts_on": null,
"ends_on": null,
"max_payments": 30,
"payment_methods": {
"automatic": true,
"pay_each_time": true
},
"status": "active",
"subscribe_url": "https://zaptrain.com/subscribe/O9yN2uK0xjoYgLQXUYBj",
"active_subscribers": 4,
"created_at": "2026-10-16T22:44:10.000Z"
}/api/v1/plans/{id}Rename a plan
Only name and description can change. Subscribers agreed to the price, tax, schedule and dates: create a new plan for different terms.
Parameters
| Parameter | In | Type | Required | Description |
|---|---|---|---|---|
| id | path | string | Yes | Plan id |
Request Body
| Parameter | In | Type | Required | Description |
|---|---|---|---|---|
| name | string | — | New name (up to 80 characters) | |
| description | string | — | New description (up to 280 characters) |
Examples
{
"id": "O9yN2uK0xjoYgLQXUYBj",
"name": "Daily Beans",
"description": null,
"amount_sats": 1000,
"tax_percent": 5,
"total_sats": 1050,
"interval": "daily",
"starts_on": null,
"ends_on": null,
"max_payments": 30,
"payment_methods": {
"automatic": true,
"pay_each_time": true
},
"status": "active",
"subscribe_url": "https://zaptrain.com/subscribe/O9yN2uK0xjoYgLQXUYBj",
"active_subscribers": 4,
"created_at": "2026-10-16T22:44:10.000Z"
}/api/v1/plans/{id}/archiveArchive a plan
Stops new sign-ups. Existing subscribers keep paying until cancelled. Archiving an archived plan returns 409 not_allowed.
Parameters
| Parameter | In | Type | Required | Description |
|---|---|---|---|---|
| id | path | string | Yes | Plan id |
Examples
{
"id": "O9yN2uK0xjoYgLQXUYBj",
"name": "Daily Beans",
"description": null,
"amount_sats": 1000,
"tax_percent": 5,
"total_sats": 1050,
"interval": "daily",
"starts_on": null,
"ends_on": null,
"max_payments": 30,
"payment_methods": {
"automatic": true,
"pay_each_time": true
},
"status": "archived",
"subscribe_url": "https://zaptrain.com/subscribe/O9yN2uK0xjoYgLQXUYBj",
"active_subscribers": 4,
"created_at": "2026-10-16T22:44:10.000Z"
}/api/v1/plans/{id}/subscribe-linkGet a subscribe link
The plan's link with a customer's name and email filled in, and your own id as client_reference_id. Nothing is emailed. The customer finishes subscribing themselves.
Parameters
| Parameter | In | Type | Required | Description |
|---|---|---|---|---|
| id | path | string | Yes | Plan id |
Request Body
| Parameter | In | Type | Required | Description |
|---|---|---|---|---|
| customer_name | string | — | Pre-fills the form | |
| customer_email | string | — | Pre-fills the form | |
| client_reference_id | string | — | Your id for them, echoed in webhooks |
Examples
{
"url": "https://zaptrain.com/subscribe/O9yN2uK0xjoYgLQXUYBj?name=Ada&email=ada%40example.com&ref=user_42"
}/api/v1/subscriptionsList subscriptions
Newest first, in the same shape as the subscription.* webhooks. Each subscription's payments are invoices: GET /invoices?subscription={id}.
Parameters
| Parameter | In | Type | Required | Description |
|---|---|---|---|---|
| plan | query | string | — | Only this plan's subscribers |
| status | query | string | — | active, past_due or cancelled |
| limit | query | integer | — | 1–100, default 20 |
| offset | query | integer | — | Default 0 |
Examples
{
"subscriptions": [
{
"id": "aB3dE5fG7hJ9kL1mN3pQ",
"client_reference_id": "user_42",
"status": "active",
"payment_method": "automatic",
"plan": {
"id": "O9yN2uK0xjoYgLQXUYBj",
"name": "Daily Beans"
},
"customer_name": "Ada",
"customer_email": "ada@example.com",
"amount_sats": 1050,
"tax_percent": 5,
"interval": "daily",
"periods_paid": 3,
"max_payments": 30,
"ends_at": null,
"current_period_end": "2026-10-20T09:00:00.000Z",
"last_error_code": null,
"cancel_reason": null,
"created_at": "2026-10-17T09:00:00.000Z",
"cancelled_at": null
}
],
"total": 1
}/api/v1/subscriptions/{id}Get a subscription
One subscription, in the same shape as GET /subscriptions and the subscription.* webhooks. Its payments are invoices: GET /invoices?subscription={id}. An unknown id returns 404.
Parameters
| Parameter | In | Type | Required | Description |
|---|---|---|---|---|
| id | path | string | Yes | Subscription id |
Examples
{
"id": "aB3dE5fG7hJ9kL1mN3pQ",
"client_reference_id": "user_42",
"status": "active",
"payment_method": "automatic",
"plan": {
"id": "O9yN2uK0xjoYgLQXUYBj",
"name": "Daily Beans"
},
"customer_name": "Ada",
"customer_email": "ada@example.com",
"amount_sats": 1050,
"tax_percent": 5,
"interval": "daily",
"periods_paid": 3,
"max_payments": 30,
"ends_at": null,
"current_period_end": "2026-10-20T09:00:00.000Z",
"last_error_code": null,
"cancel_reason": null,
"created_at": "2026-10-17T09:00:00.000Z",
"cancelled_at": null
}/api/v1/subscriptions/{id}/cancelCancel a subscription
The subscriber is emailed, no more payments are taken, and Zaptrain deletes its wallet connection.
Parameters
| Parameter | In | Type | Required | Description |
|---|---|---|---|---|
| id | path | string | Yes | Subscription id |
Examples
{
"id": "aB3dE5fG7hJ9kL1mN3pQ",
"client_reference_id": "user_42",
"status": "cancelled",
"payment_method": "automatic",
"plan": {
"id": "O9yN2uK0xjoYgLQXUYBj",
"name": "Daily Beans"
},
"customer_name": "Ada",
"customer_email": "ada@example.com",
"amount_sats": 1050,
"tax_percent": 5,
"interval": "daily",
"periods_paid": 3,
"max_payments": 30,
"ends_at": null,
"current_period_end": null,
"last_error_code": null,
"cancel_reason": "merchant",
"created_at": "2026-10-17T09:00:00.000Z",
"cancelled_at": "2026-10-21T10:00:00.000Z"
}/api/v1/subscriptions/{id}/retryRetry a past-due payment
Automatic subscriptions only; once per 30 seconds per subscription.
Parameters
| Parameter | In | Type | Required | Description |
|---|---|---|---|---|
| id | path | string | Yes | Subscription id |
Examples
{
"outcome": "paid",
"code": null,
"subscription": {
"id": "aB3dE5fG7hJ9kL1mN3pQ",
"client_reference_id": "user_42",
"status": "active",
"payment_method": "automatic",
"plan": {
"id": "O9yN2uK0xjoYgLQXUYBj",
"name": "Daily Beans"
},
"customer_name": "Ada",
"customer_email": "ada@example.com",
"amount_sats": 1050,
"tax_percent": 5,
"interval": "daily",
"periods_paid": 3,
"max_payments": 30,
"ends_at": null,
"current_period_end": "2026-10-20T09:00:00.000Z",
"last_error_code": null,
"cancel_reason": null,
"created_at": "2026-10-17T09:00:00.000Z",
"cancelled_at": null
}
}/api/v1/invitesList invites
Invites you've emailed, newest first. Open invites (sent) by default, as the Subscriptions screen lists them. subscription is the id of the subscription an invite led to, once the customer subscribes.
Parameters
| Parameter | In | Type | Required | Description |
|---|---|---|---|---|
| status | query | string | — | sent (default), subscribed, removed or all |
Examples
{
"invites": [
{
"id": "Zq8xY2wV4uT6sR8pN0mL",
"plan": {
"id": "O9yN2uK0xjoYgLQXUYBj",
"name": "Daily Beans"
},
"customer_name": "Bo",
"customer_email": "bo@example.com",
"note": null,
"status": "sent",
"subscription": null,
"sent_at": "2026-10-17T10:00:00.000Z",
"created_at": "2026-10-17T10:00:00.000Z"
}
]
}/api/v1/invitesEmail a plan to a customer
Emails the plan's subscribe link with their details filled in (Send to customer). Sending to the same customer again re-sends it; refused with 409 already_subscribed if they're already subscribed, and 502 email_failed if the email couldn't be sent.
Request Body
| Parameter | In | Type | Required | Description |
|---|---|---|---|---|
| plan | string | Yes | Plan id | |
| customer_name | string | Yes | Up to 100 characters | |
| customer_email | string | Yes | Where the invite goes | |
| note | string | — | Included in the email (≤500 chars) |
Examples
{
"id": "Zq8xY2wV4uT6sR8pN0mL",
"plan": {
"id": "O9yN2uK0xjoYgLQXUYBj",
"name": "Daily Beans"
},
"customer_name": "Bo",
"customer_email": "bo@example.com",
"note": null,
"status": "sent",
"subscription": null,
"sent_at": "2026-10-17T10:00:00.000Z",
"created_at": "2026-10-17T10:00:00.000Z"
}/api/v1/invites/{id}/resendRe-send an invite
Emails an open invite again. Only open (sent) invites qualify; others return 404. 409 already_subscribed if the customer has subscribed since; 502 email_failed if the email couldn't be sent.
Parameters
| Parameter | In | Type | Required | Description |
|---|---|---|---|---|
| id | path | string | Yes | Invite id |
Examples
{
"id": "Zq8xY2wV4uT6sR8pN0mL",
"plan": {
"id": "O9yN2uK0xjoYgLQXUYBj",
"name": "Daily Beans"
},
"customer_name": "Bo",
"customer_email": "bo@example.com",
"note": null,
"status": "sent",
"subscription": null,
"sent_at": "2026-10-17T10:00:00.000Z",
"created_at": "2026-10-17T10:00:00.000Z"
}/api/v1/invites/{id}/removeRemove an invite
Takes an open invite off the list. The emailed link is the plan's public link, so it keeps working; it just isn't tracked. Only open (sent) invites qualify; others return 404.
Parameters
| Parameter | In | Type | Required | Description |
|---|---|---|---|---|
| id | path | string | Yes | Invite id |
Examples
{
"id": "Zq8xY2wV4uT6sR8pN0mL",
"plan": {
"id": "O9yN2uK0xjoYgLQXUYBj",
"name": "Daily Beans"
},
"customer_name": "Bo",
"customer_email": "bo@example.com",
"note": null,
"status": "removed",
"subscription": null,
"sent_at": "2026-10-17T10:00:00.000Z",
"created_at": "2026-10-17T10:00:00.000Z"
}Need help? Contact us.