Webhooks
Six events, a Stripe-shaped signature, and delivery that can never slow a request down.
The events
Add an endpoint under Webhooks and subscribe it to any of these.
| Event | Description |
|---|---|
| balance.low | Balance fell below your alert threshold. This is a notification only — no credit is added automatically, so treat it as the cue to top up before requests start failing. |
| balance.depleted | Balance is exhausted. Chargeable requests are being refused. |
| credit.added | A top-up landed. Clears both balance alerts. |
| key.limit_reached | One key hit its own spend cap. |
| request.failed | A request ended in an error, with its code, message, model and request id. |
| spend.threshold | Month-to-date spend crossed your account monthly limit. |
The four alert-shaped events fire once per crossing, not once per request, and re-arm when the condition clears. A depleted account under a retry loop sends one alert, not thousands.
Verifying a delivery
Each delivery carries a Multigrid-Signature header in the shape every backend developer has already written a verifier for:
Multigrid-Signature: t=1785000000,v1=<hex hmac-sha256>The signed payload is {timestamp}.{body}, not the body alone. Signing the body by itself would let anyone who once captured a valid request replay it forever; with the timestamp inside the signature you can reject anything old and the signature still covers it.
import { createHmac, timingSafeEqual } from "node:crypto";
export function verify(header: string, body: string, secret: string): boolean {
const parts = Object.fromEntries(
header.split(",").map((kv) => kv.split("=") as [string, string]),
);
const timestamp = Number(parts.t);
// Reject anything old. The timestamp is inside the signature, so an
// attacker cannot move it without invalidating the whole thing.
if (!Number.isFinite(timestamp) || Math.abs(Date.now() / 1000 - timestamp) > 300) {
return false;
}
const expected = createHmac("sha256", secret)
.update(`${timestamp}.${body}`)
.digest("hex");
const a = Buffer.from(expected, "hex");
const b = Buffer.from(parts.v1 ?? "", "hex");
return a.length === b.length && timingSafeEqual(a, b);
}whsec_ and is displayed at creation only. We keep it encrypted so we can still sign with it, but nothing renders it back to you.Retries and failure
- Three attempts per event, each with an 8-second timeout.
- After 15 consecutive failures the endpoint is disabled and says so on the page, rather than silently retrying into a void.
- Every attempt is in the delivery log, so a receiver that was down for an hour is something you can see rather than deduce.
Delivery is never in the path of an inference request. Everything here is fired and forgotten from the request path — an endpoint having a bad day cannot add latency to your calls, let alone fail them.
Something here disagrees with what the API actually did? That is a bug in this page, and worth reporting.
Report it