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

  1. Your server creates a session with POST /api/v1/checkout/sessions and gets back a hosted_url.
  2. You redirect the customer's browser to that URL. They scan the QR with any Lightning wallet.
  3. Zaptrain sends them to your return_url with ?session_id=…&status=paid&ts=…&signature=… (status=no_payment_required for a subscription to a plan that starts later).
  4. 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");
});
Why both checks? The signature proves the redirect came from Zaptrain and is recent (10-minute window). The 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

invoice.paidAn invoice was paid. Sent once per invoice; delivery may be repeated, so treat id as the idempotency key.
subscription.createdSomeone subscribed to one of your plans and the first payment went through (or, for a plan that starts later, they signed up and 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.
subscription.past_dueA renewal is overdue: an automatic payment failed (Zaptrain retries), or a pay-each-time renewal wasn't paid by its due day (it stays payable for a grace period). The subscriber is emailed either way; decide how long to keep access open.
subscription.cancelledCancelled by the subscriber, by you, after automatic renewals kept failing, when a pay-each-time renewal went unpaid, or because it reached the plan's end (cancel_reason: ended). Revoke access here. Each successful renewal arrives as invoice.paid with an invoice.subscription object.
test.pingSent by the "Send test event" button with a sample invoice. Same shape; ignore it in production code.
{
  "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
});
Delivery rules. Answer with any 2xx within 5 seconds. Anything else — a timeout, a 5xx, a redirect — is retried after 30 seconds, 5 minutes and 30 minutes, then marked failed; the last few deliveries and their status are shown in Settings. Retries run with Zaptrain's hourly renewal job and on live traffic, so an endpoint that was down for an hour gets its events within about an hour of coming back. The webhook is a notification, not the record: confirm with GET /api/v1/invoices/{id} or the checkout session before fulfilling, and keep the payment link as the customer's receipt.
GET/api/v1/users/me

Get 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"
}
PATCH/api/v1/users/me

Update merchant profile

Update mutable profile fields: brand name, brand color, and default memo. All other fields are ignored.

Request Body

ParameterInTypeRequiredDescription
brand_namestring—Display name for invoices
brand_colorstring—Hex color (e.g., #d26434)
default_memostring—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"
}
GET/api/v1/users/me/usage

Get 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"
}
GET/api/v1/invoices

List 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

ParameterInTypeRequiredDescription
limitqueryinteger—Max results to return (1–100, default 20)
offsetqueryinteger—Number of records to skip (default 0)
typequerystring—all (default), one_time, or recurring
subscriptionquerystring—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
}
POST/api/v1/invoices

Create 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

ParameterInTypeRequiredDescription
customer_namestring—Customer name (optional)
customer_emailstring—Customer email address (optional; validated if given). Needed only for send/remind — otherwise share the payment link yourself.
memostring—Invoice description/memo
due_datestring—Due date (ISO 8601 format)
itemsarrayYesLine items with description, quantity, unit_price_sats
tax_percentnumber—Tax percentage (default 0)
tax_amount_satsinteger—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
}
GET/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

ParameterInTypeRequiredDescription
idpathintegerYesInvoice 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
    }
  ]
}
POST/api/v1/invoices/{id}/send

Send 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

ParameterInTypeRequiredDescription
idpathintegerYesInvoice internal ID

Request Body

ParameterInTypeRequiredDescription
subjectstring—Custom email subject
messagestring—Custom message to include in email body

Examples

{
  "ok": true,
  "status": "sent"
}
POST/api/v1/invoices/{id}/remind

Send 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

ParameterInTypeRequiredDescription
idpathintegerYesInvoice internal ID

Request Body

ParameterInTypeRequiredDescription
modestring—'same' to resend original, 'new' to regenerate (default 'new')
subjectstring—Custom email subject (mode='new' only)
messagestring—Custom message (mode='new' only)

Examples

{
  "ok": true,
  "status": "sent"
}
POST/api/v1/invoices/{id}/cancel

Cancel 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

ParameterInTypeRequiredDescription
idpathintegerYesInvoice 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"
  }
}
GET/api/v1/invoices/{id}/payment

Get payment status

Get payment and Lightning Network status for an invoice. Includes BOLT11 code, payment status, and finalization timestamp.

Parameters

ParameterInTypeRequiredDescription
idpathintegerYesInvoice 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
}
POST/api/v1/checkout/sessions

Create 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

ParameterInTypeRequiredDescription
modestring—"payment" (default) or "subscription"
line_itemsarray—Required in payment mode, not allowed in subscription mode. 1–50 items: { description (≤500 chars), quantity (int ≥1), unit_price_sats (int ≥1) }
planstring—Required in subscription mode: the plan id. The plan sets the price
return_urlstringYeshttps:// URL (http://localhost allowed) the customer returns to after paying; ≤2048 chars
cancel_urlstringYesURL for the customer's Cancel link on the hosted page
customer_emailstring—Pre-fills the invoice's customer
client_reference_idstring—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 _ . : @ -
metadataobject—Up to 50 keys, ≤8 KB; returned on retrieve
expires_ininteger—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
  }
}
GET/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

ParameterInTypeRequiredDescription
idstringYesSession 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"
}
GET/api/v1/plans

List 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"
    }
  ]
}
GET/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

ParameterInTypeRequiredDescription
idpathstringYesPlan 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"
}
POST/api/v1/plans

Create a plan

Same fields and rules as the Create plan screen. Set max_payments or ends_on, not both.

Request Body

ParameterInTypeRequiredDescription
namestringYesUp to 80 characters
amount_satsintegerYesPrice per period, before tax
intervalstringYesdaily, weekly, monthly or yearly
descriptionstring—Up to 280 characters; shown to subscribers
tax_percentnumber—Added to the price; subscribers pay the total
starts_onstring—YYYY-MM-DD (your timezone): joining earlier pays from this day
max_paymentsinteger—Ends after this many payments
ends_onstring—YYYY-MM-DD: no payments after this day
payment_methodsobject—{ 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"
}
PATCH/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

ParameterInTypeRequiredDescription
idpathstringYesPlan id

Request Body

ParameterInTypeRequiredDescription
namestring—New name (up to 80 characters)
descriptionstring—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"
}
POST/api/v1/plans/{id}/archive

Archive a plan

Stops new sign-ups. Existing subscribers keep paying until cancelled. Archiving an archived plan returns 409 not_allowed.

Parameters

ParameterInTypeRequiredDescription
idpathstringYesPlan 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"
}
GET/api/v1/subscriptions

List subscriptions

Newest first, in the same shape as the subscription.* webhooks. Each subscription's payments are invoices: GET /invoices?subscription={id}.

Parameters

ParameterInTypeRequiredDescription
planquerystring—Only this plan's subscribers
statusquerystring—active, past_due or cancelled
limitqueryinteger—1–100, default 20
offsetqueryinteger—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
}
GET/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

ParameterInTypeRequiredDescription
idpathstringYesSubscription 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
}
POST/api/v1/subscriptions/{id}/cancel

Cancel a subscription

The subscriber is emailed, no more payments are taken, and Zaptrain deletes its wallet connection.

Parameters

ParameterInTypeRequiredDescription
idpathstringYesSubscription 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"
}
POST/api/v1/subscriptions/{id}/retry

Retry a past-due payment

Automatic subscriptions only; once per 30 seconds per subscription.

Parameters

ParameterInTypeRequiredDescription
idpathstringYesSubscription 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
  }
}
GET/api/v1/invites

List 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

ParameterInTypeRequiredDescription
statusquerystring—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"
    }
  ]
}
POST/api/v1/invites

Email 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

ParameterInTypeRequiredDescription
planstringYesPlan id
customer_namestringYesUp to 100 characters
customer_emailstringYesWhere the invite goes
notestring—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"
}
POST/api/v1/invites/{id}/resend

Re-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

ParameterInTypeRequiredDescription
idpathstringYesInvite 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"
}
POST/api/v1/invites/{id}/remove

Remove 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

ParameterInTypeRequiredDescription
idpathstringYesInvite 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.

API documentation · Zaptrain