Skip to Content
APIWebhooks

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

EventFired ByDescription
budget.exceededGovernance chain (cost estimator)Daily or monthly budget has been exceeded. The request that triggered the breach is blocked.
budget.warningGovernance chain (cost estimator)Approaching budget limit. Fires at configurable thresholds (default 50%, 75%, 90%).
guardrail.triggeredRate limiter, PII scanner, security scanner, model allowlistA governance guardrail blocked a request. Subtypes: rate_limit, pii_detected, content_safety, security_scan, model_blocked.
approval.requestedHITL gateA high-cost request has been flagged for human approval before it can proceed.
agent.failedRunner lifecycle (private beta)A managed runner or BYOVM agent has failed. Includes runner ID, session ID, and truncated error message.
cost_anomaly.*Cost anomaly detectorUnusual cost pattern detected. Subtypes: cost_anomaly.hourly_spike, cost_anomaly.runaway_agent, cost_anomaly.model_drift, cost_anomaly.volume_surge.
budget_hierarchy_alertHierarchical budget serviceA 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:

SubtypeRule
hourly_spikeCurrent hour cost exceeds 3x the rolling 7-day hourly average
runaway_agentSingle agent consumed more than 50% of daily budget in one hour
model_driftSudden switch to expensive models (more than 5x cost increase per request)
volume_surgeRequest 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:

HeaderDescription
X-Webhook-SignatureHMAC-SHA256 signature of the request body (sha256=...)
X-Webhook-EventThe event type (e.g., budget.exceeded)
X-Webhook-TimestampUnix timestamp of when the event was generated
Content-TypeAlways 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:

AttemptDelay
1st retry1 second
2nd retry2 seconds
3rd retry4 seconds
4th retry8 seconds
5th retry16 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.