How to Receive Webhooks from Directus Flows (Self-Hosted, Step by Step)

Receive webhooks from Directus Flows on your own server: Event Hook triggers, the Webhook / Request URL operation with header authentication, Docker setup, and RabbitMQ routing, verified against a real Directus.

By the end of this guide you will have a self-hosted endpoint that receives webhooks from Directus Flows, authenticates every delivery with a secret header, and routes the events to a log, RabbitMQ, or any other destination. Everything below was verified end to end against a real Directus instance and the real Core Webhook Module Docker image: an actual items.create event fired the deliveries quoted here.

How Directus sends webhooks

Modern Directus sends outbound webhooks through Flows: the legacy standalone Webhooks feature was deprecated and removed in favor of them. A flow pairs a trigger (here, an Event Hook on data events like items.create scoped to a collection) with operations, and the operation that makes it a webhook is Webhook / Request URL: method, URL, headers, and a templated request body.

With the request body set to {{$trigger}}, a real items.create delivery looks like this:

{
  "event": "orders.items.create",
  "payload": {"title": "First order", "amount": 42},
  "key": 1,
  "collection": "orders"
}

event is <collection>.<action>, payload carries the written fields, and key is the new item's primary key (update events carry keys, an array). The request arrives with Content-Type: application/json and an axios user agent.

Authentication: Directus does not sign these requests. The operation's Headers list is where a secret token belongs, and the receiver compares it in constant time. Pick the trigger's Action (Non-Blocking) type so the webhook never delays the write it reports on.

Prerequisites

Step 1: Configure the receiver

Create a working directory with two files.

webhooks.json:

{
    "directus_events": {
        "data_type": "json",
        "module": "log",
        "header_auth": {
            "header_name": "X-Webhook-Token",
            "api_key": "{$DIRECTUS_WEBHOOK_TOKEN}",
            "case_sensitive": true
        }
    }
}

connections.json:

{}

What each parameter does:

  • directus_events is the webhook ID; it becomes the URL path (/webhook/directus_events).
  • module: log prints each authenticated event to the container log. Zero dependencies for the first run; we swap in RabbitMQ at the end.
  • header_auth requires the X-Webhook-Token header on every request and compares it in constant time.
  • {$DIRECTUS_WEBHOOK_TOKEN} pulls the token from an environment variable, so it never lives in a config file.

Generate a strong token (or use our webhook secret generator):

openssl rand -hex 32

Step 2: Run it

docker run -d --name webhook-gateway -p 8000:8000 \
  -v "$PWD/webhooks.json:/app/webhooks.json:ro" \
  -v "$PWD/connections.json:/app/connections.json:ro" \
  -e DIRECTUS_WEBHOOK_TOKEN="your-generated-token" \
  spiderhash/webhook:latest

Confirm it is up:

docker logs webhook-gateway
# ... INFO: Application startup complete.

The receiver now answers on http://localhost:8000/webhook/directus_events, with auto-generated API docs at http://localhost:8000/docs.

Step 3: Test it locally

Simulate exactly what the flow sends. Save the event payload from the top of this article as create_event.json, then:

curl -s -w "\nHTTP %{http_code}\n" -X POST http://localhost:8000/webhook/directus_events \
  -H "Content-Type: application/json" \
  -H "X-Webhook-Token: your-generated-token" \
  --data-binary @create_event.json

Expected result:

{"message":"200 OK"}
HTTP 200

Now prove the authentication works. A wrong token:

{"detail":"Invalid API key in header: X-Webhook-Token"}
HTTP 401

And a request with no token header at all:

{"detail":"Missing required header: X-Webhook-Token"}
HTTP 401

Those are the exact responses the gateway returns. docker logs webhook-gateway shows the accepted payload.

Step 4: Build the flow in Directus

In the Directus admin app: Settings > Flows > Create Flow:

  1. Name it (for example notify-gateway) and pick the Event Hook trigger.
  2. Type: Action (Non-Blocking). Scope: items.create (add items.update, items.delete as needed). Collections: the collection to watch.
  3. Add an operation: Webhook / Request URL: - Method: POST - URL: https://your-domain/webhook/directus_events - Headers: add X-Webhook-Token with your token as the value - Request Body: {{$trigger}}
  4. Save and make sure the flow's status is active.

Everything above can also be created via the API (POST /flows, POST /operations, then patch the flow's operation to the operation's id), which is exactly how this guide's verification run did it.

Now create an item in the collection; the delivery lands immediately and docker logs webhook-gateway shows the orders.items.create payload. The flow's sidebar in Directus keeps per-run logs showing each operation's resolved options and response, which is the first place to look when debugging.

Troubleshooting

  • 401 {"detail":"Invalid API key in header: X-Webhook-Token"}: the header value in the operation and DIRECTUS_WEBHOOK_TOKEN differ. They must be byte-identical.
  • 401 {"detail":"Missing required header: X-Webhook-Token"}: the Headers list on the operation is empty or the header name does not match. Check the flow's run logs to see the exact request the operation made.
  • The body arrives empty or as literal text: the Request Body must be a template that renders JSON; {{$trigger}} inserts the full trigger object. To send a subset, build a JSON body around specific values (for example {{$trigger.payload}}).
  • The flow runs but nothing was written yet: with a Blocking event hook, the flow runs before the database write and can even modify or abort it. For notifications, use Action (Non-Blocking) so the event reports a completed write and cannot delay it.

Going to production

Printing to logs is not a pipeline. Swap the module for a real destination; here is the RabbitMQ variant, verified end to end (a real items.create event landed as a message in the directus_events queue):

webhooks.json:

{
    "directus_events": {
        "data_type": "json",
        "module": "rabbitmq",
        "connection": "rabbitmq_local",
        "module-config": {
            "queue_name": "directus_events"
        },
        "header_auth": {
            "header_name": "X-Webhook-Token",
            "api_key": "{$DIRECTUS_WEBHOOK_TOKEN}",
            "case_sensitive": true
        }
    }
}

connections.json:

{
    "rabbitmq_local": {
        "type": "rabbitmq",
        "host": "{$RABBITMQ_HOST:localhost}",
        "port": "{$RABBITMQ_PORT:5672}",
        "user": "{$RABBITMQ_USER:guest}",
        "pass": "{$RABBITMQ_PASS:guest}"
    }
}

The same pattern works for 17 other destinations: archive content events to S3, publish to Kafka, or fan out to several at once with webhook chaining. Built-in rate limiting and retry handling cover the operational edges; a bulk import that fires an event per item is exactly when acknowledging fast and queueing durably pays off. The full authentication reference, including header auth and 11 other methods, is in the docs.

Subscribe to Free Webhook Tool

Sign up now to get access to the library of members-only issues.
Jamie Larson
Subscribe