Webhooks
Providers implement the HandlesWebhooks interface to receive inbound webhooks with signature verification, event routing, and deduplication.
The HandlesWebhooks interface
use Integrations\Contracts\HandlesWebhooks;
interface HandlesWebhooks
{
public function handleWebhook(Integration $integration, Request $request): mixed;
public function verifyWebhookSignature(Integration $integration, Request $request): bool;
public function resolveWebhookEvent(Request $request): ?string;
public function webhookHandlers(): array;
public function webhookDeliveryId(Request $request): ?string;
}Routes
Webhook routes are registered automatically:
| Route | Name | Purpose |
|---|---|---|
GET|POST /integrations/{provider}/webhook | integrations.webhook | Generic provider webhook |
GET|POST /integrations/{provider}/{id}/webhook | integrations.webhook.specific | Integration-specific webhook |
Point your external service's webhook URL at:
https://yourapp.com/integrations/zendesk/webhook
https://yourapp.com/integrations/zendesk/42/webhook # for a specific integrationWebhook routes have no middleware by default -- most providers can't handle CSRF or session auth.
Webhook lifecycle
When a webhook arrives:
- The provider is resolved from the URL
- Signature is verified via
verifyWebhookSignature() - The webhook is persisted to
integration_webhooks - A
WebhookReceivedevent is dispatched - A
ProcessWebhookjob is dispatched to the configured queue - The job calls your
handleWebhook()(or routed handler) - The result is logged in
IntegrationLog
Signature verification
class StripeProvider implements IntegrationProvider, HandlesWebhooks
{
public function verifyWebhookSignature(Integration $integration, Request $request): bool
{
$secret = $integration->credentialsArray()['webhook_secret'];
return hash_equals(
hash_hmac('sha256', $request->getContent(), $secret),
$request->header('Stripe-Signature', ''),
);
}
}Event type routing
Providers can declare how to extract the event type from the payload and route to specific handlers:
class StripeProvider implements IntegrationProvider, HandlesWebhooks
{
public function resolveWebhookEvent(Request $request): ?string
{
return $request->input('type'); // e.g. 'invoice.paid'
}
public function webhookHandlers(): array
{
return [
'invoice.paid' => HandleInvoicePaid::class,
'customer.created' => HandleCustomerCreated::class,
];
}
}Deduplication
Providers can declare a deduplication key to prevent processing the same webhook twice:
public function webhookDeliveryId(Request $request): ?string
{
return $request->header('X-Webhook-Id');
}When a duplicate is detected, the webhook is stored but not processed.
Queue processing
All webhooks are processed asynchronously via the ProcessWebhook job:
// config/integrations.php
'webhook' => [
'queue' => 'webhooks',
],Payloads exceeding webhook.max_payload_bytes (default 1MB) are rejected with a 413 response.
Replaying webhooks
Stored webhooks can be replayed by their webhook ID:
php artisan integrations:replay-webhook {webhookId}This reconstructs the request from stored data and re-dispatches it through handleWebhook().
Recovering stale webhooks
If a queue worker dies mid-processing, a webhook can get stuck in processing status. The recovery command finds these and re-queues them:
php artisan integrations:recover-webhooksAdd to your scheduler for automatic recovery:
Schedule::command('integrations:recover-webhooks')->hourly();A webhook is considered stale after webhook.processing_timeout seconds (default 1800 / 30 minutes).
Configuration
// config/integrations.php
'webhook' => [
'prefix' => 'integrations',
'queue' => 'default',
'max_payload_bytes' => 1_048_576, // 1MB
'processing_timeout' => 1800, // 30 minutes
'middleware' => [],
],