Reviews ONE Stripe webhook handler (the route code, in any language or framework) for the five mistakes that let a forged or repeated event move money, grant access or ship goods, and lists only those, one line each with a fixed code, the place, why it is dangerous and the fix - or exactly No findings. It catches a body parsed or re-serialized before the signature is checked, a handler that never checks the signature, a recency tolerance switched off or set huge, no record of event ids so a retry repeats the side effect, and slow work done before the 2xx answer. It leaves out the safe look-alikes, such as a raw body kept on the webhook route while the other routes parse JSON, and the default tolerance. Use to review a webhook route before going live, to audit one that someone else wrote, or to find out why signature checks fail or orders are fulfilled twice.
Stripe Webhook Review is a tested SKILL.md that reviews ONE Stripe webhook handler (the route code, in any language or framework) for the five mistakes that let a forged or repeated event move money, grant access or ship goods, and lists only those, one line each with a fixed code, the place, why it is dangerous and the fix - or exactly No findings; an agent buys it once for $0.01 over x402.
Not for
Payment amounts, refunds, API keys, IP allowlists, rolling the signing secret, thin events, Connect and any helper function whose body is missing from the paste. It reads one pasted handler and judges only what is written in it.
Tested, honestly
Tested 2026-10-19 with a strong and a weak model.
- SonnetStrong model, claude-sonnet-5-5
- Right on all 3 cases, read by hand. On the Express app it named the body rebuilt with JSON.stringify after the app-wide JSON parser, and left out the default tolerance. On the Flask route it gave two lines, the tolerance set to 0 and the credit update with no event id, and left out the raw body next to a JSON route. On the clean Next.js route it answered exactly No findings.
- HaikuWeak model, claude-haiku-5-5
- Right on all 3 cases, read by hand. It gave the same single line for the rebuilt body and the same two lines for the Flask route, and answered exactly No findings on the clean route. On the Flask route it indented its two lines by four spaces, which the format check allows.
Full test summary
Example
Our own test text, before and after the skill ran. Excerpts only.
English · claude-sonnet-5-5
Before
# app.py
import os
import stripe
from flask import Flask, request
from db import db
app = Flask(__name__)
WEBHOOK_SECRET = os.environ["STRIPE_WEBHOOK_SECRET"]
@app.post("/api/profile")
def update_profile():
data = request.get_json()
db.execute("UPDATE users SET name = %s WHERE id = %s", (data["name"], data["id"]))
db.commit()
return {"ok": True}
@app.post("/stripe/webhook")…
After
TOLERANCE-OFF — `stripe_webhook`, `tolerance=0` passed to `stripe.Webhook.construct_event` — a tolerance of 0 switches the recency check off, so anyone who captures one validly signed request can replay it later — remove the argument to use the default 5 minutes…
What is in the file
- The answer
- What Stripe says (public webhook documentation, read on 19 October 2026)
- The codes
- How to read the handler
- Rules
- Work in this order
- Short example
Languages
English. Tried in: English.
License
Perpetual, non-exclusive; use and modify for yourself incl. paid work; no resale or republishing. Holder: Georgi Kalchev, aiskills402.com. Full terms.
Versions
Current version 1.0.0, updated 2026-10-19. Whoever bought an earlier version gets new ones free through the same re-download token.
v1.0.0 · 2026-10-19
First release, written in the light-test batch of 19 October 2026 (three cases, price $0.01). The idea is ours; no foreign skill was opened, facts come from public pages (see notes/ideas.md). Output: one line per finding with a fixed code, or exactly `No findings.`
Test: three cases written by hand (an Express app that rebuilds the body, a Flask route with tolerance 0 and no event-id record, and a clean Next.js route), one run on Claude Sonnet and one on Claude Haiku, no run without the skill, no iteration on the skill text after the runs. `test/control.mjs` shows that the checks pass on the ideal answers and fail on realistic wrong ones. See `test-summary.json` for the verdicts read by hand.
FAQ
Should a raw body next to JSON routes be reported?
No. Stripe needs the raw bytes of the request for the signature check, so a webhook route that keeps them is the safe form, whatever the neighbouring routes of the app do with JSON. The skill reports a body that was parsed and written out again before the check.
Does leaving out the tolerance argument count as a defect?
No. The official libraries accept five minutes unless told otherwise, and a handler that passes no tolerance is left alone. A tolerance of 0, which switches the recency check off, is reported, and so is a value of an hour or more, which is the policy of this skill because the documentation gives no figure for huge.
Which handlers went into the test?
An Express app that serialises the body again, a Flask route with the tolerance at 0 and no record of event ids, and a clean Next.js route. Claude Sonnet and Claude Haiku answered each once, and by hand both were right every time.
What does a handler without problems get?
The sentence No findings. on its own line, which makes it usable as a merge gate. A defect is a line holding a code, the place, the risk it creates and the correction.