# Provider verifiers



`Secret`, `PublicKey`, and `Keyset` speak Standard Webhooks. For the top non-Standard-Webhooks senders, `@postel/core` ships ready-made verifiers instead of asking you to hand-write one:

| Provider          | Factory                           | Header(s)                                                               | Tolerance / replay                                                                         | Event shape                                                                                                                             |
| ----------------- | --------------------------------- | ----------------------------------------------------------------------- | ------------------------------------------------------------------------------------------ | --------------------------------------------------------------------------------------------------------------------------------------- |
| Standard Webhooks | `Secret` / `PublicKey` / `Keyset` | `webhook-id`, `webhook-timestamp`, `webhook-signature`                  | ±5 min default, configurable                                                               | `{ type, data, timestamp }`                                                                                                             |
| Stripe            | `Stripe(secret, options?)`        | `Stripe-Signature` (`t=`, one or more `v1=`)                            | ±300s default (Stripe's own default), configurable via `{ toleranceSeconds, clock }`       | `type`/`data` from the event body                                                                                                       |
| GitHub            | `GitHub(secret)`                  | `X-Hub-Signature-256` (`sha256=`), `X-GitHub-Event`                     | None — GitHub sends no timestamp, so there is no replay window at this layer               | `type` from `X-GitHub-Event`; `data` is the whole payload                                                                               |
| Shopify           | `Shopify(secret)`                 | `X-Shopify-Hmac-Sha256` (base64), `X-Shopify-Topic`                     | None — Shopify sends no timestamp, so there is no replay window at this layer              | `type` from `X-Shopify-Topic`; `data` is the whole payload                                                                              |
| Twilio            | `Twilio(authToken, url)`          | `X-Twilio-Signature` (base64 HMAC-SHA1 over `url` + sorted form params) | None — Twilio's scheme has no time component at all                                        | `type` is the fixed literal `"twilio.webhook"` (Twilio's wire format carries no event-type field); `data` is the parsed form parameters |
| Slack             | `Slack(signingSecret, options?)`  | `X-Slack-Signature` (`v0=`), `X-Slack-Request-Timestamp`                | ±300s default (Slack's own recommendation), configurable via `{ toleranceSeconds, clock }` | `type`/`data` from the event body                                                                                                       |

```ts title="lib/postel.ts"
import { Postel, Stripe, GitHub, Shopify, Twilio, Slack } from "@postel/core";
import { config } from "./config.js";

export const postel = Postel({
  inbound: {
    stripe: { verify: Stripe(config.stripeWebhookSecret) },
    github: { verify: GitHub(config.githubWebhookSecret) },
    shopify: { verify: Shopify(config.shopifyWebhookSecret) },
    twilioSms: {
      verify: Twilio(config.twilioAuthToken, "https://api.example.com/webhooks/twilio/sms"),
    },
    slack: { verify: Slack(config.slackSigningSecret) },
  },
});
```

All five are ordinary `Verifier`s — they compose in arrays/maps, report `matchedVerifierIndex`, and throw the same `SignatureInvalid`/`MalformedHeader`/`TimestampTooOld` errors as the Standard Webhooks verifiers. One important difference: the secret you pass to any of them is used as **literal UTF-8 key material**, exactly as that provider issues it (a Stripe secret keeps its `whsec_` prefix as part of the key) — it is *not* run through the `whsec_`-strip-then-base64-decode convention `Secret`/`PublicKey` use for Standard Webhooks secrets. `Twilio`'s `url` must be the exact webhook endpoint URL registered with Twilio (scheme, host, path, and query string) — Twilio signs over that URL, so a reverse proxy that rewrites what your app sees will break verification, the same caveat Twilio's own SDKs carry.

Each provider has its own page with the full recipe — the config one-liner, a gated route, the scheme's details, its gotchas, and how to test locally:

<Cards>
  <Card title="Stripe" href="/docs/inbound/providers/stripe" description="Stripe-Signature: t= + v1= HMAC-SHA256, ±300s window, secret rolls." />

  <Card title="GitHub" href="/docs/inbound/providers/github" description="X-Hub-Signature-256 over the raw payload; type from X-GitHub-Event." />

  <Card title="Shopify" href="/docs/inbound/providers/shopify" description="Base64 HMAC in X-Shopify-Hmac-Sha256; topic from X-Shopify-Topic." />

  <Card title="Twilio" href="/docs/inbound/providers/twilio" description="SHA-1 over URL + sorted form params — the exact registered URL matters." />

  <Card title="Slack" href="/docs/inbound/providers/slack" description="v0 signing scheme with its own ±300s timestamp window." />
</Cards>

## What's next [#whats-next]

* [Custom verifiers](/docs/inbound/custom-verifiers) — write one for any other sender.
* [Verify a signed request](/docs/inbound/verify) — the base recipe these plug into.
* [Key rotation](/docs/inbound/key-rotation) — composing old + new keys during a window.
