Quickstart
Goal: signed-up vendor → first paid invoice in 10 minutes. The four pieces you actually need: the mental model, authentication, creating an invoice, receiving the paid webhook.
The mental model
┌──────────┐ 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 │
└──────────┘
The xpub stays on your machine in Electrum. SatLane derives one fresh address per invoice and watches for payment. We never see your private keys — funds settle directly into your wallet on confirmation.
Authentication
| Surface | Auth |
|---|---|
Server-side API (POST /v1/invoices, etc.) | Authorization: Bearer sl_live_… or sl_test_… |
Public buyer endpoints (/pay/invoices/:id*) | None — invoice UUID is the only secret |
| Vendor dashboard | Session cookie (only relevant if you're using the dashboard) |
API keys are issued per-store from the dashboard at app.satlane.com/stores/<id>/keys. Each key is shown once on creation. Store them in your secrets manager.
Create an invoice
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" }
}'
Request fields
| Field | Type | Required | Notes |
|---|---|---|---|
amount | number | one of | Fiat amount. We lock a BTC/USD rate and convert to sats. |
currency | string | one of | Must be "USD" at MVP. |
amount_sats | string | one of | Skip fiat conversion, charge exact sats. |
order_ref | string | optional | Your internal order ID. We don't constrain format. |
callback_url | string | optional | Per-invoice webhook URL (overrides store-level endpoints). |
success_url | string | optional | Hosted checkout redirects here after payment. |
buyer_email | string | optional | If set, we can email the buyer a receipt later (Phase 9). |
expires_in_minutes | int | optional | 5–120, default from store settings. |
metadata | object | optional | Free-form string → string map echoed on every webhook. |
Response
{
"invoice": {
"id": "22872e14-4216-4c78-8fe1-088ea649f3c2",
"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,
"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,
"payment_uri": "bitcoin:tb1q…?amount=0.00150234&label=…",
"hosted_checkout_url": "https://pay.satlane.com/i/22872e14-…",
"order_ref": "ORD-12345",
"created_at": "2026-05-16T11:15:00.000Z",
"...": "..."
}
}
Receive webhooks
We POST signed JSON to your callback_url (per-invoice) or to webhook endpoints configured on the store. Retries: 1m → 5m → 30m → 2h → 12h → 24h (6 attempts), then dead-letter (replayable from dashboard).
Headers
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
Body shape
{
"event_id": "evt_abc123",
"event_type": "invoice.paid",
"created_at": "2026-05-16T11:25:00.000Z",
"livemode": true,
"data": {
"invoice": { /* same shape as POST /v1/invoices response */ }
}
}
Verify the signature
The signature header format is t=<timestamp>,v1=<hex>. The signed payload is ${timestamp}.${rawRequestBody} and the HMAC algorithm is SHA-256 with your endpoint secret as the key.
Node (using our published helper):
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:
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:
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);
}
Vendor SDKs should reject events with timestamps older than 5 minutes (replay protection). During secret rotation we keep the old secret valid for 24 hours — your verifier can pass both to secrets: [current, previous].