SatLane
Documentation

Quickstart

Goal: first paid invoice in about 10 minutes. You need four pieces: the mental model, a Bearer API key, POST /v1/invoices, and a webhook handler that verifies X-SatLane-Signature.

Overview

Code
┌──────────┐   1. POST /v1/invoices    ┌─────────┐
│ Your     │ ────────────────────────▶│ SatLane │
│ server   │                          │  API    │
│          │ ◀──────────────────────── │         │
└──────────┘   { invoice + payment_uri └────┬────┘
        │                                   │
        │ 2. Redirect buyer to              │ 5. POST webhook
        │    invoice.hosted_checkout_url    │    invoice.paid
        ▼                                   ▼
┌──────────┐                          ┌──────────┐
│  Buyer   │   3. Buyer pays the BTC  │ Your     │
│ browser  │      address from their  │ webhook  │
│          │      Bitcoin wallet      │ handler  │
└──────────┘                          └──────────┘
                                           │
                                           ▼ 6. Fulfil order
                                      ┌──────────┐
                                      │  Your    │
                                      │  app     │
                                      └──────────┘

Your xpub stays in Electrum (or equivalent). SatLane derives one fresh address per invoice and watches the chain. We never hold private keys; funds settle directly into your wallet on confirmation.

Platform billing (for integrators)

SatLane is a hybrid plan product (Trial, Basic, Pro, Custom). For API integrators:

  • On each paid live invoice, fee_sats accrues against your vendor account (take-rate from your plan).
  • Plan subscription invoices (monthly sats for Basic/Pro) are paid in Bitcoin from the vendor dashboard at /billing. There is no public billing API for plan checkout.
  • Unpaid plan invoices or an exhausted trial can lock new live invoice creation until you pay or choose a plan at /billing/plans.
  • Test-mode invoices do not accrue platform fees.

Manage plans, usage, and Bitcoin plan invoices in the app. This guide focuses on the payments API.


Authentication

SurfaceAuth
Server-side API (POST /v1/invoices, etc.)Authorization: Bearer sl_live_… or sl_test_…
Public buyer endpoints (/pay/invoices/:id*)None. The invoice UUID is the credential.
Vendor dashboardSession cookie (dashboard only)

API keys are issued per store from app.satlane.com/stores/<id>/keys. Each secret is shown once on creation. Store it in your secrets manager.

GETcURL
curl https://api.satlane.com/v1/invoices \
  -H "Authorization: Bearer sl_test_XXX"

Use a test key (sl_test_…) while building. Switch to a live key (sl_live_…) when the store is live.


Create an invoice

POSTcURL
curl -X POST https://api.satlane.com/v1/invoices \
  -H "Authorization: Bearer sl_test_XXX" \
  -H "Content-Type: application/json" \
  -H "Idempotency-Key: order-123-attempt-1" \
  -d '{
    "amount": 49.99,
    "currency": "USD",
    "order_ref": "ORD-12345",
    "callback_url": "https://yourshop.com/webhooks/satlane",
    "success_url": "https://yourshop.com/orders/ORD-12345/thanks",
    "buyer_email": "buyer@example.com",
    "expires_in_minutes": 15,
    "metadata": { "cart_id": "abc123" }
  }'

Always send an Idempotency-Key on create. We cache the response for 24 hours per key so retries after a network blip return the same invoice instead of creating duplicates.

Request fields

FieldTypeRequiredNotes
amountnumberone ofFiat amount. We lock a BTC/USD rate and convert to sats.
currencystringone ofMust be "USD" today.
amount_satsstringone ofSkip fiat conversion; charge exact sats (digits only).
order_refstringoptionalYour internal order ID. Max 255 chars.
callback_urlstringoptionalPer-invoice webhook URL (overrides store-level endpoints).
success_urlstringoptionalHosted checkout redirects here after payment.
buyer_emailstringoptionalBuyer email for receipts / support.
expires_in_minutesintoptional5–120. Default comes from store settings.
metadataobjectoptionalFree-form string → string map (values max 255 chars). Echoed on webhooks.

Provide either (amount + currency) or amount_sats, not both.

Response

JSON
{
  "invoice": {
    "id": "22872e14-4216-4c78-8fe1-088ea649f3c2",
    "store_id": "…",
    "vendor_id": "…",
    "status": "pending",
    "environment": "test",
    "address": "tb1q…",
    "amount_sats": "150234",
    "amount_btc": "0.00150234",
    "amount_fiat": 49.99,
    "fiat_currency": "USD",
    "btc_usd_rate": 33280.45,
    "rate_locked_at": "2026-05-16T11:15:00.000Z",
    "amount_tolerance_sats": "375",
    "amount_paid_sats": "0",
    "expires_at": "2026-05-16T11:30:00.000Z",
    "late_payment_grace_minutes": 60,
    "late_payment_deadline_at": "2026-05-16T12:30:00.000Z",
    "conf_threshold": 1,
    "fee_sats": "1502",
    "payment_uri": "bitcoin:tb1q…?amount=0.00150234&label=…",
    "hosted_checkout_url": "https://pay.satlane.com/i/22872e14-…",
    "payment_phase": "awaiting_payment",
    "order_ref": "ORD-12345",
    "created_at": "2026-05-16T11:15:00.000Z",
    "paid_at": null
  }
}

Response fields worth understanding

FieldWhat it means
amount_satsInvoice amount in satoshis. Vendor-facing source of truth.
amount_paid_satsRunning total of sats received on-chain so far (non-reverted). Remaining = amount_sats - amount_paid_sats.
amount_tolerance_satsSlack on the expected amount. Payments within [amount_sats − tolerance, amount_sats + tolerance] count as exact. Defaults come from platform setting payment_tolerance_bp (default 25 bp = 0.25%), clamped to [10, 1000] sats.
btc_usd_rateBTC/USD rate locked at creation. Later price moves do not change what the buyer owes.
late_payment_deadline_atISO timestamp past which payments are no longer auto-credited. We keep watching until then.
conf_thresholdConfirmations required before status flips to paid. Defaults: 1 below $100 invoice value, 2 at $100+.
fee_satsPlatform take-rate on this invoice, accrued to your vendor account when a live invoice is paid. Visible in dashboard billing.
payment_phaseBuyer-facing lifecycle dimension computed at read time (not persisted). Useful for custom UIs.
hosted_checkout_urlReady-to-redirect hosted payment page.

Receive webhooks

We POST signed JSON to your callback_url (per-invoice) or to webhook endpoints configured on the store.

Success: any 2xx. We mark delivery success and stop.

Retries (5xx + network errors / timeouts): initial attempt, then retries at 1m → 5m → 30m → 2h → 12h → 24h. That is 7 total attempts before the delivery moves to dead_letter (replayable from the dashboard).

Permanent failures (4xx): not retried. Delivery moves to failed. Common causes: wrong URL, expired auth on your reverse proxy, signature verification rejecting a legitimate event. Check response_body on the delivery in the dashboard.

Timeout: 10 seconds per attempt. Write the side effect (mark order paid) and respond 200 quickly. Queue slow work after the ack.

Headers

POSTHTTP
POST /your-handler HTTP/1.1
Content-Type: application/json
User-Agent: SatLane-Webhook/1.0
X-SatLane-Signature: t=1721481600,v1=2a3b4c5d…
X-SatLane-Event-Id: evt_abc123
X-SatLane-Event-Type: invoice.paid

Header names are case-insensitive on the wire (x-satlane-signature, etc.).

Body shape

JSON
{
  "event_id": "evt_abc123",
  "event_type": "invoice.paid",
  "created_at": "2026-05-16T11:25:00.000Z",
  "livemode": true,
  "data": {
    "invoice": { }
  }
}

data.invoice matches the public invoice shape from POST /v1/invoices.

Verify the signature

Signature header format: t=<unix_seconds>,v1=<hex>.

Signed payload: ${timestamp}.${rawRequestBody}

Algorithm: HMAC-SHA256 with your endpoint secret as the key.

Node (using @satlane/webhooks):

JavaScript
import { verifySignature } from '@satlane/webhooks';

app.post('/webhooks/satlane', express.raw({ type: 'application/json' }), (req, res) => {
  const sig = req.header('X-SatLane-Signature');
  try {
    verifySignature(req.body, sig, { secrets: [process.env.SATLANE_WEBHOOK_SECRET] });
  } catch {
    return res.status(400).end();
  }
  const event = JSON.parse(req.body);
  // Safe to act on event.data.invoice
  res.status(200).end();
});

Python:

Python
import hmac, hashlib, time

def verify(raw_body: bytes, header: str, secret: str, tolerance: int = 300):
    parts = dict(p.split('=', 1) for p in header.split(','))
    t, v1 = int(parts['t']), parts['v1']
    if abs(time.time() - t) > tolerance:
        raise ValueError('timestamp out of tolerance')
    signed = f'{t}.{raw_body.decode()}'.encode()
    expected = hmac.new(secret.encode(), signed, hashlib.sha256).hexdigest()
    if not hmac.compare_digest(expected, v1):
        raise ValueError('signature mismatch')

PHP:

PHP
function verifySatlaneSignature(string $rawBody, string $header, string $secret, int $tolerance = 300): bool {
    $parts = [];
    foreach (explode(',', $header) as $p) {
        [$k, $v] = explode('=', $p, 2);
        $parts[$k] = $v;
    }
    $t = (int) $parts['t']; $v1 = $parts['v1'];
    if (abs(time() - $t) > $tolerance) return false;
    $expected = hash_hmac('sha256', "{$t}.{$rawBody}", $secret);
    return hash_equals($expected, $v1);
}

Reject events with timestamps older than 5 minutes (replay protection). During secret rotation we keep the previous secret valid for 24 hours; pass both to secrets: [current, previous].