Skip to main content

Quickstart

Send a signed webhook

use Pushery\Webhooks\Server\PendingWebhook;

PendingWebhook::create()
->url('https://example.com/webhooks')
->payload(['invoice_id' => 'in_123', 'amount' => 4200])
->useSecret('whsec_your_endpoint_secret')
->dispatch();

The delivery is queued, signed with a Standard Webhooks signature, and retried with backoff. Run a queue worker (or use ->dispatchSync() to send inline).

dispatch() returns the WebhookDeliveryData it queued, whose messageId (stable across retries) is your correlation key — a Server-only app, with no Platform delivery row, records it against its own log and any later status callback:

$id = PendingWebhook::create()
->url('https://example.com/webhooks')
->payload(['invoice_id' => 'in_123'])
->useSecret('whsec_your_endpoint_secret')
->dispatch()
->messageId;

The full builder — rotation, schemes, backoff, timeouts, mutual TLS, tags — is on the Sending page.

Receive and verify one

Enable the Client layer and describe the producer in config/webhooks.php:

'client' => [
'enabled' => true,
'configs' => [
[
'name' => 'partner',
'secret' => env('PARTNER_WEBHOOK_SECRET'),
// 'scheme' defaults to Standard Webhooks; set it per source for others.
// 'process' MUST be a Pushery\Webhooks\Client\Jobs\ProcessWebhookJob subclass — any
// other class throws when the config resolves. Implement handle() on it.
'process' => \App\Jobs\HandlePartnerWebhook::class,
],
],
],

Your handler extends the package's base job, which hands it the stored call and the parsed envelope:

use Pushery\Webhooks\Client\Jobs\ProcessWebhookJob;

class HandlePartnerWebhook extends ProcessWebhookJob
{
public function handle(): void
{
// $this->webhookCall (the stored row) and $this->message (the parsed envelope)

// An empty payload has two meanings, and only one of them is safe to shrug off.
// See Receiving for why this check matters more than it looks.
if (! $this->message->format->readable()) {
throw new \RuntimeException('This delivery arrived in a format nothing could read');
}
}
}

Point a route at it with the macro (registered only while the Client layer is on), in routes/api.php:

// routes/api.php
use Illuminate\Support\Facades\Route;

Route::webhooks('webhooks/partner', 'partner');
danger
Not routes/web.php

That file's middleware group carries CSRF protection, and a producer has no session and no token — so every delivery is answered 419 before the signature is ever verified. Nothing in this package sees those requests, and most producers treat 4xx as permanent and stop retrying, so the deliveries are not delayed, they are lost. webhooks:preflight fails on it and names the route.

An authentic request is verified, de-duplicated, stored and dispatched to your job; a request whose signature is invalid, expired or malformed is answered 401 and never reaches your job.

The whole pipeline, and every shipped verification adapter, is on the Receiving page.

Send-only setup (no database)

If all you want is the signed, SSRF-guarded, retrying sender, switch the Platform layer off and skip the migrations entirely — no PostgreSQL, no tables, no queries:

// config/webhooks.php
'platform' => ['enabled' => false, /* … */],
'server' => ['persistence' => ['enabled' => false], /* … */],

PendingWebhook keeps working exactly as above (it needs only a queue), and the package runs on whatever database your app already uses — or none.

One thing to know before you write a listener: a send-only app receives the transport event family only, never the Platform domain events. See Events.