Dev

Send WhatsApp with PHP and cURL: API example (Guzzle and Laravel)

To send WhatsApp with PHP you POST with cURL to /api/v1/{instanceId}/send/text, with the x-api-key and x-api-secret headers and a JSON body with number and text. It works on any host with the cURL extension. Below: plain cURL, Guzzle, file sending, a Laravel job and a webhook receiver.

Published on · 3 min read

This article is also available in: Português · Español

What you need before starting

A ZapZap account, an instance connected by QR code, the API key and secret and the instance ID. Authentication uses x-api-key and x-api-secret. In PHP, keep credentials in .env or server variables, outside the repository.

Test with cURL on the command line before moving to 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."}'

How to send a text message with PHP and cURL

The function below uses PHP's cURL extension, sets timeouts and returns status and body. Use json_encode to build the JSON: concatenating strings by hand breaks with quotes and emoji.

Always check the HTTP code, not just whether the call returned something. A 401 comes back with a body and does not throw on its own.

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

How to send with Guzzle and deliver a PDF or image

With composer require guzzlehttp/guzzle, the client reuses the connection and already carries base_uri, headers and timeout. For media, use /send/media with type (image, video, audio, ptt, document, sticker and others), file (public URL), text (caption) and docName.

With http_errors set to false you decide what to do with each status instead of handling exceptions.

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

How to send from Laravel with a queue job

In Laravel, do not send WhatsApp inside the user's request. Dispatch a job to the queue: if the API is slow or fails, the page stays fast and the job retries by itself. Put credentials in config/services.php, read from .env.

The Http facade's retry covers transient errors. The job below fails explicitly on 4xx, so Laravel does not repeat a request that will never work.

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.');

How to receive replies with a PHP webhook

Set the URL in configure webhook and turn on the messages event. Read the raw body with file_get_contents('php://input'), reply 200 right away and process later. The incoming message is in data.message (text, chatid, fromMe); ignore fromMe true.

In Laravel, the webhook route must sit outside the CSRF middleware. Validate the X-ZapZap-Signature and handle redeliveries as described in the webhooks in production guide.

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'] ?? ''));

Checklist before going to production

Credentials outside the repository; connect and total timeouts; HTTP status checks; sending through a queue, not inside the request; retry only on transient errors; a validated webhook signature; and sending only to people who agreed to receive.

Other languages: Python, Node.js, Ruby, C#. All endpoints are in the API reference.

Quick summary

  • Plain cURL works on any PHP host with the cURL extension.
  • Use json_encode and always check the HTTP code of the response.
  • In Laravel, send through a queue job with backoff and fail straight away on 4xx.
  • The webhook route stays outside CSRF; reply 200 before processing.

Also read

Frequently asked questions

Does it work on shared hosting?

It works if the cURL extension is active and the host allows outbound HTTPS connections, which is the default. To receive webhooks, the server needs a public HTTPS URL.

Do I need composer?

Not for the plain cURL example. Guzzle and Laravel use composer, but they are optional.

How do I use it in WordPress?

Call wp_remote_post with the same headers and the same JSON, from a hook in your plugin or from a form webhook. Avoid putting the key and secret in theme code.

Can using an unofficial API get my number banned?

It can. An unofficial API goes against WhatsApp's terms and the number can be limited or banned. Opt-in, a human pace, warm-up and a clean list reduce the risk, but there is no guarantee. For a sanctioned channel, use Meta's official API.

Test PHP on your instance

Create the account, connect a number by QR code and run the example. R$15 per active number per month, no per-message fee.