marshmallow/laravel-facebook-webhook
Handle Facebook Webhook callbacks in Laravel with ease. This package verifies the hub challenge, validates X-Hub-Signature payloads, and routes incoming events to your own handlers, so you can focus on building Messenger bots and integrations instead of boilerplate.
Install the package
composer require marshmallow/laravel-facebook-webhook
The package is auto-discovered. Run php artisan vendor:publish (or the tag it advertises in its README, typically --tag="facebook-webhook-config" and --tag="facebook-webhook-migrations" if it ships storage) to publish its config file.
Look at config/facebook-webhook.php first. This is the single most important file to read. Expect entries for:
app_secret (used to verify X-Hub-Signature-256)verify_token (echoed back during the GET subscription handshake)page_access_token (Graph API calls to fetch lead details)graph_api_version / graph_urlSet the environment variables in .env:
FACEBOOK_APP_SECRET=...
FACEBOOK_VERIFY_TOKEN=some-random-string
FACEBOOK_PAGE_ACCESS_TOKEN=...
Never commit the app secret; store page tokens in the env/secret manager or a pages table.
Wire it up in the Meta App Dashboard.
https://your-app.test/webhooks/facebook, check the route the package registers with php artisan route:list | grep -i face).leadgen field.First use case: capture a test lead. Facebook's Lead Ads Testing Tool (in the app dashboard) lets you fire a fake leadgen event without spending money. Submit one, then check storage/logs/laravel.log for what the package received. The event payload usually contains only leadgen_id, page_id, form_id — the actual lead data requires a Graph API call, which the package handles for you.
Listen for the package event rather than editing the package. Nearly every real integration is:
// app/Providers/EventServiceProvider.php
protected $listen = [
\Marshmallow\FacebookWebhook\Events\LeadReceived::class => [
\App\Listeners\StoreFacebookLead::class,
],
];
Confirm the exact class name with grep -r "class .*Event" vendor/marshmallow/laravel-facebook-webhook/src. The vendor folder is small enough to read end-to-end in ten minutes — do that before assuming behavior.
Keep the listener queued.
class StoreFacebookLead implements ShouldQueue
{
public function handle(LeadReceived $event): void
{
Lead::updateOrCreate(
['leadgen_id' => $event->leadgenId],
['form_id' => $event->formId, 'payload' => $event->data, 'page_id' => $event->pageId],
);
}
}
Facebook expects a 2xx within a few seconds or it retries and eventually disables the subscription. Push Graph API calls and CRM syncs onto a queue.
Multi-page / multi-tenant pattern. If you manage several Pages, don't rely on a single page_access_token. Store page_id → access_token in a table and resolve the token in your listener (or bind a custom resolver if the package exposes one). The webhook payload always carries page_id, so this is straightforward.
Idempotency. Facebook delivers at-least-once. updateOrCreate keyed on leadgen_id (or a unique index) is the cheapest defense. Add a received_at column so you can later distinguish "delivered twice" from "lead was updated."
Backfill / replay pattern. Because the webhook only gives you a leadgen_id, you can always re-fetch from Graph API. Write a small artisan command that accepts a leadgen_id and calls the package's lead-fetching service (or hits Graph directly) — invaluable when a queue job fails or a lead silently doesn't arrive.
Testing pattern:
Http::fake(['graph.facebook.com/*' => Http::response(['id' => '123', 'field_data' => []])]);
$payload = json_encode(['entry' => [['changes' => [['field' => 'leadgen', 'value' => ['leadgen_id' => '123']]]]]]);
$sig = 'sha256=' . hash_hmac('sha256', $payload, config('facebook-webhook.app_secret'));
$this->postJson('/webhooks/facebook', json_decode($payload, true), [
'X-Hub-Signature-256' => $sig,
]);
A raw $this->call('POST', ...) with $request->getContent() set explicitly is more reliable than postJson for signature tests.
Integration tip: emit your own domain event. Don't let the rest of your app depend on the vendor's event class. Have StoreFacebookLead dispatch App\Events\LeadCaptured — this makes swapping the package (or moving to a direct webhook controller) a one-file change.
$request->all() or decoding-and-re-encoding JSON will break it. If the package's middleware fails here, the usual culprit is another middleware (TrimStrings, ConvertEmptyStringsToNull) mutating input — verify against $request->getContent() and consider excluding that route from the global middleware group.GET vs POST on the same URI. The verification handshake (hub.mode=subscribe, hub.challenge, hub.verify_token) is a GET; deliveries are POST. Both must be routed. If subscription fails with "URL couldn't be validated," the GET handler isn't returning the plain challenge string — some middleware or the package returning JSON instead of 200 text/plain causes exactly this./{page-id}/subscribed_apps must include leadgen. This is the #1 "the tool fires but nothing hits my server" cause.OAuthException on the Graph call.graph_api_version is a hard dependency on Meta's deprecation calendar. Old versions silently stop working on their sunset date; check the version string when leads suddenly stop mid-year.leadgen_id arrives in the payload. Everything else — name, email, phone, custom questions — is a Graph call to /{leadgen_id}. If you're seeing empty lead data, the webhook worked and the Graph fetch failed; check the token and the access_token query param on the outgoing request log.php artisan serve alone won't work; use a tunnel (ngrok http 8000, expose, or valet share) and paste the HTTPS URL into the dashboard. Re-verify the subscription every time the tunnel URL changes (free ngrok URLs rotate).app/ if maintenance lapses. Pin the version in composer.json and keep an eye on the release history.Event::listen + a thin custom route is the pragmatic fallback.How can I help you explore Laravel packages today?