Webhooks
Receive HTTPS notifications from NemoRouter for alerts, spend events, and audit actions
Last updated
Configure a webhook endpoint and NemoRouter will POST a JSON payload when a budget threshold is crossed. Webhook is one of five delivery channel types (email, Slack, Teams, Jira, webhook) — they all share the same payload shape, so anything you can do in Slack you can do over HTTP.
When webhooks fire
Today, budget threshold crossings are the event that reaches a webhook (or any other channel you've bound):
| Trigger | Severity | Source |
|---|---|---|
| A budget hits a soft threshold you configured (for example 50% / 80% / 100%) | WARNING → CRITICAL | Budget threshold dispatch |
You bind a channel to a budget's thresholds on the Budgets page — that's where each threshold's destinations are chosen. Create the channels themselves under Observability → Alerts → Channels. More alert categories (errors, latency, outages) are on the roadmap and show as "Coming soon" on the Alerts page — they don't fire yet.
Configuration
curl -X POST https://api.nemorouter.ai/nemo/channels \
-H "Authorization: Bearer sk-nemo-your-key" \
-H "Content-Type: application/json" \
-d '{
"name": "Production alerts",
"channel_type": "webhook",
"config": {
"url": "https://hooks.your-app.com/nemo",
"secret": "whsec_a-long-random-string",
"headers": {
"X-Custom-Header": "optional"
}
},
"is_active": true
}'The secret is optional but strongly recommended — without it, anyone who learns your webhook URL can replay payloads.
Payload shape
Every webhook gets the same JSON envelope, regardless of trigger:
{
"title": "[NemoRouter] Budget depletion warning — balance below threshold",
"message": "Org acme-inc is below $10 in credits.",
"severity": "HIGH",
"details": {
"org_id": "f47ac10b-58cc-4372-a567-0e02b2c3d479",
"current_balance": 4.27,
"threshold": 10,
"action": "Top up credits to avoid service interruption"
},
"service": "nemo-backend"
}title— short human-readable summary, always prefixed with[NemoRouter]message— longer description suitable for chatseverity—CRITICAL | HIGH | WARNING | INFOdetails— event-specific context (org_id, request_id, amount, key_alias, etc.)service— alwaysnemo-backend
The details object varies by event type. The keys you'll most commonly see: org_id, current_balance, threshold, request_id, key_alias, guardrail_name, model.
Signature verification
When a secret is configured, NemoRouter signs the exact bytes it sends with HMAC-SHA256 and sets the header:
X-Nemo-Signature: sha256=<hex-digest>The signed body is canonical JSON: json.dumps(payload, separators=(",", ":"), sort_keys=True). Use the same serialization when computing the verification hash.
Node.js verification
import crypto from "node:crypto";
export function verifyNemoSignature(rawBody: string, signatureHeader: string, secret: string): boolean {
if (!signatureHeader?.startsWith("sha256=")) return false;
const expected = crypto
.createHmac("sha256", secret)
.update(rawBody)
.digest("hex");
const provided = signatureHeader.slice(7);
// constant-time compare to avoid timing attacks
return crypto.timingSafeEqual(
Buffer.from(expected, "hex"),
Buffer.from(provided, "hex"),
);
}Python verification
import hmac
import hashlib
def verify_nemo_signature(raw_body: bytes, signature_header: str, secret: str) -> bool:
if not signature_header.startswith("sha256="):
return False
expected = hmac.new(secret.encode(), raw_body, hashlib.sha256).hexdigest()
provided = signature_header[7:]
return hmac.compare_digest(expected, provided)Endpoint requirements
- HTTPS only. HTTP URLs are rejected at create time.
- Public DNS resolution. Loopback, private (RFC1918), link-local, and cloud-metadata addresses are blocked by an SSRF guard.
- Respond within 10s with any 2xx status. Slower responses are recorded as failures.
- Don't return secrets or keys in the response body. Nemo only inspects the status code.
Retries and delivery semantics
Webhooks are fire-and-forget with no automatic retry. If your endpoint returns a non-2xx (or times out), the failure is logged and the event is dropped. This is intentional — we don't want to retry a payment-failure alert at midnight.
If you need guaranteed delivery, pair a webhook with an Alert Channel of type email or slack so a human sees the same event through a separate path.
Testing
Send a test payload from the dashboard (Alert Channels → row → Send test) or via the API:
curl -X POST https://api.nemorouter.ai/nemo/channels/{channel_id}/test \
-H "Authorization: Bearer sk-nemo-your-key"Replace {channel_id} with the id of a channel you already created via POST /nemo/channels. The test fires against that channel's stored configuration — no body required.
Test payloads carry severity: "INFO" and details.test: true so you can filter them out in production.
Next Steps
- Budget Controls — Cap spend per org, team, and key; route threshold alerts to your channels
- Alert Channels (email, Slack, Teams, Jira, webhook) are created in the dashboard under Observability → Alerts → Channels
- API Key Management — Rotate the key that signs management calls
- Chat Completions API — The LLM request path that triggers most webhooks