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

Request body
{
  "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

openCreated. The customer has not opened checkout, or the price they were shown expired.
awaiting_paymentThe customer has a ZEC price and deposit address, held for 20 minutes.
payment_receivedThe deposit has been seen on the Zcash network.
convertingThe swap network is converting ZEC to your stablecoin.
paid_outThe full amount reached your payout wallet. Fulfil the order.
underpaidLess ZEC than quoted arrived. The network returns it to the refund address.
refundedThe ZEC went back to the refund address. Nothing was paid out.
failedThe conversion did not complete. Contact support with the invoice ID.
expiredNobody paid before the invoice's expiry time.
canceledYou canceled it before any payment arrived.

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:

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." } }