> ## Documentation Index
> Fetch the complete documentation index at: https://docs.knoxcall.com/llms.txt
> Use this file to discover all available pages before exploring further.

# Inbound Webhook format guide

> Wire-level details for each HMAC format KnoxCall verifies — header names, signed-payload construction, replay-protection behaviour.

# Inbound Webhook format guide

This page is the wire-level reference for each format the [Inbound Webhooks verifier](/essentials/inbound-webhooks/overview) supports. If you're integrating a custom provider, the rules below tell you exactly what KnoxCall computes when it verifies a request.

All formats use **HMAC-SHA256** with the shared secret you configured on the subscription. Comparisons are constant-time (`crypto.timingSafeEqual`) — there's no timing oracle to exploit.

## Stripe

**Header:** `Stripe-Signature: t=<unix-seconds>,v1=<hex-sha256>` (multiple `v1=...` pairs accepted — Stripe rotates secrets this way during a cutover window)

**Signed payload:** `<timestamp>.<body>` — the `t=` value, a literal `.`, then the raw request body.

**Replay protection:** yes — KnoxCall rejects with `replay_window_exceeded` if `now - t > replay_window_seconds`. Default 300 (5 min) matches Stripe's recommendation.

**Compatibility:** anything that adopted Stripe's signature format. The verifier accepts a request when **any** of the listed `v1=` values matches; that's how Stripe handles secret rotation. If you rotate the secret on KnoxCall, an in-flight cutover where the upstream sends both old and new signatures will keep working until you update the upstream config.

## GitHub

**Header:** `X-Hub-Signature-256: sha256=<hex-sha256>`

**Signed payload:** the raw body bytes only.

**Replay protection:** no — GitHub webhooks include a delivery ID but don't sign it, so KnoxCall can't enforce a timestamp window. If your application can't tolerate replay (e.g. webhooks that mutate state by content rather than idempotency key), enforce the check on your side using GitHub's `X-GitHub-Delivery` header.

**Compatibility:** GitHub webhooks, Postmark, SendGrid Inbound Parse, and most internal SaaS that copied GitHub's scheme.

## Slack

**Headers:**

* `X-Slack-Signature: v0=<hex-sha256>`
* `X-Slack-Request-Timestamp: <unix-seconds>`

**Signed payload:** `v0:<timestamp>:<body>` — the literal string `v0:`, the timestamp, another `:`, then the raw body.

**Replay protection:** yes — KnoxCall enforces the configured window against `X-Slack-Request-Timestamp`. Default 300 matches Slack's documented recommendation.

**Compatibility:** Slack events, slash commands, interactive components.

## AWS SNS (HMAC variant)

**Headers (outbound):**

* `x-amz-sns-signature: <base64-hmac-sha256>`
* `x-amz-sns-signature-version: 1-hmac`

**Signed payload:** the raw body only.

**Replay protection:** no.

**Inbound verification:** KnoxCall checks only `x-amz-sns-signature`. The `x-amz-sns-signature-version` header is present on outbound deliveries but not required for inbound verification.

> ⚠️ This is **not** real AWS SNS. Real SNS uses RSA over a canonicalised message envelope, not HMAC. KnoxCall's `aws-sns` format is an HMAC variant intended for KnoxCall-to-KnoxCall flows or internal services that wanted SNS-shaped headers without the RSA tooling. If you're verifying real AWS-issued SNS messages, this format won't match — let us know if RSA SNS is a deal-breaker for you.

## KnoxCall legacy

**Header:** `X-Webhook-Signature: sha256=<hex-sha256>`

**Signed payload:** the raw body only.

**Replay protection:** no.

This is the format KnoxCall's outbound [Webhook Signing](/essentials/webhook-signing/overview) emitted before the multi-format library landed. Use it when both ends are KnoxCall (KnoxCall-to-KnoxCall internal events, or one KnoxCall tenant fanning to another).

## Custom

**Header:** *(caller-named, e.g. `X-MyApp-Signature`)* with value `sha256=<hex-sha256>`

**Signed payload:** the raw body only.

**Replay protection:** no.

For internal tools that don't match a named format. Set `custom_header_name` when you create the subscription. The verifier only knows that *one* HMAC-SHA256 of the body is in the named header — it doesn't try to be clever about timestamps.

## Reproducing a verification yourself

If you want to confirm KnoxCall's behaviour from the outside (or reproduce why a request failed), here's the GitHub-format check in seven lines of Node:

```javascript theme={"dark"}
import crypto from 'node:crypto';

const secret = 'whsec_...';
const body   = '...raw bytes from the request...';
const header = 'sha256=...';  // X-Hub-Signature-256

const expected = 'sha256=' + crypto.createHmac('sha256', secret).update(body).digest('hex');
const ok = crypto.timingSafeEqual(Buffer.from(expected), Buffer.from(header));
console.log(ok);  // → true if KnoxCall would also accept this request
```

For Stripe, the body becomes `${timestamp}.${rawBody}` and the header parses out as `t=...,v1=...`. The Slack version uses `v0:${timestamp}:${rawBody}` and the `X-Slack-Signature: v0=` header.

The verifier source is at [src/inbound-webhooks/verifier.ts](https://github.com/knoxcall/knoxcall/blob/main/src/inbound-webhooks/verifier.ts) if you want to read the canonical implementation.

## What happens when verification fails

KnoxCall surfaces the precise reason in the `signature_error` column of API Logs and (when forwarding with `on_invalid='forward_with_error_header'`) in the `X-Knox-Verify-Reason` HTTP header. Common values:

| Reason                             | Meaning                                                                                                                                      |
| ---------------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------- |
| `missing <header> header`          | The request didn't include the header KnoxCall expected for this format                                                                      |
| `malformed <header>`               | The header was present but didn't match the format's regex (e.g. not `sha256=<64-hex>`)                                                      |
| `replay_window_exceeded`           | Timestamp was outside `[now − replay_window_seconds, now + replay_window_seconds]`                                                           |
| `no v1 signature matched` (Stripe) | Header parsed correctly but none of the listed `v1=` values verified — most often a wrong-secret or rotated-secret-on-only-one-side scenario |
| `Stripe-Signature missing t=`      | The Stripe header didn't carry a timestamp; usually a misconfigured custom shim claiming to be Stripe                                        |

## Next steps

* [Inbound Webhooks overview →](/essentials/inbound-webhooks/overview)
* [Outbound Webhook Signing →](/essentials/webhook-signing/overview) — the inverse direction
