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:
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.
{ "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.
=== in a hot path is a timing oracle.
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_idas 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.grantarrives beforesession.createfor 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.
| Event | When | Notable data fields |
|---|---|---|
| tenant.created | Tenant provisioned and OIDC issuer ready. | tenant, region, oidc_issuer |
| tenant.updated | Tenant configuration changed. | tenant, diff |
| tenant.archived | Tenant soft-deleted. | tenant, archive_until |
| application.registered | New consuming app created. | tenant, client_id, name, redirect_uris |
| application.rotated | Client secret rotated. | tenant, client_id |
| application.revoked | App access withdrawn. | tenant, client_id |
| user.created | New identity created in this tenant. | tenant, sub, method |
| user.updated | Profile or PII fields changed. | tenant, sub, diff |
| user.deactivated | User can no longer authenticate. | tenant, sub, reason |
| user.erased | GDPR Art. 17 erasure complete. | tenant, sub_hash |
| session.create | User, group or agent signed in. | session_id, actor, app, method |
| session.refresh | Access token rotated via refresh. | session_id, actor |
| session.revoke | Session ended (logout, MFA change, admin). | session_id, reason |
| group.created | New group within a tenant. | tenant, group_id, owners |
| group.member.added | User added to a group. | tenant, group_id, sub |
| group.member.removed | User removed from a group. | tenant, group_id, sub |
| permission.grant | Direct grant, role assignment, or group permission change. | tenant, actor, can, by |
| permission.revoke | Permission removed (any path). | tenant, actor, can, by |
| agent.created | New agent identity. | tenant, agent_id, owner, audience, can |
| agent.revoke | Agent token instantly invalidated. | tenant, agent_id, reason |
| payment.profile.created | New routing profile attached to an actor. | tenant, profile_id, owner, providers |
| payment.recorded | Application reported a settled payment. | tenant, profile_id, amount, currency, vat_treatment |
| payment.refunded | Application reported a refund. | tenant, profile_id, amount, original_payment_id |
| key.rotated | Tenant JWKS signing key rotated. | tenant, new_kid, retired_kid |
| audit.head | New audit-chain head published (hourly). | tenant, seq, head_hash |
| canary.triggered | A 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:
- The endpoint stops receiving new events until you take action.
- A
webhook.endpoint.unhealthynotification is sent to every other healthy endpoint (so your alerting still fires). - 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.