Stripe Test Webhook: Test Payments Locally With the CLI

Learn how to run a Stripe test webhook locally with the Stripe CLI: stripe listen, stripe trigger, failure cases, duplicate events and moving off an AI builder.

Illustration of payment events routed through a listener into a local laptop, depicting a Stripe test webhook setup
On this page · 7 sections
  1. How do I send a test webhook in Stripe?
  2. How do I use stripe listen to forward events to localhost?
  3. How do I trigger specific events with the Stripe CLI?
  4. Which failure cases should I test before launch?
  5. How do I test duplicate and out-of-order events?
  6. How do I test after moving off the builder?
  7. Want a second pair of eyes?

Key takeaways

  • Use Stripe test mode and the Stripe CLI; never test with live keys or real cards.
  • stripe listen –forward-to sends events to your local endpoint and prints a signing secret just for that session.
  • stripe trigger creates sample events, but your real checkout flow is the better test of your own business logic.
  • Test failures, duplicates and out-of-order delivery, not just the happy path.
  • A few focused checks find most of the risk; fixing payment bugs costs more after real users and payments arrive.

The fastest way to run a stripe test webhook is the Stripe CLI. You log in, run stripe listen to forward events from your Stripe test mode to a local URL, then run stripe trigger to fire sample events. Your app receives real signed requests without any deploy.

This matters most for apps built with AI tools such as Lovable, Bolt or Base44, where the payment path often looks fine in a demo but has never faced a failed card, a retry or a duplicate event. I am a Brazilian engineer, and this is the routine I use before any real money flows through an app.

How do I send a test webhook in Stripe?

You have three options, and they test different things.

  • Stripe CLI: best for local development. It forwards events to localhost and lets you trigger them from the terminal.
  • Dashboard event destinations: in test mode you can point Stripe at a public URL and send a test event from the dashboard. Useful for a staging deployment.
  • Real test-mode actions: completing a Checkout with a test card produces the exact event sequence your app will see in production.

For daily work I combine the first and third. The CLI gives fast feedback, and a real test checkout confirms the whole chain: your frontend, your server, Stripe and back to your database.

How do I use stripe listen to forward events to localhost?

Install the Stripe CLI, then authenticate with stripe login. It opens a browser to link the CLI to your account. Make sure you are working in test mode.

Start your app locally, then run:

  1. stripe listen –forward-to localhost:3000/api/stripe/webhook (use your own port and path).
  2. Copy the signing secret the CLI prints. It starts with whsec_.
  3. Put that value in your local environment variable for the webhook secret.
  4. Restart your app so it reads the new value.

The most common mistake I see is using the dashboard endpoint secret locally. The CLI session has its own secret, so signature verification fails with a confusing error. If your handler returns 400 on every event, check this first.

Also confirm your handler verifies the signature against the raw request body. Frameworks that parse JSON before your code sees it will break verification. That is a code issue, not a Stripe issue.

Two terminal windows side by side on a laptop, one forwarding Stripe test webhook events to a local server
One terminal forwards events, the other runs your app.

How do I trigger specific events with the Stripe CLI?

With the listener running, open a second terminal and run stripe trigger followed by an event name. Common ones for a payment app:

  • checkout.session.completed
  • payment_intent.succeeded
  • payment_intent.payment_failed
  • invoice.paid and invoice.payment_failed
  • customer.subscription.deleted

Watch the listener output. Each line shows the event and the HTTP status your endpoint returned. You want 200 for handled events, and you want to see your own logs and database changes too.

One caveat: triggered events use generated fixture data. They will not contain your customer ID, your order ID or your metadata. So a trigger proves your endpoint receives and verifies events, but it may not prove your order logic works. For that, run a real test checkout with card number 4242 4242 4242 4242 and let the CLI forward the resulting events.

Which failure cases should I test before launch?

AI-built apps usually handle the success case. The failures are where money and data get lost. Here is the list I run through.

CaseHow to testWhat you should see
Declined cardTest card 4000 0000 0000 0002No access granted, user sees a clear error
Bad signatureSend a request with a wrong secret400 response, nothing written
Handler crashThrow an error in your handler500 response, Stripe retries later
Unknown event typeTrigger an event you do not handle200 response, event ignored safely
Abandoned checkoutClose the page before payingNo paid order is created

The key rule: your app should grant access or mark an order paid only after a verified webhook confirms payment, not because the browser landed on a success page. Anyone can open a success URL manually. A generic security scanner will not catch this, because it is business logic, not a known vulnerability pattern.

Also check that errors return a non-2xx status. If your handler swallows an exception and returns 200, Stripe believes the event was handled and will not retry, and you silently lose the payment record.

How do I test duplicate and out-of-order events?

Stripe can deliver the same event more than once, and it does not guarantee order. Your handler must survive both.

Duplicates

Use stripe events resend with an event ID from your test dashboard to deliver the same event again. Your app should not create a second order, send a second email or extend a subscription twice. The usual fix is to store processed event IDs in a table with a unique constraint and skip any you have seen. Better still, make the underlying update idempotent, for example setting an order status to paid rather than incrementing a counter.

Out of order

You might receive invoice.paid before checkout.session.completed, or a subscription update before the record exists. Test by resending events in a different order, or by temporarily deleting the related row and replaying. A robust handler either fetches the current object from the Stripe API or fails with a 500 so Stripe retries later.

Shuffled and duplicated envelopes arriving at a desk, representing out-of-order and duplicate Stripe webhook events
Events can arrive twice and in the wrong order.

Concurrency

If two copies arrive at nearly the same moment, a check-then-insert pattern can still create duplicates. The unique constraint in the database is what actually protects you, so rely on it rather than on application code alone.

How do I test after moving off the builder?

Code built in Lovable can be moved to GitHub and to your own hosting. When you do that, the webhook setup changes, and this is a common place for payments to break quietly.

  1. Register a new endpoint in Stripe for the new public URL, in test mode first.
  2. Use the new endpoint secret in the new host’s environment variables. Do not reuse the CLI secret or an old one.
  3. Check the server location. If webhooks used to run in a builder-managed function, confirm the same logic now exists in your own backend.
  4. Run a full test checkout against the deployed staging URL and confirm the database row changes.
  5. Repeat in live mode with a small real payment you refund afterward, once everything passes.

Remove the old endpoint in Stripe when you are done so events are not sent to two places. Staying on the builder is sometimes the better call, especially for a simple app with little traffic. But if you move, treat the webhook as the first thing to retest, because the app owner is responsible for payment confirmation even when the platform runs the infrastructure.

Fix the existing code before considering a rewrite. When I review a payment path, I start by looking for targeted fixes in the existing code before anything bigger.

Want a second pair of eyes?

If you want a second opinion on your webhook handler or payment flow, email me@filipeeduardo.dev with a description of your setup and I will tell you what I would check first.

Frequently asked questions

Do I need a public URL to test a Stripe webhook?

No. The Stripe CLI forwards events from Stripe to localhost, so you can test without deploying or using a tunnel tool.

Why does my local webhook return a signature error?

Usually the secret is wrong or the body was parsed before verification. Use the whsec_ value printed by stripe listen and verify against the raw body.

Does stripe trigger create my real orders?

No. It creates generic fixture objects that lack your metadata. Use a real test checkout to exercise your own order logic.

Can I replay an event I already received?

Yes. Use stripe events resend with the event ID, or resend from the test-mode dashboard, to check duplicate handling.

Is test mode enough before launch?

It covers most logic, but do one small live-mode payment after deployment to confirm live keys and the live endpoint secret are set correctly.

stripe test webhook

Filipe Eduardo

Senior Software Engineer. Seven years building web and mobile products end to end and leading the teams that ship them.

Follow along

Want to talk
about this?

I work on problems like this every day. Tell me about yours.

Get in touch