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 capauto_topup_credited/auto_topup_failed/auto_topup_exhausted: what happened to an automatic top-up. These fire only on a deployment that has switched the feature on (SABLE_AUTO_TOPUP_ENABLED; it is off by default).auto_topup_credited: one of your pre-signed payments was presented and accepted —{auto_topup_id, status, network, transaction, amount_micro_usd, credited_micro_usd, payments_remaining}, wherestatusiscreditedorpending(still confirming on-chain),amount_micro_usdis what you signed andcredited_micro_usdis the net amount Sable read arriving.auto_topup_failed: a top-up was refused or could not be verified, or the paying wallet was unlinked —{auto_topup_id, error_class, disabled: true}; the authorization is now off and does not retry.auto_topup_exhausted: nothing is left to use —{auto_topup_id, reason, pulled_micro_usd, cap_micro_usd}, withreasonone ofno_payments_left,cap_reachedorexpired. None of them carries a signatureguardrail_triggered: a guardrail detector whose action isblockmatched, whether or not enforcement acted on it. Carries the request id, rule set id, rule, category, direction, the findings (counts only) and the decision (blockedorobserved). Events from the MCP Gateway, Sable Services and sandboxes also carrysurface(mcp_call,service,sandbox); one with nosurfaceis from the inference path. Never the matched text, which is not storedsandbox_failed: a sandbox run ended in failuresandbox_session_stopped: a sandbox session ended —{session_id, reason}, where the reason isrequested,idle_timeout,max_lifetime,insufficient_credit, ornode_unreachable. An agent holding a workspace needs to know it is gone; the next command against it returns409attestation_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 assetvault_attestation: a third-party attester recorded a statement on a vault asset (the statement itself is sealed, so the event names the fact, not the text) (interest, dividend, principal). Also amount-free; holders read their share through their own sessionservice_called: a buyer paid for and received one call to a service you published. Carries the service and call ids, the price, the amount actually paid, the settlement chain and transaction, the status and the latency — never the request or the responsebatch_completed/batch_failed: a batch reached a terminal state. Carries the batch id, endpoint, status, and request counts — and only those: batch content lives in the sealed output file, never in a payload sent to a third-party URL.batch_failedadds a fixederror_class; a cancelled batch fires neither, since you asked for it*: 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.