API reference
The Lona API is JSON over HTTPS. You create an invoice in US dollars, send the customer to its hosted_url, and wait for the invoice.paid_out webhook. Lona never holds funds: the customer pays a deposit address on the swap network, which pays your wallet directly.
Authentication
Create a secret key under Developers in the dashboard. Test keys start with lona_sk_test_ and live keys with lona_sk_live_. The key decides the mode of everything you create with it.
Authorization: Bearer lona_sk_test_...Safe retries
Send an Idempotency-Key header with any POST. If the network drops and you retry with the same key and body, you get the original response back (marked Idempotent-Replayed: true) instead of a second invoice. Keys are kept for 24 hours. Reusing a key with a different body returns 422.
curl -X POST https://<your-domain>/api/v1/invoices \
-H "Authorization: Bearer $LONA_SECRET_KEY" \
-H "Idempotency-Key: order_1042" \
-H "Content-Type: application/json" \
-d '{"amount":"49.00","description":"Order 1042"}'Rate limits
Each key can make 120 requests a minute. Every response carries X-RateLimit-Limit, X-RateLimit-Remaining and X-RateLimit-Reset (seconds). Over the limit you get 429 with Retry-After.
Invoices
Create an invoice
POST/api/v1/invoices
{
"amount": "49.00", // US dollars, 1.00 to 100,000.00
"description": "Order 1042", // shown at checkout
"reference": "order_1042", // optional, returned in webhooks
"customer_email": "a@example.com", // optional
"success_url": "https://shop.example/thanks", // optional
"metadata": { "cart": "c_88" }, // optional, string values
"expires_in_minutes": 1440 // optional, 15 to 43200, default 1440
}Returns the invoice with status 201:
{
"id": "inv_8h2k...",
"object": "invoice",
"livemode": false,
"status": "open",
"amount": "49.00",
"currency": "USD",
"description": "Order 1042",
"reference": "order_1042",
"hosted_url": "https://<your-domain>/pay/inv_8h2k...",
"settlement": { "asset": "USDC", "chain": "Base", "address": "0x..." },
"payment": null,
"expires_at": "2026-10-27T12:00:00.000Z",
"paid_out_at": null,
"created_at": "2026-10-26T12:00:00.000Z"
}Once the customer opens checkout, payment holds the ZEC amount, deposit address, how long the price is held, the network status and transaction hashes.
Retrieve an invoice
GET/api/v1/invoices/:id
Fetching an invoice also refreshes its payment status from the network.
List invoices
GET/api/v1/invoices
Newest first. Query parameters: limit (1 to 100, default 25), status, reference, and starting_after (an invoice ID) to get the next page. The response has has_more when another page exists.
GET /api/v1/invoices?status=paid_out&limit=50
GET /api/v1/invoices?starting_after=inv_8h2k...Cancel an invoice
POST/api/v1/invoices/:id/cancel
Only invoices with no payment on the way can be canceled.
Invoice statuses
open | Created. The customer has not opened checkout, or the price they were shown expired. |
awaiting_payment | The customer has a ZEC price and deposit address, held for 20 minutes. |
payment_received | The deposit has been seen on the Zcash network. |
converting | The swap network is converting ZEC to your stablecoin. |
paid_out | The full amount reached your payout wallet. Fulfil the order. |
underpaid | Less ZEC than quoted arrived. The network returns it to the refund address. |
refunded | The ZEC went back to the refund address. Nothing was paid out. |
failed | The conversion did not complete. Contact support with the invoice ID. |
expired | Nobody paid before the invoice's expiry time. |
canceled | You canceled it before any payment arrived. |
Payment links
A reusable link with a fixed amount. Each customer who continues to payment gets a new invoice.
POST/api/v1/payment_links
{ "title": "Workshop ticket", "amount": "25.00", "description": "Optional" }GET/api/v1/payment_links
Webhooks
Add an endpoint under Developers. Lona sends a POST with this body for each event:
{
"id": "evt_...",
"type": "invoice.paid_out",
"created": 1793016000,
"livemode": true,
"data": { "object": { /* the invoice */ } }
}Use Send test event under Developers to deliver a signed test.ping to your endpoint and see the response. Roll secret replaces the signing secret immediately. Webhook URLs must be public addresses; for local development use a tunnel.
Event types:
invoice.payment_receivedinvoice.paid_outinvoice.underpaidinvoice.refundedinvoice.failedinvoice.expired
Respond with any 2xx status within 10 seconds. Otherwise Lona retries after 1, 5, 30, 120, 360 and 720 minutes. Events can arrive more than once; use the event id to ignore repeats.
Verify the signature
Each request carries a Lona-Signature header of the form t=timestamp,v1=hex. The signature is an HMAC-SHA256 of timestamp + "." + raw body with your endpoint's signing secret.
import { createHmac, timingSafeEqual } from "node:crypto";
export function verifySignature(secret, header, rawBody, toleranceSec = 300) {
const parts = Object.fromEntries(header.split(",").map((p) => p.split("=")));
const t = Number(parts.t);
if (!t || Math.abs(Date.now() / 1000 - t) > toleranceSec) return false;
const expected = createHmac("sha256", secret).update(`${t}.${rawBody}`).digest();
const given = Buffer.from(parts.v1 ?? "", "hex");
return given.length === expected.length && timingSafeEqual(given, expected);
}Test mode
The swap network has no test network, so Lona simulates it. Test invoices get a fixed test price and a made-up deposit address. On the test checkout page, choose how the payment ends: paid in full, underpaid, refunded or failed. The same statuses and webhooks fire as in live mode.
Errors
Errors use standard HTTP status codes and this body:
{ "error": { "code": "settlement_not_configured", "message": "Set a payout wallet in Settlement before creating invoices." } }