Skip to main content

Webhooks

Beta Feature

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

  1. Navigate to Settings > Admin > Webhooks
  2. Enter your webhook endpoint URL (must be HTTPS for production use)
  3. Click Save Webhook

Your endpoint must:

  • Accept HTTP POST requests
  • Return a 2xx status code to acknowledge receipt
  • Respond within 10 seconds

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

FieldTypeDescription
event.typestringThe specific event that occurred (e.g., document.created)
event.categorystringThe category of the event (e.g., documents)
event.versionstringPayload schema version (currently 1.0)
workspace_idstringYour workspace identifier
timestampstringISO 8601 timestamp of when the event occurred
dataobjectEvent-specific data (see below)
metadata.triggered_bystring | nullUser ID who triggered the event, if applicable
metadata.sourcestringOrigin of the event: api or admin
metadata.event_idstringUnique identifier for this event

Document Event Data

For document events, the data object contains:

FieldTypeDescription
document_idnumberUnique identifier of the document
external_pathstring | nullS3 path to the document file, if applicable
topicsarrayList of topics assigned to the document
statusstringDocument status (e.g., draft, published, archived)

Supported Events

Document Events

Event TypeDescription
document.createdA new document has been created in the workspace
document.updatedAn existing document has been modified
document.deletedA 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 TypeDescription
agent_completedAn agent has successfully completed processing a chat message
agent_failedAn agent encountered an error while processing a chat message

Agent Event Data

FieldTypeDescription
statusstringExecution status: success or error
agent_idstringUnique identifier of the agent
is_from_plannerbooleanWhether this execution was triggered by a planner
step_idnumber | nullPlanner step ID if from planner, otherwise null
errorstring | nullError message if status is error
error_typestring | nullType of error if status is error
error_codenumber | nullError code if status is error
is_voiceboolean | nullWhether 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 TypeDescription
planner_completedA planner has successfully completed all plan steps
planner_failedA planner encountered an error during execution
planner_pending_approvalA generated plan is awaiting user approval
planner_stoppedPlan execution was stopped by the user

Planner Event Data

FieldTypeDescription
statusstringExecution status: success, error, pending_approval, or stopped
planner_agent_idstringUnique identifier of the planner agent
plan_statusstringDetailed plan status: completed, error, awaiting_approval, or stopped_by_user
execution_modestring | nullExecution mode: sequential or parallel (for completed plans)
errorstring | nullError message if status is error
error_typestring | nullType 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
tip

Monitor your endpoint's availability and response times to ensure reliable webhook delivery.

Troubleshooting

Webhooks Not Arriving

  1. Verify your webhook URL is correctly saved in Settings
  2. Ensure your endpoint is publicly accessible
  3. Check that your endpoint returns a 2xx status code
  4. Verify your endpoint responds within 10 seconds

Invalid Payload Errors

  1. Ensure your endpoint accepts Content-Type: application/json
  2. Parse the request body as JSON
  3. Handle all expected event types gracefully

Future Event Types

Coming Soon

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.