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:

  1. 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 }}.
  2. 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

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_events is the webhook ID; it becomes the URL path (/webhook/nocodb_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.
  • {$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-Token with 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 and NOCODB_WEBHOOK_TOKEN differ. 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=true on 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.update payloads carry previous and new values; branch on the type field 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.

Subscribe to Free Webhook Tool

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