Can a User Fake a Successful Payment by Hitting My Success URL Directly?
6 min read
Can a user fake a successful payment by visiting your Stripe success page directly? Yes, if your app unlocks premium based on the redirect to that page. The success URL is just a link in the browser, and anyone can type it, bookmark it, or share it. The only safe way to grant access is to confirm the payment server-side with Stripe itself, using the API or a webhook, before you flip anyone to paid.
Why this happens
Stripe Checkout works by sending the user off to Stripe's page, taking their
money, then redirecting them back to a success_url you chose, often something
like /success or /thank-you. It is natural to think "they landed on the
success page, so they paid." An AI agent asked to "unlock premium after
checkout" will usually wire it up exactly that way: user hits /success, the app
sets them to paid. It works perfectly in the demo.
The problem is that the redirect is not proof of anything. The URL lives in the
browser. A user can visit /success without ever paying, reload it, or send it
to a friend. If that page is what grants access, you have handed out a free
coupon that never expires. The agent built what you asked for, "unlock after
the success page," and not the thing you left unsaid, "only if the money
actually arrived."
How to check
You can test this on your own app in under a minute.
- Sign in as a normal user who has not paid.
- Type your success URL straight into the address bar, for example
https://yourapp.com/success, and load it. - If the app now treats you as a paying customer, unlocks features, or bumps your plan, you are vulnerable. That is the whole exploit.
Also look at how the redirect is built. If your success_url contains something
like ?paid=true or a plan name, and your code reads that value to grant access,
treat it as unlocked to the world. Anything in the URL can be edited by the user.
The fix
Move the decision to your server, where the user cannot touch it.
- Include the session id in the redirect, not the outcome. Set
success_urlto something likehttps://yourapp.com/success?session_id={CHECKOUT_SESSION_ID}. Stripe fills this in. It is a lookup handle, not a grant. - Verify server-side before unlocking. On the server, call
stripe.checkout.sessions.retrieve(session_id)and check thatpayment_status === "paid"(and that the amount and currency match what you expected). Only then mark the user as paid in your database. - Prefer a webhook as the source of truth. Add a Stripe webhook endpoint and
listen for
checkout.session.completed. Verify the webhook signature with your signing secret, then grant access from that handler. Webhooks arrive even if the user closes the tab before the redirect, so you stop losing real customers too. - Never grant access from client-side code. The unlock must happen in a server route or function that the browser cannot call with made-up data.
If you are on Firebase or Supabase, the same rule holds: the function that flips a user to paid must run on the server and must confirm the payment with Stripe first.
The trap to avoid
The common wrong fix is to make the success URL harder to guess, for example adding a random token or hiding the page behind login. That changes nothing. The user paying is also the user who reaches the page, so they always have the token. Obscurity is not verification. The only thing that counts is asking Stripe, server-side, whether this specific session was actually paid.
A second trap is trusting a webhook without checking its signature. An unsigned endpoint can be called by anyone posting fake "payment succeeded" events. Always verify the signature with your webhook signing secret.
Where this fits
Payment bypass is one of those gaps that never shows up in a demo and shows up immediately once real users, and their curiosity, arrive. It sits alongside other "the app trusts the browser too much" issues like an exposed anon key and a world-readable database. The free Readiness Report checks whether your app grants access from the redirect or from a verified payment, against your real Stripe setup. If you want the fixes handed to you in order, that is what the Finishing Pass is for.