Webhook payloads and signatures
Delivery envelope
Section titled “Delivery envelope”Each delivery is a POST of this JSON body:
{ "id": "kmevt_1a2b3c...", "type": "run.failed", "created": "2026-10-10T12:00:00.000Z", "summary": "company.resolve is failing for this workspace: KingMinos storage is temporarily unavailable.", "data": { "workspace_id": "ws_1a2b3c...", "capability": "company.resolve", "source_run_id": "run_9f8e7d...", "external_ref": null, "consecutive_failures": 3, "reason": "KingMinos storage is temporarily unavailable, so the run failed before any vendor call.", "remediation": "KingMinos had a storage outage. Retry the same Idempotency-Key after backoff (503 store_unavailable is retryable; do not mint a new key). See https://kingminos.com/docs/api/" }}summaryis the human-readable line (≤240 chars, emails redacted).external_refis whatever your capability call sent, for correlation.test.pingdeliveries carrydata.messageinstead.
Headers
Section titled “Headers”| Header | Meaning |
|---|---|
KingMinos-Event-Id |
Event id (kmevt_…) |
KingMinos-Delivery-Id |
Delivery attempt id (kmwhd_…) |
KingMinos-Signature |
t=<unix>,v1=<hex> HMAC signature |
The signature scheme is the same as OutboundSync’s product webhooks — v1 is HMAC-SHA256(secret, "<t>.<raw request body>"), hex-encoded. Compare with a constant-time check and reject timestamps outside a tolerance window (for example 5 minutes).
- Read the raw request body bytes exactly as received (do not parse and re-serialize JSON before verifying).
- Parse
tandv1fromKingMinos-Signature. - Compute HMAC-SHA256 over
"${t}.${rawBody}"with yourkmwhsec_…secret. - Reject if the timestamp is stale or the hex digests differ.
const crypto = require('crypto');
// rawBody: the exact bytes received (Buffer or string), before any JSON parsing.// header: the KingMinos-Signature value. secret: the whole kmwhsec_… string.function verify(rawBody, header, secret, toleranceSec = 300) { if (typeof header !== 'string') return false; const parts = {}; for (const kv of header.split(',')) { const i = kv.indexOf('='); if (i > 0) parts[kv.slice(0, i).trim()] = kv.slice(i + 1).trim(); } const t = Number(parts.t); if (!parts.t || !parts.v1 || !Number.isInteger(t)) return false; if (Math.abs(Math.floor(Date.now() / 1000) - t) > toleranceSec) return false; const expected = crypto .createHmac('sha256', secret) .update(`${parts.t}.`) .update(rawBody) .digest('hex'); const a = Buffer.from(expected); const b = Buffer.from(parts.v1); if (a.length !== b.length) return false; return crypto.timingSafeEqual(a, b);}With Express, capture the bytes with express.raw({ type: 'application/json' }) and pass req.body (a Buffer) as rawBody.
Rotate the signing secret
Section titled “Rotate the signing secret”POST /v1/webhooks/{id}/rotate-secret returns a new secret shown once. The previous secret stops working immediately (there is no grace window), so have your receiver ready, rotate, and update its secret at once.