Webhooks & Notifications API
Receive real-time notifications when events occur in ThinkHive. This page covers the complete notification system: webhooks for programmatic integrations, notification channels for team alerting (Slack, email, PagerDuty), and notification rules for per-agent alert configuration.
This page replaces the previous Webhooks API page. All webhook endpoints documented here are the canonical v1 API. The old page is retained for reference but marked as deprecated.
Authentication
All endpoints require either a session cookie or an API key via the Authorization: Bearer header.
Authorization: Bearer thk_your_api_keySee Authentication for details on obtaining and managing API keys.
Webhooks
Webhooks deliver event payloads to your HTTP endpoints in real time. ThinkHive signs every delivery with HMAC-SHA256, retries on failure, and opens a circuit breaker after repeated errors.
List Webhooks
GET /api/v1/explainer/webhooksReturns all webhooks for the authenticated company.
Response:
{
"success": true,
"data": {
"webhooks": [
{
"id": "a1b2c3d4-...",
"name": "Production Alerts",
"url": "https://your-app.com/webhooks/thinkhive",
"events": ["trace.analyzed", "failure.detected"],
"filters": {
"agentIds": ["agent_abc123"],
"minSeverity": "high"
},
"isActive": true,
"lastTriggeredAt": "2026-03-10T08:30:00Z",
"consecutiveFailures": 0,
"circuitBreakerUntil": null
}
]
}
}Create Webhook
POST /api/v1/explainer/webhooksRequest Body:
| Field | Type | Required | Description |
|---|---|---|---|
name | string | Yes | Display name (1-255 characters) |
url | string | Yes | HTTPS endpoint URL |
events | string[] | Yes | One or more event types |
filters | object | No | Optional delivery filters (see below) |
Filters:
| Field | Type | Description |
|---|---|---|
filters.agentIds | string[] | Only deliver events for these agents |
filters.minSeverity | string | Minimum severity level |
filters.outcomes | string[] | Filter by trace outcomes |
Request example:
{
"name": "Production Alerts",
"url": "https://your-app.com/webhooks/thinkhive",
"events": ["failure.detected", "case.created", "drift.detected"],
"filters": {
"agentIds": ["agent_abc123"],
"minSeverity": "high"
}
}Response (201 Created):
{
"success": true,
"data": {
"webhook": {
"id": "a1b2c3d4-...",
"name": "Production Alerts",
"url": "https://your-app.com/webhooks/thinkhive",
"events": ["failure.detected", "case.created", "drift.detected"],
"filters": { "agentIds": ["agent_abc123"], "minSeverity": "high" },
"isActive": true,
"secret": "d4e5f6a7-...b8c9d0e1-..."
}
}
}The secret is returned only on creation. Store it securely — you will need it to verify signatures.
Update Webhook
PATCH /api/v1/explainer/webhooks/:idAll fields are optional. Only provided fields are updated.
Request Body:
| Field | Type | Description |
|---|---|---|
name | string | Updated display name |
url | string | Updated endpoint URL |
events | string[] | Updated event subscriptions |
filters | object | Updated delivery filters |
isActive | boolean | Enable or disable the webhook |
Response:
{
"success": true,
"data": { "message": "Webhook updated" }
}Delete Webhook
DELETE /api/v1/explainer/webhooks/:idPermanently removes the webhook and all associated delivery history.
Response:
{
"success": true,
"data": { "message": "Webhook deleted" }
}Test Webhook
POST /api/v1/explainer/webhooks/:id/testEnqueues a test delivery with event type webhook.test. Use this to verify your endpoint is reachable and correctly validating signatures.
Response:
{
"success": true,
"data": {
"message": "Test webhook queued for delivery",
"deliveryId": "test_dlv_...",
"eventId": "test_..."
}
}Rotate Secret
POST /api/v1/explainer/webhooks/:id/rotate-secretImmediately replaces the webhook secret. All subsequent deliveries use the new secret.
Response:
{
"success": true,
"data": {
"message": "Webhook secret rotated",
"secret": "new-secret-value"
}
}Rotate Secret (Graceful)
POST /api/v1/explainer/webhooks/:id/rotate-secret-gracefulRotates the secret with a grace period during which both old and new secrets are valid. This allows you to update your verification code without downtime.
Request Body:
| Field | Type | Default | Description |
|---|---|---|---|
gracePeriodHours | number | 24 | Hours during which both secrets are valid |
Response:
{
"success": true,
"data": {
"message": "Webhook secret rotated with grace period",
"newSecret": "new-secret-value",
"gracePeriodUntil": "2026-03-11T08:30:00.000Z",
"note": "Both old and new secrets will be valid until 2026-03-11T08:30:00.000Z"
}
}Reset Circuit Breaker
POST /api/v1/explainer/webhooks/:id/reset-circuit-breakerResets the circuit breaker after you have resolved the issue causing delivery failures. This clears the consecutive failure count and removes the cooldown window.
Response:
{
"success": true,
"data": { "message": "Circuit breaker reset" }
}Delivery Metrics
GET /api/v1/explainer/webhooks/:id/metricsReturns delivery statistics for the last 24 hours, 7 days, and all time, plus the 10 most recent errors.
Response:
{
"success": true,
"data": {
"webhookId": "a1b2c3d4-...",
"status": {
"isActive": true,
"circuitBreakerOpen": false,
"circuitBreakerUntil": null,
"consecutiveFailures": 0,
"lastTriggeredAt": "2026-03-10T08:30:00Z"
},
"allTime": {
"total": 1250,
"delivered": 1230,
"failed": 15,
"deadLetter": 5,
"pending": 0,
"successRate": 98.4,
"avgResponseTimeMs": 145,
"avgAttempts": 1.1
},
"last24h": { "total": 42, "delivered": 42, "failed": 0, "successRate": 100 },
"last7d": { "total": 310, "delivered": 305, "failed": 5, "successRate": 98.4 },
"recentErrors": []
}
}List Deliveries
GET /api/v1/explainer/webhooks/:id/deliveriesQuery Parameters:
| Parameter | Type | Default | Description |
|---|---|---|---|
status | string | all | Filter by status: pending, delivered, failed, dead_letter |
limit | number | 50 | Max results (capped at 100) |
offset | number | 0 | Pagination offset |
Response:
{
"success": true,
"data": {
"deliveries": [
{
"id": "dlv_001",
"eventId": "evt_001",
"eventType": "failure.detected",
"status": "delivered",
"attempts": 1,
"lastStatusCode": 200,
"lastError": null,
"responseTimeMs": 120,
"createdAt": "2026-03-10T08:30:00Z",
"deliveredAt": "2026-03-10T08:30:00Z"
}
],
"pagination": { "limit": 50, "offset": 0, "total": 1250 }
}
}Replay Delivery
POST /api/v1/explainer/webhooks/:id/deliveries/:deliveryId/replayRe-sends a failed or dead_letter delivery. A new delivery record is created with the original payload.
Response:
{
"success": true,
"data": {
"message": "Delivery queued for replay",
"originalDeliveryId": "dlv_001",
"newDeliveryId": "dlv_002",
"eventId": "evt_001_replay_1710000000000"
}
}Available Events
| Event | Description |
|---|---|
trace.analyzed | Trace analysis completed |
failure.detected | A failure was detected in a trace |
pattern.new | New failure pattern identified |
pattern.updated | Existing failure pattern updated |
case.created | New Case created from clustered failures |
fix.created | New fix proposed for a Case |
drift.detected | Model or behavior drift detected |
threshold.exceeded | A configured metric threshold was exceeded |
run.completed | Evaluation run completed successfully |
run.failed | Evaluation run failed |
webhook.test | Test event (sent via the test endpoint) |
Webhook Payload
Every webhook delivery includes a JSON payload with the following structure:
{
"event": "failure.detected",
"timestamp": "2026-03-10T08:30:00Z",
"eventId": "evt_a1b2c3d4",
"schemaVersion": "2.0",
"data": {
"companyId": "company_xyz",
"agentId": "agent_abc123",
"traceId": "tr_xyz789",
"failureReason": "Hallucination detected",
"severity": "high"
}
}Headers
Each delivery includes these headers:
| Header | Description |
|---|---|
Content-Type | application/json |
X-ThinkHive-Event | Event type (e.g., failure.detected) |
X-ThinkHive-Delivery | Unique delivery ID |
X-ThinkHive-Signature | HMAC-SHA256 signature |
Signature Verification
Every webhook delivery is signed with your webhook secret using HMAC-SHA256. Always verify signatures before processing payloads.
import crypto from 'crypto';
function verifyWebhookSignature(
rawBody: Buffer,
signature: string,
secret: string
): boolean {
const expected = crypto
.createHmac('sha256', secret)
.update(rawBody)
.digest('hex');
// timingSafeEqual throws if buffers differ in length
const sigBuf = Buffer.from(signature, 'utf8');
const expBuf = Buffer.from(expected, 'utf8');
if (sigBuf.length !== expBuf.length) {
return false;
}
return crypto.timingSafeEqual(sigBuf, expBuf);
}
// Express middleware example
// IMPORTANT: You need raw body access. Add this BEFORE json() parsing:
// app.use('/webhooks/thinkhive', express.raw({ type: 'application/json' }));
// This gives you req.body as a Buffer. Parse JSON yourself after verification.
app.post('/webhooks/thinkhive', (req, res) => {
const signature = req.headers['x-thinkhive-signature'] as string;
const rawBody: Buffer = req.body; // Buffer from express.raw()
const isValid = verifyWebhookSignature(
rawBody,
signature,
process.env.THINKHIVE_WEBHOOK_SECRET!
);
if (!isValid) {
return res.status(401).json({ error: 'Invalid signature' });
}
// Process the event
const parsed = JSON.parse(rawBody.toString());
const { event, data } = parsed;
console.log(`Received ${event}:`, data);
res.status(200).json({ received: true });
});Retry Policy
Failed webhook deliveries are retried automatically with exponential backoff:
| Attempt | Delay | Cumulative |
|---|---|---|
| 1 | Immediate | 0 |
| 2 | 1 minute | 1 min |
| 3 | 5 minutes | 6 min |
| 4 | 30 minutes | 36 min |
| 5 | 2 hours | ~2.5 hr |
| 6 | 24 hours | ~26.5 hr |
After all retries are exhausted, the delivery is moved to dead_letter status. You can replay dead-letter deliveries using the Replay Delivery endpoint.
Circuit Breaker
If a webhook accumulates consecutive failures, ThinkHive opens a circuit breaker to prevent overloading your endpoint:
- Circuit breaker opens after 5 consecutive failures
- While open, new deliveries are queued but not sent
- The circuit breaker automatically closes after a cooldown period
- Use the Reset Circuit Breaker endpoint to manually re-enable delivery after fixing the issue
Notification Channels
Notification channels configure company-wide delivery targets for alerts: Slack, email, PagerDuty, or generic webhooks. Sensitive configuration (webhook URLs, routing keys) is encrypted at rest.
List Channels
GET /api/v1/explainer/notification-channelsReturns all configured channels for the authenticated company. Sensitive fields are masked in the response.
Response:
{
"success": true,
"data": {
"channels": [
{
"id": "ch_001",
"channel": "slack",
"isEnabled": true,
"config": { "webhookUrl": "https://hooks.slack.com/****" },
"isVerified": true,
"createdAt": "2026-01-15T10:00:00Z"
},
{
"id": "ch_002",
"channel": "email",
"isEnabled": true,
"config": { "recipients": ["team@example.com"], "fromName": "ThinkHive Alerts" },
"isVerified": true,
"createdAt": "2026-01-15T10:00:00Z"
}
]
}
}Create or Update Channel
PUT /api/v1/explainer/notification-channels/:channelUpserts a channel configuration. The :channel parameter must be one of: slack, email, pagerduty, webhook.
Request Body:
| Field | Type | Required | Description |
|---|---|---|---|
isEnabled | boolean | No | Enable or disable the channel |
config | object | No | Channel-specific configuration (see below) |
Slack Configuration
| Field | Type | Required | Description |
|---|---|---|---|
webhookUrl | string | Yes | Slack incoming webhook URL (must start with https://hooks.slack.com/) |
{
"isEnabled": true,
"config": {
"webhookUrl": "https://hooks.slack.com/services/T00/B00/xxxx"
}
}Email Configuration
| Field | Type | Required | Description |
|---|---|---|---|
recipients | string[] | Yes | One or more email addresses |
fromName | string | No | Display name for the sender (max 100 chars) |
{
"isEnabled": true,
"config": {
"recipients": ["ops@example.com", "team-lead@example.com"],
"fromName": "ThinkHive Alerts"
}
}PagerDuty Configuration
| Field | Type | Required | Description |
|---|---|---|---|
routingKey | string | Yes | PagerDuty Events API v2 routing key |
serviceId | string | No | PagerDuty service identifier |
{
"isEnabled": true,
"config": {
"routingKey": "your-pagerduty-routing-key",
"serviceId": "PSERVICE01"
}
}Webhook Configuration
| Field | Type | Required | Description |
|---|---|---|---|
url | string | Yes | HTTPS endpoint URL |
secret | string | Yes | Signing secret for HMAC verification |
headers | object | No | Custom headers to include in deliveries |
{
"isEnabled": true,
"config": {
"url": "https://your-app.com/notifications",
"secret": "your_signing_secret",
"headers": { "X-Custom-Header": "value" }
}
}Response:
{
"success": true,
"data": {
"channel": {
"id": "ch_001",
"channel": "slack",
"isEnabled": true,
"config": { "webhookUrl": "https://hooks.slack.com/****" },
"isVerified": false,
"createdAt": "2026-03-10T08:30:00Z"
}
}
}Delete Channel
DELETE /api/v1/explainer/notification-channels/:channelRemoves the channel configuration. The :channel parameter must be one of: slack, email, pagerduty, webhook.
Response:
{
"success": true,
"data": { "message": "Channel configuration deleted" }
}Test Channel
POST /api/v1/explainer/notification-channels/:channel/testSends a test notification through the configured channel. Rate limited to one test per channel per minute per company.
Response (success):
{
"success": true,
"data": {
"success": true,
"message": "Test notification delivered"
}
}Response (rate limited, 429):
{
"success": false,
"data": { "error": "Rate limited. Wait 1 minute between tests." }
}Notification Rules
Notification rules define per-agent alerting conditions. When a rule’s conditions are met, ThinkHive sends a notification through the specified channel to the configured recipients.
List Rules
GET /api/notification-rulesQuery Parameters:
| Parameter | Type | Required | Description |
|---|---|---|---|
agentId | string | Yes | Agent to list rules for |
Response:
[
{
"id": "rule_001",
"agentId": "agent_abc123",
"name": "High-severity failures",
"description": "Alert on high/critical severity failures",
"conditions": {
"event": "failure.detected",
"minSeverity": "high"
},
"channel": "slack",
"recipients": {
"slackChannel": "#alerts-prod"
},
"cooldownMinutes": 60,
"lastTriggered": "2026-03-10T07:15:00Z",
"isActive": true,
"createdAt": "2026-01-15T10:00:00Z",
"updatedAt": "2026-03-10T07:15:00Z"
}
]Create Rule
POST /api/notification-rulesRequest Body:
| Field | Type | Required | Description |
|---|---|---|---|
agentId | string | Yes | Agent this rule applies to |
name | string | Yes | Display name for the rule |
description | string | No | Optional description |
conditions | object | Yes | Trigger conditions (JSON) |
channel | string | Yes | Delivery channel: email, slack, webhook, or in_app |
recipients | object | Yes | Channel-specific recipients (JSON) |
cooldownMinutes | number | No | Minimum minutes between triggers (default: 60) |
isActive | boolean | No | Whether the rule is active (default: true) |
Request example:
{
"agentId": "agent_abc123",
"name": "Drift Alert",
"description": "Notify when model drift is detected",
"conditions": {
"event": "drift.detected",
"minSeverity": "medium"
},
"channel": "email",
"recipients": {
"emails": ["ml-team@example.com"]
},
"cooldownMinutes": 120
}Response (201 Created):
{
"id": "rule_002",
"agentId": "agent_abc123",
"name": "Drift Alert",
"description": "Notify when model drift is detected",
"conditions": { "event": "drift.detected", "minSeverity": "medium" },
"channel": "email",
"recipients": { "emails": ["ml-team@example.com"] },
"cooldownMinutes": 120,
"lastTriggered": null,
"isActive": true,
"createdAt": "2026-03-10T08:30:00Z",
"updatedAt": "2026-03-10T08:30:00Z"
}Update Rule
PATCH /api/notification-rules/:idPartially updates a notification rule. All fields are optional.
Request example:
{
"isActive": false,
"cooldownMinutes": 30
}Response:
{
"id": "rule_002",
"agentId": "agent_abc123",
"name": "Drift Alert",
"isActive": false,
"cooldownMinutes": 30,
"updatedAt": "2026-03-10T09:00:00Z"
}Delete Rule
DELETE /api/notification-rules/:idPermanently deletes the notification rule.
Response: 204 No Content
User Notification Preferences
Individual users can configure their notification preferences to control which channels are active, minimum severity, and quiet hours.
Get Preferences
GET /api/v1/explainer/notification-preferencesResponse:
{
"success": true,
"data": {
"preferences": {
"emailEnabled": true,
"slackEnabled": true,
"inAppEnabled": true,
"webhookEnabled": true,
"pagerdutyEnabled": true,
"minSeverity": "info",
"quietHoursStart": null,
"quietHoursEnd": null,
"quietHoursTimezone": "UTC"
}
}
}Update Preferences
PUT /api/v1/explainer/notification-preferencesRequest Body:
| Field | Type | Description |
|---|---|---|
emailEnabled | boolean | Enable email notifications |
slackEnabled | boolean | Enable Slack notifications |
inAppEnabled | boolean | Enable in-app notifications |
webhookEnabled | boolean | Enable webhook notifications |
pagerdutyEnabled | boolean | Enable PagerDuty notifications |
minSeverity | string | Minimum severity: info, warning, error, critical |
quietHoursStart | string|null | Start of quiet hours in HH:MM format |
quietHoursEnd | string|null | End of quiet hours in HH:MM format |
quietHoursTimezone | string | Timezone for quiet hours (e.g., America/New_York) |
All fields are optional. Only provided fields are updated.
Request example:
{
"minSeverity": "warning",
"quietHoursStart": "22:00",
"quietHoursEnd": "08:00",
"quietHoursTimezone": "America/New_York"
}Error Codes
| Status | Description |
|---|---|
| 400 | Missing required fields or invalid input |
| 401 | Missing or invalid authentication |
| 404 | Webhook, channel, or rule not found |
| 429 | Rate limited (test endpoints) |
| 500 | Internal server error |
Next Steps
- Authentication — API key management
- Traces — Trace ingestion and querying
- Issues & Fixes — Issue management and fixes
- Guides: Webhooks — Step-by-step webhook setup guide