Webhooks
Webhooks deliver real-time HTTP notifications when governance events fire in the Curate-Me AI Gateway. Instead of polling the API, you register an endpoint URL and the platform pushes events to it as they happen — budget overruns, guardrail blocks, HITL approval requests, agent failures, and cost anomalies.
All webhook payloads use the CloudEvents v1.0 envelope format and are signed with HMAC-SHA256 for verification.
Event Catalog
| Event | Fired By | Description |
|---|---|---|
budget.exceeded | Governance chain (cost estimator) | Daily or monthly budget has been exceeded. The request that triggered the breach is blocked. |
budget.warning | Governance chain (cost estimator) | Approaching budget limit. Fires at configurable thresholds (default 50%, 75%, 90%). |
guardrail.triggered | Rate limiter, PII scanner, security scanner, model allowlist | A governance guardrail blocked a request. Subtypes: rate_limit, pii_detected, content_safety, security_scan, model_blocked. |
approval.requested | HITL gate | A high-cost request has been flagged for human approval before it can proceed. |
agent.failed | Runner lifecycle (private beta) | A managed runner or BYOVM agent has failed. Includes runner ID, session ID, and truncated error message. |
cost_anomaly.* | Cost anomaly detector | Unusual cost pattern detected. Subtypes: cost_anomaly.hourly_spike, cost_anomaly.runaway_agent, cost_anomaly.model_drift, cost_anomaly.volume_surge. |
budget_hierarchy_alert | Hierarchical budget service | A budget node (org, team, or key level) has crossed a threshold. Fires at configurable percentages per node. |
Event Payloads
budget.exceeded
Fired when a request would push spend past the configured daily or monthly budget. The triggering request is blocked.
{
"id": "evt_a1b2c3d4",
"type": "budget.exceeded",
"source": "gateway.curate-me.ai",
"time": "2026-05-04T14:23:00Z",
"data": {
"blocked_by": "daily_budget",
"daily_spent": 48.1234,
"daily_budget": 50.00,
"model": "claude-sonnet-4-20250514",
"estimated_cost": 3.2100,
"timestamp": "2026-05-04T14:23:00Z"
}
}guardrail.triggered
Fired when any governance guardrail blocks a request. The guardrail field identifies which rule fired.
{
"id": "evt_e5f6g7h8",
"type": "guardrail.triggered",
"source": "gateway.curate-me.ai",
"time": "2026-05-04T14:25:12Z",
"data": {
"guardrail": "pii_detected",
"reason": "Request body contains email addresses and SSNs",
"model": "gpt-4.1",
"details": {
"pii_types": ["email", "ssn"],
"field": "messages[0].content"
},
"timestamp": "2026-05-04T14:25:12Z"
}
}Rate limit variant:
{
"id": "evt_j9k0l1m2",
"type": "guardrail.triggered",
"source": "gateway.curate-me.ai",
"time": "2026-05-04T14:26:00Z",
"data": {
"guardrail": "rate_limit",
"current_count": 61,
"rpm_limit": 60,
"model": "claude-sonnet-4-20250514",
"timestamp": "2026-05-04T14:26:00Z"
}
}approval.requested
Fired when a request exceeds the HITL cost threshold and requires human approval before proceeding.
{
"id": "evt_n3o4p5q6",
"type": "approval.requested",
"source": "gateway.curate-me.ai",
"time": "2026-05-04T14:30:00Z",
"data": {
"request_id": "req_abc123def456",
"model": "claude-opus-4-20250514",
"estimated_cost": 12.5000,
"hitl_threshold": 10.00,
"approval_id": "apr_xyz789",
"timestamp": "2026-05-04T14:30:00Z"
}
}agent.failed
Fired when a managed runner or BYOVM agent encounters a fatal error.
{
"id": "evt_r7s8t9u0",
"type": "agent.failed",
"source": "gateway.curate-me.ai",
"time": "2026-05-04T15:00:00Z",
"data": {
"runner_id": "byovm_c8a4acd75ea7",
"runner_name": "frank",
"session_id": "sess_abc123",
"error": "Container OOMKilled after exceeding 2GB memory limit",
"timestamp": "2026-05-04T15:00:00Z"
}
}cost_anomaly.*
Fired when the statistical cost anomaly detector identifies unusual spending patterns. The event type suffix indicates the anomaly rule that triggered.
{
"id": "evt_v1w2x3y4",
"type": "cost_anomaly.hourly_spike",
"source": "gateway.curate-me.ai",
"time": "2026-05-04T15:05:00Z",
"data": {
"anomaly_id": "anom_abc123",
"anomaly_type": "hourly_spike",
"severity": "high",
"current_value": 45.20,
"threshold_value": 15.00,
"multiplier": 3.01,
"message": "Current hour cost ($45.20) exceeds 3x the 7-day hourly average ($15.00)",
"details": {},
"detected_at": "2026-05-04T15:05:00Z"
}
}Anomaly subtypes:
| Subtype | Rule |
|---|---|
hourly_spike | Current hour cost exceeds 3x the rolling 7-day hourly average |
runaway_agent | Single agent consumed more than 50% of daily budget in one hour |
model_drift | Sudden switch to expensive models (more than 5x cost increase per request) |
volume_surge | Request count exceeds 5x normal for the time of day |
Setting Up Webhooks
Register a webhook endpoint through the B2B Admin API.
Create a webhook
curl -X POST https://api.curate-me.ai/api/v1/admin/webhooks \
-H "Authorization: Bearer $TOKEN" \
-H "X-Org-ID: $ORG_ID" \
-H "Content-Type: application/json" \
-d '{
"url": "https://your-app.com/webhooks/curate-me",
"events": [
"budget.exceeded",
"budget.warning",
"guardrail.triggered",
"approval.requested",
"agent.failed"
],
"secret": "whsec_your_signing_secret"
}'Response (201):
{
"id": "wh_abc123",
"url": "https://your-app.com/webhooks/curate-me",
"events": [
"budget.exceeded",
"budget.warning",
"guardrail.triggered",
"approval.requested",
"agent.failed"
],
"status": "active",
"created_at": "2026-05-04T15:00:00Z"
}List webhooks
curl https://api.curate-me.ai/api/v1/admin/webhooks \
-H "Authorization: Bearer $TOKEN" \
-H "X-Org-ID: $ORG_ID"Update a webhook
curl -X PATCH https://api.curate-me.ai/api/v1/admin/webhooks/wh_abc123 \
-H "Authorization: Bearer $TOKEN" \
-H "X-Org-ID: $ORG_ID" \
-H "Content-Type: application/json" \
-d '{
"events": ["budget.exceeded", "agent.failed"],
"url": "https://your-app.com/webhooks/v2/curate-me"
}'Delete a webhook
curl -X DELETE https://api.curate-me.ai/api/v1/admin/webhooks/wh_abc123 \
-H "Authorization: Bearer $TOKEN" \
-H "X-Org-ID: $ORG_ID"View delivery history
curl "https://api.curate-me.ai/api/v1/admin/webhooks/wh_abc123/deliveries?offset=0&limit=20" \
-H "Authorization: Bearer $TOKEN" \
-H "X-Org-ID: $ORG_ID"Returns recent delivery attempts including HTTP status codes, response latency, and retry counts.
Webhook Headers
Every webhook delivery includes these headers:
| Header | Description |
|---|---|
X-Webhook-Signature | HMAC-SHA256 signature of the request body (sha256=...) |
X-Webhook-Event | The event type (e.g., budget.exceeded) |
X-Webhook-Timestamp | Unix timestamp of when the event was generated |
Content-Type | Always application/json |
Signature Verification
Every webhook request is signed with HMAC-SHA256 using the secret you provided when creating the webhook. Always verify the signature before processing.
Python
import hmac
import hashlib
def verify_webhook(payload: bytes, signature: str, secret: str) -> bool:
"""Verify the webhook signature.
Args:
payload: Raw request body bytes.
signature: Value of the X-Webhook-Signature header.
secret: The webhook signing secret (whsec_...).
Returns:
True if the signature is valid.
"""
expected = hmac.new(
secret.encode("utf-8"),
payload,
hashlib.sha256,
).hexdigest()
return hmac.compare_digest(f"sha256={expected}", signature)
# Usage in a FastAPI route:
from fastapi import Request, HTTPException
@app.post("/webhooks/curate-me")
async def handle_webhook(request: Request):
body = await request.body()
signature = request.headers.get("X-Webhook-Signature", "")
if not verify_webhook(body, signature, WEBHOOK_SECRET):
raise HTTPException(status_code=401, detail="Invalid signature")
event = request.headers.get("X-Webhook-Event")
data = await request.json()
match event:
case "budget.exceeded":
await handle_budget_exceeded(data["data"])
case "guardrail.triggered":
await handle_guardrail(data["data"])
case "approval.requested":
await handle_approval(data["data"])
case "agent.failed":
await handle_agent_failure(data["data"])
return {"status": "ok"}JavaScript / Node.js
const crypto = require('crypto');
function verifyWebhook(payload, signature, secret) {
const expected = crypto
.createHmac('sha256', secret)
.update(payload, 'utf8')
.digest('hex');
const expectedSignature = `sha256=${expected}`;
return crypto.timingSafeEqual(
Buffer.from(expectedSignature),
Buffer.from(signature)
);
}
// Usage in an Express route:
const express = require('express');
const app = express();
app.post('/webhooks/curate-me', express.raw({ type: 'application/json' }), (req, res) => {
const signature = req.headers['x-webhook-signature'];
const event = req.headers['x-webhook-event'];
if (!verifyWebhook(req.body, signature, WEBHOOK_SECRET)) {
return res.status(401).json({ error: 'Invalid signature' });
}
const data = JSON.parse(req.body);
switch (event) {
case 'budget.exceeded':
handleBudgetExceeded(data.data);
break;
case 'guardrail.triggered':
handleGuardrail(data.data);
break;
case 'approval.requested':
handleApproval(data.data);
break;
case 'agent.failed':
handleAgentFailure(data.data);
break;
}
res.json({ status: 'ok' });
});Retry Behavior
If your endpoint returns a non-2xx status code or the request times out (10 seconds), the platform retries with exponential backoff:
| Attempt | Delay |
|---|---|
| 1st retry | 1 second |
| 2nd retry | 2 seconds |
| 3rd retry | 4 seconds |
| 4th retry | 8 seconds |
| 5th retry | 16 seconds |
After 5 failed attempts, the delivery is moved to a dead-letter queue. Dead-letter deliveries are retained for 30 days and can be viewed and manually retried through the dashboard or the delivery history API.
Deduplication
The platform deduplicates webhook alerts using a 60-second window per organization and event type. If the same event type fires multiple times within the same minute for the same org (e.g., many rate-limited requests in a burst), only the first delivery is sent. This prevents flooding your endpoint during incident spikes.
Slack Integration
In addition to HTTP webhooks, every event is also delivered to your organization’s Slack channel when a Slack integration is configured. Events are formatted as Block Kit messages with structured detail fields and a link to the governance dashboard. Configure Slack integration in the dashboard under Settings > Integrations.