ВойтиНачать бесплатный пробный период
Meta Tech Provider

Отправка и приём WhatsApp из PHP и Laravel

Приём сообщений устроен как маршрут Laravel, который отвечает на проверочный GET от Meta и принимает POST-запросы. Параметры читаются как hub_mode, hub_verify_token и hub_challenge, потому что PHP заменяет точки на подчёркивания. Отправка сводится к одному POST через Http::withToken() на эндпоинт messages в Cloud API с идентификатором номера и токеном. SDK не нужен.

hub_challenge

PHP заменяет точки в именах входящих переменных на подчёркивания, поэтому hub.challenge читается как hub_challenge.

routes/api.php

Stateless-маршруты API в Laravel, создаваемые командой install:api, работают под префиксом /api и вне CSRF-защиты группы web.

v25.0

Версия 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, поэтому заголовок подписи на вашей стороне проверить нельзя.

routes/api.php: verification and event intake
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, работает одинаково.

Send a text reply inside the 24 hour window
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

Подключите номер на 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 за несколько минут, а не месяцев. Приложение продолжит работать на телефоне.

Начать бесплатный пробный период

Проверено по состоянию на

Отправка WhatsApp из PHP и Laravel