teamTNT/php-stripe-webhook-tester: firing Stripe events at a local endpoint without ngrok
A PHP package for testing Stripe Webhooks localy
At a glance
- What is it?
- A small Composer package that POSTs recorded Stripe event payloads at your own webhook route, so you can exercise the handler on a local machine. It has not been pushed to since 2019-09-23, and that shapes who should use it.
- Who is it for?
- Adopt it if you run a PHP or Laravel app with a webhook route you can reach over plain HTTP on the same machine and you only need to replay events that already exist under src/webhooks. Do not adopt it if your integration depends on Stripe API versions released after the payloads shipped in v1.3.0, or if you need signature verification exercised, because the package posts unsigned JSON.
- Can I use it commercially?
- Yes. MIT is a permissive licence: you can use, modify and sell software built on it, as long as you keep its copyright and licence notices.
- Is it still maintained?
- Probably not. The repository last received commits 51 months ago, on June 28, 2022.
- What is it written in?
- Mainly PHP, according to GitHub's language statistics.
Answers come from the project's GitHub data, last synced on September 27, 2026, and from our analysis. They are not legal advice.
Editorial analysis
What problem the package removes from local Stripe work
Stripe delivers events by HTTP POST to a public URL. A development machine usually is not public, so the standard answer is a tunnel such as ngrok, which adds a process, a temporary hostname and a dependency on an external service to every test run. This package takes the other route: it does not receive anything from Stripe. It constructs a POST request itself, aimed at an endpoint you name, carrying a JSON body that represents an event. The README states the goal plainly, to make testing Stripe webhooks easy on a local machine "without the use of ngrok or other similar tunneling services", and describes the behaviour as simulating a post request so that your application reacts accordingly.
The audience is narrow and identifiable. You are writing PHP, you have a route that already accepts Stripe webhook payloads, and you want to drive that route from a test or a console script. Laravel Cashier users are called out explicitly in the README, which is a strong hint about the intended shape of the host application. If your webhook handler lives in another language, or if the code path you want to test only triggers on real Stripe-side state changes, this package does not help.
How the tester builds and sends an event
The public surface is three calls. You construct TeamTNT\Stripe\WebhookTester, set an API version with setVersion, set the target URL with setEndpoint, and call triggerEvent with an event name. The README's first example does exactly that and assigns the result to $response, so the return value is the response from your endpoint rather than a boolean. Chained calls are supported too, and the README shows a constructor that accepts the endpoint URL directly, which shortens the setup to two statements.
What sits behind triggerEvent is the interesting part for anyone deciding whether to trust it. The README points at the webhooks directory, src/webhooks, as the place where available versions and events live. That means the payloads are stored in the repository as data files rather than fetched from Stripe at run time. The API version you pass to setVersion selects which set of files is used. This is a static fixture design, and it has a direct consequence: the set of events you can fire is exactly the set committed to the repository, and the payload shape is frozen at whatever Stripe's API looked like when those files were written. There is no round trip to Stripe, no authentication, and no signing step described in the README.
Because the request is assembled locally, the package never exercises the parts of a webhook integration that concern transport: signature header verification, retry behaviour, and duplicate delivery. Those are the areas where production webhook handlers actually break. The tester covers routing, deserialisation and your application's reaction to a given event body, which is a real slice of the problem, just not the whole of it.
Installing it and firing a first charge.succeeded event
Installation is a single Composer command, and the README gives it verbatim:
$ composer require TeamTNT/php-stripe-webhook-testerAfter that, the smallest useful script sets the API version and the endpoint, then triggers one event. The README's example uses charge.succeeded and a local hostname:
$tester = new TeamTNT\Stripe\WebhookTester();
$tester->setVersion('2018-05-21');
$tester->setEndpoint('http://local.dev/stripe/webhooks');
$response = $tester->triggerEvent('charge.succeeded');Run that from a test case or a scratch script while your application is serving on the endpoint you named. What you should see is your own webhook route being hit with a JSON body, and $response holding whatever your route returned. If nothing arrives, the failure is almost always the endpoint URL or the web server, not the package, because there is no network hop to Stripe that could fail.
The README also shows the chained form, which is shorter and worth using when the endpoint is fixed:
$tester = new TeamTNT\Stripe\WebhookTester('http://local.dev/stripe/webhooks);
$response = $tester->setVersion('2014-09-08')->triggerEvent('charge.succeeded');Before relying on either snippet, check the event name against src/webhooks. The directory listing is the authoritative list of what the package can fire, and the README defers to it rather than enumerating events itself.
The Laravel Cashier override you have to write yourself
This is the part of the README that matters most for real Laravel applications, and it is also the part that dates fastest. Cashier's WebhookController calls eventExistsOnStripe to confirm that an incoming event id corresponds to a real event on Stripe's side. A locally generated payload has no such counterpart, so verification fails and the controller rejects the request. The README's answer is to override that method and short-circuit it in local and testing environments:
protected function eventExistsOnStripe($id)
{
if(App::environment() == 'testing' or App::environment() == 'local') {
return true;
}
try {
return ! is_null(StripeEvent::retrieve($id, Config::get('services.stripe.secret')));
} catch (Exception $e) {
return false;
}
}The README warns that without the environment checks Cashier tries to verify the dummy event against Stripe and fails. Read that override as a security boundary, not boilerplate. It makes your application accept any webhook payload that reaches it while the environment is local or testing. If a staging host is ever configured with APP_ENV set to local or testing, that check is disabled there too. The environment comparison is string based and depends entirely on how your application sets those values, so the override is only as tight as your environment configuration.
There is a second cost here. The snippet references StripeEvent::retrieve and Config::get, which are Cashier-era APIs. Anyone on a newer Cashier will have to find the equivalent verification seam in their version, and the README does not describe one.
Where the package stops being the right tool
The last push to this repository was on 2019-09-23, and the most recent release, v1.3.0, is from the same date. Nothing here is actively maintained in any meaningful sense, and the practical effect is payload drift. Stripe's API versions move; the fixtures under src/webhooks do not. If your application is pinned to an API version newer than the ones committed to this repository, the payloads you fire will not match what production receives, and a test that passes locally can still fail against real traffic. The README's own example uses version 2018-05-21, which is a fair indication of the era the fixtures cover.
Two further limits follow from the design. First, the package posts unsigned JSON, so any handler that verifies the Stripe-Signature header will reject the request, and you cannot use this tool to test that verification path at all. Second, there is no delivery semantics: no retries, no out-of-order events, no duplicate ids. Handlers that depend on idempotency keys or on the ordering of subscription lifecycle events are outside what a single triggerEvent call can express. If your interest is in how your code copes with Stripe's delivery guarantees, a tunnel to real test-mode events is the more honest setup.
Alternatives and the difference in approach
The obvious alternative is the official Stripe CLI, which forwards real test-mode events from Stripe to a local endpoint. The difference is not cosmetic. The CLI gives you genuine payloads for the API version your account is on, plus real signature headers, so signature verification is exercised rather than bypassed. It also lets you replay events that Stripe actually generated, including ones with unusual shapes. What it costs you is a running process, a login, and a network dependency during tests. This package trades all of that away for zero external dependencies and a static fixture set, which is why it is fast and why its payloads cannot keep up.
A second option is to keep hand-written JSON fixtures in your own test suite and post them with your HTTP client of choice. That gives you full control over the payload and lets you update it when Stripe changes, at the cost of writing and maintaining the fixtures yourself. The package is essentially that pattern with the fixtures already written and a thin triggerEvent wrapper on top. If you only need two or three events, rolling your own is not much more work and removes a stale dependency from composer.json.
Licence and the cost of keeping it in composer.json
The project is MIT licensed, and the README points at LICENSE.md for the full text. MIT is permissive: you can use it in commercial applications, modify it, and ship it, provided the copyright notice and permission notice travel with it. Nothing in the README suggests dual licensing or a commercial tier, and there is no CLA discussion to worry about. This is a description of the licence, not legal advice; read LICENSE.md if the terms matter to your organisation.
The upgrade cost is the more practical question. v1.1.3 added webhooks for 2018-05-21, and v1.2.0 and v1.3.0 both removed Carbon as a dependency, with v1.3.0 landing on 2019-09-23. Those release notes tell you the package was being trimmed rather than extended in its final months. Since then there have been no releases, so the realistic maintenance model is that you fork it and edit src/webhooks yourself when you need a payload Stripe has since changed. That is a small amount of work, but it means the package is a starting point you own, not a dependency you update.
Editorial conclusion
Adopt it if you run a PHP or Laravel app with a webhook route you can reach over plain HTTP on the same machine and you only need to replay events that already exist under src/webhooks. Do not adopt it if your integration depends on Stripe API versions released after the payloads shipped in v1.3.0, or if you need signature verification exercised, because the package posts unsigned JSON. Before wiring it into a test suite, open src/webhooks and confirm the event names and API version you need are actually present, and check whether Laravel Cashier's eventExistsOnStripe override is still the right seam in your Cashier version.
Frequently asked questions
What are Stripe webhooks for?
They are how Stripe tells your application that something happened, by sending an HTTP POST with event data to a URL you configure. This package does not receive those events; it simulates the POST against a local endpoint so your handler can be exercised without a public URL.
How do I test a webhook in Stripe with php-stripe-webhook-tester?
Install the package with Composer, set the API version and your endpoint URL, then call triggerEvent with an event name such as charge.succeeded and inspect the returned response. The event names and versions available are listed under src/webhooks in the repository.
How can I test my webhooks without ngrok?
The README states the package exists precisely to test Stripe webhooks on a local machine without ngrok or similar tunneling services, by building the POST request itself rather than receiving one from Stripe.
Does php-stripe-webhook-tester work with Laravel Cashier?
Yes, but you must override eventExistsOnStripe in Laravel\Cashier\WebhookController to return true in local and testing environments, because Cashier otherwise tries to verify the dummy event against Stripe and fails. The README gives the override as a code example.
Official sources
Add this badge to your README
If you maintain this project, the badge below links readers to this analysis and shows its maintenance status from the daily GitHub snapshot. Paste the markdown into your README; add ?metric=license or ?metric=stars to the image URL for a different field.
[](https://hysenlabs.com/projects/teamtnt-php-stripe-webhook-tester)