Agent skill
Baserow Webhooks Skill
Receive and authenticate Baserow webhooks. Use when setting up a Baserow table webhook endpoint, authenticating the custom secret header, debugging 401s, or handling events like rows.created, rows.updated, rows.deleted, view.created and field.updated.
Install this skill
npx skills add hookdeck/webhook-skills --skill baserow-webhooks
Baserow, the open-source no-code database (cloud and self-hosted), sends a JSON webhook when data in a table changes: rows are created, updated or deleted, or views and fields change. Webhooks are not signed. Baserow adds only X-Baserow-Event and X-Baserow-Delivery headers plus any custom headers you configure on the webhook, so you authenticate requests by adding a secret header yourself and comparing it on your endpoint.
When to Use This Skill
- How do I receive Baserow webhooks?
- Does Baserow sign webhook payloads? (No: use a secret custom header)
- How do I authenticate a Baserow webhook with a custom header?
- How do I handle
rows.created,rows.updatedandrows.deletedevents? - Why does my Baserow webhook endpoint return 401 or never get called?
Verification (core)
const crypto = require('crypto');
// Hash both sides: equal-length buffers, so timingSafeEqual never throws.
function safeEqual(a, b) {
const ha = crypto.createHash('sha256').update(String(a)).digest();
const hb = crypto.createHash('sha256').update(String(b)).digest();
return crypto.timingSafeEqual(ha, hb);
}
function verifyBaserowRequest(headers, name = process.env.BASEROW_WEBHOOK_HEADER || 'Authorization',
secret = process.env.BASEROW_WEBHOOK_SECRET) {
if (!secret) return false; // never accept when unconfigured
const received = headers[name.toLowerCase()]; // case-insensitive lookup
return Boolean(received) && safeEqual(received, secret); // WHOLE value, no 'Bearer ' parsing
}
- The header name is whatever you typed in Baserow (
BASEROW_WEBHOOK_HEADER, defaultAuthorization). The expected value (BASEROW_WEBHOOK_SECRET) is the exact, whole header value, including anyBearerprefix you typed. Do not strip or parse a prefix. - Return
401when the header is missing or does not match,400for a body that isn't a JSON object,200once handled. The docs say success requires200 OKand failed calls are retried a limited number of times (Baserow's source counts any status below 400 as success; return200to match the docs). - Read the body raw (
express.raw({ type: '*/*' })) and authenticate before parsing it. - Accept POST, PUT, PATCH, GET and DELETE on the route: you choose the method in Baserow, and a JSON body is sent even on GET. Handle an empty body (respond
200, nothing to dispatch).
A static shared credential authenticates the sender, not the body, and gives no replay protection (no signature, no timestamp). Serve the endpoint over HTTPS only, and rotate the secret (update the Baserow header and BASEROW_WEBHOOK_SECRET together) if it leaks. Make handlers idempotent, but not on event_id alone: a large view.rows_entered call, or a rows.updated call caused by a linked table, is split into several calls that share one event_id and carry batch_id 1, 2, ... (Baserow source; batch_id is absent when there is one call), so key on event_id + batch_id.
For complete handlers with tests, see examples/express/, examples/nextjs/, examples/fastapi/.
Event Types
The payload has an event_type field, plus event_id, table_id, database_id and workspace_id. Row events carry an items array of affected rows, except rows.deleted, which carries only row_ids. Route on the body's event_type: on a call caused by a linked table, the X-Baserow-Event header can name a different event.
event_type | Sent When |
|---|---|
rows.created | Rows are added |
rows.updated | Rows are modified |
rows.deleted | Rows are removed |
view.created | A view is added |
view.updated | A view's configuration changes |
view.deleted | A view is removed |
field.created | A field is added |
field.updated | A field's properties change |
field.deleted | A field is removed |
view.rows_entered | Rows start matching a chosen view's filters (enterprise feature; not in Send everything) |
The older row.created, row.updated and row.deleted types are deprecated in favour of the batch rows.* types, and Baserow's current source no longer registers them. A conditional row update is rows.updated limited to chosen fields. See references/overview.md.
Environment Variables
BASEROW_WEBHOOK_HEADER=Authorization # the custom header name you added in Baserow
BASEROW_WEBHOOK_SECRET=your-secret # the exact, whole header value (e.g. "Bearer abc123")
Hookdeck Source Note
Hookdeck's Baserow source (type BASEROW) verifies the same way: header name plus the exact whole value. It pre-selects the POST method without managing it, so if your Baserow webhook uses another method (PUT, PATCH, GET or DELETE), also allow that method on the Hookdeck source.
Local Development
Baserow Cloud can't reach localhost. Tunnel with the Hookdeck CLI (no account required):
npx hookdeck-cli listen 3000 baserow --path /webhooks/baserow
Use the printed URL as the webhook URL in Baserow.
Resources
- references/overview.md - What Baserow webhooks are, events and payload shape
- references/setup.md - Create the webhook and add the secret header
- references/verification.md - Header check details, gotchas, debugging 401s
- examples/ - Runnable Express, Next.js and FastAPI handlers with tests