⚡ New — Kimi K3 is live: bring your own Moonshot key →
Documentation

Webhooks

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).

Event catalog

FamilyEventFires when
Budget & usagebudget.warnA monthly ₹ budget (org/team/workspace/member/key) crosses 80%.
budget.blockA budget reaches 100% — governed requests are now blocked.
usage.thresholdA usage/spend threshold crossing (mirrors budget crossings; also BYOK cap hits).
Keys & accesskey.createdA new br- API key is minted.
key.revokedAn API key is revoked.
access.decidedAn owner/admin approves or denies an access / catalog request.
Billinginvoice.createdA GST tax invoice is issued for a captured top-up.
payment.capturedA credit top-up payment captures successfully.
credit.lowPrepaid balance drops below the configured low-balance threshold.
Reliabilityprovider.circuit_openA route's circuit breaker trips open after consecutive failures (gateway-wide).
request.anomalyA spend/error/token spike is detected against the trailing baseline.

A ping event is delivered when you press Send test.

Payload envelope

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:

HeaderValue
X-BR-EventThe event type, e.g. budget.block.
X-BR-Signaturet=<unix>,v1=<hex> — see below.
X-BR-Webhook-IdThe endpoint id.
X-BR-Delivery-IdUnique per delivery attempt row (for idempotency).

Verifying the signature

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.

Node.js

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);
});

Python

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 "", 200

Delivery, retries & auto-disable

  • Respond with any 2xx to acknowledge. Any other status (or a timeout/connection error) is a failure and is retried.
  • Retries use exponential backoff — up to 5 attempts over roughly 2.5 hours. A delivery that exhausts its retries is marked dead.
  • After 5 consecutive dead deliveries an endpoint is auto-disabled; itsdisabled_reason is set and the org owner gets a notice. Re-enable it (which resets the counter) once your receiver is fixed.
  • Every POST is sent through BharatRouter's SSRF guard — the target must resolve to a public IP over HTTPS. Internal / loopback / link-local / metadata addresses are refused.
  • Deliveries are logged; inspect and re-send from Console → Webhooks → Deliveries, or via the API.

Management API

Session-authed, org-scoped, owner/admin only.

Method & pathWhat
GET /me/webhooksList endpoints + the subscribable event catalog.
POST /me/webhooksCreate an endpoint. Body: { url, subscribe_all?, event_types?, description? }. Returns secret once.
PATCH /me/webhooks/:idEnable/disable, change event_types / subscribe_all / description.
DELETE /me/webhooks/:idDelete an endpoint.
POST /me/webhooks/:id/rotate-secretRotate the signing secret (returns the new one once).
POST /me/webhooks/:id/testQueue a ping delivery.
GET /me/webhooks/:id/deliveriesRecent delivery log (status, code, attempts).
POST /me/webhooks/:id/deliveries/:did/redeliverRe-queue one delivery.