Webhooks API

Subscribe to events and receive real-time notifications

Webhooks API

Subscribe to Ferni events and receive real-time HTTP notifications. Webhooks are signed with HMAC-SHA256 for security.

Base URL:https://api.ferni.ai/api/v2/developers/webhooks


Create Webhook

Create a new webhook subscription.

POST /webhooks

Request Body

{
  "name": "Session Events",
  "url": "https://api.yourcompany.com/ferni-webhooks",
  "events": [
    "session.started",
    "session.ended",
    "tool.called",
    "workflow.completed"
  ],
  "enabled": true
}

Parameters

FieldTypeRequiredDescription
namestringYesHuman-readable name for this webhook
urlstringYesHTTPS endpoint to receive events
eventsstring[]YesEvent types to subscribe to
enabledbooleanNoActive state (default: true)
personaIdstringNoFilter events for specific persona

Response

{
  "success": true,
  "data": {
    "id": "wh_abc123xyz",
    "name": "Session Events",
    "url": "https://api.yourcompany.com/ferni-webhooks",
    "events": ["session.started", "session.ended", "tool.called", "workflow.completed"],
    "enabled": true,
    "secret": "whsec_your_signing_secret",
    "status": "active",
    "createdAt": "2026-01-11T10:00:00Z",
    "updatedAt": "2026-01-11T10:00:00Z"
  }
}

Important: The secret is only returned on creation. Store it securely for signature verification.


List Webhooks

Retrieve all webhook subscriptions.

GET /webhooks

Query Parameters

FieldTypeDescription
statusstringFilter by status: active, failing, disabled
limitnumberMax results (default: 50)
offsetnumberPagination offset

Response

{
  "success": true,
  "data": [
    {
      "id": "wh_abc123xyz",
      "name": "Session Events",
      "url": "https://api.yourcompany.com/ferni-webhooks",
      "events": ["session.started", "session.ended"],
      "enabled": true,
      "status": "active",
      "lastDeliveredAt": "2026-01-11T10:15:00Z",
      "createdAt": "2026-01-11T10:00:00Z"
    }
  ],
  "pagination": {
    "total": 1,
    "limit": 50,
    "offset": 0
  }
}

Get Webhook

Retrieve a specific webhook by ID.

GET /webhooks/:id

Response

{
  "success": true,
  "data": {
    "id": "wh_abc123xyz",
    "name": "Session Events",
    "url": "https://api.yourcompany.com/ferni-webhooks",
    "events": ["session.started", "session.ended"],
    "enabled": true,
    "status": "active",
    "deliveryStats": {
      "total": 156,
      "successful": 154,
      "failed": 2,
      "averageLatencyMs": 89
    },
    "lastDeliveredAt": "2026-01-11T10:15:00Z",
    "createdAt": "2026-01-11T10:00:00Z",
    "updatedAt": "2026-01-11T10:00:00Z"
  }
}

Update Webhook

Update an existing webhook. Only include fields you want to change.

PUT /webhooks/:id

Request Body

{
  "events": ["session.started", "session.ended", "tool.called"],
  "enabled": true
}

Response

{
  "success": true,
  "data": {
    "id": "wh_abc123xyz",
    "events": ["session.started", "session.ended", "tool.called"],
    "enabled": true,
    "updatedAt": "2026-01-11T11:00:00Z"
  }
}

Delete Webhook

Permanently remove a webhook subscription.

DELETE /webhooks/:id

Response

{
  "success": true,
  "data": {
    "deleted": true,
    "id": "wh_abc123xyz"
  }
}

Send Test Event

Send a test event to verify your endpoint.

POST /webhooks/:id/test

Response

{
  "success": true,
  "data": {
    "delivered": true,
    "statusCode": 200,
    "latencyMs": 145,
    "testedAt": "2026-01-11T10:20:00Z"
  }
}

View Delivery Logs

Get recent delivery attempts for a webhook.

GET /webhooks/:id/logs

Query Parameters

FieldTypeDescription
statusstringFilter: delivered, failed, pending
limitnumberMax results (default: 50, max: 200)

Response

{
  "success": true,
  "data": [
    {
      "id": "del_123",
      "eventId": "evt_abc123",
      "eventType": "session.ended",
      "status": "delivered",
      "attempts": 1,
      "statusCode": 200,
      "latencyMs": 145,
      "deliveredAt": "2026-01-11T10:15:01Z"
    },
    {
      "id": "del_124",
      "eventId": "evt_abc124",
      "eventType": "tool.called",
      "status": "failed",
      "attempts": 5,
      "statusCode": 500,
      "error": "Internal Server Error",
      "lastAttemptAt": "2026-01-11T12:51:01Z"
    }
  ]
}

Event Types

Session Events

EventDescription
session.startedUser started a conversation
session.endedUser ended a conversation

Payload:

{
  "id": "evt_abc123",
  "type": "session.ended",
  "timestamp": "2026-01-11T10:15:00Z",
  "publisherId": "pub_xyz",
  "data": {
    "sessionId": "sess_123",
    "userId": "usr_456",
    "personaId": "ferni",
    "duration": 900,
    "turnCount": 12
  }
}

Tool Events

EventDescription
tool.calledA tool was invoked
tool.completedTool execution finished
tool.failedTool execution failed

Payload:

{
  "id": "evt_abc125",
  "type": "tool.called",
  "timestamp": "2026-01-11T10:05:00Z",
  "publisherId": "pub_xyz",
  "data": {
    "sessionId": "sess_123",
    "toolId": "tool_789",
    "toolName": "lookup_customer",
    "arguments": { "query": "Acme Corp" }
  }
}

Workflow Events

EventDescription
workflow.startedWorkflow execution began
workflow.completedWorkflow finished successfully
workflow.failedWorkflow execution failed
workflow.step.completedIndividual step completed

Payload:

{
  "id": "evt_abc127",
  "type": "workflow.completed",
  "timestamp": "2026-01-11T10:10:00Z",
  "publisherId": "pub_xyz",
  "data": {
    "workflowId": "wf_123",
    "executionId": "exec_456",
    "status": "completed",
    "duration": 3500,
    "stepsExecuted": 5
  }
}

Activity Events

EventDescription
activity.createdCustom activity was logged

Signature Verification

Every webhook includes an X-Ferni-Signature header:

X-Ferni-Signature: t=1704985200,v1=5e8f34a2b1c9d8e7f6a5b4c3d2e1f0a9b8c7d6e5f4a3b2c1d0e9f8a7b6c5d4e3

Verification Algorithm

  1. Extract t (timestamp) and v1 (signature) from header
  2. Construct signed payload: {timestamp}.{raw_body}
  3. Compute HMAC-SHA256 using your webhook secret
  4. Compare using timing-safe comparison
  5. Reject if timestamp > 5 minutes old (replay protection)

Node.js Example

const crypto = require('crypto');

function verifySignature(payload, signature, secret) {
  const [tPart, v1Part] = signature.split(',');
  const timestamp = tPart.split('=')[1];
  const expectedSig = v1Part.split('=')[1];

  // Check timestamp tolerance (5 minutes)
  const now = Math.floor(Date.now() / 1000);
  if (Math.abs(now - parseInt(timestamp)) > 300) {
    throw new Error('Timestamp too old');
  }

  // Compute signature
  const signedPayload = `${timestamp}.${payload}`;
  const computedSig = crypto
    .createHmac('sha256', secret)
    .update(signedPayload)
    .digest('hex');

  // Timing-safe comparison
  return crypto.timingSafeEqual(
    Buffer.from(expectedSig),
    Buffer.from(computedSig)
  );
}

Retry Policy

Failed deliveries are retried with exponential backoff:

AttemptDelayTotal Time
10s0s
21m1m
35m6m
430m36m
52h2h 36m

A delivery fails if:

  • Connection timeout (30s)
  • HTTP status >= 400
  • No response received

After 5 failed attempts, the event is marked as failed and logged.


Status Values

StatusDescription
activeHealthy, recent deliveries successful
failingRecent deliveries have failed
disabledManually disabled by developer

Best Practices

  1. Respond quickly — Return 200 within 30 seconds
  2. Process async — Queue events and acknowledge immediately
  3. Implement idempotency — Use event.id to dedupe
  4. Verify signatures — Always validate HMAC
  5. Monitor delivery logs — Check for failures regularly

Related