SatLane
Documentation

Receive webhooks

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].