Esc to close · ⌘K / Ctrl-K opens search anywhere
Webhooks push signed event notifications to an HTTPS endpoint you register, so your systems react to budget, key, billing, and reliability events without polling. Manage them under Console → Webhooks, the REST API below, or thelist_webhooks / create_webhook MCP tools.
Privacy. Payloads are metadata only — ids, counts, ₹ amounts, model/provider ids, timestamps. BharatRouter never puts inference request or response content in a webhook (zero-retention posture, DPDP).
| Family | Event | Fires when |
|---|---|---|
| Budget & usage | budget.warn | A monthly ₹ budget (org/team/workspace/member/key) crosses 80%. |
budget.block | A budget reaches 100% — governed requests are now blocked. | |
usage.threshold | A usage/spend threshold crossing (mirrors budget crossings; also BYOK cap hits). | |
| Keys & access | key.created | A new br- API key is minted. |
key.revoked | An API key is revoked. | |
access.decided | An owner/admin approves or denies an access / catalog request. | |
| Billing | invoice.created | A GST tax invoice is issued for a captured top-up. |
payment.captured | A credit top-up payment captures successfully. | |
credit.low | Prepaid balance drops below the configured low-balance threshold. | |
| Reliability | provider.circuit_open | A route's circuit breaker trips open after consecutive failures (gateway-wide). |
request.anomaly | A spend/error/token spike is detected against the trailing baseline. |
A ping event is delivered when you press Send test.
Every delivery POSTs this JSON envelope. data is the per-event metadata.
{
"id": "evt_9f2c…",
"type": "budget.block",
"created": 1754131200,
"org_id": 42,
"data": { "scope": "org", "threshold": 100, "spent_inr": 5000, "cap_inr": 5000 }
}Request headers:
| Header | Value |
|---|---|
X-BR-Event | The event type, e.g. budget.block. |
X-BR-Signature | t=<unix>,v1=<hex> — see below. |
X-BR-Webhook-Id | The endpoint id. |
X-BR-Delivery-Id | Unique per delivery attempt row (for idempotency). |
The signature is replay-proof and Stripe-style. v1 is the hex HMAC-SHA256 of"<t>.<raw request body>" keyed by your endpoint's signing secret (shown once at create / rotate). Recompute it over the raw body, compare in constant time, and reject if t is more than a few minutes old.
import crypto from 'node:crypto';
function verify(secret, header, rawBody, toleranceSec = 300) {
const parts = Object.fromEntries(header.split(',').map((p) => p.trim().split('=')));
const t = Number(parts.t);
if (!Number.isFinite(t) || Math.abs(Date.now() / 1000 - t) > toleranceSec) return false;
const expected = crypto.createHmac('sha256', secret).update(t + '.' + rawBody).digest('hex');
const a = Buffer.from(expected, 'hex');
const b = Buffer.from(parts.v1 || '', 'hex');
return a.length === b.length && crypto.timingSafeEqual(a, b);
}
// Express: mount with express.raw({ type: 'application/json' }) so req.body is the raw Buffer.
app.post('/br-webhook', (req, res) => {
if (!verify(process.env.BR_WEBHOOK_SECRET, req.get('X-BR-Signature'), req.body.toString('utf8'))) {
return res.status(400).send('bad signature');
}
const event = JSON.parse(req.body.toString('utf8'));
// handle event.type … respond 2xx quickly; BR retries non-2xx with backoff.
res.sendStatus(200);
});import hmac, hashlib, time
def verify(secret: str, header: str, raw_body: bytes, tolerance_sec: int = 300) -> bool:
parts = dict(p.strip().split("=", 1) for p in header.split(","))
try:
t = int(parts["t"])
except (KeyError, ValueError):
return False
if abs(time.time() - t) > tolerance_sec:
return False
expected = hmac.new(secret.encode(), f"{t}.".encode() + raw_body, hashlib.sha256).hexdigest()
return hmac.compare_digest(expected, parts.get("v1", ""))
# Flask
@app.post("/br-webhook")
def br_webhook():
if not verify(BR_WEBHOOK_SECRET, request.headers.get("X-BR-Signature", ""), request.get_data()):
return "bad signature", 400
event = request.get_json()
# handle event["type"] … return 2xx quickly.
return "", 2002xx to acknowledge. Any other status (or a timeout/connection error) is a failure and is retried.dead.disabled_reason is set and the org owner gets a notice. Re-enable it (which resets the counter) once your receiver is fixed.Session-authed, org-scoped, owner/admin only.
| Method & path | What |
|---|---|
GET /me/webhooks | List endpoints + the subscribable event catalog. |
POST /me/webhooks | Create an endpoint. Body: { url, subscribe_all?, event_types?, description? }. Returns secret once. |
PATCH /me/webhooks/:id | Enable/disable, change event_types / subscribe_all / description. |
DELETE /me/webhooks/:id | Delete an endpoint. |
POST /me/webhooks/:id/rotate-secret | Rotate the signing secret (returns the new one once). |
POST /me/webhooks/:id/test | Queue a ping delivery. |
GET /me/webhooks/:id/deliveries | Recent delivery log (status, code, attempts). |
POST /me/webhooks/:id/deliveries/:did/redeliver | Re-queue one delivery. |