# SeaTable Webhooks

## When to Use This Skill

* How do I receive SeaTable webhooks?
* How do I verify the `X-Seatable-Signature` header?
* Why is my SeaTable webhook signature verification failing?
* How do I handle `insert_row`, `modify_row` and `delete_row` changes?
* Why did SeaTable mark my webhook as invalid?

## Verification (core)

SeaTable signs the **raw request body** with HMAC-SHA256, using the webhook's secret key as a **UTF-8 string**. The header is `X-Seatable-Signature: sha256=<hex>`.

The secret key is **optional**. If none is set on the webhook, SeaTable sends **no signature header**, so anyone who finds the URL can post to it. Always set a secret.

```javascript
const crypto = require('crypto');

function verify(rawBody, signatureHeader, secret) {
  if (!signatureHeader || !secret) return false;
  const received = signatureHeader.replace(/^sha256=/, '').toLowerCase();
  const expected = crypto.createHmac('sha256', secret).update(rawBody).digest('hex');
  try {
    return crypto.timingSafeEqual(Buffer.from(received), Buffer.from(expected));
  } catch {
    return false; // length mismatch = invalid
  }
}

```

Read the body raw for **any** content type (`express.raw({ type: '*/*' })`) and parse JSON only after verification.

> **For complete handlers with tests**, see [examples/express/](https://github.com/hookdeck/webhook-skills/tree/main/skills/seatable-webhooks/examples/express/), [examples/nextjs/](https://github.com/hookdeck/webhook-skills/tree/main/skills/seatable-webhooks/examples/nextjs/), [examples/fastapi/](https://github.com/hookdeck/webhook-skills/tree/main/skills/seatable-webhooks/examples/fastapi/).

## Common Event Types

Every delivery has `event` (currently always `update`) and `data`. What changed is in `data.op_type`:

| `data.op_type` | Triggered When |
| --- | --- |
| `insert_row` | A row is added or duplicated (UI, form or API) |
| `modify_row` | A row's content changes; `data.row_data` holds `column_name`, `value`, `old_value` |
| `delete_row` | A row is deleted |

SeaTable's docs list `insert_row` as the value, but one sample on the secret-key page shows `create_row`. The examples accept both. Structural changes (new columns, views, comments, row locks) send nothing. A multi-row paste in the web editor sends one webhook with an extra `data.row_count`.

## Environment Variables

```bash
SEATABLE_WEBHOOK_SECRET=your_webhook_secret_here   # secret key set on the webhook

```

## Local Development

```bash
# Start tunnel (no account needed)
npx hookdeck-cli listen 3000 seatable --path /webhooks/seatable

```

## Reference Materials

* [references/overview.md](https://github.com/hookdeck/webhook-skills/blob/main/skills/seatable-webhooks/references/overview.md) - Webhook concepts, payload structure, op_types
* [references/setup.md](https://github.com/hookdeck/webhook-skills/blob/main/skills/seatable-webhooks/references/setup.md) - Creating a webhook and secret key in SeaTable
* [references/verification.md](https://github.com/hookdeck/webhook-skills/blob/main/skills/seatable-webhooks/references/verification.md) - Signature details and gotchas