Skip to content

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).

EventWhen
instance.requested.v1, instance.ready.v1, instance.failed.v1Creation lifecycle
instance.paused.v1, instance.resumed.v1, instance.resized.v1, instance.deleted.v1Operations
instance.credentials.rotated.v1Rotation started or old credential revoked
backup.completed.v1, backup.failed.v1, restore.completed.v1Backups
migration.preflight.v1, migration.progress.v1, migration.verified.v1, migration.completed.v1, migration.failed.v1Migrations
quota.warning.v1, quota.exceeded.v1, budget.threshold.v1Limits and spend
org.member.invited.v1, org.member.removed.v1, org.baa_signed.v1Organization
approval.requested.v1, approval.decided.v1Approvals
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.

shell
# The signing secret is in the response, once
zb api POST /v1/orgs/{org}/webhooks -d '{"url": "https://example.com/hooks/databasezy", "events": ["instance.*", "backup.*"]}'
  • POST with Content-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.
verify.ts
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"));
});
}
instance.ready.v1
{
"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.