Build
Webhooks
A webhook POSTs a signed JSON event to your URL whenever a row is inserted, updated, or deleted. Use it to send email, sync a search index, or call another service.
Payload
{
"id": "evt_...",
"type": "row.insert",
"app": "notes_app",
"table": "notes",
"record": {"id": "...", "title": "Buy milk", "created_at": "2026-09-22T18:00:00Z"},
"old_record": null,
"occurred_at": "2026-09-22T18:00:00Z"
}type is row.insert, row.update, row.delete, or ping for a test. Each request carries these headers:
| Header | Value |
|---|---|
Berth-Signature | t=<unix seconds>,v1=<hex HMAC-SHA256 of "t.raw body" with the webhook secret> |
Berth-Event-Id | the event id, the same on every retry |
Berth-Delivery-Id | this attempt |
Create a webhook
Point it at an HTTPS endpoint you control. Leave out table for every table and events for all three types. The signing secret is returned once.
berth tables create --app "$APP" notes title:text:notnullHOOK=$(curl -sS -X POST https://api.atberth.com/v1/apps/$APP/webhooks \
-H "Authorization: Bearer $SECRET_KEY" \
-H "Content-Type: application/json" \
-d '{"url":"https://example.com/berth-webhook","table":"notes","events":["insert","update"],"description":"search sync"}')
WEBHOOK_ID=$(echo "$HOOK" | jq -er '.webhook.id')
WEBHOOK_SECRET=$(echo "$HOOK" | jq -er '.secret')
berth webhooks list --app "$APP"With the CLI: berth webhooks add --app "$APP" https://example.com/berth-webhook --table notes --events insert,update. Private, loopback, and cloud metadata addresses are refused.
Test and inspect deliveries
Send a ping, write a row, then look at what was sent and what your server answered:
berth webhooks test --app "$APP" "$WEBHOOK_ID"
berth rows add --app "$APP" notes title="Hook me"
sleep 2
berth webhooks deliveries --app "$APP" "$WEBHOOK_ID"curl -sS https://api.atberth.com/v1/apps/$APP/webhooks/$WEBHOOK_ID/deliveries \
-H "Authorization: Bearer $SECRET_KEY"Retries
Any 2xx answer within 10 seconds counts as delivered. Anything else is retried after 10 seconds, 1 minute, 5 minutes, 30 minutes, and 2 hours: six attempts in all. Retries reuse Berth-Event-Id, so store the ids you have handled and skip repeats.
Verify the signature
Always check Berth-Signature against the raw body before trusting an event, and reject old timestamps to stop replays. You can compute a signature in a shell to see the format:
BODY='{"id":"evt_test","type":"ping"}'
T=$(date +%s)
SIG=$(printf '%s.%s' "$T" "$BODY" | openssl dgst -sha256 -hmac "$WEBHOOK_SECRET" | sed 's/^.* //')
echo "Berth-Signature: t=$T,v1=$SIG"Node
import crypto from "node:crypto";
import express from "express";
const app = express();
const secret = process.env.BERTH_WEBHOOK_SECRET;
app.post("/berth-webhook", express.raw({ type: "application/json" }), (req, res) => {
const header = req.get("Berth-Signature") ?? "";
const parts = Object.fromEntries(header.split(",").map((p) => p.split("=")));
const expected = crypto
.createHmac("sha256", secret)
.update(`${parts.t}.${req.body}`)
.digest("hex");
const fresh = Math.abs(Date.now() / 1000 - Number(parts.t)) < 300;
const ok = parts.v1 && fresh &&
crypto.timingSafeEqual(Buffer.from(expected), Buffer.from(parts.v1));
if (!ok) return res.status(400).send("bad signature");
const event = JSON.parse(req.body);
console.log(event.type, event.table, event.record?.id);
res.sendStatus(204);
});
app.listen(3000);Python
import hashlib, hmac, json, os, time
from flask import Flask, request, abort
app = Flask(__name__)
SECRET = os.environ["BERTH_WEBHOOK_SECRET"].encode()
@app.post("/berth-webhook")
def berth_webhook():
raw = request.get_data()
parts = dict(p.split("=", 1) for p in request.headers.get("Berth-Signature", "").split(","))
expected = hmac.new(SECRET, parts.get("t", "").encode() + b"." + raw, hashlib.sha256).hexdigest()
fresh = abs(time.time() - int(parts.get("t", "0"))) < 300
if not (fresh and hmac.compare_digest(expected, parts.get("v1", ""))):
abort(400)
event = json.loads(raw)
print(event["type"], event["table"], event["record"])
return "", 204Update, disable, remove
berth webhooks update --app "$APP" "$WEBHOOK_ID" --events insert,update,delete --disable
berth webhooks show --app "$APP" "$WEBHOOK_ID"
curl -sS -X PATCH https://api.atberth.com/v1/apps/$APP/webhooks/$WEBHOOK_ID \
-H "Authorization: Bearer $SECRET_KEY" \
-H "Content-Type: application/json" \
-d '{"enabled":true}'berth webhooks remove --app "$APP" "$WEBHOOK_ID"Up to 20 webhooks per app.