Events and webhooks
Tested with: Webhook signing v1 (HMAC-SHA256) · Node 22 · Python 3.12 · Go 1.23
Databasezy publishes an event for every state change. Internally they flow through a transactional outbox to Kafka; externally you can receive them as signed webhooks (all plans) or as a SIEM stream (Audit log and SIEM export).
Event catalogue
Section titled “Event catalogue”| Event | When |
|---|---|
instance.requested.v1, instance.ready.v1, instance.failed.v1 | Creation lifecycle |
instance.paused.v1, instance.resumed.v1, instance.resized.v1, instance.deleted.v1 | Operations |
instance.credentials.rotated.v1 | Rotation started or old credential revoked |
backup.completed.v1, backup.failed.v1, restore.completed.v1 | Backups |
migration.preflight.v1, migration.progress.v1, migration.verified.v1, migration.completed.v1, migration.failed.v1 | Migrations |
quota.warning.v1, quota.exceeded.v1, budget.threshold.v1 | Limits and spend |
org.member.invited.v1, org.member.removed.v1, org.baa_signed.v1 | Organization |
approval.requested.v1, approval.decided.v1 | Approvals |
usage.egress.v1 (SIEM stream only) | Egress deltas every 10 seconds per connection |
Names carry a version suffix; fields are only added within a version. A new major version is published alongside the old one for at least 90 days.
Delivery
Section titled “Delivery”# The signing secret is in the response, oncezb api POST /v1/orgs/{org}/webhooks -d '{"url": "https://example.com/hooks/databasezy", "events": ["instance.*", "backup.*"]}'POSTwithContent-Type: application/json, one event per request.- Headers:
ZB-Event-Id,ZB-Event-Type,ZB-Timestamp(Unix seconds),ZB-Signature: v1=<hex hmac>. - Signature: HMAC-SHA256 over
"{timestamp}.{raw body}"with the endpoint secret. Reject timestamps older than 5 minutes. Secrets can be rotated with an overlap; during it two signatures are sent (v1=…,v1=…). - Retries with exponential backoff for 24 hours on non-2xx; deliveries are idempotent by
ZB-Event-Id. - Up to 10 endpoints per org; per-endpoint delivery log in the portal.
Verifying a signature
Section titled “Verifying a signature”import { createHmac, timingSafeEqual } from "node:crypto";
export function verify(rawBody: string, headers: Headers, secret: string): boolean { const ts = headers.get("zb-timestamp") ?? ""; if (Math.abs(Date.now() / 1000 - Number(ts)) > 300) return false; const expected = createHmac("sha256", secret).update(`${ts}.${rawBody}`).digest("hex"); return (headers.get("zb-signature") ?? "") .split(",") .some((sig) => { const [, hex] = sig.trim().split("="); return hex?.length === expected.length && timingSafeEqual(Buffer.from(hex, "hex"), Buffer.from(expected, "hex")); });}import hmac, hashlib, time
def verify(raw_body: bytes, headers: dict, secret: str) -> bool: ts = headers.get("ZB-Timestamp", "") if abs(time.time() - float(ts or 0)) > 300: return False expected = hmac.new(secret.encode(), f"{ts}.".encode() + raw_body, hashlib.sha256).hexdigest() return any( hmac.compare_digest(sig.strip().split("=", 1)[1], expected) for sig in headers.get("ZB-Signature", "").split(",") if "=" in sig )package hooks
import ( "crypto/hmac" "crypto/sha256" "encoding/hex" "math" "net/http" "strconv" "strings" "time")
func Verify(body []byte, h http.Header, secret string) bool { ts := h.Get("ZB-Timestamp") t, err := strconv.ParseFloat(ts, 64) if err != nil || math.Abs(float64(time.Now().Unix())-t) > 300 { return false } mac := hmac.New(sha256.New, []byte(secret)) mac.Write([]byte(ts + ".")) mac.Write(body) expected := hex.EncodeToString(mac.Sum(nil)) for _, sig := range strings.Split(h.Get("ZB-Signature"), ",") { if _, v, ok := strings.Cut(strings.TrimSpace(sig), "="); ok && hmac.Equal([]byte(v), []byte(expected)) { return true } } return false}Payload
Section titled “Payload”{ "id": "evt_01J9QK8X6Z4M2T5N8R3B7C1D9E", "type": "instance.ready.v1", "time": "2026-09-27T10:14:03.221Z", "org_id": "org_01J8...", "data": { "instance_id": "inst_01J9...", "engine": "postgres", "version": "17", "size": "s1", "region": "us-east", "placement": "shared", "host": "postgres-7f3k.us-east.databasezy.com", "port": 5432 }}Payloads never contain credentials.