Webhooks
Webhooks are currently in BETA. The feature is fully functional, but the payload structure and supported event types may evolve based on feedback. We recommend implementing flexible parsing to accommodate potential changes.
Webhooks allow your applications to receive real-time HTTP notifications when specific events occur in your ToothFairyAI workspace. Instead of polling the API for changes, webhooks push data to your endpoint as events happen.
Configuration
Setting Up Your Webhook URL
- Navigate to Settings > Admin > Webhooks
- Enter your webhook endpoint URL (must be HTTPS for production use)
- Click Save Webhook
Your endpoint must:
- Accept HTTP POST requests
- Return a 2xx status code to acknowledge receipt
- Respond within 10 seconds
Menu Location
Settings > Admin > Webhooks
Payload Structure
All webhook payloads follow a consistent structure to ensure predictable parsing and future extensibility.
Base Payload Format
{
"event": {
"type": "document.created",
"category": "documents",
"version": "1.0"
},
"workspace_id": "abc123-def456-ghi789",
"timestamp": "2025-02-11T10:30:00.000Z",
"data": {
"document_id": 12345,
"external_path": "s3://bucket/path/to/file.pdf",
"topics": ["sales", "contracts"],
"status": "published"
},
"metadata": {
"triggered_by": "user-uuid-here",
"source": "api",
"event_id": "documents_1707647400000_abc123xyz"
}
}
Payload Fields
| Field | Type | Description |
|---|---|---|
event.type | string | The specific event that occurred (e.g., document.created) |
event.category | string | The category of the event (e.g., documents) |
event.version | string | Payload schema version (currently 1.0) |
workspace_id | string | Your workspace identifier |
timestamp | string | ISO 8601 timestamp of when the event occurred |
data | object | Event-specific data (see below) |
metadata.triggered_by | string | null | User ID who triggered the event, if applicable |
metadata.source | string | Origin of the event: api or admin |
metadata.event_id | string | Unique identifier for this event |
Document Event Data
For document events, the data object contains:
| Field | Type | Description |
|---|---|---|
document_id | number | Unique identifier of the document |
external_path | string | null | S3 path to the document file, if applicable |
topics | array | List of topics assigned to the document |
status | string | Document status (e.g., draft, published, archived) |
Supported Events
Document Events
| Event Type | Description |
|---|---|
document.created | A new document has been created in the workspace |
document.updated | An existing document has been modified |
document.deleted | A document has been deleted from the workspace |
Agent Events
Agent events are triggered when an AI agent completes or fails to process a chat message.
| Event Type | Description |
|---|---|
agent_completed | An agent has successfully completed processing a chat message |
agent_failed | An agent encountered an error while processing a chat message |
Agent Event Data
| Field | Type | Description |
|---|---|---|
status | string | Execution status: success or error |
agent_id | string | Unique identifier of the agent |
is_from_planner | boolean | Whether this execution was triggered by a planner |
step_id | number | null | Planner step ID if from planner, otherwise null |
error | string | null | Error message if status is error |
error_type | string | null | Type of error if status is error |
error_code | number | null | Error code if status is error |
is_voice | boolean | null | Whether this was a voice chat execution |
Planner Events
Planner events are triggered when a planner agent completes, fails, or requires user interaction during plan execution.
| Event Type | Description |
|---|---|
planner_completed | A planner has successfully completed all plan steps |
planner_failed | A planner encountered an error during execution |
planner_pending_approval | A generated plan is awaiting user approval |
planner_stopped | Plan execution was stopped by the user |
Planner Event Data
| Field | Type | Description |
|---|---|---|
status | string | Execution status: success, error, pending_approval, or stopped |
planner_agent_id | string | Unique identifier of the planner agent |
plan_status | string | Detailed plan status: completed, error, awaiting_approval, or stopped_by_user |
execution_mode | string | null | Execution mode: sequential or parallel (for completed plans) |
error | string | null | Error message if status is error |
error_type | string | null | Type of error if status is error |
Example Payloads
document.created
{
"event": {
"type": "document.created",
"category": "documents",
"version": "1.0"
},
"workspace_id": "abc123-def456-ghi789",
"timestamp": "2025-02-11T10:30:00.000Z",
"data": {
"document_id": 12345,
"external_path": "s3://tf-documents/workspace-abc/document.pdf",
"topics": ["hr", "policies"],
"status": "published"
},
"metadata": {
"triggered_by": "user-123",
"source": "api",
"event_id": "documents_1707647400000_x7k9m2"
}
}
document.updated
{
"event": {
"type": "document.updated",
"category": "documents",
"version": "1.0"
},
"workspace_id": "abc123-def456-ghi789",
"timestamp": "2025-02-11T11:45:00.000Z",
"data": {
"document_id": 12345,
"external_path": "s3://tf-documents/workspace-abc/document.pdf",
"topics": ["hr", "policies", "compliance"],
"status": "published"
},
"metadata": {
"triggered_by": "user-456",
"source": "admin",
"event_id": "documents_1707651900000_p3q8r1"
}
}
document.deleted
{
"event": {
"type": "document.deleted",
"category": "documents",
"version": "1.0"
},
"workspace_id": "abc123-def456-ghi789",
"timestamp": "2025-02-11T14:20:00.000Z",
"data": {
"document_id": 12345,
"external_path": "s3://tf-documents/workspace-abc/document.pdf",
"topics": ["hr", "policies", "compliance"],
"status": "archived"
},
"metadata": {
"triggered_by": "user-789",
"source": "api",
"event_id": "documents_1707661200000_m5n2k8"
}
}
agent.completed
{
"event": {
"type": "agent_completed",
"category": "agent",
"version": "1.0"
},
"workspace_id": "abc123-def456-ghi789",
"chat_id": "chat-uuid-123",
"timestamp": "2025-02-15T10:30:00.000Z",
"data": {
"status": "success",
"agent_id": "agent-uuid-456",
"is_from_planner": false,
"step_id": null
},
"metadata": {
"triggered_by": "user-123",
"source": "agent",
"event_id": "agent_1707997800000_a1b2c3d4"
}
}
agent.failed
{
"event": {
"type": "agent_failed",
"category": "agent",
"version": "1.0"
},
"workspace_id": "abc123-def456-ghi789",
"chat_id": "chat-uuid-123",
"timestamp": "2025-02-15T10:31:15.000Z",
"data": {
"status": "error",
"error": "Rate limit exceeded",
"error_type": "RateLimitExceededError",
"agent_id": "agent-uuid-456",
"is_from_planner": false,
"error_code": 464
},
"metadata": {
"triggered_by": "user-123",
"source": "agent",
"event_id": "agent_1707997875000_e5f6g7h8"
}
}
planner.completed
{
"event": {
"type": "planner_completed",
"category": "planner",
"version": "1.0"
},
"workspace_id": "abc123-def456-ghi789",
"chat_id": "chat-uuid-789",
"timestamp": "2025-02-15T11:45:00.000Z",
"data": {
"status": "success",
"planner_agent_id": "planner-uuid-123",
"plan_status": "completed",
"execution_mode": "sequential"
},
"metadata": {
"triggered_by": "user-456",
"source": "planner",
"event_id": "planner_1708002300000_i9j0k1l2"
}
}
planner.failed
{
"event": {
"type": "planner_failed",
"category": "planner",
"version": "1.0"
},
"workspace_id": "abc123-def456-ghi789",
"chat_id": "chat-uuid-789",
"timestamp": "2025-02-15T11:47:30.000Z",
"data": {
"status": "error",
"error": "Max steps exceeded (20)",
"error_type": "MaxStepsExceeded",
"planner_agent_id": "planner-uuid-123"
},
"metadata": {
"triggered_by": "user-456",
"source": "planner",
"event_id": "planner_1708002450000_m3n4o5p6"
}
}
planner.pending_approval
{
"event": {
"type": "planner_pending_approval",
"category": "planner",
"version": "1.0"
},
"workspace_id": "abc123-def456-ghi789",
"chat_id": "chat-uuid-789",
"timestamp": "2025-02-15T11:30:00.000Z",
"data": {
"status": "pending_approval",
"planner_agent_id": "planner-uuid-123",
"plan_status": "awaiting_approval"
},
"metadata": {
"triggered_by": "user-456",
"source": "planner",
"event_id": "planner_1708001400000_q7r8s9t0"
}
}
Implementing Your Webhook Endpoint
Basic Example (Node.js/Express)
const express = require('express');
const app = express();
app.use(express.json());
app.post('/webhook/toothfairy', (req, res) => {
const { event, workspace_id, timestamp, data, metadata } = req.body;
console.log(`Received ${event.type} event for workspace ${workspace_id}`);
switch (event.type) {
case 'document.created':
console.log(`New document created: ${data.document_id}`);
// Handle document creation
break;
case 'document.updated':
console.log(`Document updated: ${data.document_id}`);
// Handle document update
break;
case 'document.deleted':
console.log(`Document deleted: ${data.document_id}`);
// Handle document deletion
break;
case 'agent_completed':
console.log(`Agent ${data.agent_id} completed successfully`);
// Handle successful agent execution
break;
case 'agent_failed':
console.log(`Agent ${data.agent_id} failed: ${data.error}`);
// Handle agent failure (e.g., notify user, retry)
break;
case 'planner_completed':
console.log(`Planner ${data.planner_agent_id} completed plan`);
// Handle plan completion
break;
case 'planner_failed':
console.log(`Planner ${data.planner_agent_id} failed: ${data.error}`);
// Handle planner failure
break;
case 'planner_pending_approval':
console.log(`Planner ${data.planner_agent_id} awaiting approval`);
// Notify user that plan needs approval
break;
default:
console.log(`Unknown event type: ${event.type}`);
}
// Always return 200 to acknowledge receipt
res.status(200).json({ received: true });
});
app.listen(3000);
Basic Example (Python/Flask)
from flask import Flask, request, jsonify
app = Flask(__name__)
@app.route('/webhook/toothfairy', methods=['POST'])
def handle_webhook():
payload = request.json
event_type = payload['event']['type']
workspace_id = payload['workspace_id']
chat_id = payload.get('chat_id')
data = payload['data']
print(f"Received {event_type} event for workspace {workspace_id}")
if event_type == 'document.created':
print(f"New document created: {data['document_id']}")
# Handle document creation
elif event_type == 'document.updated':
print(f"Document updated: {data['document_id']}")
# Handle document update
elif event_type == 'document.deleted':
print(f"Document deleted: {data['document_id']}")
# Handle document deletion
elif event_type == 'agent_completed':
print(f"Agent {data['agent_id']} completed in chat {chat_id}")
# Handle successful agent execution
elif event_type == 'agent_failed':
print(f"Agent {data['agent_id']} failed: {data.get('error')}")
# Handle agent failure (e.g., notify user, retry)
elif event_type == 'planner_completed':
print(f"Planner {data['planner_agent_id']} completed")
# Handle plan completion
elif event_type == 'planner_failed':
print(f"Planner {data['planner_agent_id']} failed: {data.get('error')}")
# Handle planner failure
elif event_type == 'planner_pending_approval':
print(f"Planner {data['planner_agent_id']} awaiting approval")
# Notify user that plan needs approval
# Always return 200 to acknowledge receipt
return jsonify({'received': True}), 200
if __name__ == '__main__':
app.run(port=3000)
Best Practices
Respond Quickly
Your endpoint should return a 2xx response as quickly as possible. Process webhook data asynchronously if needed:
app.post('/webhook/toothfairy', async (req, res) => {
// Acknowledge immediately
res.status(200).json({ received: true });
// Process asynchronously
processWebhookAsync(req.body).catch(console.error);
});
Handle Duplicates
In rare cases, the same event may be delivered more than once. Use the metadata.event_id field to deduplicate:
const processedEvents = new Set();
app.post('/webhook/toothfairy', (req, res) => {
const eventId = req.body.metadata.event_id;
if (processedEvents.has(eventId)) {
return res.status(200).json({ received: true, duplicate: true });
}
processedEvents.add(eventId);
// Process the event...
res.status(200).json({ received: true });
});
Validate the Payload
Always validate incoming payloads before processing:
app.post('/webhook/toothfairy', (req, res) => {
const { event, workspace_id, data } = req.body;
if (!event?.type || !workspace_id || !data) {
return res.status(400).json({ error: 'Invalid payload' });
}
// Process valid payload...
res.status(200).json({ received: true });
});
Retry Behaviour
If your endpoint returns a non-2xx status code or times out, ToothFairyAI will retry the webhook delivery:
- Retry attempts: Up to 3 times
- Timeout: 10 seconds per attempt
- Failed webhooks: Moved to a dead-letter queue for investigation
Monitor your endpoint's availability and response times to ensure reliable webhook delivery.
Troubleshooting
Webhooks Not Arriving
- Verify your webhook URL is correctly saved in Settings
- Ensure your endpoint is publicly accessible
- Check that your endpoint returns a 2xx status code
- Verify your endpoint responds within 10 seconds
Invalid Payload Errors
- Ensure your endpoint accepts
Content-Type: application/json - Parse the request body as JSON
- Handle all expected event types gracefully
Future Event Types
Additional event types will be added in future releases, including:
- Chat events (
chat.created,chat.updated) - Agent lifecycle events (
agent.created,agent.updated) - Workspace events (
workspace.updated)
Subscribe to our changelog to stay informed about new webhook events.