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

Messenger Laravel Package

kerox/messenger

Kerox Messenger is a flexible PHP library for adding private messaging to your app. It provides threads, participants, and messages out of the box, with a storage-agnostic design, Doctrine support, and optional Laravel integration for quick setup.

View on GitHub
Deep Wiki
Context7

Getting Started

kerox/messenger is a thin, framework-agnostic PHP client for the Facebook Messenger Platform — not a Laravel package. There is no service provider and no publishable config, so you do the wiring yourself in ~10 minutes.

composer require kerox/messenger

1. Put the credentials in config/services.php (or a dedicated config/messenger.php). You need three values from the Meta app dashboard: Page Access Token, App Secret, and a Verify Token you invent yourself.

// config/messenger.php
return [
    'app_secret'   => env('FB_APP_SECRET'),
    'page_token'   => env('FB_PAGE_TOKEN'),
    'verify_token' => env('FB_VERIFY_TOKEN'),
];

2. Bind the entry point in AppServiceProvider::register(). Messenger is a stateless factory — every API call takes the page token as an argument, which is what makes it multi-page friendly later.

use Kerox\Messenger\Messenger;

$this->app->singleton(Messenger::class, fn () => new Messenger());

3. Where to look first: the Kerox\Messenger\Api\* classes (Send, User, Profile, MessengerProfile, Page) tell you what's possible; the Kerox\Messenger\Model\* tree tells you how to build payloads. Skim the README's send-API examples and stop — you don't need the rest on day one.

First use case: a webhook that echoes a reply.

// routes/web.php
Route::get('/webhook/messenger', function (Request $request) {
    abort_unless(
        $request->input('hub_mode') === 'subscribe' &&
        hash_equals(config('messenger.verify_token'), (string) $request->input('hub_verify_token')),
        403
    );

    return response($request->input('hub_challenge'), 200);
});
Route::post('/webhook/messenger', function (Request $request, Messenger $messenger) {
    $payload = $request->getContent();
    $signature = 'sha1=' . hash_hmac('sha1', $payload, config('messenger.app_secret'));
    abort_unless(hash_equals($signature, (string) $request->header('X-Hub-Signature')), 403);

    foreach ($request->all()['entry'] ?? [] as $entry) {
        foreach ($entry['messaging'] ?? [] as $messaging) {
            if (isset($messaging['message']['text'])) {
                $messenger->send(config('messenger.page_token'))
                    ->add(Message::create('You said: ' . $messaging['message']['text']))
                    ->send();
            }
        }
    }

    return response('EVENT_RECEIVED', 200);
});

Implementation Patterns

Always answer Meta with 200 first, then queue the work. Meta retries unacknowledged webhooks, and a slow send() call inside the request will cost you duplicate replies. Standard shape: verify signature → dispatch(new HandleMessengerEvent($messaging)) → return response('EVENT_RECEIVED', 200).

Map raw payloads to typed events instead of hand-reading arrays. The package ships Kerox\Messenger\Event\EventFactory, which turns a messaging entry into MessageEvent, PostbackEvent, QuickReplyEvent, OptinEvent, ReferralEvent, DeliveryEvent, etc. Follow the README's exact factory signature, then convert each event into one of your Laravel events:

$event = EventFactory::createEvent($messaging);

match (true) {
    $event instanceof PostbackEvent => PostbackReceived::dispatch($event->getSenderId(), $event->getPayload()),
    $event instanceof MessageEvent  => MessageReceived::dispatch($event->getSenderId(), $event->getText()),
    default                         => null,
};

Never call Messenger from a controller. Wrap it in a small service (MessengerService / FacebookSender) that exposes intent-level methods — sendText(), sendButtons(), sendProductCard(). Everything downstream benefits: you can fake the wrapper in tests, swap to the Instagram API later, and keep the payload-building noise out of your app code.

Build templates fluently; the models are chainable builders.

$element = GenericElement::create('Espresso Blend', 'https://shop.test/coffee')
    ->setSubtitle('$18 · 250g')
    ->setImageUrl('https://shop.test/img/coffee.jpg')
    ->addButton(WebUrl::create('Buy now', 'https://shop.test/coffee'))
    ->addButton(Postback::create('Remind me', 'REMIND_COFFEE'));

$template = GenericTemplate::create()->addElement($element);

$messenger->send($token)
    ->add(Message::create()->addAttachment($template))
    ->send();

Store the PSID (sender.id) as a column on your users table (messenger_psid, indexed, nullable). It's the only handle you get, and it's stable per page. Then $messenger->user($token)->getProfile($psid) gives you first/last name, locale, timezone and avatar if you need to bootstrap a record — but don't call it on every message; it's an extra HTTP round trip and can fail if the user removed the app.

Batch where you can. Send::add() accepts multiple messages before ->send() flushes once. This is the cheapest performance win available — Meta bills you in round trips, not messages.

Use the Messenger Profile API in a one-off artisan command rather than at boot: persistent menu, greeting text, get-started button, whitelisted domains. It's configuration-as-code, so keep it in a migration-style command you can re-run per environment.

Gotchas and Tips

  • Laravel mangles webhook dots. hub.mode, hub.verify_token and hub.challenge arrive as hub_mode, etc., because $request->input('hub.mode') treats the dot as a nested path. Use the underscored keys or $request->all()['hub.mode']. This trips up nearly everyone on the verification step.
  • Verify the signature against the raw body, not $request->all(). Any re-encoding breaks the HMAC. Meta sends X-Hub-Signature (SHA-1); prefer X-Hub-Signature-256 where available.
  • The 24-hour window is the real constraint on your product. Free-form replies only within 24h of the user's last message. Outside it you must attach a message tag (CONFIRMED_EVENT_UPDATE, POST_PURCHASE_UPDATE, ACCOUNT_UPDATE) via the message's tag setter, or the send fails silently-ish with a 400. Model this as a policy in your wrapper class, not as a caller decision.
  • Text messages cap at 2000 UTF-8 characters; templates cap at three buttons (generic) or ten elements. Validate in your wrapper and fail loudly in dev.
  • Nothing shows up in Telescope or Laravel's HTTP logs. The package uses Guzzle directly. If you need observability, log the toArray() output of your message models before sending — every model implements a payload serialiser, so you get a full audit trail without hitting the API.
  • Treat it as a thin HTTP client, not a bot framework. It won't track conversation state, retry, dedupe, or persist anything. Dedupe on message.mid — Meta will redeliver. If you want conversation flow management, note that BotMan's Facebook driver sits on top of this same library and is a much better fit than hand-rolling state.
  • Multi-tenancy works, but not through the singleton. The page token is passed per call, so config('messenger.page_token') is only correct for a single-page app. For several pages, inject a token resolver (Tenant::current()->fb_page_token) into your wrapper and pass it down explicitly.
  • Pin the major version and read the changelog before bumping. Releases are infrequent (last: Jan 2025) and the model classes are the API surface — a minor bump can change a constructor or add a required argument to a template. Vendor the composer lockfile and run your payload tests.
  • Extension points are limited by design. Subclass an Api\* class if you need a Graph endpoint the package hasn't caught up with, and keep your own wrapper interface in front of everything so a future swap (or a Meta API version bump) touches one file.
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.
shitoudev/permissionmanager
johannesclimacus/laravel-tui
hocvt/seotools
vitalybaev/sypexgeo
amphibee/themosis-ignition
gito-me/fast-excel
frankyso/laravel-ipaymu
geolize-studio/laravel-chile
boomerangui/chat
jinas/dhivatar
store-doc/laravel-storage-driver
plus-it/laravel-translation-manager
diephp/perhaps
diephp/telegraph
osmaviation/spreadsheet
microsoft/fluentui-system-icons
mmccook/php-json-canonicalization-scheme
middlewares/firewall
qubiqx/montapacking
nyoncode/laravel-package-console