Weave Code
Weaver
Helps Laravel developers discover, compare, and choose open-source packages. See popularity, security, maintainers, and scores at a glance to make better decisions.
Feedback
Share your thoughts, report bugs, or suggest improvements.
Subject
Message

Laravel Facebook Webhook Laravel Package

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.

View on GitHub
Deep Wiki
Context7

Getting Started

  1. 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.

  2. 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_url
    • Route registration (URI, middleware, enabled/disabled) Read the config comments before touching anything else — most "why isn't this working" cases trace back to these keys.
  3. Set 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.

  4. Wire it up in the Meta App Dashboard.

    • Add the webhook callback URL (e.g. https://your-app.test/webhooks/facebook, check the route the package registers with php artisan route:list | grep -i face).
    • Enter the same verify token.
    • Subscribe the Page to the leadgen field.
  5. 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.

Implementation Patterns

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.

Gotchas and Tips

  • Signature verification must use the raw body. The HMAC is computed over the exact bytes Facebook sent. $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.
  • Respond 200 even when processing fails. If your handler throws, Laravel returns 500, Facebook retries with backoff, and after repeated failures disables the subscription — which you won't notice until leads stop arriving. Catch exceptions in the controller/listener, log, and return 200.
  • App must be in Live mode, and the Page must be subscribed. In Development mode only admins/testers/dev-mode users trigger webhooks. Also, subscribing the app is separate from subscribing the Page — /{page-id}/subscribed_apps must include leadgen. This is the #1 "the tool fires but nothing hits my server" cause.
  • Page tokens expire. User-derived page tokens are short-lived unless exchanged for long-lived ones; system-user tokens from Business Manager are the stable option for production. Plan for rotation — the package will happily use a stale token and return OAuthException on the Graph call.
  • Graph API version pinning. The config's 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.
  • Only 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.
  • Local development needs a public URL. 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).
  • Small package, small community (13 stars). There are zero dependents and a single maintainer, so budget for reading and potentially forking the source. The upside: the surface area is tiny, the license is MIT, and the code is easy to vendor into app/ if maintenance lapses. Pin the version in composer.json and keep an eye on the release history.
  • Extension points to check in the source: whether the route can be disabled (in case you want to own the controller for custom middleware), whether the signature check is skippable via config (useful for local replay testing, dangerous in prod), and whether the Graph API client is bound in the container so you can swap it for a faked one in tests. If any of these are missing, an Event::listen + a thin custom route is the pragmatic fallback.
Weaver

How can I help you explore Laravel packages today?

Conversation history is not saved when not logged in.
Prompt
Add packages to context
No packages found.
socialdept/tenant-domains
tautvydasr/blade-class-names
socialdept/color-scales
maherelgamil/controller
coleus/widgets
coleus/table
coleus/users
coleus/music
coleus/health
coleus/calendar
coleus/notes
coleus/support
coleus/settings
coleus/apps
seampact/laravel
emmyconceptng/laravel-ng-postcode
jjsquady/round-robin
skyraptor/chatter
edlugz/sasapay
abbasudo/artisan-route