Webhooks API
Webhooks deliver real-time notifications when events occur in your project. Configure webhook endpoints to receive HTTP POST requests for customer changes, usage alerts, and request completions.
Quick Reference
GET /api/v1/projects/:id/webhooks List webhooks
POST /api/v1/projects/:id/webhooks Create webhook
GET /api/v1/projects/:id/webhooks/:webhook_id Get webhook
PUT /api/v1/projects/:id/webhooks/:webhook_id Update webhook
DELETE /api/v1/projects/:id/webhooks/:webhook_id Delete webhook
GET /api/v1/projects/:id/webhooks/:webhook_id/events List delivery events
Authentication
Webhook management endpoints require a session bearer token (dashboard authentication). These endpoints are project-scoped and accessed via the dashboard.
List Webhooks
GET /api/v1/projects/:id/webhooks
Returns all webhook configurations for the project.
Response
{
"webhooks": [
{
"id": "550e8400-e29b-41d4-a716-446655440000",
"project_id": "550e8400-e29b-41d4-a716-446655440001",
"endpoint_url": "https://api.example.com/webhooks/modelrelay",
"signing_secret": "whsec_abc123...",
"enabled": true,
"events": ["customer.created", "customer.updated", "usage.threshold_exceeded"],
"created_at": "2024-01-15T10:30:00Z",
"updated_at": "2024-01-15T10:30:00Z"
}
]
}
Create Webhook
POST /api/v1/projects/:id/webhooks
Creates a new webhook endpoint. A signing secret is generated automatically.
Request Body
| Field | Type | Required | Description |
|---|---|---|---|
endpoint_url |
string | Yes | HTTPS URL to receive webhook events |
events |
array | Yes | Event types to subscribe to |
enabled |
boolean | No | Whether the webhook is active (default: true) |
Example
curl -X POST https://api.modelrelay.ai/api/v1/projects/550e8400-e29b-41d4-a716-446655440001/webhooks \
-H "Authorization: Bearer <session_token>" \
-H "Content-Type: application/json" \
-d '{
"endpoint_url": "https://api.example.com/webhooks/modelrelay",
"events": ["customer.created", "customer.updated", "usage.threshold_exceeded"],
"enabled": true
}'
Response
{
"webhook": {
"id": "550e8400-e29b-41d4-a716-446655440000",
"project_id": "550e8400-e29b-41d4-a716-446655440001",
"endpoint_url": "https://api.example.com/webhooks/modelrelay",
"signing_secret": "whsec_abc123def456...",
"enabled": true,
"events": ["customer.created", "customer.updated", "usage.threshold_exceeded"],
"created_at": "2024-01-15T10:30:00Z",
"updated_at": "2024-01-15T10:30:00Z"
}
}
Get Webhook
GET /api/v1/projects/:id/webhooks/:webhook_id
Returns a single webhook configuration.
Example
curl https://api.modelrelay.ai/api/v1/projects/550e8400-e29b-41d4-a716-446655440001/webhooks/550e8400-e29b-41d4-a716-446655440000 \
-H "Authorization: Bearer <session_token>"
Update Webhook
PUT /api/v1/projects/:id/webhooks/:webhook_id
Updates a webhook configuration. Use rotate_secret to generate a new signing secret.
Request Body
| Field | Type | Required | Description |
|---|---|---|---|
endpoint_url |
string | Yes | HTTPS URL to receive webhook events |
events |
array | Yes | Event types to subscribe to |
enabled |
boolean | No | Whether the webhook is active |
rotate_secret |
boolean | No | Generate a new signing secret |
Example
curl -X PUT https://api.modelrelay.ai/api/v1/projects/550e8400-e29b-41d4-a716-446655440001/webhooks/550e8400-e29b-41d4-a716-446655440000 \
-H "Authorization: Bearer <session_token>" \
-H "Content-Type: application/json" \
-d '{
"endpoint_url": "https://api.example.com/webhooks/modelrelay",
"events": ["customer.created", "request.completed"],
"enabled": true,
"rotate_secret": true
}'
Delete Webhook
DELETE /api/v1/projects/:id/webhooks/:webhook_id
Removes a webhook configuration. Pending deliveries are canceled.
Example
curl -X DELETE https://api.modelrelay.ai/api/v1/projects/550e8400-e29b-41d4-a716-446655440001/webhooks/550e8400-e29b-41d4-a716-446655440000 \
-H "Authorization: Bearer <session_token>"
List Delivery Events
GET /api/v1/projects/:id/webhooks/:webhook_id/events
Returns recent delivery attempts for a webhook. Use this to monitor delivery health and debug failures.
Query Parameters
| Parameter | Type | Default | Description |
|---|---|---|---|
limit |
integer | 50 | Maximum events to return |
Response
{
"events": [
{
"id": "550e8400-e29b-41d4-a716-446655440010",
"webhook_config_id": "550e8400-e29b-41d4-a716-446655440000",
"event_type": "customer.created",
"event_id": "evt_abc123",
"payload": {...},
"status": "delivered",
"attempt_count": 1,
"response_status": 200,
"latency_ms": 142,
"created_at": "2024-01-15T10:30:00Z",
"updated_at": "2024-01-15T10:30:05Z"
}
]
}
Webhook Object
| Field | Type | Description |
|---|---|---|
id |
uuid | Unique webhook identifier |
project_id |
uuid | Project this webhook belongs to |
endpoint_url |
string | HTTPS URL receiving events |
signing_secret |
string | Secret for verifying signatures |
enabled |
boolean | Whether the webhook is active |
events |
array | Subscribed event types |
created_at |
datetime | When the webhook was created |
updated_at |
datetime | When the webhook was last modified |
Event Types
Customer Events
| Event | Description |
|---|---|
customer.created |
A new customer was created |
customer.updated |
A customer’s details changed (tier, email, metadata) |
customer.deleted |
A customer was deleted |
Usage Events
| Event | Description |
|---|---|
usage.threshold_exceeded |
Customer usage crossed a threshold (50%, 80%, 100%) |
Request Events
| Event | Description |
|---|---|
request.completed |
An LLM request completed (success or failure) |
Billing Events
| Event | Description |
|---|---|
billing.subscription_changed |
Customer subscription status changed |
Event Payloads
Webhook Envelope
All webhook deliveries use this envelope structure:
{
"event_type": "customer.created",
"event_id": "evt_abc123def456",
"timestamp": "2024-01-15T10:30:00Z",
"project_id": "550e8400-e29b-41d4-a716-446655440001",
"data": {...}
}
customer.created / customer.updated
{
"event_type": "customer.created",
"event_id": "evt_abc123",
"timestamp": "2024-01-15T10:30:00Z",
"project_id": "550e8400-e29b-41d4-a716-446655440001",
"data": {
"customer": {
"id": "550e8400-e29b-41d4-a716-446655440002",
"email": "alice@example.com",
"tier_id": "550e8400-e29b-41d4-a716-446655440003",
"tier_code": "pro",
"external_id": "user_123",
"metadata": {"plan": "annual"},
"created_at": "2024-01-15T10:30:00Z",
"updated_at": "2024-01-15T10:30:00Z"
}
}
}
customer.deleted
{
"event_type": "customer.deleted",
"event_id": "evt_abc123",
"timestamp": "2024-01-15T10:30:00Z",
"project_id": "550e8400-e29b-41d4-a716-446655440001",
"data": {
"customer": {
"id": "550e8400-e29b-41d4-a716-446655440002",
"email": "alice@example.com",
"tier_id": "550e8400-e29b-41d4-a716-446655440003",
"tier_code": "pro",
"external_id": "user_123"
}
}
}
usage.threshold_exceeded
{
"event_type": "usage.threshold_exceeded",
"event_id": "evt_abc123",
"timestamp": "2024-01-15T10:30:00Z",
"project_id": "550e8400-e29b-41d4-a716-446655440001",
"data": {
"customer_id": "550e8400-e29b-41d4-a716-446655440002",
"tier_id": "550e8400-e29b-41d4-a716-446655440003",
"tier_code": "pro",
"plan_type": "spend",
"unit": "cents",
"threshold_percent": 80,
"percentage_used": 82.5,
"used": 825,
"limit": 1000,
"remaining": 175,
"window_start": "2024-01-01T00:00:00Z",
"window_end": "2024-02-01T00:00:00Z"
}
}
request.completed
{
"event_type": "request.completed",
"event_id": "evt_abc123",
"timestamp": "2024-01-15T10:30:00Z",
"project_id": "550e8400-e29b-41d4-a716-446655440001",
"data": {
"request_id": "req_xyz789",
"response_id": "msg_01abc",
"model": "claude-sonnet-4-20250514",
"provider": "anthropic",
"status": 200,
"success": true,
"streaming": false,
"input_tokens": 150,
"output_tokens": 300,
"total_tokens": 450,
"duration_ms": 1250,
"customer_id": "550e8400-e29b-41d4-a716-446655440002",
"tier_id": "550e8400-e29b-41d4-a716-446655440003",
"tier_code": "pro"
}
}
billing.subscription_changed
{
"event_type": "billing.subscription_changed",
"event_id": "evt_abc123",
"timestamp": "2024-01-15T10:30:00Z",
"project_id": "550e8400-e29b-41d4-a716-446655440001",
"data": {
"customer_id": "550e8400-e29b-41d4-a716-446655440002",
"tier_id": "550e8400-e29b-41d4-a716-446655440003",
"tier_code": "pro",
"billing_provider": "stripe",
"billing_customer_id": "cus_abc123",
"billing_subscription_id": "sub_xyz789",
"subscription_status": "active",
"current_period_start": "2024-01-15T00:00:00Z",
"current_period_end": "2024-02-15T00:00:00Z"
}
}
Signature Verification
All webhook deliveries include a signature header for verification:
X-ModelRelay-Signature: t=1705312200,v1=abc123...
Verifying Signatures
- Extract the timestamp (
t) and signature (v1) from the header - Construct the signed payload:
{timestamp}.{request_body} - Compute HMAC-SHA256 using your signing secret
- Compare with the provided signature
Example (Node.js)
import crypto from 'crypto';
function verifyWebhookSignature(payload, header, secret) {
const [timestampPart, signaturePart] = header.split(',');
const timestamp = timestampPart.replace('t=', '');
const signature = signaturePart.replace('v1=', '');
const signedPayload = `${timestamp}.${payload}`;
const expectedSignature = crypto
.createHmac('sha256', secret)
.update(signedPayload)
.digest('hex');
return crypto.timingSafeEqual(
Buffer.from(signature),
Buffer.from(expectedSignature)
);
}
Example (Go)
func VerifyWebhookSignature(payload, header, secret string) bool {
parts := strings.Split(header, ",")
timestamp := strings.TrimPrefix(parts[0], "t=")
signature := strings.TrimPrefix(parts[1], "v1=")
signedPayload := fmt.Sprintf("%s.%s", timestamp, payload)
mac := hmac.New(sha256.New, []byte(secret))
mac.Write([]byte(signedPayload))
expected := hex.EncodeToString(mac.Sum(nil))
return hmac.Equal([]byte(signature), []byte(expected))
}
Delivery Status
| Status | Description |
|---|---|
pending |
Event queued for delivery |
processing |
Delivery in progress |
delivered |
Successfully delivered (2xx response) |
failed |
Delivery failed after retries |
Retry Policy
Failed deliveries are retried with exponential backoff:
| Attempt | Delay |
|---|---|
| 1 | Immediate |
| 2 | 1 minute |
| 3 | 5 minutes |
| 4 | 30 minutes |
| 5 | 2 hours |
After 5 failed attempts, the delivery is marked as failed.
Requirements
- HTTPS only: Webhook endpoints must use HTTPS
- 2xx response: Return a 2xx status code within 30 seconds
- Idempotency: Handle duplicate deliveries gracefully (use
event_id)
Error Codes
| Status | Description |
|---|---|
| 400 | Invalid request (missing fields, invalid URL) |
| 401 | Unauthorized |
| 404 | Webhook not found |
Next Steps
- Customers API - Customer lifecycle events
- Tiers API - Usage limits that trigger threshold events