AvoPay Web API
Accept Bitcoin Lightning payments on any website. Create a checkout session in one API call, redirect the customer, and get notified via webhook when they pay.
Quick start
-
1
Get your secret key from the Dashboard → Developer → Integration. It starts with
ak_live_. Keep it on your server — never put it in HTML or commit it to version control. -
2
Create a payment session from your backend:
Node.jsconst resp = await fetch('https://api.avopay.dev/api/v1/payments', { method: 'POST', headers: { 'Authorization': `Bearer ${process.env.AVOPAY_SECRET_KEY}`, 'Content-Type': 'application/json', }, body: JSON.stringify({ amount: 49.99, currency: 'EUR', redirect_url: 'https://yoursite.com/order/complete', }), }); const { checkout_url } = await resp.json(); // redirect the customer to checkout_url
cURLcurl -X POST https://api.avopay.dev/api/v1/payments \ -H "Authorization: Bearer $AVOPAY_SECRET_KEY" \ -H "Content-Type: application/json" \ -d '{"amount":49.99,"currency":"EUR","redirect_url":"https://yoursite.com/order/complete"}' -
3
Redirect the customer to
checkout_url. They'll see a Lightning invoice and an L-BTC address. You'll receive a webhook event when payment confirms.
Authentication
All API requests require a key in the Authorization header.
Authorization: Bearer <your_api_key>
| Key type | Prefix | Use |
|---|---|---|
| Secret key | ak_live_… |
Server-side only. Grants full access. Never expose in HTML or client-side code. |
| Publishable key | ak_pub_… |
Safe to embed in frontend code and HTML. Can create payment sessions only. Used with the JS SDK. |
Find both keys in your dashboard under Developer → Integration.
Create a payment
POST/api/v1/payments
Creates a Bitcoin Lightning checkout session. Returns a checkout_url to redirect the customer to.
Request body
| Field | Type | Description | |
|---|---|---|---|
amount | number | required | Amount to charge (e.g. 49.99) |
currency | string | required | ISO 4217 code — "EUR", "USD", "GBP", "CAD", "AUD", "JPY", "CHF", "DKK", "BRL" |
redirect_url | string | optional | URL to return the customer to after payment. Query params payment_id=…&status=paid are appended. |
metadata | object | optional | Any key/value pairs you want to attach (max 2 KB). Included in webhook payloads. |
Response 201
{
"payment_id": "pmt_abc123",
"checkout_url": "https://avopay.dev/pay/pmt_abc123",
"expires_at": "2026-05-15T14:23:00.000Z"
}
The session is valid for 10 minutes. After that the customer sees an "expired" page.
Full example
// POST handler for your checkout page app.post('/checkout', async (req, res) => { const resp = await fetch('https://api.avopay.dev/api/v1/payments', { method: 'POST', headers: { 'Authorization': `Bearer ${process.env.AVOPAY_SECRET_KEY}`, 'Content-Type': 'application/json', }, body: JSON.stringify({ amount: req.body.total, currency: 'EUR', redirect_url: `https://yoursite.com/orders/${req.body.orderId}`, metadata: { order_id: req.body.orderId }, }), }); if (!resp.ok) throw new Error('Payment creation failed'); const { checkout_url } = await resp.json(); res.redirect(303, checkout_url); });
Get a payment
GET/api/v1/payments/:payment_id
Retrieve the current state of a payment session. Useful for server-side status polling.
Response 200
{
"payment_id": "pmt_abc123",
"status": "paid",
"amount_fiat": 49.99,
"currency": "EUR",
"amount_sats": 62341,
"created_at": "2026-05-15T14:08:00.000Z",
"checkout_url":"https://avopay.dev/pay/pmt_abc123"
}
Payment statuses
| Status | Meaning |
|---|---|
| pending | Awaiting customer payment. The checkout page is live. |
| paid | Payment confirmed. Settled to the merchant's AQUA wallet. |
| expired | The 10-minute window closed without payment, or the swap expired. |
Payment lifecycle
Your backend AvoPay API Customer browser AQUA wallet
│ │ │ │
│ POST /payments │ │ │
│─────────────────────▶│ │ │
│ { checkout_url } │ │ │
│◀─────────────────────│ │ │
│ │ │ │
│ redirect to │ │ │
│ checkout_url ───────────────────────────────▶│ │
│ │ │ scan QR / tap link │
│ │ │─────────────────────▶│
│ │ │ │ pay
│ │◀──────────────── Lightning settled ───────────│
│ │ │ │
│ POST webhook │ │ │
│◀─────────────────────│ page redirects ──────▶│ │
│ payment.confirmed │ to redirect_url │ │
Payment links
Payment links are reusable URLs (avopay.dev/pay/lnk_…) that can be shared as-is or embedded as a button. Customers visit the link, enter an amount if needed, and pay — no backend call required at checkout time.
POST /api/v1/payment-links accepts a Bearer ak_live_… secret key in addition to a dashboard session. Create product or booking links programmatically, then embed the returned URL as a plain HTML button — zero backend required after setup. See AI Agent Onboarding for the full flow.Create and manage payment links from the Developer → Payment Links tab in your dashboard, or via the API with your secret key. Two types:
- Fixed amount — price is set when creating the link. Customer sees the amount immediately.
- Open amount — customer types any amount before paying. Great for donations or tips.
Public endpoints
These endpoints are called by the hosted checkout page (avopay.dev/pay/lnk_…) and do not require authentication.
GET/api/v1/checkout/link/:link_id
Returns the link's public metadata.
{
"id": "lnk_abc",
"title": "Coffee tip",
"description": null,
"amount_fiat": 5.00, // null for open-amount links
"currency": "EUR",
"status": "active"
}
POST/api/v1/checkout/link/:link_id/pay
Creates a payment session from the link. For open-amount links, send the amount in the body.
{ "amount_fiat": 10.00, "currency": "EUR" }
201{ "payment_id": "pmt_xyz", "checkout_url": "https://avopay.dev/pay/pmt_xyz" }
Embed a button
Copy the embed code from the dashboard or paste this snippet — replace lnk_… with your link ID:
<a href="https://avopay.dev/pay/lnk_abc" style="display:inline-block;background:#E65C00;color:#fff; padding:10px 22px;border-radius:8px;text-decoration:none; font-family:sans-serif;font-weight:600;"> Pay with Bitcoin </a>
JS SDK
A lightweight browser script that creates a payment session and redirects the customer — no backend needed. Uses your publishable key (ak_pub_…).
Include the SDK
<script src="https://avopay.dev/js/avopay.js"></script>
AvoPay.pay(options)
| Option | Type | Description | |
|---|---|---|---|
publishableKey | string | required | Your ak_pub_… key |
amount | number | required | Amount to charge (e.g. 9.99) |
currency | string | required | ISO 4217 code — "EUR", "USD", "GBP", "CAD", "AUD", "JPY", "CHF", "DKK", "BRL" |
redirectUrl | string | optional | Return URL after payment |
metadata | object | optional | Key/value pairs (max 2 KB) |
onError | function | optional | Called with an Error if session creation fails. Default: console.error. |
Example
<script src="https://avopay.dev/js/avopay.js"></script> <button onclick="payNow()">Pay €9.99 with Bitcoin</button> <script> function payNow() { AvoPay.pay({ publishableKey: 'ak_pub_YOUR_KEY', amount: 9.99, currency: 'EUR', redirectUrl: 'https://yoursite.com/thank-you', onError: err => alert(err.message), }); } </script>
CommonJS / ESM
The SDK also works in Node.js or as a bundled module:
const AvoPay = require('./avopay.js'); // or: import AvoPay from './avopay.js' await AvoPay.pay({ publishableKey: 'ak_pub_…', amount: 9.99, currency: 'EUR' });
Webhooks
AvoPay sends a signed POST request to your configured endpoint when a payment event occurs. Configure your endpoint in Developer → Webhooks in the dashboard.
Events
| Event | When |
|---|---|
payment.confirmed | Lightning invoice settled, or L-BTC transaction seen in mempool. L-BTC payments are confirmed at mempool (not final block) — this is a deliberate UX choice; Liquid confirms in ~60 s. |
payment.expired | Session expired without payment (invoice.failedToPay or swap.expired from Boltz). |
payment.test | Test event sent from the dashboard. Includes "test": true. |
Payload
{
"event": "payment.confirmed",
"payment_id": "pmt_abc123",
"link_id": null, // "lnk_…" if payment originated from a link
"amount_fiat": 49.99,
"currency": "EUR",
"amount_sats": 62341,
"metadata": { "order_id": "1234" },
"settled_at": "2026-05-15T14:09:41.000Z"
}
Request headers
| Header | Description |
|---|---|
X-AvoPay-Signature | HMAC-SHA256 of the raw request body, prefixed sha256=…. Only present if you've configured a signing secret. |
X-AvoPay-Delivery-ID | Unique hex string per delivery attempt — useful for idempotency. |
Content-Type | application/json |
Your endpoint must return a 2xx HTTP status. AvoPay retries failed deliveries up to 4 times with exponential backoff (0 s → 60 s → 5 min → 15 min).
Signature verification
Verify the X-AvoPay-Signature header to confirm the request came from AvoPay.
const crypto = require('crypto'); app.post('/webhooks/avopay', express.raw({ type: 'application/json' }), (req, res) => { const secret = process.env.AVOPAY_WEBHOOK_SECRET; const signature = req.headers['x-avopay-signature']; if (secret && signature) { const expected = 'sha256=' + crypto .createHmac('sha256', secret) .update(req.body) .digest('hex'); const sigBuf = Buffer.from(signature); const expBuf = Buffer.from(expected); if (sigBuf.length !== expBuf.length || !crypto.timingSafeEqual(sigBuf, expBuf)) { return res.sendStatus(401); } } const event = JSON.parse(req.body.toString()); if (event.event === 'payment.confirmed') { // mark order paid in your database console.log('Paid:', event.payment_id, event.amount_fiat, event.currency); } res.sendStatus(200); });
express.raw() (not express.json()) for the webhook route — HMAC verification requires the raw bytes, not the parsed object.Handling duplicates
AvoPay may deliver the same event more than once (network retries, at-least-once delivery). Make your handler idempotent: use payment_id as the key and skip processing if you've already marked that payment as paid in your database.
if (event.event === 'payment.confirmed') { const already = db.query('SELECT 1 FROM orders WHERE payment_id = ? AND paid = 1', [event.payment_id]); if (!already) { db.run('UPDATE orders SET paid = 1 WHERE payment_id = ?', [event.payment_id]); } }
Testing webhooks
Click Send test in the dashboard Webhooks panel to fire a payment.test event to your endpoint. The response status is shown immediately.
For local development, use a tunneling tool like ngrok or cloudflared to expose your local server.
AI Agent Onboarding
AvoPay supports fully programmatic account setup. An AI agent or autonomous system can register, pay a Lightning invoice, connect a wallet, and start accepting payments in 4 API calls — no browser, no dashboard, no human steps.
Overview — 4 steps
-
1
Register — POST your email, password, and plan. Receive a BOLT11 Lightning invoice.
-
2
Pay the invoice from any Lightning wallet or node. Your account activates server-side automatically.
-
3
Poll the status endpoint — your
api_keyis returned once on the first successful poll. Save it immediately. -
4
Connect a wallet (if your agent controls a Liquid HD wallet) and create payment links or payment sessions.
Register an account
POST/api/v1/account/register
Creates a registration and returns a Lightning invoice. Rate-limited to 5 requests per hour per IP.
Request body
| Field | Type | Description | |
|---|---|---|---|
email | string | required | Account email address |
password | string | required | Minimum 8 characters |
plan | string | required | "monthly" ($9/mo) or "yearly" ($72/yr), both in sats at the live BTC rate |
terms_accepted | boolean | required | Must be true |
Response 201
{
"registration_id": "a1b2c3d4e5f6...",
"invoice": "lnbc154200n1...",
"sats": 15420,
"plan": "lightning_monthly",
"expires_at": "2026-05-25T12:00:00.000Z",
"message": "Pay this Lightning invoice to activate your account..."
}
Pay the invoice from any Lightning wallet or node. The invoice is valid for 24 hours. After payment, activation happens automatically via Boltz webhook — no additional call is needed before polling.
Full example
curl -s -X POST https://api.avopay.dev/api/v1/account/register \
-H "Content-Type: application/json" \
-d '{
"email": "[email protected]",
"password": "YourSecurePass123",
"plan": "monthly",
"terms_accepted": true
}'
Poll for your API key
GET/api/v1/account/register/status/:registration_id
Returns the current state of a registration. Once the Lightning invoice is paid, this endpoint returns your api_key once only — save it immediately. Subsequent calls return api_key_retrieved: true.
Responses
{ "status": "pending", "invoice": "lnbc...", "sats": 15420, "expires_at": "..." }
{
"status": "active",
"api_key": "ak_live_ABC123...",
"plan": "lightning_monthly",
"note": "Save this API key — it will not be shown again."
}
{ "status": "active", "api_key_retrieved": true }
{ "status": "expired" }
api_key is shown exactly once. Store it in a secure secrets manager immediately. If you miss it, create a new account — it cannot be recovered.Polling pattern
import time, requests reg_id = "a1b2c3d4..." while True: r = requests.get(f"https://api.avopay.dev/api/v1/account/register/status/{reg_id}") data = r.json() if data["status"] == "active" and "api_key" in data: api_key = data["api_key"] # store api_key securely — shown only once break elif data["status"] == "expired": raise Exception("Registration expired — create a new one") time.sleep(5)
Connect a wallet via descriptor
POST/api/v1/connect/descriptor
Connects a Liquid HD wallet by supplying SLIP-0077 descriptors directly. This bypasses the AQUA QR scan flow and is designed for agents that control their own Liquid wallet. Auth: Bearer ak_live_….
Request body
| Field | Type | Description | |
|---|---|---|---|
lbtc_descriptor | string | required | SLIP-0077 Liquid confidential descriptor. Must start with ct(slip77(. Max 500 chars. |
btcln_descriptor | string | required | SLIP-0077 descriptor for the Lightning-backing Liquid address. Same format and constraints. |
Descriptor format
ct(slip77(master_blinding_key_hex),elsh(wpkh([fingerprint/49'/1776'/0']xpub.../0/*)))
This is the format exported by the AQUA wallet (JAN3) and compatible wallets using BIP49 paths on the Liquid Network. The path 49'/1776'/0' is Liquid mainnet.
Response 200
{ "connected": true }
409). This prevents accidental address index resets which could cause duplicate addresses. Contact support to reset if needed.Full cURL example
curl -s -X POST https://api.avopay.dev/api/v1/connect/descriptor \
-H "Authorization: Bearer ak_live_..." \
-H "Content-Type: application/json" \
-d '{
"lbtc_descriptor": "ct(slip77(aabbcc...),elsh(wpkh([fp/49'"'"'/1776'"'"'/0'"'"']xpub.../0/*)))",
"btcln_descriptor": "ct(slip77(aabbcc...),elsh(wpkh([fp/49'"'"'/1776'"'"'/0'"'"']xpub.../0/*)))"
}'
Static site payment links (agent flow)
Once your agent account is active, create a reusable payment link for each product or service. Embed the URL as a plain HTML button — no per-session server call needed.
curl -s -X POST https://api.avopay.dev/api/v1/payment-links \
-H "Authorization: Bearer ak_live_..." \
-H "Content-Type: application/json" \
-d '{
"title": "1hr Consultation",
"amount_fiat": 150.00,
"currency": "USD",
"redirect_url": "https://yoursite.com/booked"
}'
{
"id": "lnk_abc123",
"url": "https://avopay.dev/pay/lnk_abc123",
"amount_fiat": 150.00,
"currency": "USD",
"status": "active"
}
<a href="https://avopay.dev/pay/lnk_abc123" style="display:inline-block;background:#D4C5A9;color:#000; padding:10px 22px;border-radius:8px;text-decoration:none; font-family:sans-serif;font-weight:600;"> Book Consultation — Pay in Bitcoin </a>
The link is reusable — any number of customers can open it. AvoPay hosts the checkout, collects payment, and redirects to your redirect_url on success.
Agent endpoint rate limits
| Endpoint | Limit | Window |
|---|---|---|
POST /api/v1/account/register | 5 requests | 1 hour per IP |
GET /api/v1/account/register/status/:id | 30 requests | 1 minute per IP |
POST /api/v1/account/register/boltz-webhook | 120 requests | 1 minute (Boltz only) |
POST /api/v1/connect/descriptor | 30 requests | 1 minute per IP |
POST /api/v1/payment-links | 30 requests | 1 minute per key |
/api/v1/ endpoints is available at api.avopay.dev/api/v1/openapi.json. Load it into any OpenAPI-to-MCP converter to auto-generate tools for Claude, GPT-4o, or LangChain.Rate limits
All API endpoints are rate-limited per IP address. Limits reset on a rolling window.
| Endpoint group | Limit | Window |
|---|---|---|
Payment creation (POST /payments) | 30 requests | 1 minute |
Webhook delivery (/webhooks/*) | 120 requests | 1 minute |
Auth (/auth/login) | 10 requests | 15 minutes |
| Account signup | 5 requests | 1 hour |
When a limit is exceeded the API returns 429 Too Many Requests. The response includes a Retry-After header (seconds until the window resets).
Error reference
All errors return JSON with an error field describing what went wrong.
HTTP/1.1 400 Bad Request
{ "error": "amount and currency are required" }
| HTTP | Error | Meaning |
|---|---|---|
400 | amount and currency are required | Missing required fields in request body. |
400 | Amount too small — minimum is 1000 sats equivalent | The fiat amount converts to less than 1000 sats. |
400 | Wallet not connected — connect your AQUA wallet first | No AQUA wallet has been linked to this account. Connect from the Dashboard → Setup. |
400 | metadata must be 2KB or less | The metadata object is too large. |
401 | Invalid or missing API key | Authorization header missing or key revoked. |
402 | trial_limit_reached: subscribe at avopay.dev | Your 5-payment free trial is exhausted and no active subscription was found. Subscribe to continue accepting payments — existing pending sessions are unaffected. |
403 | Secret key required for this endpoint | A publishable key was used on a secret-only route. |
404 | Payment not found | The payment_id does not exist or belongs to a different account. |
503 | Failed to fetch exchange rate — please retry | CoinGecko/CoinCap rate fetch timed out. Retry after a few seconds. |
503 | Payment provider unavailable — please retry | Lightning liquidity provider unavailable. Retry after a few seconds. |