Gareth Wilson Gareth Wilson

Guide to Baserow Webhooks: Features and Best Practices

Published


Baserow webhooks push changes out of your Baserow tables as they happen. When rows are created, updated or deleted, or a view or field changes, Baserow can call your endpoint with the details, so you can sync records into other systems, trigger workflows and keep caches fresh without polling the Baserow API.

This guide covers how Baserow webhooks are configured and authenticated, the events and payloads they send, how large row changes are split into batches, how retries and deactivation work, and the practices that make a Baserow receiver reliable.

What are Baserow webhooks?

Baserow webhooks belong to a table. Each one has a URL, an HTTP method, a set of events and optional custom headers. When a selected event happens in the table, Baserow sends a JSON body describing it. That works the same on Baserow Cloud and on self-hosted Baserow.

The method is yours to choose: GET, POST, PUT, PATCH or DELETE. Whichever you pick, Baserow sends the JSON body with it, even on GET.

Baserow doesn't sign webhook requests. Alongside your custom headers, it sends only Content-type: application/json, X-Baserow-Event (the event type) and X-Baserow-Delivery (the event ID). To authenticate requests, you add a secret custom header to the webhook and check it on your side, which is what Baserow's docs mean by "Include authentication tokens".

Baserow webhook features

FeatureDetails
ConfigurationThe table's three-dot menu > Webhooks > Create webhook
MethodsGET, POST, PUT, PATCH or DELETE, chosen per webhook
FormatJSON body with every method
Headers sentContent-type, X-Baserow-Event, X-Baserow-Delivery, plus your custom headers
AuthenticationA secret custom header you add. No signature or timestamp
Event fieldevent_type in the body, repeated in X-Baserow-Event
SuccessThe docs require 200 OK
Retries"A limited number of times" (see below for self-hosted defaults)
ThroughputOne call at a time per webhook. Baserow Cloud keeps at most 5,000 pending calls per webhook and drops the rest
TestingTrigger test webhook, and a Call log per webhook
Source IPsNot published

Common events

Areaevent_typeFires when
Rowsrows.created, rows.updated, rows.deletedRows are added, modified or removed
Viewsview.created, view.updated, view.deletedA view is added, its configuration changes, or it is removed
Fieldsfield.created, field.updated, field.deletedA field is added, its properties change, or it is removed

The per-row row.created, row.updated and row.deleted types are deprecated, and Baserow asks you to move to the batch rows.* types. Baserow also offers two filtered options, a conditional row update that fires only when chosen fields change and a "row enters view" trigger. Log the event_type they send before you rely on them.

Every body carries table_id, database_id, workspace_id, event_id and event_type, plus the affected object: items for row events, with old_items added on rows.updated, view for view events and field for field events. Here is Baserow's view.created example, trimmed:

{
  "table_id": 50000,
  "database_id": 1000,
  "workspace_id": 300,
  "event_id": "00000000-0000-0000-0000-000000000000",
  "event_type": "view.created",
  "view": {
    "id": 0,
    "table_id": 0,
    "name": "View",
    "order": 1,
    "type": "grid",
    "filter_type": "AND",
    "filters_disabled": false,
    "public": false
  }
}

Row payloads contain the full rows. To know who made a change, Baserow suggests adding a Last Modified By field to the table, which then appears in every row.

Large row changes arrive in batches

Baserow's open-source code splits a row event that affects more rows than its batch size (200 by default on self-hosted Baserow) into several calls. They share one event_id and carry batch_id, offset, batch_size and total_count, and they are sent one after another. By default, Baserow stops after 5 batches and notifies the workspace admins that the payload was too large. If you deduplicate on event_id alone, you drop every batch after the first.

Inspect Baserow webhooks as they arrive. Point a webhook at a Hookdeck Inspect URL and click Trigger test webhook to see the exact headers and body, with no account or setup required.

Setting up Baserow webhooks

Pick a header name and a long random value first, for example Authorization and Bearer followed by the output of openssl rand -hex 32.

  1. Click the three-dot menu beside your table or view and select Webhooks.
  2. Click Create webhook +.
  3. Enter a name, choose the HTTP method and enter your HTTPS URL.
  4. Choose Send everything, or select the events you need.
  5. Add your secret header as a custom header.
  6. Click Trigger test webhook and check that your endpoint returns 200.
  7. Click Save.

To route through Hookdeck, create a source with the Baserow type and use its URL as the webhook URL. Enter the header's name in API Key Header and its exact value, including any Bearer prefix, in API Key. The source pre-selects POST but doesn't manage the method, so if your webhook uses GET, PUT, PATCH or DELETE, allow that method on the source too. Otherwise those requests are refused.

Securing Baserow webhooks

Read your header case-insensitively and compare its whole value with the one you stored, in constant time. Accept every method your webhook might use, and handle an empty body.

const crypto = require('crypto');
const express = require('express');

const HEADER = (process.env.BASEROW_WEBHOOK_HEADER || 'Authorization').toLowerCase();
const SECRET = process.env.BASEROW_WEBHOOK_SECRET; // the exact, whole header value

function safeEqual(received, expected) {
  const a = Buffer.from(received, 'utf8');
  const b = Buffer.from(expected, 'utf8');
  // timingSafeEqual throws on unequal lengths, so check first.
  return a.length === b.length && crypto.timingSafeEqual(a, b);
}

function handleBaserow(req, res) {
  const received = req.get(HEADER);
  if (!SECRET || typeof received !== 'string' || !safeEqual(received, SECRET)) {
    return res.sendStatus(401);
  }
  if (!req.body || req.body.length === 0) return res.sendStatus(200);

  let event;
  try {
    event = JSON.parse(req.body.toString('utf8'));
  } catch {
    return res.sendStatus(400);
  }

  // Key on event_id plus batch_id, queue the event, then respond.
  res.sendStatus(200);
}

const app = express();
const route = '/webhooks/baserow';
const raw = express.raw({ type: '*/*' });
// Register only the method(s) your webhook uses; add app.get or app.delete the same way if you chose GET or DELETE.
app.post(route, raw, handleBaserow);
app.put(route, raw, handleBaserow);
app.patch(route, raw, handleBaserow);

The same check in Python:

import hmac
import os

HEADER = os.environ.get("BASEROW_WEBHOOK_HEADER", "Authorization")
SECRET = os.environ.get("BASEROW_WEBHOOK_SECRET", "")


def verify_baserow_request(headers) -> bool:
    if not SECRET:
        return False  # fail closed when unconfigured
    received = headers.get(HEADER)  # Starlette and Flask headers are case-insensitive
    if not received:
        return False
    # Compare bytes: compare_digest raises TypeError on a non-ASCII str.
    return hmac.compare_digest(received.encode("utf-8"), SECRET.encode("utf-8"))

A static header authenticates the sender, not the body. It can't detect a modified payload, and with no signed timestamp a captured request can be replayed and still pass. Use HTTPS, a long random value, and change the header on the webhook and in your receiver together if it might have leaked.

Make Baserow webhooks production-ready. Hookdeck Event Gateway checks your secret header on every Baserow call, answers straight away, and queues each event durably so a slow receiver doesn't back up the webhook.

Baserow webhook limitations and pain points

One call at a time

The Problem: A slow receiver delays every later event, and on Baserow Cloud a long enough backlog loses events.

Why It Happens: Baserow sends one call at a time per webhook, and Baserow Cloud keeps at most 5,000 pending calls per webhook. "Any additional calls beyond this limit will be dropped."

Workarounds:

  • Respond with 200 as soon as the header checks out, and process in the background.
  • Split busy tables across several webhooks with narrower event selections.

How Hookdeck Can Help: Hookdeck accepts each call as soon as it arrives, so Baserow's queue drains at Hookdeck's speed rather than your service's. Hookdeck then delivers to your service at a max delivery rate you set.

Limited retries, then deactivation

The Problem: An outage on your side loses events and, if it lasts long enough, switches the webhook off.

Why It Happens: The docs say failed calls are retried "a limited number of times". In Baserow's open-source code, the self-hosted defaults are a 5-second timeout, up to 8 retries per call with exponential backoff, and deactivation of the webhook on the ninth consecutive failed trigger. Self-hosted administrators can change these, and Baserow doesn't publish the Cloud values.

Workarounds:

  • Keep the handler fast, and check the webhook's Call log after an outage.
  • Watch for the deactivation notification, and reactivate the webhook once the receiver is healthy.

How Hookdeck Can Help: Hookdeck stays up when your service doesn't, so Baserow sees successful calls and never deactivates the webhook. Hookdeck delivers to your service with retries you configure and lets you replay any event.

Batches share an event ID

The Problem: After a bulk edit, only the first 200 rows reach your system.

Why It Happens: Large row events are split into several calls with the same event_id and different batch_id values. Deduplication on event_id treats batches 2 and later as duplicates.

Workarounds:

  • Key on event_id and batch_id together (a missing batch_id means a single call).
  • Use total_count and offset to check that you received every row.

How Hookdeck Can Help: Hookdeck's deduplication can use several fields, so you can deduplicate on event_id and batch_id together.

The method is configurable

The Problem: A webhook set to PUT or GET gets 404 or 405 from a receiver that only accepts POST, and the calls count as failures.

Why It Happens: Each webhook chooses its own method, and the body is sent whatever the method.

Workarounds:

  • Use POST unless the receiving system needs something else.
  • Register a route for each method your webhooks use.

How Hookdeck Can Help: Allow the methods your webhooks use on the Hookdeck source, and Hookdeck forwards each request to your destination.

Best practices

Store the full header value

Keep the exact value, Bearer prefix included, and compare it whole. Put the name and value in configuration so you can rotate them.

Route on event_type

Read event_type from the body or the X-Baserow-Event header. Acknowledge anything you don't recognise, including the deprecated row.* types if they still arrive, rather than failing the call.

Deduplicate on event_id and batch_id

Store the pair before you process a call, and skip pairs you've already handled. See our guide to webhook idempotency.

Use old_items for diffs

rows.updated carries the previous values in old_items, so you can see what changed without keeping your own copy of the table.

Acknowledge fast and process asynchronously

With one call at a time per webhook, a slow handler slows everything. See why to process webhooks asynchronously.

Conclusion

Baserow table webhooks send a JSON body for each row, view or field event, with the method you choose and no signature. Add a secret custom header and compare it whole, accept the methods your webhooks use, deduplicate on event_id and batch_id, and respond quickly, because Baserow sends one call at a time and gives up after a limited number of retries.

Hookdeck Event Gateway sits in front of your receiver, checks the header, answers Baserow at once, and keeps every event until your service has processed it.

Get started with Hookdeck for free and handle Baserow webhooks reliably in minutes.


Gareth Wilson

Gareth Wilson

Product Marketing

Multi-time founding marketer, Gareth is PMM at Hookdeck and author of the newsletter, Community Inc.