The problem: a provider can’t call a URL that doesn’t exist
Stripe (and every provider webhook system built the same way) delivers events by
making an outbound HTTP request to a URL you registered. localhost is not a
URL the public internet — or Stripe’s delivery infrastructure — can reach, so
the direct path is closed by construction, not by a missing setting.
Every approach below solves that same problem differently: some make your machine temporarily reachable from the outside, some avoid needing that entirely, and some skip the provider altogether and construct the event by hand. Which one is right depends on what you’re actually trying to prove.
The complete workflow with a local sandbox
This is the path that needs no inbound exposure and no provider account.
- Install the extension and open its side panel. No sign-in and no cloud connection are involved.
- Point it at your app. Enter the
localhostor127.0.0.1origin your app listens on, with the exact port and path your webhook route expects. The extension requests a Chrome permission scoped to only that origin, and runs a connectivity check against it before anything else happens. - Set a test-only signing secret. This is the same signing-secret concept Stripe’s own webhook endpoints use (see the provider notes below) — pick any value for local testing, use it to verify signatures in your handler, and never reuse a real endpoint’s live secret here.
- Configure the payment, or don’t. You can set an amount, currency, merchant reference, and safe metadata for the run, or leave every field blank to get the scenario’s unmodified default payment.
- Run “Payment succeeds.” The extension delivers two ordered, correctly signed events to your endpoint — an authorization success followed by a payment success — the same event pair a real successful Stripe payment produces. The generated section below this guide shows the exact event timeline, a signature-verification snippet, and a handler skeleton for this run, generated from the same scenario definition the extension executes, not written by hand for this page.
- Read the timeline, or export it. The run’s timeline survives even if the extension’s background worker restarts mid-run, and you can export a full or redacted record of what was sent and received.
Nothing here opens a port, registers a public endpoint, or asks Stripe for anything: the events are generated locally and delivered directly to the loopback address you configured.
Troubleshooting an unreachable endpoint
Most first-run failures are one of these:
- The app isn’t listening yet. Start your server before running the scenario, and confirm it’s the same port the extension’s connectivity check targets — a common mismatch is a framework’s default dev port differing from the one you typed.
localhostvs.127.0.0.1disagree. Some frameworks bind to one and not the other. If the connectivity check fails, try the address your framework’s own startup log prints, not the one you assumed.- The path is wrong. A webhook route mounted at
/webhooks/stripewill not receive anything if the extension is configured to send to/. Match the exact route your handler registers. - The handler returns a non-2xx status. This is the one failure mode that isn’t a configuration mistake: it’s the extension confirming your endpoint is reachable but your handler itself rejected the request, commonly because signature verification failed against the wrong secret. The run’s timeline records the response your handler actually sent, and you can replay the delivery once you’ve fixed it.
- A delivery seems to have vanished. An in-flight delivery whose outcome is unknown — for example your dev server restarted mid-request — is recovered automatically rather than silently dropped or silently duplicated; if you instead want to reproduce a provider redelivering the same event on purpose, that’s a different, separately documented scenario.
Comparing the ways to get a webhook onto localhost
| Approach | Setup cost | Inbound exposure | Account/credentials needed | Deterministic & repeatable | Reproduces duplicates/reordering | Evidence of a run | CI-friendly |
|---|---|---|---|---|---|---|---|
| Inbound tunnel (e.g. ngrok) | Install an agent, authenticate it, start a tunnel per session | Yes — your machine becomes reachable from the public internet for as long as the tunnel is open | Tunnel-provider account and an authtoken | No — you get whatever event a real account fires, whenever it fires | Only by coincidence, not on demand | Whatever the provider’s own dashboard shows you | Awkward — a long-lived public tunnel in CI is a real exposure, not a convenience |
| Provider CLI listener (e.g. Stripe CLI) | Install the CLI, log in once | No — it opens an outbound connection from your machine and forwards events over that; no public URL is created | A provider account, via CLI login | No — trigger fires the provider’s real fixture logic against your live sandbox account, not a fixed payload |
Not directly — you’d need to trigger the same event twice yourself | Provider dashboard plus your own logs | Workable in CI with stored credentials, but ties every run to one shared sandbox account |
| Hand-rolled fixtures and curl | Write and sign a payload yourself | None | None | Only as deterministic as you make it — easy to sign correctly, easy to get subtly wrong | Whatever you script — but nothing checks that it matches what the provider would actually send | Only what you log yourself | Fully scriptable, but the fixture is only as trustworthy as its author |
| Hosted request inspector (e.g. Webhook.site) | Open a URL, sometimes register for a longer-lived one | Yes for the inspector’s own hosted endpoint, though not for your machine directly — you then still have to relay the request onward yourself | None for casual use; an account for durable URLs | Shows you whatever arrives, unmodified | No — it inspects, it does not generate or replay a provider’s delivery behavior | Excellent for eyeballing one request; not built for asserting on a sequence | Not designed for this — it’s an interactive inspection tool |
| Finxture (deterministic local sandbox) | Install the extension, approve one loopback origin | None — delivery goes straight from the extension to your configured loopback address | None — no account, no cloud connection | Yes — a reviewed scenario definition, not whatever an account happens to produce | Yes — this is what the scenario catalog is for; a duplicate-delivery scenario exists for exactly this | A durable, exportable run timeline that survives a service-worker restart | Not yet — it’s a Chrome side-panel UI today, with no headless or CLI entry point |
Two of these rows deserve a direct statement instead of a table cell: a provider CLI listener is the right tool when you specifically need a real account’s real test-mode event — verifying against actual provider behavior is not something a fixed scenario can substitute for. An inbound tunnel is the right tool when the real provider genuinely needs to reach your machine, for example while working through a live integration issue with the provider’s own support. Neither is worse than the other; they answer different questions than “does my handler correctly process the events my own tests need to prove.”
When you should not use Finxture
This is deliberately not a soft caveat. Finxture is the wrong tool for:
- Verifying against live provider behavior. A reviewed scenario proves your handler does the right thing with the event shapes and orderings the scenario defines. It cannot tell you whether your Stripe account, in its current configuration, actually produces those shapes — for that, trigger a real event with the Stripe CLI or complete a real checkout in test mode.
- Provider objects the extension doesn’t serve. The extension currently ships two scenarios — a successful payment and a duplicate delivery. If you need to test against refunds, disputes, multicapture, or any other object the catalog doesn’t yet cover, this tool has nothing for you today.
- Anything requiring authoritative payment state. The extension delivers signed webhooks to your loopback endpoint; it does not expose a provider-compatible API. If your test needs to call back into “Stripe” to fetch or mutate the state of the object a webhook referenced, there is no such endpoint here — that belongs to a future, separate cloud sandbox and does not exist yet.
If any of those is what you actually need, use the Stripe CLI or a real test account instead, and come back to a deterministic sandbox for the repeatable regression tests those tools are a poor fit for.
Why “no ngrok” is the point, not a slogan
An inbound tunnel is not unsafe by accident — it is unsafe by design, in the specific sense that its entire job is to make a process on your laptop reachable from the public internet. That is the correct trade for some tasks. It is a strange trade for the very common task of proving “when my endpoint receives this exact event, my handler does the right thing,” which needs nothing from the internet at all — only a source of correctly shaped, validly signed events and a place to send them. A local sandbox that never opens your machine to inbound traffic answers that question with a strictly smaller attack surface, no account, and no per-session setup step to forget.