Webhooks
Subscribe to lifecycle events out-of-band from the inference path. Create subscriptions from the dashboard.
Event types
key_created: a new API key was mintedkey_revoked: an API key was revokedwebhook_created: this webhook (or a sibling) was just createdwebhook_revoked: a webhook was disableddeposit_credited: a USDT deposit confirmed and credited your balancebalance_low: a debit took the balance below your threshold. Fires once per downward crossing, not on every debit below it; set your own threshold on the portal's Billing page or viaPUT /v1/billing/alerts(GETreads it back), else the deployment default appliesspend_cap_reached: a key hit its monthly spend capsandbox_failed: a sandbox run ended in failureattestation_failed: a confidential-tier request was refused because no backend could be attestedreceipt_minted: a signed receipt was minted for a metered call (inference, embeddings,/v1/messages, or a sandbox run). Fires once per receipt and carries the full receipt, its signature, and signer, so it doubles as an audit stream: see belowvault_transfer_sent/vault_transfer_received: a Sable Vault position moved. Deliberately amount-free: vault amounts are sealed, and webhook payloads travel to third-party URLs; fetch details through your sessionvault_distribution: an issuer paid out to holders of a vault asset (interest, dividend, principal). Also amount-free; holders read their share through their own session*: subscribe to everything
The receipt stream
receipt_minted is the one to point a collector at. It fires once for every
signed receipt, the moment it is minted, across inference, embeddings,
/v1/messages, and sandbox runs. The payload carries the receipt,
signature, and signer, plus the kind, status, and cost_micro_usd, so
every metered call lands in your own observability or audit stack as it happens,
already signed and independently verifiable through
POST /v1/receipts/verify.
It is metadata only by construction, the same as the receipt itself: no prompt, completion, or code ever travels in the event. That makes it safe to route to a SIEM, a data warehouse, or a spend dashboard without opening a hole in the privacy contract.
Delivery
Each event POSTs JSON to your URL with two headers:
X-Sable-Event: the event type, e.g.key_revokedX-Sable-Signature:sha256=<hex>HMAC of the raw body using your webhook secret
Verifying signatures
import { createHmac, timingSafeEqual } from "node:crypto";
function verifySable(req: Request, secret: string): boolean {
const sig = req.headers.get("x-sable-signature") ?? "";
const expected = "sha256=" + createHmac("sha256", secret)
.update(req.body)
.digest("hex");
return timingSafeEqual(Buffer.from(sig), Buffer.from(expected));
}Retries & auto-disable
Delivery is retried with exponential backoff: an immediate attempt, then
retries after 30s, 5m, and 1h, up to four attempts per event.
Any failed attempt retries, whether it was a non-2xx response or a network
error, until that budget is spent. Events queue in a durable outbox, so the
retry schedule survives gateway restarts and deploys rather than dying with
the process. Each event records exactly one webhook_deliveries row capturing
the number of attempts and the last status seen.
After 5 consecutive events fail every attempt, the webhook is automatically
disabled (disabled_at is set) and stops receiving events until you re-create
it. A single delivered event resets the counter.
Inspecting and testing deliveries
Two session-authed endpoints make a misbehaving receiver debuggable without guessing:
GET /v1/webhooks/:id/deliveries: the webhook's last 50 delivery attempts:event_type,response_status,attempts,delivered_at. Delivery-log rows are pruned after 30 days.POST /v1/webhooks/:id/test: fires a syntheticwebhook_testevent at that webhook immediately, signed like any real event, so you can verify your endpoint and HMAC check end-to-end without waiting for something to happen.