Webhooks

A webhook sends your database changes to a URL you choose: your own server, or a service like Slack or a CRM. When a record in the collection is created, updated or deleted, Koolbase POSTs the event there, signed so the receiver can check it came from Koolbase, and retries if the receiver is down.

Webhooks or Functions?

Use a trigger when the work runs inside Koolbase. Use a webhook when another system needs to hear about the change: it keeps working even when that system is written in something Koolbase does not run.

Create a webhook

Manage webhooks from the CLI (v0.8.2 or later) or the API. Creating, listing and deleting them needs a project owner or admin, or an admin-scoped API key, because a webhook sends project data outside Koolbase.

curl -X POST https://api.koolbase.com/v1/projects/PROJECT_ID/webhooks \
  -H "Authorization: Bearer YOUR_ADMIN_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "url": "https://example.com/koolbase-webhook",
    "collection": "orders",
    "events": ["db.record.created", "db.record.updated"]
  }'

The response includes the webhook and its signing secret:

{
  "webhook": { "id": "…", "url": "https://example.com/koolbase-webhook", "collection": "orders",
               "events": ["db.record.created", "db.record.updated"], "enabled": true },
  "secret": "whsec_…"
}

The secret is shown once

Store secret straight away, for example as an environment variable on your server. Koolbase keeps it encrypted and never returns it again. If you lose it, delete the webhook and create a new one.
db.record.createdA record was inserted into the collection
db.record.updatedA record was updated
db.record.deletedA record was deleted

From the CLI

The same, from a terminal. --event can be repeated or comma-separated, and insert, update and delete work as short names. The secret is printed once, when you create the webhook.

koolbase webhooks create --project PROJECT_ID \
  --url https://example.com/koolbase-webhook --collection orders --event insert,update

koolbase webhooks list --project PROJECT_ID
koolbase webhooks deliveries WEBHOOK_ID --project PROJECT_ID
koolbase webhooks redeliver DELIVERY_ID --project PROJECT_ID
koolbase webhooks delete WEBHOOK_ID --project PROJECT_ID

What your server receives

A POST with a JSON body. data is the same payload a Function trigger gets: record for created and updated, record_id for deleted.

{
  "id": "evt_4c1b…",
  "type": "db.record.created",
  "collection": "orders",
  "created_at": "2026-09-24T12:00:00Z",
  "data": { "record": { "$id": "rec_123", "total": 42 } }
}
X-Koolbase-Signaturet=<unix time>,v1=<signature>. Check it before trusting the body.
X-Koolbase-EventThe event type, e.g. db.record.created
X-Koolbase-Event-IdThe event's id; the same on every retry. Use it to ignore repeats.
X-Koolbase-DeliveryThis delivery's id, for the delivery log

Verify the signature

v1 is a hex HMAC-SHA256 of "<t>.<raw body>" using your webhook secret. Check it against the raw request body, before parsing it, and reject requests more than five minutes old. These examples are tested against Koolbase's signing code.

verify.ts
TypeScript
import { createHmac, timingSafeEqual } from "node:crypto"
import { Buffer } from "node:buffer"

// Returns true only if the request really came from Koolbase and is recent.
export function verifyKoolbaseSignature(secret: string, header: string, rawBody: string, toleranceSeconds = 300): boolean {
  const parts: Record<string, string> = {}
  for (const piece of header.split(",")) {
    const i = piece.indexOf("=")
    if (i > 0) parts[piece.slice(0, i).trim()] = piece.slice(i + 1).trim()
  }
  const t = Number(parts.t)
  if (!parts.v1 || !Number.isFinite(t)) return false
  if (Math.abs(Date.now() / 1000 - t) > toleranceSeconds) return false
  const expected = createHmac("sha256", secret).update(t + "." + rawBody).digest("hex")
  const a = Buffer.from(expected)
  const b = Buffer.from(parts.v1)
  return a.length === b.length && timingSafeEqual(a, b)
}
verify.py
python
import hashlib
import hmac
import time

# Returns True only if the request really came from Koolbase and is recent.
def verify_koolbase_signature(secret: str, header: str, raw_body: bytes, tolerance_seconds: int = 300) -> bool:
    parts = dict(p.strip().split("=", 1) for p in header.split(",") if "=" in p)
    try:
        t = int(parts["t"])
    except (KeyError, ValueError):
        return False
    if abs(time.time() - t) > tolerance_seconds:
        return False
    expected = hmac.new(secret.encode(), f"{t}.".encode() + raw_body, hashlib.sha256).hexdigest()
    return hmac.compare_digest(expected, parts.get("v1", ""))

Answer with any 2xx status once you have accepted the event, and do slow work afterwards: Koolbase waits at most 15 seconds for a response.

Retries and the delivery log

Any answer other than 2xx, or no answer, is retried after 1, 2, 4, 8 and 16 minutes: six attempts in all. After the last one the delivery is marked failed. Redirects are not followed. Every attempt carries the same event id, so a receiver that saw an event once can ignore it the next time.

# Recent deliveries for a webhook
curl https://api.koolbase.com/v1/projects/PROJECT_ID/webhooks/WEBHOOK_ID/deliveries \
  -H "Authorization: Bearer YOUR_ADMIN_KEY"

# Send one again, from the start
curl -X POST https://api.koolbase.com/v1/projects/PROJECT_ID/webhooks/deliveries/DELIVERY_ID/redeliver \
  -H "Authorization: Bearer YOUR_ADMIN_KEY"

Each delivery shows its status (pending, delivered or failed), attempt, next_attempt_at, last_status_code and last_error. List webhooks with GET /v1/projects/PROJECT_ID/webhooks and remove one with DELETE /v1/projects/PROJECT_ID/webhooks/WEBHOOK_ID; its deliveries go with it.

Rules

  • The URL must use https, on port 443 or 8443.
  • It must be a public address. Local, private and cloud-metadata addresses are refused, both when you create the webhook and again at the moment of every delivery, whatever the hostname resolves to.
  • A webhook watches one collection. Create more than one to cover several collections.