Отправка и приём WhatsApp из PHP и Laravel
Приём сообщений устроен как маршрут Laravel, который отвечает на проверочный GET от Meta и принимает POST-запросы. Параметры читаются как hub_mode, hub_verify_token и hub_challenge, потому что PHP заменяет точки на подчёркивания. Отправка сводится к одному POST через Http::withToken() на эндпоинт messages в Cloud API с идентификатором номера и токеном. SDK не нужен.
PHP заменяет точки в именах входящих переменных на подчёркивания, поэтому hub.challenge читается как hub_challenge.
Stateless-маршруты API в Laravel, создаваемые командой install:api, работают под префиксом /api и вне CSRF-защиты группы web.
Версия Graph API взята из актуальных примеров отправки сообщений в документации Meta: POST /{phone-number-id}/messages.
Как принимать сообщения WhatsApp в Laravel?
Нужны GET и POST по одному пути в routes/api.php. Если файла нет, сначала выполните php artisan install:api. Маршруты API не хранят состояние и находятся вне группы web, поэтому POST от Meta не отклоняется из-за отсутствия CSRF-токена, как это происходит на маршруте в routes/web.php.
GET отвечает на проверку от Meta. Meta присылает hub.mode, hub.verify_token и hub.challenge, но PHP заменяет точки в именах входящих переменных на подчёркивания, и Laravel видит hub_mode, hub_verify_token и hub_challenge. Код, который читает имена с точками, получает null и проверку не проходит. Возвращайте challenge простым текстом, а не через response()->json().
POST передаёт тело запроса в задание очереди и сразу возвращает 200. Добавьте в путь длинный случайный ключ и проверяйте его на обоих маршрутах: Meta подписывает доставки секретом приложения Tech Provider, поэтому заголовок подписи на вашей стороне проверить нельзя.
use App\Jobs\HandleWhatsAppEvent;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\Route;
Route::get('/whatsapp/{key}', function (Request $request, string $key) {
abort_unless(hash_equals(config('services.whatsapp.url_key'), $key), 404);
// PHP turns hub.mode into hub_mode: read the underscored names
if ($request->query('hub_mode') === 'subscribe'
&& hash_equals(config('services.whatsapp.verify_token'),
(string) $request->query('hub_verify_token'))) {
return response((string) $request->query('hub_challenge'), 200)
->header('Content-Type', 'text/plain');
}
abort(403);
});
Route::post('/whatsapp/{key}', function (Request $request, string $key) {
abort_unless(hash_equals(config('services.whatsapp.url_key'), $key), 404);
HandleWhatsAppEvent::dispatch($request->json()->all());
return response('', 200);
});Как отправить сообщение WhatsApp из Laravel?
Через HTTP-клиент Laravel: один POST на эндпоинт messages для идентификатора номера, с токеном доступа в виде токена Bearer. Оба значения берутся из подключения: они показаны в панели управления и возвращаются методом get_api_credentials. Храните их в config/services.php на основе переменных окружения, а не в коде.
Формат тела запроса зависит от времени. В течение 24 часов после последнего сообщения клиента подходит объект text, как в примере ниже. Вне этого окна Meta принимает только объект template с названием одобренного шаблона и языком, а свободный текст завершается ошибкой 131047. Увидеть это можно в ответе: неудачный вызов возвращает объект error, в поле code которого указана причина, так что записывайте его в журнал, а не отбрасывайте.
В запросе нет ничего, что относилось бы только к EasyCoexistence. Это тот же вызов, что описан в документации Meta, поэтому любой HTTP-клиент PHP, Guzzle или обычный cURL, работает одинаково.
use Illuminate\Support\Facades\Http;
$response = Http::withToken(config('services.whatsapp.token'))
->post('https://graph.facebook.com/v25.0/'
. config('services.whatsapp.phone_number_id') . '/messages', [
'messaging_product' => 'whatsapp',
'to' => '14155550188', // digits only, with country code
'type' => 'text',
'text' => ['body' => 'Thanks, we are on it.'],
]);
if ($response->failed()) {
// error.code 131047: outside the window, send a template instead
logger()->warning('WhatsApp send failed', $response->json('error', []));
}Что нужно боевому обработчику Laravel сверх этого?
Настоящая очередь, дедупликация и проверка окна.
Очередь важна, потому что Meta повторяет доставку при медленном ответе. HandleWhatsAppEvent::dispatch() возвращается быстро, только если подключена настоящая очередь с запущенным воркером. На драйвере sync задание выполняется внутри запроса, и медленный вызов модели или запись в базу превращаются в повторную доставку.
Дедуплицируйте по идентификатору сообщения, то есть по wamid в каждом сообщении: повторы являются нормой, а не исключением. Достаточно уникального индекса по этому столбцу или блокировки в кеше с этим ключом.
И сохраняйте время, когда каждый контакт писал в последний раз. С этой меткой путь отправки сам выберет text или template, а не узнает об ошибке 131047, когда клиент уже ждёт. Ответы, набранные на телефоне в приложении WhatsApp Business, приходят на тот же маршрут как события echo, так что обработчик видит и момент, когда разговор взял на себя человек.
Распространённые ошибки
- Чтение hub.challenge с точкой. PHP переименовал параметр в hub_challenge, поэтому обработчик возвращает пустой ответ, и Meta не проходит проверку.
- Размещение вебхука в routes/web.php. У POST от Meta нет CSRF-токена, и он отклоняется с кодом 419.
- Отправка в очередь sync. Задание выполняется до ответа 200, медленная обработка вызывает повторы, и сообщения обрабатываются дважды.
- Возврат challenge через response()->json(). Кавычки вокруг значения не проходят проверку Meta.
Подключите номер на easycoexistence.com, укажите адрес вебхука на ваш маршрут Laravel с ключом в пути и возьмите идентификатор номера и токен в панели управления. От US$ 9 за подключённый номер в месяц, до US$ 1 по мере добавления номеров, первые 3 дня бесплатно.
Часто задаваемые вопросы
Есть ли SDK для PHP?
От нас нет, и он не нужен. API принадлежит Meta и описан в её документации, а HTTP-клиент Laravel или Guzzle обращается к нему напрямую.
Работает ли это без Laravel?
Да. В чистом PHP читайте $_GET['hub_challenge'], возвращайте его как text/plain, а отправляйте через cURL. Правило про подчёркивания действует в любом фреймворке PHP.
Зачем нужен ключ в пути URL?
Потому что это единственная рабочая аутентификация. Подпись Meta использует секрет приложения Tech Provider, который никому не передаётся, поэтому именно секрет в URL не даёт посторонним отправлять поддельные события.
Может ли команда по-прежнему пользоваться телефоном?
Да. Coexistence сохраняет работу приложения WhatsApp Business на номере, а сообщения, отправленные с телефона, приходят на ваш маршрут как события echo.
Читайте дальше
Готовы начать?
Настройте WhatsApp Coexistence за несколько минут, а не месяцев. Приложение продолжит работать на телефоне.
Начать бесплатный пробный периодПроверено по состоянию на