Receive webhooks
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
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
{
"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):
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);
}
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].