How to Receive Webhooks from NocoDB (Self-Hosted, Step by Step)
Receive NocoDB record webhooks on your own server: secret header authentication, the {{ json event }} body template, the NC_ALLOW_LOCAL_HOOKS gotcha, Docker setup, and RabbitMQ routing, verified against a real NocoDB.
By the end of this guide you will have a self-hosted endpoint that receives NocoDB webhooks, authenticates every delivery with a secret header, and routes the record events to a log, RabbitMQ, or any other destination. Everything below was verified end to end against a real NocoDB instance and the real Core Webhook Module Docker image: actual record inserts triggered the deliveries quoted here, and this run surfaced two NocoDB behaviors that are easy to lose an hour on.
How NocoDB webhooks work
NocoDB attaches webhooks to tables. On the trigger you pick (a record event like After Insert, After Update, After Delete, or "Send Me Everything"), it sends an HTTP request you define: method, URL, headers, and body. A real After Insert delivery looks like this:
{
"type": "records.after.insert",
"id": "52daed96-9e98-4989-a334-3acfca3ccb9a",
"version": "v3",
"data": {
"table_id": "mwsehm89ejq2bwy",
"table_name": "orders",
"rows": [
{"Id": 3, "CreatedAt": "2026-08-29 19:54:25+00:00", "Title": "Signed order", "Amount": 120}
]
}
}
The request arrives with Content-Type: application/json and an axios user agent. Two behaviors verified against a live instance that you should know up front:
- The default body is empty. A v3 webhook with no body template sends a POST with
Content-Length: 0. To get the event payload shown above, set the webhook's Body to the Handlebars expression{{ json event }}. - Private-network targets are blocked by default. If your receiver lives on a private or local address (a lab setup, a shared Docker network), deliveries are silently dropped unless NocoDB runs with
NC_ALLOW_LOCAL_HOOKS=true.
Authentication: NocoDB does not sign webhook payloads, but the webhook form takes custom headers. Put a secret token in a header and let the receiver compare it in constant time. Conditions on the webhook can filter which records trigger it; View, Field, and Comment trigger sources are paid-plan features, while the Record triggers used here are free.
Prerequisites
- Docker on any machine (your laptop is fine for the test run)
- A public HTTPS URL for production, unless NocoDB and your receiver share a network. Two ways to get one:
- HTTPS for webhooks with nginx and Let's Encrypt if you have a public server
- Receiving webhooks through Cloudflare Tunnel if you do not want to open inbound ports
Step 1: Configure the receiver
Create a working directory with two files.
webhooks.json:
{
"nocodb_events": {
"data_type": "json",
"module": "log",
"header_auth": {
"header_name": "X-Webhook-Token",
"api_key": "{$NOCODB_WEBHOOK_TOKEN}",
"case_sensitive": true
}
}
}
connections.json:
{}
What each parameter does:
nocodb_eventsis the webhook ID; it becomes the URL path (/webhook/nocodb_events).module: logprints each authenticated event to the container log. Zero dependencies for the first run; we swap in RabbitMQ at the end.header_authrequires theX-Webhook-Tokenheader on every request and compares it in constant time.{$NOCODB_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 NOCODB_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/nocodb_events, with auto-generated API docs at http://localhost:8000/docs.
Step 3: Test it locally
Simulate exactly what NocoDB sends. Save the After Insert payload from the top of this article as insert_event.json, then:
curl -s -w "\nHTTP %{http_code}\n" -X POST http://localhost:8000/webhook/nocodb_events \
-H "Content-Type: application/json" \
-H "X-Webhook-Token: your-generated-token" \
--data-binary @insert_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: Create the webhook in NocoDB
Open the table, then Details tab in the top bar > Webhooks > Add New Webhook:
- Name: something like
webhook-gateway - Trigger: Record > After Insert (add more webhooks or "Send Me Everything" as needed)
- Method:
POST, URL:https://your-domain/webhook/nocodb_events - Headers: add
X-Webhook-Tokenwith your token as the value - Body:
{{ json event }}- do not skip this; without it the delivery has an empty body
Use the Test webhook button, then save. Insert a record in the table; the delivery lands immediately, and docker logs webhook-gateway shows the records.after.insert payload.
If your NocoDB and receiver share a private network, start NocoDB with the SSRF guard relaxed for local targets:
docker run -d --name nocodb -p 8080:8080 -e NC_ALLOW_LOCAL_HOOKS=true nocodb/nocodb:latest
Troubleshooting
401 {"detail":"Invalid API key in header: X-Webhook-Token"}: the header value in the webhook form andNOCODB_WEBHOOK_TOKENdiffer. They must be byte-identical.400 {"detail":"Malformed JSON payload"}: the webhook's Body template is empty, so NocoDB sent a zero-length body. Set Body to{{ json event }}.- Nothing arrives at all on a private address: NocoDB blocks hook targets on local and private networks by default. Set
NC_ALLOW_LOCAL_HOOKS=trueon the NocoDB container (verified: the identical webhook started delivering the moment the instance restarted with this flag), or give the receiver a public HTTPS URL. - The rows look different per event:
records.after.updatepayloads carry previous and new values; branch on thetypefield in your consumer.
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 record insert landed as a message in the nocodb_events queue):
webhooks.json:
{
"nocodb_events": {
"data_type": "json",
"module": "rabbitmq",
"connection": "rabbitmq_local",
"module-config": {
"queue_name": "nocodb_events"
},
"header_auth": {
"header_name": "X-Webhook-Token",
"api_key": "{$NOCODB_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 record history 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 row 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.