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.
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);
});
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.
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.$request->all(). Any re-encoding breaks the HMAC. Meta sends X-Hub-Signature (SHA-1); prefer X-Hub-Signature-256 where available.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.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.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.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.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.How can I help you explore Laravel packages today?