On this page

Webhooks

Send inserts, updates, deletes and Auth events to your own HTTPS endpoint: signed, retried for 24 hours, logged with the response, and replayable.

A webhook sends an HTTPS POST to your endpoint each time a row in a chosen table is inserted, updated or deleted, or a user signs up, confirms, signs in, changes or is deleted. You don't write triggers or functions, and a slow or failing endpoint never slows down your database: events are queued and delivered in the background.

Create one in Studio under Database → Webhooks: a name, the endpoint URL, the events and, for database events, the tables. The signing secret is shown once you save; you can show it again later.

#Events

EventSent whenrecord / old_record
db.insertA row was added to a chosen tableThe new row / null
db.updateA row of a chosen table changedThe row after / before the change
db.deleteA row was deletednull / the deleted row
auth.user.createdA user signed up, was invited or created by an adminThe user / null
auth.user.confirmedA user confirmed their e-mail or phoneThe user / null
auth.user.signed_inA user signed in, with any methodThe user / null
auth.user.updatedE-mail, phone, metadata or ban changedThe user after / before
auth.user.deletedA user was deletednull / the user
types.updatedNew generated types after a schema change{ version, sha, languages, urls } / null
budget.alertThe project crossed a budget threshold{ resource, threshold, used, budget, percent, stopped, period, test } / null
crash.issue.new, crash.issue.regressedA new crash issue, or a resolved one came back{ issue_id, type, message, culprit, platform, release, events, users, first_seen, url } / null
monitor.down, monitor.failed, monitor.long, monitor.recoveredA job monitor missed a run, failed, ran too long, or is back to normal{ monitor_id, name, kind, target, alert, state, last_run_at, last_message, detail, url } / null
health.alert, health.recoveredA health check finding is new or worse, or it cleared{ id, title, level, service, measured, baseline, threshold, next_step, url } / null
metering.thresholdA customer crossed a usage meter alert threshold{ alert_id, alert_name, meter, meter_name, unit, customer, threshold, value, period, period_start, fired_at } / null

The user in Auth events has id, email, phone, the confirmation and sign-in times, user_metadata, app_metadata, is_anonymous and banned_until. Password hashes and tokens are never sent. TRUNCATE sends no events, and neither do changes made while triggers are off (for example a restore).

#Payload

JSON
{
  "id": "abcdefghijklmnop_4182",
  "type": "db.update",
  "created_at": "2026-10-25T12:00:00.000Z",
  "project_ref": "abcdefghijklmnop",
  "schema": "public",
  "table": "orders",
  "record": { "id": 42, "status": "paid", "total": 1999 },
  "old_record": { "id": 42, "status": "pending", "total": 1999 }
}

created_at is the time of the change, not of the delivery. If a row is larger than 256 KB, record and old_record keep only id and the payload has "truncated": true: read the row from the API instead.

#Verify the signature

Every request has these headers:

  • StitchDB-Webhook-Id: the event id, the same on every retry. Use it to skip events you already processed.
  • StitchDB-Webhook-Timestamp: Unix seconds when this attempt was sent.
  • StitchDB-Event: the event type.
  • StitchDB-Signature: v1=<hex>, the HMAC-SHA256 of <timestamp>.<raw body> with your signing secret.
import crypto from 'node:crypto'

// rawBody: the request body exactly as received (a string or Buffer), before JSON parsing.
export function verify(secret, headers, rawBody) {
  const ts = headers['stitchdb-webhook-timestamp']
  if (Math.abs(Date.now() / 1000 - Number(ts)) > 300) return false // older than 5 minutes
  const want = 'v1=' + crypto.createHmac('sha256', secret).update(ts + '.' + rawBody).digest('hex')
  const got = String(headers['stitchdb-signature'] || '')
  return got.length === want.length && crypto.timingSafeEqual(Buffer.from(got), Buffer.from(want))
}

#Delivery and retries

  • Any 2xx answer within 15 seconds counts as delivered. Answer quickly and do slow work afterwards.
  • Otherwise the event is retried after 10 seconds, 1 minute, 5 minutes, 30 minutes, 2 hours, 6 hours, 12 hours and 24 hours. After the last attempt it is marked Failed and kept in the log.
  • Delivery is at least once: an event can arrive twice (for example after a timeout your endpoint did process). Order is not guaranteed between events; use created_at or your own version column when order matters.
  • URLs must be https:// on a public address. Redirects are not followed. You can add up to 10 headers of your own (for example Authorization); they are stored encrypted.

#Delivery log and replay

Open a webhook in Studio to see every delivery: the request body, the status code and the first 2 KB of your endpoint's response, how long it took, and each attempt. Replay sends one delivery again; Replay failed sends every failed delivery again, oldest first. Send test sends a test.ping event right away and shows the answer, without logging or retrying it.

#When an endpoint keeps failing

If every delivery to a webhook has failed for an hour, the project owner gets an e-mail. After 3 days of failures the webhook is turned off and the owner gets another e-mail; the events it missed stay in the log, so fix the endpoint, turn the webhook on and use Replay failed.

#Limits

FreeProScaleEnterprise
Webhooks per project22050200
Events per project per month10,0001,000,0005,000,00050,000,000
Delivery log kept1 day7 days14 days30 days

An event counts once however many webhooks receive it. When a project reaches its monthly limit, new events are not sent until the next month and the owner gets an e-mail; deliveries already queued are still sent.

#Management API

Terminal
curl -X POST https://stitchdb.com/platform/v1/projects/<ref>/webhooks \
  -H "Authorization: Bearer $STITCHDB_TOKEN" -H 'Content-Type: application/json' \
  -d '{ "name": "Orders", "url": "https://api.example.com/hooks", "events": ["db.insert", "db.update"], "tables": ["public.orders"] }'
RequestDoes
GET /projects/<ref>/webhooksWebhooks with delivery counts, plan limits and this month's events
POST /projects/<ref>/webhooksCreate (returns the signing secret)
PATCH / DELETE /projects/<ref>/webhooks/<id>Change (name, url, events, tables, headers, enabled) or delete
GET …/<id>/secret, POST …/<id>/rotate-secretShow or rotate the signing secret
POST …/<id>/testSend a test.ping now
GET …/<id>/deliveries?status=deadThe delivery log (pending, delivered or dead)
GET …/<id>/deliveries/<delivery>One delivery with payload, response and attempts
POST …/<id>/replay{ "delivery_id": "…" } for one, or { "status": "dead", "since": "…" } for all failed ones

Changing webhooks needs the Administrator role on the project (tokens: projects:write). To call other services from SQL yourself, see HTTP from SQL.

Was this helpful?