ZapAPI Docs
Get StartedConceptsAI GatewayGuidesPayment GatewaySDKAPI ReferenceResources
Payment Gateway

API Reference (Draft)

Public API surface, webhook catalog and SDK sketch.

Base URL: https://api.zapapi.com/api/v1 Auth: Authorization: Bearer zap_live_… (per-project key, scoped). Test keys use the zap_test_ prefix.

Conventions:

  • Amounts in sats (integer) unless currency given (then amount is decimal fiat)
  • Idempotency: send Idempotency-Key: <uuid> header on POST
  • All timestamps ISO-8601 UTC

Response envelope — every JSON response is wrapped:

{
 "status": "success",
 "statusCode": 200,
 "message": "Request successful",
 "data": {},
 "timestamp": "2026-10-24T00:00:00.000Z",
 "path": "/api/v1/invoices"
}

Errors use the same envelope with status: "error" and an errors array of field-level messages:

{
 "status": "error",
 "statusCode": 422,
 "message": "Validation failed",
 "errors": ["amount must be a positive integer"],
 "timestamp": "2026-10-24T00:00:00.000Z",
 "path": "/api/v1/invoices"
}

The examples below show the unwrapped data payload for brevity.


Team Access (project members)

Share a project with teammates — invite by email, they accept in-app.

POST /projects/:projectId/members
{ "email": "teammate@example.com", "role": "editor" } // owner only

GET /projects/members/pending invitations FOR you
POST /projects/members/:id/accept join
GET /projects/shared projects shared with you
PATCH /projects/members/:id/role owner changes roles
DELETE /projects/members/:id leave / revoke
  • Roles: viewer (read-only) · editor (read + write resources) · the project owner has full control incl. member management.
  • Members query project resources as the owner — the ProjectRoleGuard enforces membership server-side on every project-scoped route.

Invoices

Create invoice

POST /invoices
{
 "amount": 21000,
 "currency": "sat", // or "USD" converted at creation
 "description": "Pro plan",
 "expirySeconds": 3600,
 "metadata": { "orderId": "ord_123" },
 "successUrl": "https://shop.example/success",
 "webhookUrl": "https://shop.example/hooks/zapapi" // optional per-invoice override
}
 201
{
 "id": "inv_8f3…",
 "status": "pending",
 "bolt11": "lnbc210n1pj…",
 "paymentHash": "a41c…",
 "checkoutUrl": "https://pay.zapapi.com/i/inv_8f3…",
 "amountSats": 21000,
 "fiat": { "currency": "USD", "amount": 21.00, "rate": 100000 },
 "expiresAt": "2026-10-23T13:00:00Z"
}

Get invoice

GET /invoices/:id adds status: paid, preimage, paidAt when settled.

List invoices

GET /invoices?status=paid&from=…&to=…&limit=50&cursor=…

Cancel invoice

POST /invoices/:id/cancel — cancels a pending invoice so it can no longer be paid.


Payments — labels & statistics

PUT /payments/:paymentHash/labels replace labels (max 10, 50 chars each)
GET /payments/labels all labels in use (autocomplete)
GET /payments?label=… filter by label

GET /payments/stats/daily?days=30 income/spend/fee/count per UTC day
GET /payments/stats/count?by=status|project|type|label

NWC service mode (Nostr Wallet Connect provider)

Your ZapAPI wallet can ACT as a NIP-47 wallet service — apps connect to IT (the inverse of connecting an NWC wallet to ZapAPI as a provider).

POST /api/v1/nwc/connections
{ "label": "My AI agent",
 "permissions": ["get_info","get_balance","make_invoice","lookup_invoice","pay_invoice"],
 "budgetSats": 10000, "budgetPeriod": "daily" }
 { "uri": "nostr+walletconnect://<pubkey>?relay=wss://…&secret=…" } // shown ONCE

GET /api/v1/nwc/connections list (spent/remaining budget)
DELETE /api/v1/nwc/connections/:id revoke instantly
  • The URI's client secret is shown once (only a hash is stored, for revocation).
  • Budgets cap spend: sats per day/week/month or a total; exhausted budgets answer RESTRICTED — and decodable invoices are rejected BEFORE payment when they'd exceed the remaining budget.
  • Permissions are per connection (default = read + receive; pay_invoice is explicit opt-in).
  • The relay listener runs on long-lived servers (serverless: CRUD only).
  • Use cases: AI agents with spending limits, app-specific wallets, scoped team access.

LNDHub (BlueWallet / Zeus / BitBanana)

Your ZapAPI wallet speaks the canonical LNDHub API — point any LNDHub-compatible wallet app at it and use it as a remote control:

lndhub://<login>:<password>@https://api.zapapi.com/api/v1/lndhub/ext
  • Dashboard Wallet Connect your wallet app generates the URI (QR) — the password is shown ONCE; rotating invalidates the old one.
  • POST /lndhub/ext/auth exchanges login/password (or refresh_token) for a 2h access token + 30d refresh token.
  • Supported surface: getinfo, balance, addinvoice, payinvoice, gettxs (sends), getuserinvoices (receives), decodeinvoice.
  • POST/DELETE /api/v1/lndhub/credentials (dashboard JWT) manages access; disabling revokes all tokens immediately.
  • Your sats stay on ZapAPI — the app never gets withdrawal-level keys.

Auto-forward / Scrub (treasury)

Sweep incoming sats to cold storage automatically — keep a liquidity reserve in the wallet for refunds and fees, forward everything above.

PUT /scrub/rule
{ "destinationAddress": "cold@wallet.com", "reserveSats": 10000, "label": "Cold storage sweep" }

GET /scrub/rule current rule + totals (forwarded sats, forward count)
DELETE /scrub/rule stop forwarding
GET /scrub/executions forward audit trail (status + payment hash)
  • Runs on EVERY incoming settle path: project invoices (after revenue splits), personal LNURL address payments, and zaps.
  • Splits first, scrub second — the reserve math sees the remainder.

Revenue Splits (affiliate / revenue share)

Auto-pay a percentage of every settled project invoice to lightning addresses — up to 5 targets, the remainder stays with you.

PUT /splits/rules?projectId=…
{ "targets": [
 { "address": "seller@wallet.com", "percent": 90, "label": "Seller" },
 { "address": "platform@wallet.com", "percent": 10, "label": "Marketplace fee" }
] }
GET /splits/rules?projectId=…
DELETE /splits/rules?projectId=…
GET /splits/executions?projectId=… payout audit trail (status + payment hash)
  • Percentages must sum to ≤ 100%; payouts execute on invoice settlement.
  • Each payout is recorded with status and payment hash — failures never break the payment flow (they're logged for retry).

Tabs (running customer bills)

Open a tab for a customer, add charges as they go, share the link — they settle with Lightning whenever they're ready.

POST /tabs { "name": "Table 12", "customerName": "Alice", "limitSats": 50000 }
POST /tabs/:id/entries
{ "entryType": "charge", "amountSats": 250, "quantity": 2, "description": "IPA" }
 // or { "entryType": "credit", "amountSats": 300, "description": "Comp" }

# Customer (public)
GET /tabs/public/:id balance + recent charges
POST /tabs/public/:id/settle
{ "amountSats": 400 } // omit = full balance
GET /tabs/public/settlements/:id completion check (poll)
  • Charges increase the balance; credits decrease it (comps, corrections — never below zero).
  • Optional spending limit blocks charges above it.
  • Completed settlements reduce the balance immediately; pending ones can be cancelled by the owner.

Paid Calendar (appointment scheduling)

Sell your time in slots — buyers pick an open slot and pay to book.

POST /calendar/schedules
{ "name": "Consulting call",
 "startDay": 1, "endDay": 5, // Mon–Fri (0 = Sun; wraps e.g. 5–1)
 "startTime": "09:00", "endTime": "17:00",
 "slotMinutes": 60, "pricePerSlotSats": 5000 }

# Buyer flow (public)
GET /calendar/public/schedules/:id window + booked + blocked slots
POST /calendar/public/schedules/:id/book
{ "name": "Alice", "email": "alice@example.com",
 "info": "Topic", "startTime": "2026-11-03T10:00:00Z" }
 { appointmentId, bolt11 }
GET /calendar/public/appointments/:id confirmation check (poll)

# Owner
POST /calendar/schedules/:id/unavailable block a period (vacation, conflicts)
GET /calendar/schedules/:id/appointments who booked what
  • Availability = weekly window − paid bookings − blocked periods; the server re-validates every rule at booking time (no race-condition double-booking for paid slots).
  • Free (unpaid) bookings never block the calendar.

Event Tickets (paid sales + QR check-in)

Sell paid tickets for an event; buyers get a QR code you scan at the door.

POST /events
{ "name": "LNConf 2026", "info": "The Lightning conference",
 "startDate": "2026-11-01T14:00:00Z", "endDate": "2026-11-01T18:00:00Z",
 "closingDate": "2026-10-31T23:59:00Z",
 "pricePerTicketSats": 1000, "amountTickets": 100 }

# Buyer flow (public)
GET /events/public/:id info, price, availability
POST /events/public/:id/tickets
{ "name": "Alice", "email": "alice@example.com" }
 { ticketId, bolt11, paymentHash }
GET /events/public/tickets/:ticketId reveals checkCode once paid

# Door (owner)
PUT /events/:id/check-in?code=… checked-in | already | unpaid | invalid
  • Tickets are per-purchase invoices on the organizer's wallet; the checkCode is the QR secret revealed only after payment.
  • Check-in is one-shot — a second scan reports already.
  • sold counter flips to sold out at capacity; sales close at closingDate.

Paywalls (paid URL unlocks)

Charge for access to any URL — payers see title + price; the destination is revealed only after payment.

POST /paywalls
{ "title": "Premium article",
 "destinationUrl": "https://blog.example/premium",
 "amountSats": 210 }
 { "shortId": "pw_abc123…" } share https://pay.zapapi.app/pay/w/pw_abc123…

# Payer flow (public, no auth)
GET /paywalls/public/:shortId title + price (never the destination)
POST /paywalls/public/:shortId/invoice { bolt11, paymentHash }
GET /paywalls/public/:shortId/access?paymentHash=…
 { unlocked: true, destinationUrl } once paid
  • The payment hash is the payer's receipt — kept client-side, it re-unlocks the content on return visits.
  • unlocks + earnedSats counters track conversion per paywall.
  • Owner endpoints (CRUD, status toggle) accept API keys with payment-links:read/write scopes or a dashboard JWT.

Payment Links

POST /payment-links
{ "name": "Coffee tip", "type": "fixed" | "open", // open = payer picks amount
 "amount": 5000, "redirectUrl": "…", "collectPayerNote": true }
 { "id": "pl_…", "url": "https://pay.zapapi.com/l/pl_…" }

GET /payment-links · GET /payment-links/:id · PATCH /payment-links/:id · DELETE


Lightning Address (public, no auth)

GET /.well-known/lnurlp/:username LNURL-pay params (LUD-06/16, allowsNostr)
GET /lnurl/callback/:username?amount=… { "pr": "lnbc…", "verify": "…" }
 [&comment=…] [&nostr=<zap request>] [&webhook_url=… (LUD-22)]
GET /lnurl/verify/:paymentHash LUD-21 { "settled": bool, "preimage", "pr" }
GET /.well-known/nostr.json?name=… NIP-05 mapping
  • LUD-21 — every callback response carries a verify URL the payer's wallet can poll for settlement + preimage.
  • LUD-22 — pass webhook_url on the callback; we POST once when the invoice settles ({settled, payment_hash, preimage, amount_sats}).
  • NIP-05 pubkey binding requires proof — POST /lnurl/claim/pubkey-challenge, then submit the signed kind-27235 event as pubkeyProof.

Subscriptions (creator-side)

POST /creator-plans
{ "name": "Supporter", "amount": 21000, "interval": "month", "trialDays": 7,
 "perks": ["Exclusive posts", "Badge"] }
GET /creator-plans · PATCH /creator-plans/:id · DELETE /creator-plans/:id

GET /creator-plans/subscriptions?status=active
POST /creator-plans/subscriptions/:id/cancel
GET /subscribe/:planId hosted checkout (QR + NWC connect option)
GET /subscribe/portal/:token subscriber portal (manage/cancel) — the
 token arrives via email/nostr magic link

Customers (CRM)

GET /customers?tag=vip&q=…
GET /customers/:id customer with payments, subscriptions, LTV
PATCH /customers/:id tags, notes

Watch-Only Wallets (xpub + PSBT)

Import an xpub — the server derives receive addresses and tracks on-chain balances, but NEVER holds keys. Spend by building a PSBT for external signing.

POST /watchonly/wallets { "xpub": "zpub…", "title": "Savings" }
POST /watchonly/wallets/:id/addresses/fresh?note=invoice-42
POST /watchonly/wallets/:id/refresh balances via mempool.space
POST /watchonly/wallets/:id/psbt
{ "destinationAddress": "bc1q…", "amountSats": 50000 }
 base64 BIP-174 PSBT (witness_utxo per input) — sign in Sparrow/Coldcard
  • Supports xpub/ypub/zpub (tpub/upub/vpub on testnet); the xpub is AES-encrypted at rest.
  • Addresses derive with a 5-deep lookahead; fresh addresses reuse the newest unused one (no address reuse for payers).
  • PSBT builder is p2wpkh-only for v1; change returns to a fresh address.

On-chain Swaps (Boltz submarine)

Move between Lightning and Bitcoin on-chain without trusting us — swaps run through the Boltz exchange.

GET /swaps/info availability, limits, fees

# Withdraw: Lightning Bitcoin address
POST /swaps/submarine
{ "address": "bc1q…", "amountSats": 50000 }
 swap created; the invoice is paid from your wallet immediately;
 Boltz sends on-chain (track via refresh)

# Deposit: on-chain BTC your wallet
POST /swaps/reverse { "amountSats": 50000 }
 { lockupAddress + bip21 } — send on-chain; Boltz pays your wallet

GET /swaps history
POST /swaps/:id/refresh poll Boltz status
 (invoice.paid transaction.mempool transaction.confirmed done)
  • Fees: Boltz percentage + miner fee, shown upfront in /swaps/info.
  • Platform-wide kill switch: BOLTZ_ENABLED=false; self-hostable Boltz via BOLTZ_API_URL.

Wallets & Withdrawals

GET /wallet/info { "balanceSats": 123456, … }
GET /my-wallet/payments transaction history

POST /my-wallet/withdrawals
{ "type": "bolt11", "invoice": "lnbc…" }
 // or { "type": "lightning_address", "address": "you@wallet.com", "amount": … }
 { "id": "wd_…", "status": "processing" } webhook withdrawal.completed

Scannable vouchers that PULL sats from your wallet — hand someone a QR code instead of asking for an invoice. Sats stay in your wallet until claimed.

POST /my-wallet/withdraw-links
{ "title": "Conference vouchers", "amountSats": 21000, "uses": 50 }
 { "id": "…", "lnurl": "lnurl1dp68g…", "remaining": 50, … }

GET /my-wallet/withdraw-links
GET /my-wallet/withdraw-links/:id current used count
DELETE /my-wallet/withdraw-links/:id unclaimed sats stay in the wallet
  • Fixed amount per claim (amountSats), 1–1000 uses per link, 50 links max.
  • Balance check at creation: amountSats × uses must be covered.
  • Each claim uses a unique k1 (double-claim proof).

Zap Feeds (NIP-57)

Zaps are nostr-native tips with signed receipts.

GET /nostr/zaps/mine your feed + lifetime totals
 (totalReceivedSats, supportersCount)
GET /nostr/zaps/public/:address public feed for a lightning address
 (settled zaps only, ≤ 50)

Receipts are signed with YOUR key via NIP-46 (Dashboard Identity Remote Signer) or the platform key as fallback. The bio page shows the supporters wall automatically when "Recent supporters" is enabled.

Webhooks

Register

POST /webhooks { "url": "https://…", "events": ["invoice.paid","zap.received"] }

Signature verification

X-ZapAPI-Signature: t=1698064000,v1=5257a869e7ecebeda32affa62cdca3fa51cad7e77a0e56ff536d0ce8e108d8bd
hmac_sha256(webhookSecret, `${t}.${rawBody}`) === v1 // reject if |now - t| > 5min

Event catalog

EventPayload highlight
invoice.created / invoice.paid / invoice.expiredinvoice object (+preimage on paid)
payment_link.paidlink id, payer note, invoice
zap.receivedzap request JSON, amount, sender pubkey
subscription.created/renewed/past_due/canceledsubscription + plan + customer
withdrawal.completed/failedwithdrawal object
refund.completed/failedrefund object + original invoice

Retries

Failed deliveries are retried automatically — 5 attempts with exponential backoff (1s 2s 4s 8s 16s between attempts). Only 2xx responses count as success; timeouts (10s) and non-2xx statuses trigger the retry ladder.

Delivery history, manual retry & event replay

Every delivery attempt is recorded with its HTTP outcome, error message and duration. Query them, retry a single delivery, or replay a whole event:

GET /webhooks/deliveries?webhookId=…&event=invoice.paid&status=failed&page=1&limit=20
GET /webhooks/deliveries/stats
GET /webhooks/deliveries/:id

POST /webhooks/deliveries/:id/retry re-deliver to the original endpoint
POST /webhooks/events/:eventId/replay re-deliver to ALL endpoints that
 received the event originally
  • Replays deliver the stored payload byte-identically (same evt_… id) — consumers can dedup on the event id (at-least-once semantics, like Stripe).
  • Endpoints deleted since the original delivery are skipped.
  • Manual retries and replays are flagged manual: true in the delivery log.
  • Scopes: webhooks:read for history, webhooks:write for retry/replay.
  • The same actions are available in the dashboard under Webhooks.

Test delivery

POST /webhooks/:id/test
 { "success": true, "message": "Test webhook sent", "eventId": "evt_test_…" }

Sends a test event through the real pipeline (signing, retries, delivery log) so it appears in the delivery history like any other attempt.


Rate limits

  • zap_test_: 100 req/min
  • zap_live_: 600 req/min, invoices endpoint 120/min
  • Public LNURL endpoints: 300/min per IP (cached params)

SDK sketch (@zapapi/sdk)

import ZapAPI from '@zapapi/sdk';
const zap = new ZapAPI('zap_live_…');

const inv = await zap.invoices.create({ amount: 21000, description: 'Pro' });
renderQR(inv.checkoutUrl);

zap.webhooks.verify(rawBody, signature, whsec); // helper

Embed widget

<script src="https://js.zapapi.com/v1/checkout.js"></script>
<button data-zapapi data-amount="21000" data-project="proj_…">
 Pay with Lightning
</button>
<!-- opens modal: QR WebLN attempt paid callback -->

On this page