Skip to main content
Every delivery is signed with your endpoint’s secret so you can confirm it came from Kit and the body wasn’t altered in transit. Always verify before acting on a payload.

The signature

Kit sends an X-Kit-Signature header with a timestamp and one or more signatures, comma-separated:
  • t — Unix timestamp (seconds) of when this POST was signed.
  • v1 — HMAC-SHA256 of the string "{t}.{raw_body}" (the timestamp, a literal ., then the raw request body), keyed with your endpoint’s secret and hex-encoded. During secret rotation the header carries one v1 entry per valid secret.
The timestamp is part of the signed string, so an attacker can’t replay an old body with a fresh timestamp. The secret is returned in plaintext only when you create the endpoint or rotate the secret — store it securely; Kit never returns it again.

Verifying

Parse the header, rebuild the signed string from t and the exact bytes you received, compute the HMAC, and compare it against every v1 entry using a constant-time comparison. Reject stale timestamps to guard against replays — 5 minutes is a sensible tolerance (retries are re-signed at send time, so a legitimate late retry still carries a fresh t).
Verify against the raw request body, byte for byte. If your framework parses JSON before you can read the raw bytes, capture the raw body first — re-serializing changes whitespace and key order and the signature won’t match.

Secret rotation

Rotate an endpoint’s secret at any time with POST /v4/webhook_endpoints/{id}/rotate_secret — the response is the only place the new secret appears in plaintext. To avoid dropping deliveries while you roll it out, the old secret keeps verifying until the overlap window closes (the endpoint’s previous_secret_expires_at). During the window Kit signs with both secrets, so the header carries two v1 entries:
This is why the examples above check whether any v1 value matches — do that and your verification keeps working across a rotation with no code change. Once you’ve switched to the new secret you can close the window early with POST /v4/webhook_endpoints/{id}/revoke_previous_secret.
Rotating again while a previous rotation’s window is still open returns 409 — pass force: true to rotate anyway, immediately expiring the older secret. A retried delivery is signed with whichever secrets are valid at send time, so a late retry that lands after the window closes carries a single v1.

Idempotency

The same event can arrive more than once — a retry re-POSTs the whole delivery after your server was briefly unreachable, or an upstream job retry re-emits the event in a fresh delivery. Every event carries a UUID id that stays the same across re-sends: record it and skip any event you’ve already processed. Design your handler to be safe to run twice regardless.
Deduplicate on the event id, not on delivery_id — a re-emitted event can arrive under a different delivery.