Reference · 05~5 minute read

Webhooks

When something happens in Kimezu that your application needs to know about, we POST a signed JSON payload to the endpoint(s) you configured. Delivery is at-least-once, ordered per-event-type, and idempotent.

Setting up a webhook

Configure webhook endpoints in the operator console under Tenant → Webhooks, or via the API:

POST   /v1/webhooks — register an endpoint
curl "https://api.kimezu.com/v1/webhooks" \
  -H "Authorization: Bearer $OP_TOKEN" \
  -H "X-Tenant-Id: rozuro" \
  -H "Content-Type: application/json" \
  -d '{
    "url":    "https://rozuro.com/internal/kimezu",
    "events": ["session.create", "permission.grant", "agent.revoke"],
    "secret": "<generated by Kimezu>"
  }'

# → 201 Created — { "webhook_id": "wh_8f2b...", "secret": "shown once" }

You can register up to 25 endpoints per tenant on the Operator plan, unlimited on Self-hosted. Each endpoint subscribes to a list of event types — wildcards ("session.*") are supported.

Payload shape

Every webhook delivery is a single JSON object with the same envelope. The data object varies per event.

POST   Webhook payload
{
  "event_id": "evt_71e03c...",
  "event_type": "session.create",
  "tenant": "rozuro",
  "created_at": "2026-05-14T08:21:04Z",
  "data": {
    "session_id": "ses_19f2...",
    "actor": "usr_8f2b...",
    "app": "rozuro.com",
    "method": "magic-link"
  }
}

# Headers sent with every delivery:
#   X-Kimezu-Event-Id:     evt_71e03c...
#   X-Kimezu-Event-Type:   session.create
#   X-Kimezu-Signature:    sha256=<hex hmac>
#   X-Kimezu-Timestamp:    1715760064
#   X-Kimezu-Delivery-Id:  del_4f9a...
#   X-Kimezu-Attempt:      1

Verifying the signature

Every delivery is signed with HMAC-SHA-256 using the endpoint secret. Verify the signature before processing — Kimezu's IP range is not a stable trust boundary.

!
Constant-time comparison. Use a constant-time compare for the signature. === in a hot path is a timing oracle.
NODE   Verify a Kimezu webhook
import crypto from "node:crypto";

function verify(req, secret) {
  const sig  = req.headers["x-kimezu-signature"];
  const ts   = req.headers["x-kimezu-timestamp"];
  const body = req.rawBody; // keep raw bytes — do NOT re-stringify JSON

  // 1. Reject deliveries older than 5 minutes (replay)
  if (Math.abs(Date.now() / 1000 - Number(ts)) > 300)
    throw new Error("stale");

  // 2. Compute expected signature
  const mac = crypto.createHmac("sha256", secret)
    .update(ts + "." + body)
    .digest("hex");
  const expected = "sha256=" + mac;

  // 3. Constant-time compare
  if (!crypto.timingSafeEqual(Buffer.from(sig), Buffer.from(expected)))
    throw new Error("bad signature");
}

Delivery semantics

  • At-least-once. If we don't get a 2xx within 10 seconds, we retry. Your handler must be idempotent — use event_id as the idempotency key.
  • Retries. 1m, 5m, 30m, 2h, 12h, 24h. After 24 hours of failure, the delivery is dropped and the endpoint is marked unhealthy in the operator console.
  • Order. Deliveries of the same event type to the same endpoint are ordered. Deliveries across event types are not — your handler shouldn't assume that permission.grant arrives before session.create for the same actor.
  • 2xx = ack. Anything in the 200–299 range counts as a successful delivery. Return as soon as you have the payload persisted; do real work asynchronously.
  • 4xx = dead-letter. A 400 / 401 / 404 / 410 from your endpoint causes the delivery to land in the dead-letter queue without further retry. Inspect it from the console.
  • 5xx = retry. Anything in the 500–599 range schedules the next retry per the back-off schedule above.

Event catalogue

Every event Kimezu emits. Names are stable; new events are additive within a major version.

EventWhenNotable data fields
tenant.createdTenant provisioned and OIDC issuer ready.tenant, region, oidc_issuer
tenant.updatedTenant configuration changed.tenant, diff
tenant.archivedTenant soft-deleted.tenant, archive_until
application.registeredNew consuming app created.tenant, client_id, name, redirect_uris
application.rotatedClient secret rotated.tenant, client_id
application.revokedApp access withdrawn.tenant, client_id
user.createdNew identity created in this tenant.tenant, sub, method
user.updatedProfile or PII fields changed.tenant, sub, diff
user.deactivatedUser can no longer authenticate.tenant, sub, reason
user.erasedGDPR Art. 17 erasure complete.tenant, sub_hash
session.createUser, group or agent signed in.session_id, actor, app, method
session.refreshAccess token rotated via refresh.session_id, actor
session.revokeSession ended (logout, MFA change, admin).session_id, reason
group.createdNew group within a tenant.tenant, group_id, owners
group.member.addedUser added to a group.tenant, group_id, sub
group.member.removedUser removed from a group.tenant, group_id, sub
permission.grantDirect grant, role assignment, or group permission change.tenant, actor, can, by
permission.revokePermission removed (any path).tenant, actor, can, by
agent.createdNew agent identity.tenant, agent_id, owner, audience, can
agent.revokeAgent token instantly invalidated.tenant, agent_id, reason
payment.profile.createdNew routing profile attached to an actor.tenant, profile_id, owner, providers
payment.recordedApplication reported a settled payment.tenant, profile_id, amount, currency, vat_treatment
payment.refundedApplication reported a refund.tenant, profile_id, amount, original_payment_id
key.rotatedTenant JWKS signing key rotated.tenant, new_kid, retired_kid
audit.headNew audit-chain head published (hourly).tenant, seq, head_hash
canary.triggeredA canary token was used — investigate.tenant, canary_id, ip, ua

Testing in development

The operator console has a Send test event button next to every webhook. It crafts a representative payload (signed correctly), POSTs it to your endpoint, and shows the response inline — body, headers, latency.

For local development, expose your endpoint via ssh -R, ngrok, or Cloudflared. Kimezu sends from a static IP range published at ips.kimezu.com; allow-list it on your edge.

Failure handling

When an endpoint fails delivery enough times to be marked unhealthy, three things happen:

  1. The endpoint stops receiving new events until you take action.
  2. A webhook.endpoint.unhealthy notification is sent to every other healthy endpoint (so your alerting still fires).
  3. Failed deliveries are stored for 7 days; the console offers a one-click "replay all" once you've fixed the endpoint.

This is the part of the protocol most often blamed for outages that aren't actually outages — write your handler to ack fast and process slow.