---
title: "Webhook payloads and signatures"
description: ""
canonical: "https://kingminos.com/docs/webhooks/payloads-and-signatures/"
sourceKind: "source-mdx"
---

## Delivery envelope

Each delivery is a `POST` of this JSON body:

```json
{
  "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/"
  }
}
```

- `summary` is the human-readable line (≤240 chars, emails redacted).
- `external_ref` is whatever your capability call sent, for correlation.
- `test.ping` deliveries carry `data.message` instead.

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

1. Read the raw request body bytes exactly as received (do not parse and re-serialize JSON before verifying).
2. Parse `t` and `v1` from `KingMinos-Signature`.
3. Compute HMAC-SHA256 over `"${t}.${rawBody}"` with your `kmwhsec_…` secret.
4. Reject if the timestamp is stale or the hex digests differ.

```js
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

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