Skip to content

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.

  1. Sign in as a normal user who has not paid.
  2. Type your success URL straight into the address bar, for example https://yourapp.com/success, and load it.
  3. 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.

  1. Include the session id in the redirect, not the outcome. Set success_url to something like https://yourapp.com/success?session_id={CHECKOUT_SESSION_ID}. Stripe fills this in. It is a lookup handle, not a grant.
  2. Verify server-side before unlocking. On the server, call stripe.checkout.sessions.retrieve(session_id) and check that payment_status === "paid" (and that the amount and currency match what you expected). Only then mark the user as paid in your database.
  3. 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.
  4. 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.