# php-stripe-webhook-tester: no tunnel needed

> A small PHP package that simulates a Stripe webhook POST against a local endpoint, so event handling can be exercised on your own machine without ngrok. The API is three calls, the API version is a parameter rather than a constant, and Laravel Cashier needs one method overridden to accept the dummy event.

**teamtnt/php-stripe-webhook-tester** — A PHP package for testing Stripe Webhooks localy

- Repository: https://github.com/teamtnt/php-stripe-webhook-tester
- Stars: 100 · Forks: 25
- Language: PHP
- License: MIT
- Published: 2026-08-17 · Updated: 2026-08-18 · Language: en
- Canonical page: https://hysenlabs.com/projects/teamtnt-php-stripe-webhook-tester

## The point is to take ngrok out of the loop

Testing a webhook normally means getting a public URL in front of your development machine, because Stripe delivers events over the internet to a URL you registered. That is the friction this package removes. Its stated goal is to make testing Stripe webhooks easy on a local machine without the use of ngrok or other similar tunneling services. What it does instead is simulate a POST request to an endpoint you specify, carrying JSON with event data, so your application receives something shaped like the real thing and reacts accordingly. Nothing about your handler changes: it still reads the request body, still finds an event type, still runs its branch. The only thing that is missing is the trip through Stripe, which is exactly the part that is not under test anyway.

## Three calls, and the response comes back

The whole interface is visible in the usage example:

```php
$tester = new TeamTNT\Stripe\WebhookTester();
$tester->setVersion('2018-05-21');
$tester->setEndpoint('http://local.dev/stripe/webhooks');

$response = $tester->triggerEvent('charge.succeeded');
```

You construct the tester, tell it which Stripe API version to speak, point it at the endpoint that should receive the simulated request, and then name the event to fire. The return value is the response from your own endpoint, which is what makes assertions possible: you can check the status code or inspect the body your handler produced. So the package is not only a request generator, it is a round trip, which is why it can be used in a test as easily as in a console script while you are still building the handler.

## The chained example in the README has an unclosed quote

There is a second example, and it is worth reading carefully before copying it. It shows the endpoint passed to the constructor and then chains the rest, which tells you the constructor accepts an endpoint argument:

```php
$tester = new TeamTNT\Stripe\WebhookTester('http://local.dev/stripe/webhooks);
$response = $tester->setVersion('2014-09-08')->triggerEvent('charge.succeeded');
```

As printed in the README the URL is missing its closing quote and parenthesis, so that line will not parse; the intent is clear, and the three-call form above is the version to copy. The example also uses a different API version, 2014-09-08, where the first used 2018-05-21, which is a reminder that the version is a parameter and that the two examples are not meant to be consistent with each other.

## The API version is a parameter, and the fixtures live in a directory

Because the version is set per call rather than configured globally, the same tester can exercise different Stripe payload shapes, which matters when you are migrating an endpoint between API versions. Where those shapes come from is documented by directory rather than by table: the available versions and events are in the webhooks directory under src/webhooks. So the catalogue is inspectable in the repository, and adding a new version is a release rather than a configuration change. The release history shows exactly that pattern, with v1.1.3 titled as adding the 2018-05-21 webhooks in July 2018. For a maintainer that is the extension point to know about, and for a user it is the place to check whether the event you need is present before writing a test against it.

## Laravel Cashier needs one method overridden

Cashier is the case the README spends most of its space on, because the dummy event fails against it by design. Cashier verifies that the event it received is real by asking Stripe about it, so a simulated event does not exist there and the request is rejected. The fix is to override eventExistsOnStripe() in the Laravel Cashier webhook controller:

```php
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 structure is the important part. The environment check short circuits to true for testing and local, while every other environment keeps the real verification against Stripe using the configured secret, and a failure returns false rather than throwing.

## Carbon left the dependency list in two releases five minutes apart

The release history is short and says something specific. v1.3.0 is titled remove carbon as a dependency and v1.2.0 is titled removing carbon as a dependency, both published on 23 September 2019, about five minutes apart, which suggests a correction and a re-release of the same change rather than two separate pieces of work. Before those, v1.1.3 added the 2018-05-21 webhooks in July 2018. So the package stopped taking a date library dependency at some point in 2019, and that is the kind of change that matters if you are integrating it into a project where Carbon is already present, because it stops the two libraries fighting over the same global state. The default branch is master, the repository is not archived, and the last recorded push is 28 June 2022, which is later than the newest tag and shows that unreleased work sits on the branch.

## Tests are run with phpunit directly

There is no test wrapper to learn. The testing section is one command:

```bash
$ phpunit
```

The tree explains the rest of the shape. There is composer.json for the package definition, phpunit.xml and phpunit.php for the test configuration, src/ for the library itself with the webhooks fixtures inside it, tests/ for the suite, a .gitignore, a CHANGELOG.md, a CONTRIBUTING.md and LICENSE.md for the MIT terms. Continuous integration is referenced through badges for Travis and Scrutinizer, with a .travis.yml at the root, which places the project in an older generation of PHP tooling. Installation is a single Composer line, composer require TeamTNT/php-stripe-webhook-tester, and the package is published on Packagist under the TeamTNT vendor name, so nothing else is needed to start.

## Conclusion

php-stripe-webhook-tester fits a PHP developer who wants to exercise webhook handling without exposing a local machine to the internet, and it is small enough to read in one sitting, with the event fixtures sitting in a directory you can browse. Two things to know before you rely on it. The package last released in September 2019, with two releases minutes apart that both removed Carbon as a dependency, and the last commit on master is 28 June 2022, so check that your PHP version and the event fixtures you need are still in step. And if you use Laravel Cashier, the integration is not automatic, because Cashier verifies the event against Stripe and you have to override that check. MIT licensed, installed through Composer under the TeamTNT vendor name, and the tests are run with phpunit directly.

## FAQ

### How do I test a Stripe webhook locally?

With this package, by simulating the request instead of receiving it. Create a TeamTNT\Stripe\WebhookTester, set the API version with setVersion, point it at your endpoint with setEndpoint, then call triggerEvent('charge.succeeded'), which sends the POST with event JSON and returns the response from your endpoint.

### How do I install the PHP Stripe webhook tester?

With Composer: composer require TeamTNT/php-stripe-webhook-tester. The package is published on Packagist under the TeamTNT vendor name, and the MIT licence text is in LICENSE.md at the repository root.

### Why does my local webhook test fail under Laravel Cashier?

Because Cashier checks the event against Stripe, and a simulated event does not exist there. Override eventExistsOnStripe() in the Cashier webhook controller to return true when the environment is testing or local, keeping the real Stripe lookup for every other environment.

### Which Stripe events can the PHP webhook tester trigger?

The available versions and events are the ones in the webhooks directory under src/webhooks. The set grows with releases, for example v1.1.3 added the 2018-05-21 webhooks, so check that directory for the event you need before writing a test.

### Do I need ngrok to test webhooks with this package?

No. Removing ngrok and similar tunneling services is the stated purpose: the package posts simulated event JSON straight to a local endpoint you specify, so your handler runs as it would in production.

## Sources

- [Official README](https://github.com/teamtnt/php-stripe-webhook-tester#readme)
- [Project repository](https://github.com/teamtnt/php-stripe-webhook-tester)
- [Release notes](https://github.com/teamtnt/php-stripe-webhook-tester/releases)

---

Hysen Labs editorial analysis, written from the project's own repository and release notes. Cite the canonical page: https://hysenlabs.com/projects/teamtnt-php-stripe-webhook-tester
