# Guide to Baserow Webhooks: Features and Best Practices

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

| Feature | Details |
| --- | --- |
| Configuration | The table's three-dot menu > **Webhooks** > **Create webhook** |
| Methods | GET, POST, PUT, PATCH or DELETE, chosen per webhook |
| Format | JSON body with every method |
| Headers sent | `Content-type`, `X-Baserow-Event`, `X-Baserow-Delivery`, plus your custom headers |
| Authentication | A secret custom header you add. No signature or timestamp |
| Event field | `event_type` in the body, repeated in `X-Baserow-Event` |
| Success | The docs require `200 OK` |
| Retries | "A limited number of times" (see below for self-hosted defaults) |
| Throughput | One call at a time per webhook. Baserow Cloud keeps at most 5,000 pending calls per webhook and drops the rest |
| Testing | **Trigger test webhook**, and a **Call log** per webhook |
| Source IPs | Not published |

## Common events

| Area | `event_type` | Fires when |
| --- | --- | --- |
| Rows | `rows.created`, `rows.updated`, `rows.deleted` | Rows are added, modified or removed |
| Views | `view.created`, `view.updated`, `view.deleted` | A view is added, its configuration changes, or it is removed |
| Fields | `field.created`, `field.updated`, `field.deleted` | A 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:

```json
{
  "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](https://inspect.hookdeck.com) 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.

```javascript
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:

```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](/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](/docs/destinations#set-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](/docs/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](/docs/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](/webhooks/guides/implement-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](/webhooks/guides/why-implement-asynchronous-processing-webhooks).

## 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](https://hookdeck.com) 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](https://dashboard.hookdeck.com/signup) for free and handle Baserow webhooks reliably in minutes.