Usage webhooks

A usage webhook sends each settled request to an HTTPS endpoint you control. The event is the usage record the dashboard shows: request id, model, tokens, price, cost and timing. It never carries a prompt or an answer.

Configure it#

Open Settings, then Account, and find Usage webhook. Save an https URL on a public host, then copy the secret shown once. Turn deliveries on when the endpoint is ready. Send a test posts one synthetic usage.settled event so the receiver can verify the signature before the first real request.

An endpoint is off until you turn it on. Removing it removes its delivery log with it.

The envelope#

json
{  "id": "019b2c3d-...",  "type": "usage.settled",  "created": "2026-11-12T09:00:00Z",  "data": { "request_id": "req_...", "model": "deepseek-v4.1-flash", "tokens": 0, "cost_eur": "0.0000" }}

The id is the delivery id, stable across retries. The request_id inside data is the dedupe key for a receiver that sees a retry after a slow first attempt. The order of two different events is not promised.

Headers and signature#

text
Content-Type: application/jsonUser-Agent: zdr.sh-Webhooks/1X-Rackduck-Event: usage.settledX-Rackduck-Delivery: <delivery id>X-Rackduck-Signature: t=<unix seconds>,v1=<hex HMAC-SHA256(secret, t + "." + body)>

Verify the signature over the exact request body, reject a timestamp more than five minutes old, and compare in constant time:

python
import hashlib, hmac, timedef verify(secret: str, header: str, body: bytes, now: int | None = None) -> bool:    parts = dict(part.split("=", 1) for part in header.split(","))    ts, got = int(parts["t"]), parts["v1"]    if abs((now or int(time.time())) - ts) > 300:        return False    want = hmac.new(secret.encode(), f"{ts}.".encode() + body, hashlib.sha256).hexdigest()    return hmac.compare_digest(want, got)

The secret is never in the request. Rotating it replaces the old one immediately.

Delivery and retries#

A delivery is attempted immediately. A 2xx ends it as delivered, and a 404 or 410 ends it as gone. A timeout, a 408, 425, 429 or 5xx is retried after 1, 5, 30, 120 and 360 minutes, with jitter; after the fifth retry it is failed. Other 4xx answers and every redirect are final, and a redirect is not followed.

The delivery log keeps 30 days of request ids, statuses, attempts and the last error. An endpoint that fails 100 consecutive deliveries is turned off, and the panel shows the failure.

What the event never carries#

The event is a usage record: no prompt, answer, file or image, on any model. The end-user id and metadata a client sent are on that record, because they are the client's own attribution. Zero retention says what is kept on a request.