Webhooks let your integration react to changes in a Kodori workspace without polling. Open a subscription on /webhooks (owners + admins only) by pointing at an HTTPS URL and optionally listing the event types you care about.
The signing secret is shown exactly once at creation. Copy it into your secret store immediately — only the prefix is visible afterward. Format: whsec_<prefix8>_<secret40>. Distinct from the API-key shape (k_<prefix>_<secret>) so a leaked one can't be confused with the other.
What you get per delivery:
- Method: POST - Headers: Content-Type: application/json X-Kodori-Event: <event type, e.g. "document.created"> X-Kodori-Event-Id: <uuid of the originating event> X-Kodori-Subscription: <uuid of your subscription> X-Kodori-Signature: sha256=<hex> (HMAC-SHA256 over <ts>.<body>) X-Kodori-Timestamp: <iso8601> User-Agent: Kodori-Webhooks/1.0 - Body: a JSON object with id, type, tenantId, streamId, streamVersion, actorId, actorKind, createdAt, payload.
Verify every delivery — see /help/webhook-signature-verification for the exact recipe. A delivery fails (we retry it) if your endpoint times out (10s), returns a non-2xx response, or doesn't respond. Inngest's step-retry handles up to the per-subscription retry cap with exponential backoff; after that the delivery is recorded as failed in the /webhooks delivery log.
**Per-subscription retry policy.** Each subscription carries a max_retry_attempts value (default 4, range 1-10). Tune it with the "Retries" input on the /webhooks per-subscription row. Use a lower value (e.g. 2) for downstreams known to be flaky where excess retries DOS them; use a higher value (e.g. 8) for critical receivers where you want maximal effort to deliver. The function-level Inngest cap is 10 (the absolute ceiling); the per-sub value is the operator-controllable knob below it. Saving emits webhook.retry-policy-set on the audit chain with previous + new values in the payload. Idempotent — saving the same value emits no event. Takes effect on the next event delivery; in-flight retries finish under the prior cap.
**Send test event.** When setting up a new endpoint or troubleshooting a misconfigured receiver, click "Send test" on the active subscription's row. Kodori posts a real signed payload — same HMAC scheme, same headers, same body shape as a production delivery — with X-Kodori-Event: webhook.test and a payload containing `isTest: true`, `initiatedBy` (your email), the destination URL, and an explanatory message. Reuses the production delivery code path so a passing test means real deliveries work. Test events land on the audit chain (event type `webhook.test`) so compliance can verify "admin tested webhook X at timestamp Y" without a separate test log. The test direct-dispatches to the targeted subscription only — bypasses fanout so other subs whose eventTypes filter accepts `webhook.test` don't receive the test (clean isolation). Refuses on paused / revoked subscriptions ("Resume it first") to avoid accidental re-arming.
**At-a-glance 7-day health badge.** Every active subscription on /webhooks renders an inline health badge: `7d: <success_pct>% · <total> deliveries · last fail <relative>`. Color-coded — green when success ≥ 95% with no recent failures, amber when 80-95% OR a failure in the last 4h on an otherwise-healthy sub, red when < 80%. `7d: no deliveries` (neutral gray) when the sub had zero matching events in the window — silence isn't failure. Hover for the raw counts (`Last 7 days: 21 succeeded / 23 total (2 failed)`). Closes the "did my receiver start failing?" question without drilling into the per-sub deliveries log. The deliveries log itself (Deliveries → link) still has the full per-row detail with All / Succeeded / Failed filter chips for triage.
**Bulk-redeliver after an outage.** When a receiver is restored after downtime, the per-sub deliveries page surfaces a "Redeliver all failed" form (admin / owner only, visible only when at least one failed delivery exists). Window dropdown — last 24h / 7d / 14d / 30d. Click; Kodori re-fires every DISTINCT failed event in the window through Inngest. selectDistinct on eventId means multiple failed retries of the same event collapse into one redelivery (so a heavily-retried event doesn't fan out into N redelivers). Hard cap of 500 unique events per call — narrow the window if you have more. Refuses on paused / revoked subs ("Resume it first").
Common event types to subscribe to:
- document.created — a new document was uploaded - document.version-committed — a new version was added to an existing doc - document.content-extracted — extraction finished and text is searchable - document.tombstoned — a document was soft-deleted - document.metadata-set — metadata jsonb was patched (key + value in payload) - document.sensitivity-changed - document.retention-class-changed - collection.member-added / collection.member-removed - permission.granted / permission.revoked - legal-hold.applied / legal-hold.released
Leave the event-type filter empty to subscribe to every event in your tenant — useful for an audit-export integration that wants the complete chain.
Status of a subscription is one of active / paused / revoked. Pause when you're shipping a deploy that might break your endpoint; resume when ready. Revoke is permanent — the secret can't be re-used.
## Auditing deliveries
The /webhooks page surfaces a flat last-50 across the whole tenant for the at-a-glance health check. Drill into a single subscription with the "Deliveries →" link on its row — that opens /webhooks/<id>/deliveries which shows:
- Top-of-page stats: total deliveries / succeeded / failed / success rate (color-coded green ≥95%, amber ≥80%, red below) - All / Succeeded / Failed status filter chips with live counts - 25 rows per page with Older / Newer pagination - Per-row: timestamp, event type + short id, status badge with HTTP code, response-body snippet on failure, attempts count - A "Redeliver" button on every row (admin only) that re-fires the same (event × subscription) match through Inngest. Use this after fixing a receiver — does NOT recreate the source event (audit log stays single-truth), only enqueues a fresh delivery attempt. Refused when the subscription is paused / revoked; resume it first.
Use the /deliveries panel to triage: filter by failed, scan response snippets, fix the receiver, redeliver one or two failed events, confirm green, then either replay the rest one-by-one or accept the loss.