Skip to content

Quickstart

Quickstart

1. Install

composer require cboxdk/laravel-webhook-signature

The service provider is auto-discovered. Publish the config when you are ready to declare endpoints:

php artisan vendor:publish --tag=webhook-signature-config

2. Declare an endpoint

An endpoint binds a name to a scheme and its secrets. Application code refers to the name, so the secret never appears in a controller, a test, or a diff.

// config/webhook-signature.php
'endpoints' => [
    'github' => [
        'scheme'  => 'github',
        'secrets' => [env('GITHUB_WEBHOOK_SECRET')],
    ],
],

3. Protect the route

Route::post('/webhooks/github', GithubController::class)
    ->middleware('webhook.signature:github');

Register it early in the stack. Anything that reads and rewrites the request body before this point changes the bytes the signature covers, and every delivery will then fail for reasons that look nothing like the cause.

4. Use the proven payload

use Cbox\WebhookSignature\ValueObjects\VerifiedWebhook;

class GithubController
{
    public function __invoke(Request $request)
    {
        $webhook = VerifiedWebhook::fromRequestOrFail($request);

        ProcessGithubEvent::dispatch($webhook->json())
            ->onQueue('webhooks');

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

Read the payload from the VerifiedWebhook, not from $request->all(). They contain the same data today; the difference is that one of them is the data that was actually signed.

5. Test it

use Cbox\WebhookSignature\Testing\InteractsWithWebhookSignatures;

uses(InteractsWithWebhookSignatures::class);

it('accepts a genuine delivery', function () {
    $this->fakeWebhookEndpoint('github', 'github', 'test-secret');

    $this->postSignedWebhook('/webhooks/github', 'github', ['action' => 'opened'])
        ->assertOk();
});

it('refuses an unsigned one', function () {
    $this->fakeWebhookEndpoint('github', 'github', 'test-secret');

    $this->postUnsignedWebhook('/webhooks/github', ['action' => 'opened'])
        ->assertUnauthorized();
});

Write both. A suite that only posts valid signatures proves the happy path and nothing at all about the control.

6. Try it by hand

php artisan webhook:sign github --body='{"action":"opened"}' --curl

Prints a runnable curl command signed with the endpoint's real secret.

7. Send signed webhooks of your own

Declare an endpoint the same way, then:

Http::webhookSignature('outbound')->post($subscriber->url, $payload);

That is the whole API — no headers to attach and no encoded body to keep in a variable. The signature is applied by middleware inside the HTTP client, at the moment the request goes out, so what is signed is by construction what is sent. See sending signed webhooks.

Next