Iniciar sessãoIniciar teste gratuito
Meta Tech Provider

Enviar e receber WhatsApp com PHP e Laravel

Receber é uma rota Laravel que responde ao GET de verificação da Meta e aceita POSTs, lendo os parâmetros como hub_mode, hub_verify_token e hub_challenge, porque o PHP converte os pontos em sublinhados. Enviar é um único POST com Http::withToken() para o endpoint de mensagens da Cloud API, com o phone number id e o token do número. Não é preciso nenhum SDK.

hub_challenge

O PHP converte os pontos nos nomes das variáveis recebidas em sublinhados, por isso hub.challenge é lido como hub_challenge.

routes/api.php

As rotas de API sem estado do Laravel, criadas por install:api, são servidas sob /api e ficam fora da proteção CSRF do grupo web.

v25.0

A versão da Graph API usada nos exemplos atuais de envio de mensagens da Meta, em POST /{phone-number-id}/messages.

Como se recebem mensagens de WhatsApp no Laravel?

Com um GET e um POST no mesmo caminho em routes/api.php. Execute primeiro php artisan install:api se o ficheiro não existir. As rotas de API não têm estado e ficam fora do grupo web, pelo que o POST da Meta não é rejeitado por falta de token CSRF, o que acontece numa rota em routes/web.php.

O GET responde à verificação da Meta. A Meta envia hub.mode, hub.verify_token e hub.challenge, mas o PHP reescreve os pontos nos nomes das variáveis recebidas como sublinhados, por isso o Laravel vê hub_mode, hub_verify_token e hub_challenge. O código que lê os nomes com ponto obtém null e nunca verifica. Devolva o challenge como texto simples, não através de response()->json().

O POST entrega o payload a um job em fila e devolve 200 de imediato. Coloque uma chave longa e aleatória no caminho e valide-a nas duas rotas: a Meta assina as entregas com o app secret do Tech Provider, pelo que o cabeçalho da assinatura não pode ser validado do seu lado.

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);
});

Como se envia uma mensagem de WhatsApp a partir do Laravel?

Com o cliente HTTP do Laravel: um POST ao endpoint de mensagens do phone number id, com o token de acesso como bearer token. Os dois valores vêm da ligação, são mostrados no painel e devolvidos por get_api_credentials, e devem ficar em config/services.php, assentes em variáveis de ambiente, e não no código.

A forma do corpo depende do momento. Nas 24 horas seguintes à última mensagem do cliente, funciona um objeto text, como abaixo. Fora dessa janela, a Meta só aceita um objeto template que indique um template aprovado e o respetivo idioma, e o texto livre falha com o erro 131047. A resposta é o sítio onde se vê isso: uma chamada falhada devolve um objeto error cujo código diz o que correu mal, por isso registe-o em log em vez de o descartar.

Nada no pedido é específico da EasyCoexistence. É a chamada da documentação da própria Meta, pelo que qualquer cliente HTTP de PHP, Guzzle ou cURL simples, funciona da mesma maneira.

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', []));
}

O que precisa um handler Laravel em produção, além disto?

Uma fila a sério, deduplicação e uma verificação da janela.

A fila é importante porque a Meta volta a tentar quando a resposta é lenta. HandleWhatsAppEvent::dispatch() só devolve rapidamente quando a ligação da fila é real e há um worker em execução. No driver sync, o job corre dentro do pedido, e uma chamada lenta a um modelo ou uma escrita na base de dados transforma-se numa entrega duplicada.

Deduplique pelo id da mensagem, o wamid de cada mensagem, porque as novas tentativas são normais e não excecionais. Basta um índice único nessa coluna, ou um lock de cache com essa chave.

E guarde quando cada contacto escreveu pela última vez. Com essa data e hora, o envio escolhe sozinho entre texto e template, em vez de descobrir pelo erro 131047 quando o cliente já estava à espera. As respostas escritas no telemóvel, na app WhatsApp Business, chegam como eventos echo à mesma rota, pelo que o handler também consegue ver quando uma pessoa assumiu uma conversa.

Erros comuns

  • Ler hub.challenge com ponto. O PHP mudou-lhe o nome para hub_challenge, por isso o handler devolve um corpo vazio e a Meta nunca verifica.
  • Colocar o webhook em routes/web.php. O POST da Meta não tem token CSRF e é rejeitado com 419.
  • Despachar para a fila sync. O job corre antes do 200, o processamento lento é repetido e as mensagens são processadas duas vezes.
  • Devolver o challenge com response()->json(). As aspas à volta do valor fazem falhar a verificação da Meta.
Fazer isto com a EasyCoexistence

Ligue o número em easycoexistence.com, defina o destino do webhook como a sua rota Laravel com a chave no caminho e leia o phone number id e o token no painel. A partir de US$ 9 por número ligado por mês, descendo até US$ 1 à medida que se adicionam números, com os primeiros 3 dias grátis.

Perguntas frequentes

Existe um SDK de PHP?

Não da nossa parte, e não é preciso nenhum. A API é da Meta, documentada pela Meta, e o cliente HTTP do Laravel ou o Guzzle acedem-lhe diretamente.

Isto funciona sem Laravel?

Sim. Em PHP simples, leia $_GET['hub_challenge'], devolva-o como text/plain e envie com cURL. A regra dos sublinhados aplica-se a todas as frameworks de PHP.

Porque é que o URL precisa de uma chave no caminho?

Porque é a autenticação que funciona. A assinatura da Meta usa o app secret do Tech Provider, que nunca é partilhado, e por isso o segredo no URL é o que impede estranhos de enviar eventos falsos.

A equipa pode continuar a usar o telemóvel?

Sim. A Coexistence mantém a app WhatsApp Business a funcionar no número, e as mensagens enviadas a partir do telemóvel chegam à sua rota como eventos echo.

Continue a ler

Pronto para começar?

Configure a Coexistência do WhatsApp em minutos, não em meses. A aplicação continua a funcionar no telemóvel.

Começar período experimental gratuito

Verificado em

Enviar WhatsApp com PHP e Laravel