LOCALTR / DOCS

Test a Laravel webhook locally from 422 to 200

Use a local-only JSON endpoint in Laravel 13 and distinguish validation, a narrow CSRF exception and real replay side effects.

Environment
Your Laravel 13 project · PHP/Composer required
Evidence scope
HTTP flow checked in a local Laravel 13.38.0 / PHP 8.5.11 example project. Test the real public tunnel and provider connection separately.
Expected outcome
In the supplied local test, check that missing currency gives 422 and adding it gives 200.

In this guide

Prerequisites

Your Laravel 13.x project must satisfy its Composer platform requirements. Check the CLI with php --version and php artisan --version. This HTTP flow was checked in an isolated Laravel 13.38.0 / PHP 8.5.11 fixture. That does not verify a LocalTR public tunnel or provider integration. Run the local test without LocalTR, then check app steps separately with an available Mac release and the required permissions.

Register the test route only in local mode

Add this route to routes/web.php in your test project. Keep the environment guard; this example route is absent in production:

use Illuminate\Http\Request;
use Illuminate\Support\Facades\Route;

if (app()->environment('local')) {
    Route::post('/webhooks/localtr-demo', function (Request $request) {
        abort_unless($request->isJson(), 415);
        abort_if(strlen($request->getContent()) > 4096, 413);
        $request->validate([
            'event' => ['required', 'in:payment.test'],
            'currency' => ['required', 'in:TRY'],
        ]);
        return response()->json(['status' => 'received']);
    });
}

The handler only validates a fictional event. It creates no payment, order, email or persistent record. Do not replace a real endpoint’s signature verification with this fixture.

Limit the CSRF exception to one URI

In Laravel 13, add only this URI to the existing withMiddleware block in bootstrap/app.php, retaining other settings:

$middleware->preventRequestForgery(except: ['webhooks/localtr-demo']);

Do not use * or remove protection from the entire web group. Keep route registration local-only and remove this exception when removing the fixture route. An exception does not authenticate a provider. Laravel’s PHPUnit testing mode can handle CSRF differently, so check the normal HTTP request below as well.

Send the original and corrected request

Start your local project with php artisan serve --host=127.0.0.1 --port=8000. In another terminal:

curl -i http://127.0.0.1:8000/webhooks/localtr-demo \
  -H 'Accept: application/json' -H 'Content-Type: application/json' \
  --data '{"event":"payment.test"}'

Expect 422 with an errors.currency entry in JSON. Change only the body to {"event":"payment.test","currency":"TRY"}; expect 200 and {"status":"received"}. Without Accept: application/json, the error response format can differ. This fixture does not create a new business record when repeated; real POST endpoints require separate consideration of side effects and deduplication.

Troubleshooting, safety and next step

For 419, compare the request URI and narrow CSRF exception; do not disable protection globally. For 404, check the environment and php artisan route:list --path=webhooks. For 422, inspect JSON fields; for 405, verify POST. Keep real secrets, sessions and customer data out. Provider signatures may depend on the original raw body; do not assume an old signature stays valid after editing it.

Official references: Laravel routing, CSRF and validation. Next, check your target using the webhook/replay guide; app Replay sends a real request. Without the installer, inspect text using the HTTP example tool.

To try the same 422 → 200 distinction without a Laravel project, run the standalone Node.js webhook starter. This local test does not establish public tunnel or provider acceptance.

Your next step

Notify me when downloads open

The signed installer is in preparation. Check the download page for current release availability.

Does a step look different?

Contact support with your app version