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
currencygiven (thenamountis 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|labelNWC 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_invoiceis 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/authexchanges login/password (orrefresh_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
checkCodeis the QR secret revealed only after payment. - Check-in is one-shot — a second scan reports
already. soldcounter flips to sold out at capacity; sales close atclosingDate.
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+earnedSatscounters track conversion per paywall.- Owner endpoints (CRUD, status toggle) accept API keys with
payment-links:read/writescopes 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
verifyURL the payer's wallet can poll for settlement + preimage. - LUD-22 — pass
webhook_urlon 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 aspubkeyProof.
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/cancelFollower-facing (no API key — magic link token)
GET /subscribe/:planId hosted checkout (QR + NWC connect option)
GET /subscribe/portal/:token subscriber portal (manage/cancel) — the
token arrives via email/nostr magic linkCustomers (CRM)
GET /customers?tag=vip&q=…
GET /customers/:id customer with payments, subscriptions, LTV
PATCH /customers/:id tags, notesWatch-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 viaBOLTZ_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.completedLNURL-withdraw links (LUD-03 vouchers)
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 × usesmust 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=5257a869e7ecebeda32affa62cdca3fa51cad7e77a0e56ff536d0ce8e108d8bdhmac_sha256(webhookSecret, `${t}.${rawBody}`) === v1 // reject if |now - t| > 5minEvent catalog
| Event | Payload highlight |
|---|---|
invoice.created / invoice.paid / invoice.expired | invoice object (+preimage on paid) |
payment_link.paid | link id, payer note, invoice |
zap.received | zap request JSON, amount, sender pubkey |
subscription.created/renewed/past_due/canceled | subscription + plan + customer |
withdrawal.completed/failed | withdrawal object |
refund.completed/failed | refund 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: truein the delivery log. - Scopes:
webhooks:readfor history,webhooks:writefor 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/minzap_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); // helperEmbed 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 -->