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.updated and rows.deleted events?
  • 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, default Authorization). The expected value (BASEROW_WEBHOOK_SECRET) is the exact, whole header value, including any Bearer prefix you typed. Do not strip or parse a prefix.
  • Return 401 when the header is missing or does not match, 400 for a body that isn't a JSON object, 200 once handled. The docs say success requires 200 OK and failed calls are retried a limited number of times (Baserow's source counts any status below 400 as success; return 200 to 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_typeSent When
rows.createdRows are added
rows.updatedRows are modified
rows.deletedRows are removed
view.createdA view is added
view.updatedA view's configuration changes
view.deletedA view is removed
field.createdA field is added
field.updatedA field's properties change
field.deletedA field is removed
view.rows_enteredRows 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


Repository

hookdeck/webhook-skills

v0.1.0 · MIT · Updated Nov 5, 2026

View on GitHub →