API de WhatsApp em PHP: cURL, Guzzle e webhook em qualquer hospedagem
Para usar a API de WhatsApp em PHP você não precisa de framework nem de servidor dedicado: uma função com cURL envia a mensagem e um arquivo que lê php://input recebe o webhook. É o caminho para sistemas legados e hospedagens compartilhadas, onde não dá para instalar worker nem abrir porta.
O cenário: sistema antigo, hospedagem simples
Muito sistema de gestão, loja e área do cliente no Brasil ainda roda em PHP procedural, em uma hospedagem compartilhada com cPanel, sem acesso root. Reescrever para colocar WhatsApp não é opção. A boa notícia é que a API de WhatsApp da D-API é REST: não exige biblioteca, processo em segundo plano nem conexão permanente com o WhatsApp do lado do seu servidor. Quem mantém a conexão com o número é a D-API; o seu PHP só faz requisições HTTPS e recebe requisições HTTPS.
O que o servidor precisa ter:
- extensão cURL ativa, presente em praticamente toda hospedagem;
- saída para
https://api.d-api.cloudna porta 443; - um endereço público com HTTPS para o arquivo do webhook;
- acesso ao cron do painel, se você quiser processar eventos em lote.
Enviar mensagem com cURL puro
A função abaixo cabe em um arquivo dapi.php e pode ser incluída em qualquer tela do sistema. Ela define tempo máximo de conexão e de resposta, trata falha de rede e converte a resposta de erro da API, que chega como {"success": false, "error": "...", "statusCode": 400}, em exceção.
<?php
// dapi.php
define('DAPI_API_KEY', getenv('DAPI_API_KEY'));
function dapi_post(string $caminho, array $payload): array
{
$ch = curl_init('https://api.d-api.cloud' . $caminho);
curl_setopt_array($ch, [
CURLOPT_POST => true,
CURLOPT_RETURNTRANSFER => true,
CURLOPT_CONNECTTIMEOUT => 3, // segundos para abrir a conexão
CURLOPT_TIMEOUT => 10, // segundos para a requisição inteira
CURLOPT_HTTPHEADER => [
'Authorization: ' . DAPI_API_KEY,
'Content-Type: application/json',
],
CURLOPT_POSTFIELDS => json_encode($payload),
]);
$corpo = curl_exec($ch);
if ($corpo === false) {
$erro = curl_error($ch);
curl_close($ch);
throw new RuntimeException('Falha de rede com a D-API: ' . $erro);
}
$status = curl_getinfo($ch, CURLINFO_RESPONSE_CODE);
curl_close($ch);
$json = json_decode($corpo, true) ?: [];
if ($status >= 400) {
throw new RuntimeException($json['error'] ?? 'HTTP ' . $status, $status);
}
return $json;
}
// uso
dapi_post('/api/v1/messages/send/text', [
'sessionId' => 'loja',
'to' => '5511999999999',
'text' => 'Recebemos seu pedido 4812. Obrigado!',
]);Trocando o caminho, a mesma função envia imagem com legenda: use /api/v1/messages/send/image com os campos image (URL pública ou base64) e caption. O timeout é o que impede que a página do sistema fique carregando se a rede oscilar. Em hospedagem compartilhada o PHP costuma ter limite de execução baixo, e uma chamada sem timeout pode derrubar a requisição do usuário inteira.
A mesma chamada com Guzzle
Se o projeto já usa Composer, o Guzzle deixa o código mais legível e separa com clareza erro de rede de erro devolvido pela API.
<?php
use GuzzleHttp\Client;
use GuzzleHttp\Exception\BadResponseException;
use GuzzleHttp\Exception\ConnectException;
$dapi = new Client([
'base_uri' => 'https://api.d-api.cloud',
'timeout' => 10,
'connect_timeout' => 3,
'headers' => ['Authorization' => getenv('DAPI_API_KEY')],
]);
try {
$dapi->post('/api/v1/messages/send/text', [
'json' => ['sessionId' => 'loja', 'to' => '5511999999999', 'text' => 'Pedido enviado.'],
]);
} catch (BadResponseException $e) {
$erro = json_decode((string) $e->getResponse()->getBody(), true);
error_log('D-API recusou: ' . ($erro['error'] ?? $e->getMessage()));
} catch (ConnectException $e) {
error_log('D-API inacessível ou timeout: ' . $e->getMessage());
}BadResponseException cobre respostas 4xx e 5xx. ConnectException cobre DNS, conexão recusada e tempo esgotado. Na primeira, reveja o payload; na segunda, vale tentar de novo mais tarde.
Receber o webhook com php://input
O webhook chega como POST com corpo JSON. Em PHP isso significa ler php://input: o $_POST vem vazio porque não é um formulário. Em hospedagem compartilhada, o jeito mais seguro de lidar com eventos é gravar primeiro e processar depois, porque o arquivo precisa responder rápido e o PHP-FPM do provedor costuma ter poucos processos disponíveis.
<?php
// webhook-whatsapp.php (URL cadastrada: https://seusite.com.br/webhook-whatsapp.php?token=SEGREDO)
require __DIR__ . '/config.php';
if (!hash_equals(WEBHOOK_TOKEN, $_GET['token'] ?? '')) {
http_response_code(401);
exit;
}
$bruto = file_get_contents('php://input');
$evento = json_decode($bruto, true);
if (!is_array($evento) || !isset($evento['event'])) {
http_response_code(400);
exit;
}
// chave única evita gravar duas vezes o mesmo evento reenviado
$chave = $evento['data']['id'] ?? $evento['traceId'];
$pdo->prepare(
'INSERT IGNORE INTO whatsapp_eventos (chave, evento, sessao, payload, criado_em)
VALUES (?, ?, ?, ?, NOW())'
)->execute([$chave, $evento['event'], $evento['sessionId'], $bruto]);
http_response_code(200);
echo 'ok';A tabela whatsapp_eventos precisa de índice único em chave; é ele que faz o INSERT IGNORE descartar repetições. O webhook não tem assinatura, então o token na query string é a proteção do endpoint. Um script agendado no cron, rodando a cada minuto, lê os eventos pendentes e faz o trabalho mais demorado: responder o cliente, atualizar o pedido, avisar o financeiro.
Nesse script, marque cada linha como processada só depois de concluir o trabalho e limite quantas linhas ele pega por execução, por exemplo 100. Assim uma fila acumulada não estoura o tempo máximo de execução do PHP configurado pelo provedor.
Armadilhas comuns em servidor compartilhado
| Sintoma | Causa provável | O que fazer |
|---|---|---|
| Eventos param de chegar depois de uma mudança no site | Regra do .htaccess redirecionando o arquivo e devolvendo 404 | Exclua o caminho do webhook das regras de rewrite. Respostas 404 e 410 encerram as tentativas |
| Webhook recebe 403 sem motivo aparente | Firewall de aplicação do provedor barrando POST com JSON | Peça ao suporte uma exceção para aquele arquivo |
| Mesma mensagem processada duas vezes | Resposta lenta fez a entrega ser repetida | Gravar e responder, processar no cron, índice único |
| Envio falha com erro de certificado | Pacote de CAs desatualizado no servidor | Atualizar o PHP ou o bundle de certificados, nunca desligar a verificação SSL |
Onde isso se encaixa no seu sistema
Os pontos de entrada mais comuns em sistemas PHP antigos são o fechamento do pedido, a emissão do boleto e o agendamento. Uma chamada a dapi_post logo depois do INSERT que já existe resolve a maior parte. Para régua de cobrança, veja o guia de cobrança pelo WhatsApp. Se o seu projeto está em Laravel, o guia de Laravel mostra a versão com filas e notificações. Os detalhes de cada evento recebido estão em webhook de WhatsApp.
A conexão usada nesses exemplos é a não oficial, pareada por QR Code, que não exige aprovação de template para mandar a primeira mensagem. As regras e cuidados desse modelo estão em API não oficial de WhatsApp, e os planos, cobrados por conexão, em preços.
Perguntas frequentes
Funciona em hospedagem compartilhada?
Na maioria dos casos, sim. O envio precisa só da extensão cURL e de saída liberada para HTTPS. O webhook é um arquivo PHP comum acessível pela internet. Confirme com o provedor se requisições de saída não são bloqueadas.
Preciso do Composer?
Não. O exemplo com cURL usa apenas funções nativas do PHP e cabe em um único arquivo incluído com require. O Composer só entra se você quiser usar o Guzzle ou já usa em outras partes do projeto.
Qual versão mínima do PHP?
Os exemplos desta página rodam em PHP 7.4 ou superior. Se o sistema ainda estiver em uma versão mais antiga, a lógica é a mesma, mas será preciso remover as declarações de tipo das funções.
Meu webhook recebe o POST mas $_POST vem vazio. Por quê?
Porque o corpo chega como JSON, e o PHP só preenche $_POST para formulários. Leia o corpo bruto com file_get_contents em php://input e decodifique com json_decode.
Posso usar o mesmo código em um Laravel?
Pode, mas o Laravel já tem o Http client, filas e notificações que deixam a integração mais organizada. Para esse caso existe um guia específico de Laravel.
Continue lendo
Teste a API de WhatsApp da D-API
Trial de 3 dias com acesso completo. Sem cartão, sem fidelidade.