Webhooks
Receive HTTPS notifications from Nemo Router for alerts, spend events, and audit actions
Last updated
Configure a webhook endpoint and Nemo Router will POST a JSON payload whenever an alert fires. Webhooks are one of five delivery channels for alerts (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
| Trigger | Severity | Source |
|---|---|---|
Org balance drops below $10 | HIGH | Credit reserve+settle pipeline |
| Budget hits a threshold (50% / 80% / 100%) | WARNING → CRITICAL | Budget polling |
| RPM/TPM limit hit repeatedly | WARNING | Rate-limit window observer |
| Guardrail blocks a request | INFO | Guardrail hook |
| Cost-leak detector trips | CRITICAL | Background scanner |
| Admin action (key created/rotated/deleted, member added) | INFO | Audit-trail writer |
Channel bindings let you route each event type to a different webhook. Configure under Manage → Logging → Alert Channels in the dashboard.
Configuration
curl -X POST https://api.nemorouter.ai/nemo/channel/new \
-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": "[Nemo Router] 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[Nemo Router]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, Nemo Router 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/channel/test \
-H "Authorization: Bearer sk-nemo-your-key" \
-H "Content-Type: application/json" \
-d '{
"channel_type": "webhook",
"config": {
"url": "https://hooks.your-app.com/nemo",
"secret": "whsec_a-long-random-string"
}
}'Test payloads carry severity: "INFO" and details.test: true so you can filter them out in production.
Next Steps
- Alert Channels — Email, Slack, Teams, Jira, webhook setup
- API Key Management — Rotate the key that signs management calls
- Chat Completions API — The LLM request path that triggers most webhooks