InboundProvider verifiers

Provider verifiers

Ready-made verifiers for Stripe, GitHub, Shopify, Twilio, and Slack — one factory each, composing like any other Verifier.

View as Markdown

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:

ProviderFactoryHeader(s)Tolerance / replayEvent shape
Standard WebhooksSecret / PublicKey / Keysetwebhook-id, webhook-timestamp, webhook-signature±5 min default, configurable{ type, data, timestamp }
StripeStripe(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
GitHubGitHub(secret)X-Hub-Signature-256 (sha256=), X-GitHub-EventNone — GitHub sends no timestamp, so there is no replay window at this layertype from X-GitHub-Event; data is the whole payload
ShopifyShopify(secret)X-Shopify-Hmac-Sha256 (base64), X-Shopify-TopicNone — Shopify sends no timestamp, so there is no replay window at this layertype from X-Shopify-Topic; data is the whole payload
TwilioTwilio(authToken, url)X-Twilio-Signature (base64 HMAC-SHA1 over url + sorted form params)None — Twilio's scheme has no time component at alltype is the fixed literal "twilio.webhook" (Twilio's wire format carries no event-type field); data is the parsed form parameters
SlackSlack(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
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 Verifiers — 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:

What's next

On this page