Skip to content
Back to field notes
PaymentsJune 6, 20266 min read

Stripe webhook endpoint exposure: what a public scan can and cannot tell you

A public Stripe webhook route is normal, but an unauthenticated GET that returns 200 with metadata, or a POST that accepts any payload, is a signal worth checking before launch.

PaymentsStripeWebhooks

A public webhook route is normal; what it answers with is the signal

Stripe has to reach your app from the outside, so a route like POST /api/stripe/webhook or POST /webhooks/stripe is supposed to be publicly reachable. That part is by design. A route existing on the public surface is not the launch risk here, and this note is not about hiding it.

The Stripe webhook endpoint security check is about what that route answers with when something other than Stripe knocks on it. Two responses are worth a closer look before you share the URL:

Keep two things separate. What is observable from outside is the status code, the headers, and the response body. What is only possible, and not proven by those alone, is that the handler trusts unsigned input and would change billing state because of it. The first is a public-surface signal. The second is a code question.

  • An unauthenticated GET to the webhook path that returns 200 with a body, especially one that echoes config, environment names, route metadata, or a friendly JSON status. A webhook endpoint accepts POST event payloads; it has no reason to render a useful page to a browser GET.
  • A POST with an arbitrary, unsigned payload that the route accepts and acts on instead of rejecting it. If a hand-crafted body gets a 200 and appears to be processed, the handler may not be verifying the Stripe signature at all.

Why AI-built apps land here

AI coding tools are good at producing a webhook route that runs. They are less reliable at producing one that rejects everything except a genuine, signed Stripe event. The gap is predictable.

A generated handler often parses the request body and branches on the event type before, or instead of, verifying the signature. The Stripe SDK verifies signatures with a step that needs the raw, unparsed request body and the signing secret; that detail is exactly the kind of thing a fast scaffold skips or wires up wrong. The route still works against test traffic, so it looks done.

This is an authorization-of-the-message problem, not a broken login. The endpoint is meant to be anonymous; the control that matters is whether the message is a verified Stripe event, which is what signature verification decides. Confirming that lives in the handler code, covered in Stripe webhook signature verification.

  • Generated scaffolds frequently add a body parser that consumes the raw body, which quietly breaks signature verification even when the verify call is present.
  • A debug or health response is sometimes left on the same path so the developer can confirm "the endpoint is up" in a browser. That GET response is what later leaks detail.
  • Fast deploy means the route is live on the public URL before anyone tests it with a request that is not from Stripe. The first hostile POST is then a real visitor, not a test.

Check your own webhook route from the outside

You can run this against your own app with a browser and a single request. Do this only against endpoints you own.

First, find the path. Look in your Stripe Dashboard under Developers, Webhooks for the endpoint URL you registered, or grep your own repo for the route. Common shapes are /api/stripe/webhook, /webhooks/stripe, /api/webhooks, and /stripe.

Then send a plain GET to that path and read the response:

Next, send a POST with a small arbitrary JSON body and no valid Stripe-Signature header, and read the status:

A safer result on both checks is encouraging, not proof. A handler can return the right status to your test and still be wrong in a way an outside request cannot see, which is the limit covered in the next section.

  • Suspicious result: 200 with a body that includes JSON status, environment or config values, a stack trace, or any readable message. Note the response too, because a verbose error can leak as much as a success page.
  • Safer result: 405 Method Not Allowed, 404, 401, or 400 with an empty or minimal body. A webhook that only speaks POST and refuses a GET is behaving as expected.
  • Suspicious result: 200, or any response that suggests the payload was parsed and processed. That is the signal that signature rejection may be missing.
  • Safer result: 400 with a signature-verification error. Stripe's verify step is supposed to reject a missing or bad Stripe-Signature header before the handler does anything, so a clean 400 is the response you want.

How to fix it, and what the fix does not cover

The fixes here are narrow and worth doing in order. They close the two observable signals above.

What these fixes do not cover: they stop the endpoint from leaking detail and make it reject unsigned input, but they do not make the rest of the billing flow correct. Replay handling, idempotency so a duplicated event is not processed twice, and the logic that decides what each event is allowed to change are separate concerns. Signature verification proves the message came from Stripe; it does not prove your handler does the right thing with a valid event.

  • Turn off any debug, health, or status response on the webhook path. The route should answer POST events and reject everything else. Move uptime checks to a separate, non-sensitive endpoint.
  • Restrict the method so a GET returns 405 rather than rendering anything. This removes the easiest way for the route to leak detail to a browser or crawler.
  • Verify the Stripe signature on every request using the SDK's event-construction step with the raw request body and your signing secret, and return 400 when verification fails. Make sure no body parser consumes the raw body before that step runs.
  • Keep error responses generic. A failed verification should return a short 400, not a stack trace or a description of what was missing.

Rejecting unsigned POSTs and hiding the GET response addresses the public-surface signals. It does not validate your event-handling logic, your idempotency, or whether a verified event maps to the correct billing change. Those are server-side code questions, not header or status-code questions.

Where VibeCodeGuard fits, and the next step

VibeCodeGuard's Launch Check works from the public surface, so it can do exactly the outside-the-app part of this check. It can probe known and common webhook route patterns for an unauthenticated GET that returns 200 or leaks metadata, and it can flag the absence of a clean rejection response, with severity and fix direction. That covers the two observable signals: a route answering a browser GET with content, and a route that does not reject as expected.

What it cannot see is the signature logic. Whether your handler calls the SDK's verify step, whether it has the raw body available, and whether it returns 400 on a forged payload is invisible to a public scan and needs code review. A clean scan means the endpoint did not leak detail to the requests the scanner sent; it does not confirm the handler verifies signatures, and it does not confirm correct billing behavior. While you are checking payment surfaces, it also pairs with confirming you shipped live-mode rather than test keys, covered in Stripe test vs live key in production, and with the broader sweep in unauthenticated API endpoints in AI-built apps.

Next step: send a GET and an unsigned POST to your own webhook path and read both responses, then open the handler and confirm it verifies the Stripe signature before doing anything else. Run a focused Launch Check against the public URL before sharing it, so an exposed webhook response shows up in your scan rather than in someone else's request.

A passing public-surface check is one signal, not a guarantee that your webhook is safe. The decisive control, signature verification on the raw body, lives in code the scan cannot read.

> launch check

Scan the public surface before launch.

Get severity, evidence, and practical fix guidance for the checks VibeCodeGuard can run from the outside.