Directus webhooks allow you to create HTTP endpoints that trigger workflows, enabling integration with external systems and automation of tasks via HTTP requests.
Overview
Webhooks in Directus are implemented through the Flows system using webhook triggers. This provides a powerful way to create custom API endpoints that execute automated workflows.
Webhooks in Directus are part of the Flows automation system. For more details on flows, see Flows & Automation .
Creating Webhook Flows
A webhook flow consists of a trigger and operations:
{
name : 'Process Incoming Data' ,
trigger : 'webhook' ,
status : 'active' ,
options : {
method : 'POST' ,
async : false ,
return : '$last'
},
operations : [
// Operations to execute
]
}
Webhook Trigger Options
HTTP Method
Specify which HTTP method triggers the flow:
{
trigger : 'webhook' ,
options : {
method : 'POST' // GET, POST, PUT, PATCH, DELETE
}
}
Async Execution
Control whether the webhook waits for completion:
{
options : {
async : false // Wait for operations to complete
}
}
async: false - HTTP request waits for flow completion, returns result
async: true - Returns immediately, flow executes in background
Return Value
Specify what data to return in the HTTP response:
{
options : {
return : '$last' // Return result of last operation
// Can also be: '$trigger', specific operation ID, or custom value
}
}
Webhook URL
Once created, webhook flows are accessible at:
POST https://your-project.directus.app/flows/trigger/{flow-id}
The flow ID is a UUID automatically assigned when the flow is created.
Accessing Webhook Data
Webhook payload is available in operations via $trigger:
{
operation : 'item-create' ,
options : {
collection : 'orders' ,
payload : {
customer_id : '{{$trigger.customer_id}}' ,
items : '{{$trigger.items}}' ,
total : '{{$trigger.total}}' ,
received_at : '{{$NOW}}'
}
}
}
Webhook Context
Additional context available:
{{$trigger.body}} - Request body (JSON)
{{$trigger.query}} - Query parameters
{{$trigger.headers}} - HTTP headers
{{$accountability.user}} - Authenticated user (if token provided)
{{$accountability.role}} - User’s role
{{$accountability.ip}} - Request IP address
Authentication
Webhooks can be authenticated using access tokens:
curl -X POST https://your-project.directus.app/flows/trigger/{flow-id} \
-H "Authorization: Bearer YOUR_ACCESS_TOKEN" \
-H "Content-Type: application/json" \
-d '{"data": "value"}'
Public Webhooks
To create a public webhook (no authentication required):
Create the flow with webhook trigger
Set permissions for the Public role
Allow access to the flow
Public webhooks should validate input carefully and implement rate limiting to prevent abuse.
Example Workflows
Simple Data Ingestion
Receive data from external service:
{
trigger : 'webhook' ,
options : {
method : 'POST' ,
async : false ,
return : '$last'
},
operations : [
{
operation: 'item-create' ,
options: {
collection: 'events' ,
payload: {
type: '{{$trigger.type}}' ,
data: '{{$trigger.data}}' ,
source_ip: '{{$accountability.ip}}' ,
timestamp: '{{$NOW}}'
},
permissions: '$full'
}
}
]
}
Call the webhook:
curl -X POST https://your-project.directus.app/flows/trigger/abc-123 \
-H "Content-Type: application/json" \
-d '{
"type": "purchase",
"data": {
"product_id": "prod_123",
"amount": 99.99
}
}'
Conditional Processing
Process different data types:
{
trigger : 'webhook' ,
options : { method : 'POST' , async : false },
operations : [
{
id: 'check-type' ,
operation: 'condition' ,
options: {
filter: {
type: { _eq: 'order' }
}
},
resolve: 'process-order' ,
reject: 'process-event'
},
{
id: 'process-order' ,
operation: 'item-create' ,
options: {
collection: 'orders' ,
payload: '{{$trigger}}'
}
},
{
id: 'process-event' ,
operation: 'item-create' ,
options: {
collection: 'events' ,
payload: '{{$trigger}}'
}
}
]
}
External API Integration
Receive webhook, process, and call external API:
{
trigger : 'webhook' ,
options : { method : 'POST' , async : true }, // Async for long operations
operations : [
{
id: 'save-webhook' ,
operation: 'item-create' ,
options: {
collection: 'webhook_logs' ,
payload: {
data: '{{$trigger}}' ,
received_at: '{{$NOW}}'
}
}
},
{
id: 'call-api' ,
operation: 'request' ,
options: {
method: 'POST' ,
url: 'https://api.example.com/process' ,
headers: {
'Authorization' : 'Bearer {{$env.API_TOKEN}}' ,
'Content-Type' : 'application/json'
},
body: JSON . stringify ({
webhook_id: '{{$last.id}}' ,
data: '{{$trigger.data}}'
})
}
},
{
id: 'update-status' ,
operation: 'item-update' ,
options: {
collection: 'webhook_logs' ,
key: '{{$last.id}}' ,
payload: {
status: 'processed' ,
result: '{{$last}}'
}
}
}
]
}
Validation and Error Handling
Validate input and handle errors:
{
trigger : 'webhook' ,
options : { method : 'POST' , async : false },
operations : [
{
id: 'validate' ,
operation: 'condition' ,
options: {
filter: {
_and: [
{ email: { _nempty: true } },
{ email: { _contains: '@' } },
{ name: { _nempty: true } }
]
}
},
resolve: 'process' ,
reject: 'error'
},
{
id: 'process' ,
operation: 'item-create' ,
options: {
collection: 'subscribers' ,
payload: '{{$trigger}}'
},
resolve: 'success'
},
{
id: 'success' ,
operation: 'transform' ,
options: {
json: {
success: true ,
message: 'Subscriber created' ,
id: '{{$last.id}}'
}
}
},
{
id: 'error' ,
operation: 'transform' ,
options: {
json: {
success: false ,
message: 'Invalid input: email and name are required'
}
}
}
]
}
GET Webhooks with Caching
For GET webhooks, enable caching:
{
trigger : 'webhook' ,
options : {
method : 'GET' ,
async : false ,
cacheEnabled : true // Cache GET responses
},
operations : [
{
operation: 'item-read' ,
options: {
collection: 'products' ,
query: {
filter: { status: { _eq: 'active' } },
fields: [ 'id' , 'name' , 'price' ],
limit: 100
}
}
}
]
}
Successful Response
{
"data" : {
// Result from specified return operation
}
}
Error Response
{
"errors" : [
{
"message" : "Error description" ,
"extensions" : {
"code" : "ERROR_CODE"
}
}
]
}
Query Parameters
Access query parameters in operations:
GET /flows/trigger/{flow-id}?status=active&limit=10
{
operation : 'item-read' ,
options : {
collection : 'articles' ,
query : {
filter : { status : { _eq : '{{$trigger.query.status}}' } },
limit : '{{$trigger.query.limit}}'
}
}
}
Access request headers:
{
operation : 'condition' ,
options : {
filter : {
'x-api-key' : { _eq: '{{$env.WEBHOOK_SECRET}}' }
}
},
resolve : 'process' ,
reject : 'unauthorized'
}
Rate Limiting
Implement rate limiting for webhooks:
{
operations : [
{
operation: 'item-read' ,
options: {
collection: 'webhook_calls' ,
query: {
filter: {
ip: { _eq: '{{$accountability.ip}}' },
timestamp: { _gte: '$NOW(-1 hour)' }
},
aggregate: { count: '*' }
}
}
},
{
operation: 'condition' ,
options: {
filter: { count: { _lte: 100 } } // Max 100 per hour
},
resolve: 'process' ,
reject: 'rate-limited'
}
]
}
Webhook Logs
Log all webhook calls for debugging:
{
operations : [
{
operation: 'item-create' ,
options: {
collection: 'webhook_logs' ,
payload: {
flow_id: '{{$flow.id}}' ,
body: '{{$trigger}}' ,
headers: '{{$trigger.headers}}' ,
ip: '{{$accountability.ip}}' ,
timestamp: '{{$NOW}}'
}
}
},
// ... rest of operations
]
}
Best Practices
Always validate webhook input using condition operations before processing data.
Use Async for Long Operations
Set async: true for webhooks that trigger long-running operations to avoid timeouts.
Use Bearer tokens or API keys to secure webhooks. Don’t rely on obscurity alone.
Keep logs of webhook calls for debugging and audit purposes.
Use condition operations to detect errors and return appropriate error messages.
Rate Limit Public Webhooks
Implement rate limiting to prevent abuse of public webhook endpoints.