ngrok for Laravel Webhook Testing

3 min readUpdated on

ngrok gives a local endpoint a temporary public HTTPS URL. It is useful for development, but every inbound request remains hostile until the provider signature is verified. Register the generated callback URL in the provider console as normal prose, not a heading hidden in the middle of an article.

bash
php artisan serve
ngrok http 8000

For Stripe, verify the raw body with the official signing secret. md5, $request->all() and a client-provided event type prove nothing and can also alter the payload before verification.

php
<?php

declare(strict_types=1);

use Illuminate\Http\Request;
use Stripe\Exception\SignatureVerificationException;
use Stripe\Webhook;

public function __invoke(Request $request): Response
{
    try {
        $event = Webhook::constructEvent($request->getContent(), (string) $request->header('Stripe-Signature'), config('services.stripe.webhook_secret'));
    } catch (SignatureVerificationException) {
        abort(400, 'Invalid Stripe signature.');
    }

    ProcessStripeEvent::dispatch($event->id, $event->type, $event->data->object);

    return response()->noContent();
}

Persist the provider event ID under a unique database constraint before irreversible work; retries are normal. Return a fast 2xx after validation and queue slow work. Remove dd() before a provider test: it produces failed deliveries and may expose payment data in a debug page. ngrok's inspector is valuable for comparing raw payloads and headers, never as authentication.

Create a tunnel that matches the local host

Install ngrok, authenticate once and forward the port Laravel really serves. A free URL changes between sessions; a reserved domain makes a shared development callback stable. Neither is a production ingress.

bash
ngrok config add-authtoken "$NGROK_AUTHTOKEN"
php artisan serve --host=127.0.0.1 --port=8000
ngrok http --domain=webhooks-dev.example.ngrok.app 8000

Herd and Valet use a hostname rather than localhost. Preserve it when the application depends on host-based routes, signed URLs or cookie domains.

bash
ngrok http --host-header=app.test 80

Persist first, then queue an idempotent delivery

The raw body must be verified before parsing. A unique provider event ID, not an in-memory flag, survives concurrent retries and worker restarts.

php
<?php

declare(strict_types=1);

namespace App\\Http\\Controllers\\Webhooks;

use App\\Jobs\\ProcessStripeEvent;
use App\\Models\\WebhookDelivery;
use Illuminate\\Http\\Request;
use Illuminate\\Http\\Response;
use Stripe\\Exception\\SignatureVerificationException;
use Stripe\\Webhook;

final class StripeWebhookController
{
    public function __invoke(Request $request): Response
    {
        try {
            $event = Webhook::constructEvent($request->getContent(), (string) $request->header('Stripe-Signature'), (string) config('services.stripe.webhook_secret'));
        } catch (SignatureVerificationException) {
            return response()->noContent(400);
        }

        $delivery = WebhookDelivery::query()->firstOrCreate(['provider' => 'stripe', 'provider_event_id' => $event->id]);

        if ($delivery->wasRecentlyCreated) {
            ProcessStripeEvent::dispatch($delivery->getKey());
        }

        return response()->noContent();
    }
}

Inspector versus a feature test

Use the inspector to compare raw headers, payload and response after a valid, forged and duplicate delivery. It is a diagnostic tool, never authentication. Most branches are faster and more reliable as a feature test; reserve ngrok for the provider-console and real-retry seam.

php
<?php

declare(strict_types=1);

it('rejects a forged Stripe signature', function (): void {
    $this->postJson('/webhooks/stripe', ['id' => 'evt_test'], ['Stripe-Signature' => 't=1,v1=forged'])
        ->assertBadRequest();
});

When not to use ngrok

Do not treat a temporary tunnel as a production ingress, and do not use it when an official provider CLI can forward signed events more faithfully. Automated tests should use signed fixtures and replay paths without requiring an external tunnel.

Related articles

Existing system support

Need help with a live application?

I help companies improve live systems, clean up delivery workflows, and ship new features without adding avoidable complexity.

Comments (0)
Sign in to leave a comment

You need to be signed in to add a comment.

Login

Need someone to take responsibility for the next step?

Let’s talk about your project and define a scope that actually makes sense for your goals.