Skip to content

Webhooks

Webhooks let Belarel call your endpoint when something happens to your API keys, so you can react without polling. One event is available today, api_key.cap_reached; more events will follow.

Webhooks are managed by an owner or admin in your organization dashboard, not through the public API.

  1. Open Governance → API keys → Webhooks and select New webhook.
  2. Give it a name, choose the event An API key reached its spend cap, and enter your endpoint. It must be an https:// URL.
  3. Copy the signing secret. It is shown once. Store it next to your other secrets; you need it to verify every delivery.

From the same tab you can see each webhook’s last delivery (Pending, Delivered, Failed, Dropped), rotate its secret, or delete it.

It fires when a call is refused with 402 key_cap_exceeded because a key has reached its daily or monthly spend cap. If the refusal came from the parent’s cap (a sub-key whose parent is capped), the event names the parent key.

It is sent at most once per key and per UTC day: further refusals of the same key on the same day do not produce new events.

{
"id": "6d1f4b3e-2a8c-4f0e-9b7d-1c5e3a9f2b40",
"type": "api_key.cap_reached",
"version": "v1",
"occurred_at": "2026-10-02T14:03:11.482+00:00",
"object": {
"type": "api_key",
"id": "0b8e6c2a-5d14-4f3b-a9e7-2f6d8c1b4a53",
"key": "bel_live_k3x9a0qz",
"saas_entity_id": "…"
},
"data": {
"organization_id": "…",
"key_name": "customer-acme",
"caps": { "usd_day": 50, "usd_month": null },
"spent": { "usd_day": 50.0123, "usd_month": 412.88 }
}
}
Field Meaning
id The delivery id. Same value as the x-belarel-delivery header, and stable across retries — use it to deduplicate.
type, version api_key.cap_reached, payload version v1. A future change of shape gets a new version.
occurred_at When the cap refusal happened.
object.id, object.key The id and the prefix of the capped key — never the key itself.
object.saas_entity_id An opaque account identifier. Compare it, but do not read meaning into it; you can ignore it.
data.organization_id An opaque identifier of your organization.
data.key_name The key’s name.
data.caps The key’s daily and monthly caps in USD (null = no cap).
data.spent What is counted against each cap in the key’s most recent daily and monthly windows, in USD — settled spend plus calls in progress.
Header Value
content-type application/json
user-agent Belarel-Events/1.0
x-belarel-event The event type, api_key.cap_reached.
x-belarel-delivery The delivery id (same as id in the body).
x-belarel-signature t=<unix seconds>,v1=<hex HMAC-SHA256>

Belarel signs each delivery with your webhook’s signing secret:

  • the signed string is the timestamp t, a dot, and the raw request body: `${t}.${body}`;
  • the signature v1 is the hex HMAC-SHA256 of that string, keyed with the secret.

Use the secret exactly as shown, as a UTF-8 string. Do not hex-decode or base64-decode it before computing the HMAC.

To verify, read the raw body before any JSON parsing, rebuild the string, compute the HMAC, compare it to v1 in constant time, and reject timestamps more than five minutes away from your clock to block replays.

import hmac, hashlib, os, time
from flask import Flask, request, abort
app = Flask(__name__)
SECRET = os.environ["BELAREL_WEBHOOK_SECRET"].encode()
TOLERANCE_S = 300
def verify(header: str, body: bytes) -> bool:
parts = dict(p.strip().split("=", 1) for p in header.split(",") if "=" in p)
try:
t = int(parts["t"])
v1 = parts["v1"]
except (KeyError, ValueError):
return False
if abs(time.time() - t) > TOLERANCE_S:
return False
expected = hmac.new(SECRET, f"{t}.".encode() + body, hashlib.sha256).hexdigest()
return hmac.compare_digest(expected, v1)
@app.post("/belarel/webhooks")
def belarel_webhook():
body = request.get_data() # raw bytes, before any parsing
if not verify(request.headers.get("x-belarel-signature", ""), body):
abort(400)
event = request.get_json()
if event["type"] == "api_key.cap_reached":
... # deduplicate on event["id"], then alert, pause the customer, etc.
return "", 204
  • A delivery succeeds when your endpoint answers with a 2xx status within 10 seconds. Answer fast and do the work asynchronously.
  • Redirects are not followed: a 3xx counts as a failure. Point the webhook at the final URL.
  • Endpoints must use https:// and resolve to a public address; private and local addresses are refused.
  • A failed delivery is retried, up to 5 attempts in total, with growing delays between attempts: about 1 minute, 5 minutes, 15 minutes, then 1 hour. After the fifth failure, the delivery is marked Failed.
  • Deliveries can arrive more than once, and not necessarily in order. Deduplicate on id (or x-belarel-delivery).
  • Each webhook has an hourly delivery limit. Events beyond it within the same hour are not sent; a single Dropped entry per webhook per hour records that the limit was reached, however many events it held back. Deliveries for a webhook that was deleted or is no longer active also appear as Dropped.

Select Rotate secret on the webhook. The new secret is shown once and replaces the old one for every following delivery. Deploy the new secret to your endpoint promptly: deliveries signed with the new secret fail verification against the old one.