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.

AvoPay is a payment method only. It handles Bitcoin collection — your existing system stays in charge of orders, invoices, shipping, inventory, and fulfilment. AvoPay plugs in as one more payment option and notifies you when a payment confirms.
New here? Create a free account to get your API keys. Your first 5 payments are free — no credit card required.

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.js
    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:       49.99,
        currency:     'EUR',
        redirect_url: 'https://yoursite.com/order/complete',
      }),
    });
    const { checkout_url } = await resp.json();
    // redirect the customer to checkout_url
    cURL
    curl -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.

Header
Authorization: Bearer <your_api_key>
Key typePrefixUse
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.
Your secret key has full API access. Store it in an environment variable and never log or commit it.

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

FieldTypeDescription
amountnumberrequiredAmount to charge (e.g. 49.99)
currencystringrequiredISO 4217 code — "EUR", "USD", "GBP", "CAD", "AUD", "JPY", "CHF", "DKK", "BRL"
redirect_urlstringoptionalURL to return the customer to after payment. Query params payment_id=…&status=paid are appended.
metadataobjectoptionalAny key/value pairs you want to attach (max 2 KB). Included in webhook payloads.

Response 201

JSON
{
  "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

Node.js
// 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

JSON
{
  "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

StatusMeaning
pendingAwaiting customer payment. The checkout page is live.
paidPayment confirmed. Settled to the merchant's AQUA wallet.
expiredThe 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       │                      │
For real-time settlement notifications, use webhooks instead of polling. Webhooks fire within seconds of confirmation.


JS SDK

A lightweight browser script that creates a payment session and redirects the customer — no backend needed. Uses your publishable key (ak_pub_…).

The JS SDK is designed for buyer-facing checkout pages where you can't make a server-side call. If your stack has a backend (Node.js, PHP, Python, etc.), create the payment session there using your secret key — it's more secure and gives you better control over error handling.

Include the SDK

HTML
<script src="https://avopay.dev/js/avopay.js"></script>

AvoPay.pay(options)

OptionTypeDescription
publishableKeystringrequiredYour ak_pub_… key
amountnumberrequiredAmount to charge (e.g. 9.99)
currencystringrequiredISO 4217 code — "EUR", "USD", "GBP", "CAD", "AUD", "JPY", "CHF", "DKK", "BRL"
redirectUrlstringoptionalReturn URL after payment
metadataobjectoptionalKey/value pairs (max 2 KB)
onErrorfunctionoptionalCalled with an Error if session creation fails. Default: console.error.

Example

HTML + JS
<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:

Node.js
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

EventWhen
payment.confirmedLightning 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.expiredSession expired without payment (invoice.failedToPay or swap.expired from Boltz).
payment.testTest event sent from the dashboard. Includes "test": true.

Payload

payment.confirmed — example
{
  "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

HeaderDescription
X-AvoPay-SignatureHMAC-SHA256 of the raw request body, prefixed sha256=…. Only present if you've configured a signing secret.
X-AvoPay-Delivery-IDUnique hex string per delivery attempt — useful for idempotency.
Content-Typeapplication/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.

Node.js (Express)
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);
});
Use 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.

Node.js — idempotent handler
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.

No free trial for agent accounts. Agent registration requires an upfront Lightning payment. Human merchants signing up via the dashboard still receive 5 free test payments.

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_key is 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

FieldTypeDescription
emailstringrequiredAccount email address
passwordstringrequiredMinimum 8 characters
planstringrequired"monthly" ($9/mo) or "yearly" ($72/yr), both in sats at the live BTC rate
terms_acceptedbooleanrequiredMust be true

Response 201

JSON
{
  "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
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

Pending — invoice not yet paid
{ "status": "pending", "invoice": "lnbc...", "sats": 15420, "expires_at": "..." }
Active — first poll after payment (api_key shown once)
{
  "status":  "active",
  "api_key": "ak_live_ABC123...",
  "plan":    "lightning_monthly",
  "note":    "Save this API key — it will not be shown again."
}
Active — key already retrieved
{ "status": "active", "api_key_retrieved": true }
Expired — 24h window passed without payment
{ "status": "expired" }
The 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

Python — poll every 5 seconds
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_….

Skip this step if you prefer the AQUA QR wallet connect flow (available from the dashboard). Both methods result in the same wallet state.

Request body

FieldTypeDescription
lbtc_descriptorstringrequiredSLIP-0077 Liquid confidential descriptor. Must start with ct(slip77(. Max 500 chars.
btcln_descriptorstringrequiredSLIP-0077 descriptor for the Lightning-backing Liquid address. Same format and constraints.

Descriptor format

Expected 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

JSON
{ "connected": true }
Wallet connection is one-way — you cannot overwrite descriptors via API once connected (returns 409). This prevents accidental address index resets which could cause duplicate addresses. Contact support to reset if needed.

Full cURL example

cURL
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 — create a product link
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"
  }'
Response
{
  "id":          "lnk_abc123",
  "url":         "https://avopay.dev/pay/lnk_abc123",
  "amount_fiat": 150.00,
  "currency":    "USD",
  "status":      "active"
}
HTML — embed as a button
<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

EndpointLimitWindow
POST /api/v1/account/register5 requests1 hour per IP
GET /api/v1/account/register/status/:id30 requests1 minute per IP
POST /api/v1/account/register/boltz-webhook120 requests1 minute (Boltz only)
POST /api/v1/connect/descriptor30 requests1 minute per IP
POST /api/v1/payment-links30 requests1 minute per key
The full OpenAPI 3.0.3 specification covering all /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 groupLimitWindow
Payment creation (POST /payments)30 requests1 minute
Webhook delivery (/webhooks/*)120 requests1 minute
Auth (/auth/login)10 requests15 minutes
Account signup5 requests1 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).

Payment creation requests should always originate from your backend, not the buyer's browser — this prevents any single IP from hitting the limit.

Error reference

All errors return JSON with an error field describing what went wrong.

Error response
HTTP/1.1 400 Bad Request

{ "error": "amount and currency are required" }
HTTPErrorMeaning
400amount and currency are requiredMissing required fields in request body.
400Amount too small — minimum is 1000 sats equivalentThe fiat amount converts to less than 1000 sats.
400Wallet not connected — connect your AQUA wallet firstNo AQUA wallet has been linked to this account. Connect from the Dashboard → Setup.
400metadata must be 2KB or lessThe metadata object is too large.
401Invalid or missing API keyAuthorization header missing or key revoked.
402trial_limit_reached: subscribe at avopay.devYour 5-payment free trial is exhausted and no active subscription was found. Subscribe to continue accepting payments — existing pending sessions are unaffected.
403Secret key required for this endpointA publishable key was used on a secret-only route.
404Payment not foundThe payment_id does not exist or belongs to a different account.
503Failed to fetch exchange rate — please retryCoinGecko/CoinCap rate fetch timed out. Retry after a few seconds.
503Payment provider unavailable — please retryLightning liquidity provider unavailable. Retry after a few seconds.