OneShot

← All packs

Webhook Signature Verification Starter

verified

Check that an incoming webhook really came from Stripe, GitHub, Shopify or Resend — and reject the ones that didn't.

You have a webhook endpoint. Free, and it takes about a minute to prove it works.

How you actually use this

You don’t install a library or wire up an SDK. Your own coding agent builds the code in your project, and you keep it — no runtime dependency on us.

  1. Step 1

    Download and unzip

    You get a folder: the docs that tell an agent what to build, a starting skeleton, and the test suite that decides when it's done.

  2. Step 2

    Open it in Claude Code or Cursor

    Point your coding agent at the folder. Nothing to install, no account with us, no API key.

  3. Step 3

    Paste one prompt

    The pack contains the exact prompt. Paste it as your first message and leave it alone — it works through the build itself, choosing a cheaper or stronger model per task.

  4. Step 4

    Run ./verify.sh

    One command. It prints a pass or fail for every check. Green means the build is done — the same script we ran to produce the receipt on this page.

Typical build: about 7 minutes of your agent working, mostly unattended. Then you integrate the working module into your app the way you would any code you’d written yourself.

Technically: Correct inbound webhook signature verification for Stripe, GitHub, Shopify and Svix/Resend behind one seam — raw bytes, constant-time comparison, and an honest account of which two of the four schemes actually bound replay — as a free build pack with 30 hermetic checks your own agent must turn green.

Execution receipt3 of 3 runs passed
Model
claude-sonnet-5
Run at
2026-08-04T19:28:25+00:00
Pack version
78a3c8afc257
Tokens in / out
3,625,293 / 50,472 · per-run mean · billed input includes 3,315,196 cache reads, priced far below fresh input
Wall time
7 min · per-run mean
Run cost
$2.42 · what this verification run cost us at API pricing, per build pass · your plan and model prices differ
Deploy
not deployed

Acceptance checks

  • node-versionPASS
  • installPASS
  • contract-filesPASS
  • typecheckPASS
  • stripe-validPASS
  • stripe-tampered-bodyPASS
  • stripe-wrong-secretPASS
  • stripe-expired-timestampPASS
  • stripe-rotated-secret-multi-v1PASS
  • stripe-v0-downgrade-refusedPASS
  • github-validPASS
  • github-tampered-bodyPASS
  • github-wrong-secretPASS
  • github-sha1-not-acceptedPASS
  • github-replay-window-absentPASS
  • shopify-validPASS
  • shopify-tampered-bodyPASS
  • shopify-wrong-secretPASS
  • shopify-triggered-at-unsignedPASS
  • svix-validPASS
  • svix-tampered-bodyPASS
  • svix-wrong-secretPASS
  • svix-expired-timestampPASS
  • svix-future-timestampPASS
  • svix-secret-base64-decodedPASS
  • raw-body-vs-parsed-bodyPASS
  • constant-time-comparisonPASS
  • header-case-insensitivePASS
  • missing-headers-rejectedPASS
  • framework-raw-body-adaptersPASS

Versions lock

node-typescript

node24.18.1
typescript6.0.3
@types/node24.3.0
stripe-api-version2026-06-24.dahlia
github-rest-api-version2022-11-28
shopify-admin-api-version2026-07

Scaffold integrity

Built pristine — no scaffold files modified

Webhook signature verification looks like four lines of crypto.createHmac and is wrong in about six specific ways — verifying a re-serialised body instead of the raw bytes, comparing digests with ===, falling back to GitHub's legacy SHA-1 header, keeping only the first v1= during a Stripe secret rotation, treating Svix's whsec_ secret the way Stripe's is treated, and enforcing a replay window off a timestamp header that isn't inside the signature. This pack has your own coding agent build one verifier for Stripe, GitHub, Shopify and Svix/Resend behind a single function, then proves it against 30 hermetic checks that run offline in under a minute. The execution receipt on this page is the published proof; the better one is the suite itself, which ships in the pack and needs no accounts, no keys and no Docker to run.

Example use cases

  • An indie SaaS adding its first Stripe webhook. The endpoint works in stripe listen and 401s the first time a customer with a non-ASCII name pays, because the handler parses the body before verifying and the JSON round-trip is only byte-identical for simple payloads. The raw-body-vs-parsed-body check is that bug, caught before deploy.
  • A GitHub App receiving push and pull_request events. The obvious implementation reads whichever signature header is present, which quietly accepts SHA-1. GitHub still sends it "only for legacy purposes"; a verifier that falls back to it has downgraded itself. github-sha1-not-accepted is a four-line fix and a check that stops it coming back.
  • A Shopify app developer who copy-pasted a working GitHub verifier. Shopify's digest is base64, GitHub's is hex. Every delivery 401s, the secret looks wrong, and an hour goes into the wrong hypothesis.
  • A team on Resend (or Clerk, Brex, Lithic, Loops — anything on Svix) that already has Stripe working. Both secrets start with whsec_. Stripe's is the HMAC key verbatim; Svix's is base64 that must be decoded first. Reusing the Stripe code rejects 100% of genuine deliveries while looking completely reasonable.
  • Anyone reviewing someone else's webhook handler. The 30 check names are the review checklist, and running them takes less time than reading the diff.
  • Anyone evaluating OneShot. This is a complete pack — same layout, same hermetic verify, same receipt contract as the paid ones — that costs nothing and takes a minute to run.

Why this pack

Honest comparison. This pack is free, so there is no cost saving to claim and none is claimed; what varies between the columns is correctness risk and what you own afterwards.

Bare agent, no packHosted webhook gateway (Hookdeck / Svix-class)Hand-rolling from the docsThis pack
Costfree (agent time only)recurring subscription, priced by event volume — check their current pricingfree (your time: realistically a few hours per provider, most of it reading)free
What it actually solvesusually one provider, usually correctly for the happy pathdelivery: it terminates the webhook, verifies, retries, replays, and forwards to youwhatever you got to before shippingauthenticity across four schemes, with the six specific failure modes below pre-solved and graded
Correctness riskmoderate and silent. The bugs below are not exotic; they are what a confident implementation looks likelow for schemes they support — but you have moved the trust boundary to themmoderate, and concentrated in the details that only bite in productionbounded by 30 checks, including four that assert a re-serialised body is rejected — and currently unproven by a receipt
Lock-innonereal: your ingestion path and your event ids run through their infrastructurenonenone — one folder of plain TypeScript, zero runtime dependencies, no call home
What you still have to buildeverything after "is it authentic"the samethe samethe same, and the pack says so in ARCHITECTURE D11 rather than implying otherwise

The six failure modes it pre-solves — specific bug classes, not a claim that every implementation hits every one:

  • Verifying a re-serialised body. JSON.parse then JSON.stringify produces different bytes the moment the payload has non-ASCII text, unusual key order or a float. Stripe's own docs list the mutations frameworks apply; Shopify names express.json() explicitly. This is the number-one cause and it is intermittent, which is what makes it expensive.
  • Comparing digests with ===. Short-circuits at the first differing byte. GitHub, Stripe and Svix each tell you not to, in those words.
  • "Fixing" that with timingSafeEqual and no length guard. It throws RangeError on unequal lengths, and the signature is attacker-controlled — so the security fix converts every malformed signature into an unhandled 500.
  • Falling back to a weaker header. GitHub still sends SHA-1. Stripe still sends a v0 test scheme and explicitly says to ignore it.
  • Handling one v1 when there can be several. Stripe sends one signature per active secret for up to 24 hours while an endpoint secret rolls. Reading only the first fails roughly half of deliveries during exactly the window you least want an outage.
  • Enforcing a replay window that doesn't exist. Shopify sends X-Shopify-Triggered-At and does not sign it, so an attacker replaying a captured delivery just rewrites it. GitHub sends no timestamp at all. The pack records this in data (SCHEMES[p].signedTimestamp) and proves it with a check that forges the header to 1970 and asserts verification still passes.

Scale envelope

Derived from the pinned stack — a node:crypto HMAC and a byte comparison — not from a benchmark. Nothing here is load-tested; the suite proves correctness, not throughput, and ACCEPTANCE.md says so.

  • Per request the module does exactly one thing: one HMAC-SHA256 over the body, one constant-time comparison, and for two of the four providers one integer subtraction. No I/O, no allocation beyond the body itself, no shared state, nothing to warm up. It is not the bottleneck in any webhook endpoint, and it does not need to be tuned.
  • The realistic bound is your framework's request concurrency, exactly as it was before you added this.
  • First ceiling: body size, not rate. Both raw-body helpers buffer the whole body in memory, because an HMAC cannot be checked until the last byte has arrived — that is a property of the construction, not a shortcut. At typical webhook payload sizes (single-digit kilobytes) this is irrelevant. If you receive multi-megabyte bodies, cap the size before buffering, or hash the stream incrementally and compare at the end. Either way, reject oversized bodies at the edge; an unbounded buffer on an unauthenticated endpoint is a denial-of-service surface that no signature check can close.
  • Second ceiling — and the real one: there is no second webhook problem solved here. The module has no state, so it scales trivially and stops being the interesting question immediately. The next question is deduplication, and that one needs a database.
  • Upgrade path: add a size cap at the edge; add a fifth provider (one file, one entry in two records); then, when duplicates or ordering start mattering, move to a durable pipeline — see below.

What it comfortably covers as shipped: any application receiving webhooks from one to four of these providers, at any volume its own framework can serve. What it isn't: a webhook pipeline. It is the front door.

Verified against

  • claude-opus-5
  • claude-sonnet-5
  • claude-haiku-4-5

Supported stacks

  • node-typescript
    node24.18.1
    typescript6.0.3
    @types/node24.3.0
    stripe-api-version2026-06-24.dahlia
    github-rest-api-version2022-11-28
    shopify-admin-api-version2026-07

Estimated buyer token cost

361,000 tokens for one build pass (estimate, not a guarantee).

FAQ

It's free — what's the catch?

There isn't one, but there is a business model and you should know it. This pack is top-of-funnel: it exists so that people who try OneShot's paid packs have already run one of ours end to end. It is monetised two ways, both disclosed. One is a Railway referral link in GETTING-STARTED — Railway pays us a commission or credit if you sign up through it, it does not change what you pay, and nothing in the pack requires Railway or any host at all. The other is that the pack names webhook-durability ($149) as the paid companion for the problem it stops before. The code, the tests and the docs are the same standard as the paid packs; a free pack that doesn't work would cost us more than it earns.

What if the build fails?

Run ./verify.sh — it is deterministic, offline, and needs no accounts, so it fails the same way on your machine as on ours. There is no refund policy here because there is no purchase; if it fails on the pinned stack and a model from the matrix, open an issue on the public repo with the verify output. If your agent modified a frozen file (test/, verify.sh, package.json, tsconfig.json or src/verify/types.ts) to make a check pass, that is the first thing to check — the pack's own verify prompt tells it to fix the implementation, never the test.

What does the receipt actually prove, and what does it not?

It records what a clean-room harness run measured: how many of N runs passed on the pinned model, per-check results, per-run token volume and wall time, the locked dependency versions, and whether the buyer-agent left the scaffold pristine. It is a measurement of this pack on that model, not a promise about your run — your codebase, your agent and your prompts all differ. The arbiter is the same either way: the acceptance suite ships in the pack, so you can run it before you trust a word here. Where a figure elsewhere in the pack is an author estimate it says so, and the receipt overrides it.

Why these four providers and not Twilio / Slack / PayPal / Apple / Google?

Stripe and GitHub are the two most widely implemented webhook verifications in existence. Shopify is the one whose base64 digest breaks copy-pasted GitHub code. Svix is the shared transport under Resend, Clerk, Brex, Lithic, Loops and a long tail of others, so implementing it once covers many senders. Four schemes is enough to make the differences the lesson — hex versus base64, one timestamp window versus two directions versus none, one whsec_ prefix meaning two different things. Apple and Google are deliberately excluded: both are asymmetric schemes (a JWS with a certificate chain; a Google-signed OIDC token) where hand-rolling is the wrong advice, and both are covered by webhook-durability using Apple's own verifier library.

Do I need all four?

No. Each is one file and one entry in two records; the ones you don't configure simply have no secret. Building all four is still worth it even if you use one, because the cross-provider differences are where the design decisions become obvious, and the suite grades all four regardless.

What models does this need?

Tasks are pre-tagged cheap/standard/capable in TASKS.md and routed accordingly. Exactly one task — the final fix pass — is Opus-tier; two are Sonnet, five are Haiku. The PRD's estimate is 1 agent session inside a $20/mo plan, whose binding constraint is prompt volume in Claude's 5-hour rolling window rather than raw tokens (Anthropic publishes no exact token-per-window figure, so treat the session count as an expectation). Sonnet-only completion is expected to work end to end.

Can I customise it after the build, and do I own the code?

Yes to both — it is your repo: eight files of plain TypeScript, zero runtime dependencies, no call home. Adding a fifth provider is one verifier file plus one line in VERIFIERS and one entry in SCHEMES. The two things worth keeping as they are: verifyWebhook throws rather than returning a boolean (a false you forget to branch on is an unauthenticated webhook processed as authentic), and SCHEMES records signedTimestamp: false honestly for GitHub and Shopify rather than inventing a tolerance.

What happens when a provider changes its scheme?

MIGRATION.md ships in the pack and names every moving part, links each provider's changelog, and carries a copy-paste prompt that has your own agent read the upstream notes, apply the change and re-run the acceptance suite until green. That suite is the whole argument for self-service migration. Our own commitment, stated the same way in every pack: packs listed for sale are re-verified against current platform rules on our schedule (quarterly, plus whenever an authority announces a mandate); every re-verified version is free to prior buyers; we do not promise a delivery date, and we delist rather than sell a pack we can no longer verify. For this pack "free to prior buyers" is trivially satisfied — git pull.

Prescribed services

Disclosure: some links on this page are affiliate links. We may earn a commission if you sign up through them, at no extra cost to you. We only link to services the pack actually verified against.