Somewhere in your routing table sits an endpoint like /webhooks/stripe. It is public by design — the internet must be able to reach it — and it is the only endpoint in your system where a well-formed POST can upgrade an account to the enterprise plan. Anyone who guesses the URL can send it an invoice.paid event with a customer’s ID inside. If your handler trusts the body, you have built a self-service plan-upgrade API for the entire internet, with no login required.
Part 1 decided whose data is whose, Part 2 whose identity is whose, and Part 3 how you prove what happened. This part is about money — and money changes the attacker’s incentives. Nobody forges a webhook to vandalize your audit log; they forge one because the payload is worth dollars. Billing is also the one place where “we’ll add checks later” doesn’t become technical debt — it becomes fraud, chargebacks, and a card-network monitoring program with your name on it.
The signature is the authentication
Your webhook endpoint has no session, no SSO, no SCIM — the HMAC signature in the Stripe-Signature header is the authentication, and skipping verification is the equivalent of disabling auth on the admin panel. The scheme itself is simple: the provider signs the raw request body plus a timestamp with your endpoint’s secret, and your job is to recompute and compare. The failure modes are all in the details:
- Verify the raw bytes, not the parsed JSON. Frameworks that eagerly parse request bodies —
express.json()being the classic offender — will reserialize the payload and break the HMAC before your verification code ever runs. This is the single most common reason teams “temporarily” disable verification, and some never turn it back on. - Keep the timestamp check on. The signed timestamp with a five-minute tolerance is what stops a captured, validly-signed payload from being replayed next week. Setting the tolerance to
0doesn’t tighten the window — it disables the check entirely, which is a spectacular footgun for a parameter named “tolerance.” - One secret per destination. Stripe’s newer thin events — the
v2.*family rolling out through 2025 — cannot share a destination with classic snapshot events, so you now hold two signing secrets and must verify each route against its own. The wider ecosystem is converging on the same shape via the Standard Webhooks spec (webhook-id,webhook-timestamp,webhook-signatureheaders), which OpenAI and others adopted in 2025 — learn the pattern once, apply it everywhere. - IP allowlisting is seasoning, not the meal. Restricting ingress to the provider’s published IP ranges stops casual scanning, but the signature is the control. Do both; rely on the second.
Verify first, work second
The endpoint’s job ends at verification. Everything else — updating the subscription, granting seats, sending the receipt — happens after a fast 200, on a worker, because the provider’s retry policy is not patient: exceed a few seconds and the delivery is marked failed and retried with exponential backoff for up to three days. A slow handler doesn’t just risk timeouts; it manufactures duplicates.
Duplicates are guaranteed anyway — at-least-once delivery means the same event arrives twice, out of order, occasionally after you already processed its successor. The consumer must be idempotent on the event ID, and the entitlement write must survive replays: processing a replayed customer.subscription.deleted should leave the subscription deleted, not flip some toggle a second time.
The deeper rule: grant access from the provider’s API, not from the payload. The event tells you something happened; the API tells you the current truth. Stripe’s thin events make this official — the payload carries little more than an ID, and you fetch the object yourself — but the discipline predates them: a payload is a claim, and Part 2 taught you what to do with unverified claims. And because events can be lost entirely (your endpoint was down, the destination was disabled), a periodic reconciliation job that polls the API and diffs subscription state is the backstop that turns “we think billing is right” into “we checked.”
One more boundary from Part 1 applies with full force: the entitlement record is tenant data. The tenant it lands on comes from the mapping you established at checkout — client_reference_id, metadata you set — never from a field the user could edit between payment and webhook.
The PCI boundary you do and don’t own
The good news of 2026: you should never touch a card number. Hosted fields and iframe checkouts send the PAN straight to the provider and hand you back a token, which is what keeps you in the light SAQ A tier instead of the SAQ D abyss. The bad news arrived on March 31, 2025, when PCI DSS 4.0.1’s fifty-one future-dated requirements became fully enforceable — and two of them are aimed squarely at that checkout page you think you outsourced:
- Requirement 6.4.3 — every script on your payment page must be inventoried, authorized, given a written business justification, and integrity-checked. That includes your tag manager, your analytics pixel, and the A/B testing snippet a marketer added in 2024.
- Requirement 11.6.1 — you must run a mechanism that detects unauthorized changes to those scripts and to security-relevant HTTP headers, alerting at least weekly.
The reason is Magecart: e-skimming attacks inject a script into your page and read the card data as the customer types, before the iframe’s protection ever matters — invisible to your server, invisible to the provider. The iframe boundary holds only as long as your page isn’t contaminated, and PCI now formally agrees: the page is a controlled zone. Full redirect to a provider-hosted page shrinks this further; a direct API integration where card data transits your server buys you SAQ D and should require a written confession of why.
Two rules that cost nothing: never let a PAN touch your infrastructure (not in logs, not in analytics, not in error reports), and never store CVV, stripe data, or PIN under any circumstances — PCI prohibits it even encrypted. What you never store, nobody can steal from you.
Fraud finds your checkout before customers do
Your $5 trial checkout is not just a revenue surface — to a card tester, it is a card-validation oracle that answers “does this stolen number work?” for free. Stripe’s Radar team documented the current wave in early 2025: attackers have shifted from blind enumeration to verification attacks using high-quality dumps of phished card data, so authorization rates — the old tell — no longer stand out. The scale is industrial: among large attacks Stripe blocks, one in four involves over a million attempted transactions against a single business. Stripe’s answer was a payments foundation model that lifted detection of these attacks on its largest users from 59% to 97% — which is exactly the point: most of this defense is the provider’s job, and choosing a processor is partly choosing whose models stand between you and the dumps.
What remains yours is the blast radius. Card testing becomes your problem at the card networks, not the processor: Visa’s VAMP program — consolidated in 2025 — rolls disputes and fraud reports into a single ratio with explicit enumeration-attack thresholds, and Mastercard runs its own ECP/EFM equivalents. Cross them and you enter monitoring programs with monthly fines; stay in them and you lose the ability to process cards at all. The controls in your hands are unglamorous: rate limits and bot detection on the checkout attempt endpoint, Radar-style blocking rules, refusing to build your own card form, and treating a sudden spike in declined micro-transactions as an incident, not a curiosity.
The merchant-of-record question
There is a way to move much of this off your plate: don’t be the seller. A merchant of record — Paddle, Lemon Squeezy, or Stripe’s own Managed Payments, launched after the 2024 Lemon Squeezy acquisition — legally is the merchant: their name on the statement, their tax registrations, their chargeback liability, their fraud exposure. You invoice them; they invoice your customers. The fee difference is real (roughly 5% + $0.50 against 2.9% + $0.30) and buys tax compliance across 150–200+ jurisdictions plus the fraud stack above.
Think of it as a risk-transfer decision with a fee attached, not a fee decision with risk attached. The security-relevant caveats: MoR or not, their webhooks into your system still need the same signature verification and idempotent write path — the trust boundary moved, it didn’t vanish. And concentration is its own risk: the Lemon Squeezy migration path into Stripe Managed Payments, published in January 2026, is a reminder that your billing provider’s roadmap is now a dependency of your revenue pipeline. Whichever you choose, the entitlement write path from earlier is the part no vendor can sell you, because it lives in your database.
Where the series goes next
Current thinking for the next door: the SOC 2 report itself — the document the questionnaire keeps asking for, what auditors actually test, and why “we have the controls” and “we can prove we had them operating for six months” are different sentences. Incident response — what happens the day one of these controls fires for real — is the other strong candidate. As before: the order is decided when we get there.
Practice
- Attack your own webhook endpoint: craft a POST with a plausible
invoice.paidbody and no valid signature. If it returns anything other than a 400 — or worse, if you’re not sure — verification is off or the parsed-body bug has it. Then check the tolerance value;0counts as a finding. - Replay last week’s real event against staging twice. The entitlement state after the second delivery must equal the state after the first, and your dedupe store must show the event ID. If replaying changes anything, so will your provider’s next retry storm.
- Do a 6.4.3 drill on your checkout page: list every script in the DOM, with an owner and a one-line justification each. Anything nobody can justify comes off the page — and whatever process let it appear without a justification is the actual audit finding.