Dev

Enviar WhatsApp con PHP y cURL: ejemplo de API (Guzzle y Laravel)

Para enviar WhatsApp con PHP haces un POST con cURL a /api/v1/{instanceId}/send/text, con los encabezados x-api-key y x-api-secret y un JSON con number y text. Funciona en cualquier hosting con la extensión cURL. Abajo: cURL puro, Guzzle, envío de archivos, un job en Laravel y un receptor de webhook.

Publicado el · 3 min de lectura

Este artículo también está disponible en: Português · English

Qué necesitas antes de empezar

Cuenta en ZapZap, instancia conectada por código QR, clave y secreto de la API y el ID de la instancia. La autenticación usa x-api-key y x-api-secret. En PHP, guarda las credenciales en .env o en variables del servidor, fuera del repositorio.

Prueba con cURL en la línea de comandos antes de pasar a PHP.

cURL

curl -X POST https://api.zapzapapi.com/api/v1/SEU_INSTANCE_ID/send/text \
  -H "Content-Type: application/json" \
  -H "x-api-key: SUA_API_KEY" \
  -H "x-api-secret: SEU_API_SECRET" \
  -d '{"number":"5511999999999","text":"Olá! Teste de envio pela API."}'

Cómo enviar un mensaje de texto con PHP y cURL

La función de abajo usa la extensión cURL de PHP, define timeouts y devuelve el estado y el cuerpo. Usa json_encode para armar el JSON: concatenar cadenas a mano se rompe con comillas y emojis.

Comprueba siempre el código HTTP, no solo si la llamada devolvió algo. Un 401 vuelve con cuerpo y no lanza excepción por sí solo.

PHP (cURL)

<?php
function zapzap_post(string $caminho, array $dados): array {
    $instancia = getenv('ZAPZAP_INSTANCE_ID');
    $ch = curl_init("https://api.zapzapapi.com/api/v1/$instancia$caminho");
    curl_setopt_array($ch, [
        CURLOPT_POST           => true,
        CURLOPT_RETURNTRANSFER => true,
        CURLOPT_CONNECTTIMEOUT => 5,
        CURLOPT_TIMEOUT        => 30,
        CURLOPT_HTTPHEADER     => [
            'Content-Type: application/json',
            'x-api-key: ' . getenv('ZAPZAP_API_KEY'),
            'x-api-secret: ' . getenv('ZAPZAP_API_SECRET'),
        ],
        CURLOPT_POSTFIELDS     => json_encode($dados, JSON_UNESCAPED_UNICODE),
    ]);
    $corpo = curl_exec($ch);
    $status = curl_getinfo($ch, CURLINFO_HTTP_CODE);
    $erro = curl_error($ch);
    curl_close($ch);
    if ($corpo === false) {
        throw new RuntimeException("Falha de rede: $erro");
    }
    return ['status' => $status, 'corpo' => json_decode($corpo, true)];
}

$r = zapzap_post('/send/text', [
    'number' => '5511999999999',
    'text'   => 'Olá! Seu pedido saiu para entrega.',
]);
print_r($r);

Cómo enviar con Guzzle y mandar un PDF o una imagen

Con composer require guzzlehttp/guzzle, el cliente reutiliza la conexión y ya trae base_uri, encabezados y timeout. Para multimedia, usa /send/media con type (image, video, audio, ptt, document, sticker y otros), file (URL pública), text (pie) y docName.

Con http_errors en false decides qué hacer con cada estado, en lugar de tratar excepciones.

PHP (Guzzle)

<?php
use GuzzleHttp\Client;

$http = new Client([
    'base_uri' => 'https://api.zapzapapi.com/api/v1/' . getenv('ZAPZAP_INSTANCE_ID') . '/',
    'headers'  => [
        'x-api-key'    => getenv('ZAPZAP_API_KEY'),
        'x-api-secret' => getenv('ZAPZAP_API_SECRET'),
    ],
    'timeout'     => 30,
    'http_errors' => false,
]);

$r = $http->post('send/media', ['json' => [
    'number'  => '5511999999999',
    'type'    => 'document',
    'file'    => 'https://seusite.com/arquivos/proposta.pdf',
    'docName' => 'Proposta.pdf',
    'text'    => 'Segue a proposta.',
]]);
echo $r->getStatusCode(), ' ', $r->getBody();

Cómo enviar desde Laravel con un job de cola

En Laravel, no envíes WhatsApp dentro de la solicitud del usuario. Despacha un job a la cola: si la API tarda o falla, la página sigue rápida y el job se repite solo. Pon las credenciales en config/services.php, leídas del .env.

El retry del Http facade cubre errores transitorios. El job de abajo falla de forma explícita en 4xx, para que Laravel no repita una solicitud que nunca va a funcionar.

PHP (Laravel)

<?php
namespace App\Jobs;

use Illuminate\Bus\Queueable;
use Illuminate\Contracts\Queue\ShouldQueue;
use Illuminate\Foundation\Bus\Dispatchable;
use Illuminate\Queue\InteractsWithQueue;
use Illuminate\Support\Facades\Http;

class EnviarWhatsApp implements ShouldQueue
{
    use Dispatchable, InteractsWithQueue, Queueable;

    public int $tries = 4;
    public array $backoff = [10, 60, 300];

    public function __construct(public string $numero, public string $texto) {}

    public function handle(): void
    {
        $cfg = config('services.zapzap');
        $r = Http::withHeaders([
                'x-api-key'    => $cfg['key'],
                'x-api-secret' => $cfg['secret'],
            ])
            ->timeout(20)
            ->post("https://api.zapzapapi.com/api/v1/{$cfg['instance']}/send/text", [
                'number' => $this->numero,
                'text'   => $this->texto,
            ]);

        if ($r->clientError()) {
            $this->fail(new \RuntimeException('ZapZap ' . $r->status() . ' ' . $r->body()));
            return;
        }
        $r->throw(); // 5xx: o Laravel repete conforme o backoff
    }
}

// Uso: EnviarWhatsApp::dispatch('5511999999999', 'Seu pedido foi enviado.');

Cómo recibir las respuestas con un webhook en PHP

Configura la URL en configurar webhook y activa el evento messages. Lee el cuerpo crudo con file_get_contents('php://input'), responde 200 enseguida y procesa después. El mensaje recibido está en data.message (text, chatid, fromMe); ignora fromMe verdadero.

En Laravel, la ruta del webhook debe quedar fuera del middleware de CSRF. Valida la firma X-ZapZap-Signature y trata las reentregas según la guía de webhook en producción.

PHP (puro)

<?php
$raw = file_get_contents('php://input');
$evento = json_decode($raw, true);

http_response_code(200); // confirma primeiro
header('Content-Length: 0');
if (function_exists('fastcgi_finish_request')) {
    fastcgi_finish_request(); // libera a resposta e continua o trabalho
}

$msg = $evento['data']['message'] ?? null;
if (($evento['event'] ?? '') !== 'messages' || !$msg || !empty($msg['fromMe'])) {
    exit;
}
error_log('mensagem de ' . ($msg['chatid'] ?? '?') . ': ' . ($msg['text'] ?? ''));

Lista de verificación antes de producción

Credenciales fuera del repositorio; timeouts de conexión y total; comprobación del estado HTTP; envío por cola, no dentro de la solicitud; retry solo en error transitorio; firma del webhook validada; y enviar solo a quien aceptó recibir.

Otros lenguajes: Python, Node.js, Ruby, C#. Todos los endpoints están en la referencia de la API.

Resumen rápido

  • cURL puro funciona en cualquier hosting PHP con la extensión cURL.
  • Usa json_encode y comprueba siempre el código HTTP de la respuesta.
  • En Laravel, envía por un job de cola con backoff y falla directo en 4xx.
  • La ruta del webhook queda fuera del CSRF; responde 200 antes de procesar.

Lee también

Preguntas frecuentes

¿Funciona en hosting compartido?

Funciona si la extensión cURL está activa y el hosting permite conexiones salientes por HTTPS, que es lo normal. Para recibir webhooks, el servidor necesita una URL pública con HTTPS.

¿Necesito composer?

No para el ejemplo con cURL puro. Guzzle y Laravel usan composer, pero son opcionales.

¿Cómo lo uso en WordPress?

Llama a wp_remote_post con los mismos encabezados y el mismo JSON, desde un hook de tu plugin o de un webhook de formulario. Evita poner la clave y el secreto en el código del tema.

¿Usar una API no oficial puede llevar al bloqueo del número?

Puede. Una API no oficial va contra los términos de WhatsApp y el número puede ser limitado o bloqueado. Opt-in, ritmo humano, calentamiento y lista limpia reducen el riesgo, pero no hay garantía. Para un canal homologado, usa la API oficial de Meta.

Prueba PHP en tu instancia

Crea la cuenta, conecta un número por código QR y ejecuta el ejemplo. R$15 por número activo al mes, sin cobro por mensaje.