Notificações de pedido pelo WhatsApp são a parte do e-commerce que o cliente mais percebe depois do pagamento: “pedido confirmado”, “pagamento aprovado”, “saiu para entrega com rastreio”. Quando chegam na hora, cortam a maior parte dos “cadê meu pedido” no suporte; quando chegam duplicadas, atrasadas ou com o nome errado, viram motivo de reclamação.
Em Laravel, a estrutura certa para isso já vem no framework: o cliente Http para chamar a API, a Queue para tirar o envio da requisição do usuário, retentativas com backoff e um scheduler para o que precisa de acompanhamento. A API de WhatsApp da APIBrasil entra como o canal de envio, com um endpoint de texto simples e um webhook que devolve o status de cada mensagem.
O que segue é um desenho completo: quais eventos avisar, o job que envia com retry, os templates por status, o webhook que confirma a entrega e o fallback por SMS quando o WhatsApp não chega, tudo com opt-in registrado.
Quais notificações de pedido pelo WhatsApp valem a pena
A tentação é avisar tudo. Na prática, cinco eventos cobrem a jornada sem cansar o cliente, e cada um tem uma informação que justifica a mensagem:
| Evento | O que a mensagem entrega | Quando disparar |
|---|---|---|
| Pedido confirmado | Número do pedido e itens resumidos | Logo após o checkout |
| Pagamento aprovado | Confirmação e prazo estimado | Ao receber o retorno do gateway |
| Enviado | Código de rastreio e link | Ao registrar a postagem |
| Entregue | Confirmação e canal para problemas | Ao receber o evento da transportadora |
| Pesquisa de satisfação | Uma pergunta, resposta pelo próprio chat | Um ou dois dias após a entrega |
Pedidos que nascem fora do checkout tradicional seguem a mesma jornada. Quem vende em transmissão ao vivo, por exemplo, fecha dezenas de pedidos em minutos e precisa confirmar cada um no WhatsApp antes que o cliente esfrie; o guia sobre como vender na Shopee Live mostra esse fluxo do lado de quem transmite. Para o seu sistema, é só mais uma origem de pedido entrando na mesma fila.
Setup: Laravel, cliente Http e Queue
O projeto precisa de três coisas: as credenciais da APIBrasil em config, uma fila funcionando e uma tabela para registrar cada notificação. Comece pelo .env e pelo config/services.php, que é o lugar certo para credenciais de serviços externos no Laravel:
// .env
APIBRASIL_TOKEN=seu_bearer_token
APIBRASIL_DEVICE_TOKEN=device_token_do_dispositivo_whatsapp
APIBRASIL_WEBHOOK_SECRET=um-uuid-dificil-de-adivinhar
QUEUE_CONNECTION=database
// config/services.php
'apibrasil' => [
'base_url' => env('APIBRASIL_BASE_URL', 'https://gateway.apibrasil.io/api/v2'),
'token' => env('APIBRASIL_TOKEN'),
'device_token' => env('APIBRASIL_DEVICE_TOKEN'),
'webhook_secret' => env('APIBRASIL_WEBHOOK_SECRET'),
'session_key' => env('APIBRASIL_SESSION_KEY'), // opcional: assinatura do webhook
],
O DeviceToken é específico das APIs de WhatsApp da APIBrasil, que são cobradas por plano e exigem um dispositivo conectado no painel; as demais APIs usam só o Bearer Token. A fila em banco é suficiente para começar (php artisan make:queue-table e migrate); em produção com volume, troque por Redis sem mudar o código do job.
A tabela de notificações é o que torna o sistema auditável e idempotente. Cada linha representa um aviso de um evento para um pedido, e a chave única impede que o mesmo evento gere duas mensagens:
Schema::create('notificacoes_pedido', function (Blueprint $table) {
$table->id();
$table->foreignId('pedido_id')->constrained('pedidos');
$table->string('evento'); // confirmado, pago, enviado, entregue, pesquisa
$table->string('telefone', 20); // 5511999999999 (só dígitos)
$table->text('texto');
$table->string('status')->default('pendente'); // pendente, enviada, falhou
$table->string('mensagem_id')->nullable(); // id devolvido pela API
$table->string('status_entrega')->nullable(); // SENT, RECEIVED, READ, FAILED...
$table->text('erro')->nullable();
$table->timestamp('enviada_em')->nullable();
$table->timestamp('fallback_em')->nullable(); // quando o SMS de reserva saiu
$table->timestamps();
$table->unique(['pedido_id', 'evento']); // um aviso por evento, nunca dois
});

Job de envio com retry
O envio nunca acontece dentro do controller ou do webhook do gateway de pagamento. Ele vira um job na fila, que tenta, espera e tenta de novo sem segurar a resposta para o usuário. Três peças: um serviço que fala com a API, o job que usa o serviço e o ponto em que o job é despachado.
O serviço que chama a APIBrasil
O endpoint de texto é POST /whatsapp/sendText, com number (formato internacional, só dígitos) e text. Como em toda API do gateway, o erro pode vir com HTTP 200 e error igual a true no corpo, então o serviço confere os dois e carrega o status HTTP na exceção para o job decidir se repete:
<?php
namespace App\Services;
use Illuminate\Support\Facades\Http;
use RuntimeException;
class ApiBrasilWhatsApp
{
public function enviarTexto(string $numero, string $texto): array
{
$resposta = Http::baseUrl(config('services.apibrasil.base_url'))
->withToken(config('services.apibrasil.token'))
->withHeaders(['DeviceToken' => config('services.apibrasil.device_token')])
->timeout(15)
->post('/whatsapp/sendText', [
'number' => $numero,
'text' => $texto,
]);
$corpo = $resposta->json() ?? [];
// Erro pode chegar com HTTP 200: sempre confira "error"
if ($resposta->failed() || ($corpo['error'] ?? true) === true) {
throw new RuntimeException(
$corpo['message'] ?? 'Falha ao enviar WhatsApp',
$resposta->status() // o código HTTP viaja na exceção
);
}
return $corpo; // o id da mensagem vem na resposta; confira o caminho exato na documentação
}
}
O job
O job carrega a notificação, não o texto solto, para que uma mudança de status no banco (cancelada, já enviada) seja respeitada mesmo que o job tenha sido enfileirado antes. Erros 4xx (token, número inválido, plano) falham de vez; timeout e 5xx voltam para a fila com espera crescente:
<?php
namespace App\Jobs;
use App\Models\NotificacaoPedido;
use App\Services\ApiBrasilWhatsApp;
use Illuminate\Contracts\Queue\ShouldQueue;
use Illuminate\Foundation\Queue\Queueable;
use RuntimeException;
use Throwable;
class EnviarNotificacaoPedido implements ShouldQueue
{
use Queueable; // Laravel 11+; no 10, use Dispatchable, InteractsWithQueue, Queueable e SerializesModels
public int $tries = 4; // 1 tentativa + 3 retentativas
public int $timeout = 30;
public function __construct(public NotificacaoPedido $notificacao) {}
/** Espera entre tentativas, em segundos. */
public function backoff(): array
{
return [30, 120, 600];
}
public function handle(ApiBrasilWhatsApp $api): void
{
if ($this->notificacao->status !== 'pendente') {
return; // já enviada ou cancelada: idempotência
}
try {
$resposta = $api->enviarTexto($this->notificacao->telefone, $this->notificacao->texto);
} catch (RuntimeException $e) {
if ($e->getCode() >= 400 && $e->getCode() < 500) {
$this->fail($e); // 4xx não melhora com retry
return;
}
throw $e; // timeout/5xx: a fila tenta de novo com backoff
}
$this->notificacao->update([
'status' => 'enviada',
'mensagem_id' => data_get($resposta, 'data.id'), // confira o caminho na documentação
'enviada_em' => now(),
]);
}
public function failed(Throwable $e): void
{
$this->notificacao->update(['status' => 'falhou', 'erro' => $e->getMessage()]);
EnviarFallbackSms::dispatch($this->notificacao); // o cliente não fica sem aviso
}
}
Onde o job é despachado
Um observer no model Pedido é o ponto mais simples: sempre que o status muda, ele monta o texto, grava a notificação e despacha o job. Se o cliente não deu opt-in ou o status não tem template, nada acontece:
// app/Observers/PedidoObserver.php
public function updated(Pedido $pedido): void
{
if (! $pedido->wasChanged('status') || ! $pedido->cliente->aceita_whatsapp) {
return;
}
$texto = TemplatesPedido::para($pedido);
if ($texto === null) {
return; // status sem aviso (ex.: em separação)
}
$notificacao = NotificacaoPedido::firstOrCreate(
['pedido_id' => $pedido->id, 'evento' => $pedido->status],
['telefone' => $pedido->cliente->telefone_e164, 'texto' => $texto]
);
if ($notificacao->wasRecentlyCreated) {
EnviarNotificacaoPedido::dispatch($notificacao)->onQueue('whatsapp');
}
}
Rode o worker com php artisan queue:work –queue=whatsapp,default e monitore jobs falhos com queue:failed. Se já usa Horizon, basta incluir a fila whatsapp na configuração.
Templates de mensagem por status
Template é código, não configuração solta no banco, pelo menos no começo: fica versionado, testável e sem risco de alguém salvar uma mensagem com variável errada. Uma classe com match por status resolve, e o primeiro nome do cliente humaniza sem exagero:
<?php
namespace App\Support;
use App\Models\Pedido;
class TemplatesPedido
{
public static function para(Pedido $p): ?string
{
$nome = explode(' ', trim($p->cliente->nome))[0];
return match ($p->status) {
'confirmado' => "Oi, {$nome}! Recebemos o pedido #{$p->numero}. Assim que o pagamento for aprovado, avisamos por aqui.",
'pago' => "Pagamento do pedido #{$p->numero} aprovado. Previsão de envio: {$p->previsao_envio}.",
'enviado' => "Pedido #{$p->numero} enviado! Rastreio: {$p->codigo_rastreio}. Acompanhe em {$p->link_rastreio}",
'entregue' => "Pedido #{$p->numero} entregue. Se algo não estiver certo, responda esta mensagem.",
'pesquisa' => "{$nome}, de 0 a 10, quanto você recomendaria nossa loja? Responda só com o número.",
default => null,
};
}
}
Regras que evitam atrito: uma informação nova por mensagem, sem oferta embutida em aviso transacional, e sempre um caminho de resposta (“responda esta mensagem”). O template de entrega é o último aviso operacional; a partir dele começa outra conversa, de pós-venda, recompra e indicação, que tem lógica própria e está bem descrita em vendas pelo WhatsApp: funil e follow-up. Não misture as duas no mesmo job.
Webhook de status e reenvio
A resposta do envio só diz que a mensagem foi aceita. O que aconteceu depois chega pelo webhook configurado no dispositivo: a APIBrasil faz um POST em JSON com o campo wook identificando o evento. Para status de mensagem, wook vem como MESSAGE_STATUS, com id (o mesmo do envio), status (SENT, RECEIVED, READ no caminho normal; FAILED, EXPIRED e outros em falha) e dateTime. Quando a sessão tem chave, o header x-webhook-signature traz um HMAC-SHA256 do corpo bruto, que vale conferir.
// routes/api.php
Route::post('/webhooks/whatsapp/{segredo}', WhatsAppWebhookController::class);
// app/Http/Controllers/WhatsAppWebhookController.php
public function __invoke(Request $request, string $segredo)
{
abort_unless(hash_equals(config('services.apibrasil.webhook_secret'), $segredo), 404);
$assinatura = $request->header('x-webhook-signature');
if ($assinatura && ($chave = config('services.apibrasil.session_key'))) {
$esperada = 'sha256=' . hash_hmac('sha256', $request->getContent(), $chave);
abort_unless(hash_equals($esperada, $assinatura), 401);
}
$evento = $request->json()->all();
if (($evento['wook'] ?? null) === 'MESSAGE_STATUS') {
NotificacaoPedido::where('mensagem_id', $evento['id'] ?? '')
->update(['status_entrega' => $evento['status'] ?? null]);
}
if (($evento['wook'] ?? null) === 'RECEIVE_MESSAGE'
&& str_contains(mb_strtoupper($evento['content'] ?? ''), 'PARAR')) {
Cliente::where('telefone_e164', $evento['from'] ?? '')->update(['aceita_whatsapp' => false]);
}
return response()->noContent(); // responda rápido; o processamento pesado vai para a fila
}
Com o status gravado, o reenvio vira uma regra de scheduler: notificação enviada que ficou em FAILED, ou sem RECEIVED depois de algumas horas, ganha um fallback por SMS. O job EnviarFallbackSms chama POST /sms/send/credits com number e message, só com o Bearer Token; a implementação em PHP está detalhada em como enviar SMS com PHP. Deduplique eventos pelo par (id, wook): o mesmo status pode chegar mais de uma vez.
// routes/console.php
Schedule::call(function () {
NotificacaoPedido::where('status', 'enviada')
->where(fn ($q) => $q->whereNull('status_entrega')->orWhereIn('status_entrega', ['FAILED', 'EXPIRED']))
->where('enviada_em', '<', now()->subHours(2))
->whereNull('fallback_em')
->each(fn ($n) => EnviarFallbackSms::dispatch($n));
})->everyThirtyMinutes();
Opt-in e LGPD
Aviso de pedido é mensagem transacional, mas isso não dispensa consentimento claro. No checkout, uma opção desmarcada por padrão (“quero receber o andamento do pedido pelo WhatsApp”) grava aceita_whatsapp, a data e o texto exibido. A pesquisa de satisfação fica em um aceite separado se você pretende usá-la para marketing depois.
O número precisa ser do cliente e precisa ter WhatsApp. Confirmar o telefone no cadastro com um código evita notificar um estranho por um dígito trocado; o passo a passo de código de verificação OTP no Laravel se encaixa antes de qualquer envio. Guarde o telefone já normalizado (55 + DDD + número, só dígitos), que é o formato que a API espera.
Por fim, descadastro: a palavra PARAR recebida pelo webhook desliga o opt-in na hora, e o observer deixa de criar notificações. Mantenha os registros de envio pelo tempo necessário para auditoria e apague o resto; texto de mensagem com nome e endereço é dado pessoal como qualquer outro.
Perguntas frequentes
Preciso da API oficial do WhatsApp para enviar notificações de pedido?
A APIBrasil oferece planos com a API oficial e planos baseados em dispositivo conectado; nos dois casos a chamada usa Bearer Token e DeviceToken. A escolha depende de volume, exigências de conformidade e orçamento; consulte o catálogo na documentação. O código deste guia funciona com o endpoint de texto, independentemente do plano.
Como evito mandar a mesma mensagem duas vezes?
Com três travas: a chave única (pedido_id, evento) na tabela, o firstOrCreate no observer e a checagem de status pendente no início do job. Assim, nem um reprocessamento do webhook do gateway de pagamento nem um retry da fila geram mensagem duplicada.
O que acontece se o cliente não tiver WhatsApp?
O envio falha ou nunca recebe o status RECEIVED. O scheduler detecta isso e despacha o fallback por SMS, que chega em qualquer celular. Se quiser evitar a tentativa, confirme o número por OTP no cadastro e pergunte o canal preferido.
Posso usar Redis e Horizon em vez da fila em banco?
Sim, e é o recomendado quando o volume cresce. O job não muda; basta trocar QUEUE_CONNECTION para redis e incluir a fila whatsapp na configuração do Horizon, que ainda dá painel de monitoramento e métricas de falha.
Pesquisa de satisfação conta como marketing?
Uma pergunta sobre o pedido entregue ainda é parte da jornada, mas o uso posterior dos dados pode ser marketing. Trate a pesquisa com aceite próprio, limite a uma mensagem e respeite o PARAR como em qualquer outro envio.
Próximos passos
Com fila, templates e webhook funcionando, as notificações de pedido pelo WhatsApp passam a ser um fluxo auditável: cada aviso tem linha no banco, status de entrega e fallback. Se o seu backend for Node.js, a mesma arquitetura está em enviar WhatsApp com Node.js. Para começar, gere o token no painel da APIBrasil, conecte um dispositivo e envie o primeiro template para o seu próprio número.
![]()









