# 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)

```javascript
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/](https://github.com/hookdeck/webhook-skills/tree/main/skills/baserow-webhooks/examples/express/), [examples/nextjs/](https://github.com/hookdeck/webhook-skills/tree/main/skills/baserow-webhooks/examples/nextjs/), [examples/fastapi/](https://github.com/hookdeck/webhook-skills/tree/main/skills/baserow-webhooks/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](https://github.com/hookdeck/webhook-skills/blob/main/skills/baserow-webhooks/references/overview.md).

## Environment Variables

```bash
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):

```bash
npx hookdeck-cli listen 3000 baserow --path /webhooks/baserow

```

Use the printed URL as the webhook URL in Baserow.

## Resources

* [references/overview.md](https://github.com/hookdeck/webhook-skills/blob/main/skills/baserow-webhooks/references/overview.md) - What Baserow webhooks are, events and payload shape
* [references/setup.md](https://github.com/hookdeck/webhook-skills/blob/main/skills/baserow-webhooks/references/setup.md) - Create the webhook and add the secret header
* [references/verification.md](https://github.com/hookdeck/webhook-skills/blob/main/skills/baserow-webhooks/references/verification.md) - Header check details, gotchas, debugging 401s
* [examples/](https://github.com/hookdeck/webhook-skills/tree/main/skills/baserow-webhooks/examples/) - Runnable Express, Next.js and FastAPI handlers with tests