Webhooks
ORYVOX sends signed webhooks to endpoints you add in the dashboard under
ORYVOX sends signed webhooks to endpoints you add in the dashboard under
Core > Observability > Incidents & alerts (/app/core/monitoring), in the webhooks list.
The URL must be a public https:// address; internal and private network addresses are refused.
Each endpoint gets its own signing secret (whsec_...), shown once when you add it. An endpoint
receives every public event, or only the types you pick. Send test delivers a test.ping
(always delivered, whatever the filter).
Events
| Type | When |
|---|---|
security.alert | A warning or critical security event: blocked or masked secrets, blocked personal data, denied IPs, scope denials, rate-limit hits, new key IPs, budget stops, sign-in and policy events. At most once per type every 15 minutes. |
incident.opened | An alert rule or the automatic watch opened an incident (error rate, latency, provider trouble...). |
incident.resolved | That incident is resolved. |
budget.warning | A project's daily or monthly spend reached its warning level (budget_warn_at, default 80%). |
budget.exceeded | A project's spend reached 100% of its budget. |
credits.low | The credit balance fell below your low-balance threshold. |
credits.exhausted | The balance reached zero; prepaid requests now fail with 402 insufficient_credits. |
invoice.created | A monthly invoice was issued. |
invoice.paid | An invoice was paid. |
invoice.failed | A payment failed. |
test.ping | Sent by Send test. |
Payload
{
"id": "evt_k2j4...",
"type": "budget.warning",
"version": 1,
"created": 1790000000,
"data": {"project": "Support bot", "project_id": 12, "period": "month", "budget": 500.0, "spent": 412.37}
}version is the event schema version. data depends on the type; budget amounts are in
charged USD.
Signature
Every request has an Oryvox-Signature header:
Oryvox-Signature: t=1790000000,v1=5257a869e7ecebeda32affa62cdca3fa51cad7e77a0e56ff536d0ce8e108d8bdtis the Unix time the request was signed.v1is the hex HMAC-SHA256 of"{t}.{raw_body}"keyed with the endpoint secret.
To verify: recompute the HMAC over the raw body, compare in constant time, and reject
timestamps older than 5 minutes (replay protection). The header carries one v1 value; the
SDK verifiers also accept several (any match passes).
The SDKs do all of this:
import Oryvox from '@oryvox/sdk';
app.post('/webhooks/oryvox', express.raw({ type: 'application/json' }), (req, res) => {
let event;
try {
event = Oryvox.webhooks.verify(req.body, req.header('Oryvox-Signature'), process.env.ORYVOX_WEBHOOK_SECRET!);
} catch {
return res.sendStatus(400);
}
handle(event); // event.id, event.type, event.version, event.created, event.data
res.sendStatus(200);
});from oryvox import Oryvox, WebhookVerificationError
try:
event = Oryvox.webhooks.verify(raw_body, headers["Oryvox-Signature"], WEBHOOK_SECRET)
except WebhookVerificationError:
return 400The optional last argument sets the tolerance in seconds (default 300).
Manual verification (any language):
import hashlib, hmac, time
def verify(raw_body: bytes, header: str, secret: str, tolerance: int = 300) -> bool:
parts = dict(p.split("=", 1) for p in header.split(","))
t = parts["t"]
expected = hmac.new(secret.encode(), f"{t}.".encode() + raw_body, hashlib.sha256).hexdigest()
return hmac.compare_digest(expected, parts["v1"]) and abs(time.time() - int(t)) <= toleranceDelivery and retries
- Respond with any
2xxwithin 10 seconds. Do slow work asynchronously. - Anything else (another status, a timeout, a connection error) is retried, up to 8 attempts in total, with exponential backoff: 30 s, 2 min, 8 min, 32 min, about 2 h, then 6 h (the cap), so the last attempt is about 15 hours after the first.
- After the last failed attempt the delivery is marked failed. The webhooks list shows each endpoint's last status and delivery time.
- Delivery is at least once: the same event can arrive more than once, and events can
arrive out of order. Deduplicate by
event.idand usecreatedto order them.