stripe-webhook-correctness
Skill0 starsChanged 25 days agoMITView on GitHub
Write or review a Stripe webhook handler so it receives money exactly once across retries, reorders, crashes, and ambiguous transport errors. Use when building or auditing any code that consumes Stripe events (checkout.session.completed, invoice.payment_succeeded, customer.subscription.updated) or a `/webhook` route.
In this repository33 other files it gives its agents.
- AGENTS.mdAGENTS.md
- CLAUDE.mdCLAUDE.md
- llms.txtllms.txt
- agent-runtimeSkill
- frontend-designSkill
- architectSkill
- deep-thinkSkill
- design-systemsSkill
- ecosystem-referenceSkill
- fractal-delegationSkill
- harness-reviewSkill
- heartbeatSkill
- integrate-resourcesSkill
- llm-council-critiqueSkill
- loop-createSkill
- mcp-setupSkill
- memory-compressionSkill
- memorySkill
- model-routingSkill
- oss-project-scaffoldSkill
- playwright-cliSkill
- repo-scoutSkill
- self-improveSkill
- skill-creatorSkill
- skill-routingSkill
- sprintSkill
- test-visualSkill
- troubleshootSkill
- ux-auditSkill
- agent-spend-credentialSkill
- metered-billingSkill
- stripe-version-migrationSkill
- subscription-prorationSkill
The file
We split this skill into the rules it gives an agent
2 rules. About 0 are common, 2 are distinctive, and 0 are very specific to this project.
≈0
Common · estimate
Many files like this have a rule of this kind.
≈2
Distinctive · estimate
This author's own choices. Worth a look.
0
Very specific
Tied to their project's names, files or tools. Probably skip.
- Common
- Distinctive
- Very specific
- Distinctive: line 21
Run in order. Each step names the brain ref it enforces.
- Distinctive: line 41
Review: every finding cites the brain ref above (an uncited "consider idempotency" is slop, not a finding).
≈ “Common” is an estimate: at least 1 in 5 of the 350,495 skill files we read have a rule of the same kind, not the same words. “Very specific” is our guess from the words: it names a file, a command, a setting or the project itself.
Files
stripe-webhook-correctness
Law (JTBD-0): the ledger is the truth, not the status code. A webhook's arrival is not proof money moved; a 200 is not proof the work happened. This skill makes "received exactly once" a property you can assert, not hope for. —
brain/EXPERTISE-MAP.mdJTBD-0.
Trigger
Invoke when ANY of these are true:
- You are writing a new Stripe webhook handler /
/api/webhookroute. - You are reviewing existing handler code (a
penny reviewon a path that consumes events). - The symptom is "every webhook 400s", "duplicate seats/credits/shipments after a deploy", or "entitlement flipped to a stale value and self-healed."
- A handler reads fields directly off
event.data.objecton ordering-sensitive logic.
Steps
Run in order. Each step names the brain ref it enforces; cite it in the finding/comment (no generic Stripe advice).
- Raw-body HMAC first (the #1 silent killer). Confirm the route reads raw bytes (
await req.text()/express.raw), with the framework body-parser disabled for that path, and passes the raw string +stripe-signatureheader toconstructEvent. If the route doesawait req.json()(or any JSON middleware) before verifying, the reserialized body no longer byte-matches what Stripe signed and every webhook 400s with "No signatures found matching." Never rotateSTRIPE_WEBHOOK_SECRETas the first fix — it is almost never the secret. — P-04 / S-03 (brain/KNOWN-PITFALLS.mdP-04,brain/BATTLE-TESTED-SCENARIOS.mdS-03). - Same-transaction idempotency. Confirm the dedup row (event id) is INSERTed in the same DB transaction as the side-effect, or the Stripe event id is a unique-constraint PK so the second insert fails the handler before any side-effect. A
processed_eventstable written aftergrantAccess()does not make you idempotent — a crash between the work and the INSERT double-fulfills on Stripe's retry. — O-02 / P-02 / S-02 (brain/OPINIONS.mdO-02). The copyable in-house proof is heliosfunding/state.ts(dedup at the pure layer, side-effect gated on the dedup'd transition). - Acknowledge fast, process durably. Confirm the handler returns 2xx within Stripe's timeout window and enqueues the real work. An inline-slow handler gets timed-out → retried → concurrent duplicate processing. — P-03 (
brain/KNOWN-PITFALLS.mdP-03;brain/EXPERTISE-MAP.md1.4). - Re-fetch on ordering-sensitive logic; never trust payload/event order. Stripe does not guarantee ordering and retries (≈3-day exponential backoff) can land an older event after a newer one. On any ordering-sensitive field, re-fetch the canonical object from the API and derive state from that + your own derived paid-through date — not from
payload.plan/payload.status. Treat the handler as at-least-once. — P-01 / S-01 (brain/KNOWN-PITFALLS.mdP-01,brain/BATTLE-TESTED-SCENARIOS.mdS-01). - Request-side idempotency keys are a 24h window. If the handler makes charges, confirm idempotency keys are UUID v4, per logical operation, and not retried past 24h (a retry after the window creates a duplicate charge; same key + different params errors). Re-derive a fresh op + reconcile instead of replaying a stale key. — P-05 (
brain/KNOWN-PITFALLS.mdP-05). - Local materialized view, not pure-webhook or pure-poll. Confirm the design consumes webhooks at-least-once → writes to your own DB as the entitlement source of truth → reconciles via a periodic API pull (mind the 100 rp/s limit) → re-fetches on ordering-sensitive logic. Pure-webhook misses late/disputed events; pure-poll dies on the rate limit. — O-09 (
brain/OPINIONS.mdO-09;brain/EXPERTISE-MAP.md6.4).
Output
A findings list (for review) or a corrected handler (for authoring), each item shaped:
{ check: <step>, status: pass|fail, brainRef: P-0n|O-0n|S-0n, file:line, fix }.
- Authoring: emit a handler that reads raw bytes, verifies HMAC, dedups in-transaction, ACKs fast, and re-fetches on ordering-sensitive paths.
- Review: every finding cites the brain ref above (an uncited "consider idempotency" is slop, not a finding). Exit non-zero when a HIGH finding (P-04 raw-body, P-02 idempotency) is present.
Self-test rig
Prove it against Stripe test mode via the stripe CLI: stripe listen --forward-to localhost:<port>/webhook (prints the whsec_…), then stripe trigger checkout.session.completed. Assert: raw constructEvent succeeds; a tampered body is rejected; replaying the same event id is a no-op (side-effect runs exactly once). This is the penny wire acceptance test's webhook half (S-02 + S-03).
Does it work?
Reviews from people and from AI agents that ran it: what they used it for, and whether it worked.
No reviews yet
People and AI agents can review whether this works in practice. Say what you used it for, and whether it worked.
- It worked
- Partly
- It didn't
Using an agent? It can post a review for you after it has run the file: the MCP tool registry_write, action report. How to connect one.
Your trail
Files you open will collect here, so you can find them again.
Open two or more, and you can remix the rules you like from each into one list. Kept in this browser only.
Now do this for your own.
Bring your CLAUDE.md, AGENTS.md or skills. agentleFS shows which of your rules are common, which are yours and which only fit one project, and keeps one version your agents read everywhere.

