BlogVibecode

Payments work in test but not live

The test payment goes through and the real one does not. Five causes, one of which is so common it is worth checking first.

By Bas Voets7 min read

Payments are the part where 'nearly working' costs the most. A list that does not load costs you a complaint; a payment that gets collected but never arrives in your system costs you a customer. Fortunately the causes are few.

1. You did not switch the keys

By far the most common, and the easiest to miss because nothing breaks: with test keys on your live site everything works, only no real money ever moves. Test keys and live keys look alike; the difference is usually one word inside the key itself. Check both, including the webhook's.

2. The webhook still points at your test environment

The payment provider tells your app that a payment came in. If that address still points at your preview link or at localhost, the payment does get collected, but your app never hears about it. Symptom: money in, order stuck on 'pending'. This is why your customer calls while your dashboard says everything went fine.

3. The webhook arrives but gets rejected

Every notification is signed, and the key you verify that signature with is a different one from your normal key, and differs between test and live as well. If it does not match, your app rejects every notification. All payment providers show in their dashboard what they sent and what your app answered; it is spelled out there.

4. Your account is not approved yet

Taking live payments requires an approved account: company details, a bank account, sometimes an identity check. While that is pending, test mode works and the real thing does not. This takes days, not minutes, so start it before you want to go live.

5. Payment methods other than the one you tested

In the Netherlands most people pay with iDEAL, which works differently from a card: the customer leaves for their bank and comes back. If you only test with a test card, you never test the path your customers actually take, including the case where they drop out halfway.

Specific to Stripe: what does not move from test to live

Stripe keeps test mode and live mode strictly apart. Each mode has its own keys, and objects from one mode do not exist in the other. That is deliberate, so your test data never ends up in production by accident, but it means that switching over involves recreating more than just a key.

  • Your keys. Test keys start with pk_test_ and sk_test_, live keys with pk_live_ and sk_live_. Check which one is actually set in your live site's environment variables, not in your code.
  • Your prices. Products and prices you created in test mode cannot be used live. You create them again in live mode and get new price IDs there. If your code or settings still hold a price_... from test, Stripe rejects the request with the error code resource_missing: that ID does not exist in the mode you are asking in.
  • Your webhook. A webhook you created in test mode does not exist live. You create the endpoint again in live mode, and that endpoint gets its own signing secret, again starting with whsec_. If your live site still holds the secret from test, your logs say: 'No signatures found matching the expected signature for payload'.
  • Your customer portal. If you use Stripe's customer portal for subscriptions, you configure it again for live mode. Stripe keeps separate portal settings per mode; whatever you set up in test is not there live.

That signature error has a second cause I run into a lot: your framework turns the incoming notification into JSON before your code verifies it. Stripe computes the signature over the raw text exactly as it was sent, so one shifted space is enough. If the webhook worked in test with the Stripe CLI and fails live, it is almost always the secret; if it fails everywhere, it is usually the body.

The order to check it in

  • Check your payment provider's dashboard for the payment. If it is not there, it is cause 1 or 4.
  • If it is there: check the webhooks for whether your app answered. No attempt is cause 2, a rejected attempt is cause 3.
  • Always make one real payment of one euro to yourself, with the method your customers use. You can refund it, but the transaction fee on it does not come back; that test costs you a few cents, not a euro.
Test mode proves your code is right. Only a real euro proves your setup is right.

Which provider fits best is covered in choosing Stripe or Mollie, and the build itself on Stripe integration or Mollie integration. If you are stuck on the integration itself, send over what you are seeing; this is usually a matter of hours, not days.

If payments are not the only shaky part of your AI-built app, start with the free vibecode scan. I spend ten minutes looking at your project and you get an honest verdict back.

Tell me what you want to build.

A half-hour call. You get an honest answer about what it costs, how long it takes and whether building it is smart at all.

0:00 / 0:00