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

  1. Extract the timestamp (t) and signature (v1) from the header
  2. Construct the signed payload: {timestamp}.{request_body}
  3. Compute HMAC-SHA256 using your signing secret
  4. 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