Dev

Send a WhatsApp message via API with curl: ready examples

To send a WhatsApp message through an API with curl, make a POST to /api/v1/{instanceId}/send/text with the x-api-key and x-api-secret headers and a JSON body with number (with country code, digits only) and text. The answer carries messageId and status. This guide goes from the first send to a paced list, and every command can be copied and run in the terminal. For Node, Python and PHP, see the guides for Node.js in 5 minutes, Python and PHP with cURL.

Published on · 4 min read

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

What you need before starting

A ZapZap account, an instance connected with a QR code, the API key and secret (in the dashboard, under Settings and API) and the instance ID. Authentication uses only two headers, with no OAuth. Keep the three values in environment variables so you do not paste secrets into commands or leave them in the terminal history.

The second command prints only the HTTP code: 200 means the key and secret are right; 401 means one of them is wrong.

Variáveis e teste de credencial

# Keep credentials out of the command and out of the shell history
export ZAPZAP_INSTANCE="your-instance-id"
export ZAPZAP_API_KEY="your-api-key"
export ZAPZAP_API_SECRET="your-api-secret"

# Quick check: a 401 here means key or secret is wrong, not the instance
curl -s -o /dev/null -w "%{http_code}\n" "https://api.zapzapapi.com/api/v1/account" \
  -H "x-api-key: $ZAPZAP_API_KEY" -H "x-api-secret: $ZAPZAP_API_SECRET"

The first send: a text message

The number goes with country code and digits only, with no +, spaces or dashes: 5511999999999 for a mobile in Sao Paulo. The minimum body has number and text. The synchronous answer carries messageId and status, which keep the trail of the message in your system.

By default, the API shows 'typing...' proportional to the text length before sending, which humanizes the send. If you need a faster answer, send digitando as false; to delay the send in milliseconds, use delay.

curl: texto

curl -X POST "https://api.zapzapapi.com/api/v1/$ZAPZAP_INSTANCE/send/text" \
  -H "Content-Type: application/json" \
  -H "x-api-key: $ZAPZAP_API_KEY" \
  -H "x-api-secret: $ZAPZAP_API_SECRET" \
  -d '{"number":"5511999999999","text":"Hello! Your order left for delivery."}'
# Expected answer: {"messageId":"ABCDEF123456","status":"sent"}

Sending an image and a PDF

The /send/media endpoint takes the type (image, video, audio, document, sticker and others) and the public URL of the file in the file field. For image and video, the text field becomes the caption. For documents, docName is required and becomes the file name the customer sees.

If the URL is not public, the send fails: the file must be reachable without a login. For private files, generate a temporary signed link in your storage.

curl: mídia

# Image with caption (the file must be a public URL)
curl -X POST "https://api.zapzapapi.com/api/v1/$ZAPZAP_INSTANCE/send/media" \
  -H "Content-Type: application/json" \
  -H "x-api-key: $ZAPZAP_API_KEY" -H "x-api-secret: $ZAPZAP_API_SECRET" \
  -d '{"number":"5511999999999","type":"image","file":"https://example.com/promo.jpg","text":"This week offer"}'

# PDF: type document, and docName is required
curl -X POST "https://api.zapzapapi.com/api/v1/$ZAPZAP_INSTANCE/send/media" \
  -H "Content-Type: application/json" \
  -H "x-api-key: $ZAPZAP_API_KEY" -H "x-api-secret: $ZAPZAP_API_SECRET" \
  -d '{"number":"5511999999999","type":"document","file":"https://example.com/quote.pdf","docName":"quote.pdf"}'

Receiving the customer's reply with a webhook

Sending is half the way. Register a public HTTPS URL on the instance webhook endpoint and the API starts POSTing to it on each event, such as a received message and a status change. Use excludeMessages so you do not get back the messages you sent yourself.

During development, a local URL receives nothing. See why the webhook does not reach localhost and, for production, idempotency and retry on the webhook.

curl: webhook

# Receive replies: register your HTTPS URL on the instance
curl -X PUT "https://api.zapzapapi.com/api/v1/instances/$ZAPZAP_INSTANCE/webhook" \
  -H "Content-Type: application/json" \
  -H "x-api-key: $ZAPZAP_API_KEY" -H "x-api-secret: $ZAPZAP_API_SECRET" \
  -d '{"webhook_url":"https://yoursite.com/webhook","events":"messages,status","excludeMessages":true}'

A small list, at the right pace

The script below reads a CSV (number and name) and sends one message at a time, with a random pause of 8 to 20 seconds between them. Never in parallel. It is the lowest-risk pattern for a list of people who already asked to hear from you, and it suits reminders and confirmations, not cold campaigns.

No interval removes the ban risk, because ZapZap uses an unofficial channel. For larger volumes, read the guides on mass sending and anti-ban.

bash: lista com intervalo

# Small list, human pace: one message every 8 to 20 seconds, never in parallel
while IFS=, read -r number name; do
  curl -s -X POST "https://api.zapzapapi.com/api/v1/$ZAPZAP_INSTANCE/send/text" \
    -H "Content-Type: application/json" \
    -H "x-api-key: $ZAPZAP_API_KEY" -H "x-api-secret: $ZAPZAP_API_SECRET" \
    -d "{\"number\":\"$number\",\"text\":\"Hi $name, your appointment is tomorrow. Reply 1 to confirm.\"}" \
    -w " -> HTTP %{http_code}\n"
  sleep $(( (RANDOM % 13) + 8 ))
done < contacts.csv

Most common errors and how to read the answer

401: wrong key or secret, or a header with a swapped name. 404: the instance ID is wrong or was pasted twice in the URL. 400: invalid body, such as number with letters or a missing required field. 429 and 5xx: transient errors, worth retrying with growing waits; 400, 401 and 403 do not improve by repeating.

If the API answered 'sent' and the customer did not receive it, the problem is usually after the send. See WhatsApp sent but the customer did not receive it. The full field reference is in send message and in the API reference.

Quick summary

  • One POST to /send/text with x-api-key, x-api-secret, number (country code, digits only) and text sends the message.
  • Credentials in environment variables, never pasted into the command.
  • Media needs a public URL in the file field; a document needs docName.
  • Small list: one message at a time, with a random pause, only for people who asked to receive.

Also read

Frequently asked questions

How do I send a WhatsApp message through an API with curl?

Make a POST to https://api.zapzapapi.com/api/v1/{instanceId}/send/text with the headers x-api-key, x-api-secret and Content-Type application/json, and the body {number, text}. The number carries the country code and digits only.

Does the number need a + or spaces?

No. Use digits only with the country code, like 5511999999999. To clean whole lists, use the bulk number formatter in the tools area.

Can I send an image and a PDF with curl?

Yes, through the /send/media endpoint, with type image or document and the public URL of the file in file. For a document, provide docName.

How do I receive the customer's reply?

Register your server's HTTPS URL with PUT to /api/v1/instances/{id}/webhook. The API POSTs to it on each message and status event.

Is it safe to blast a whole list with a loop?

Only a small list, of people who asked to receive, with a pause between messages. The channel is unofficial and no interval removes the ban risk. For volume, follow the anti-ban guides.

Run your first curl today

Create the account, connect the number with a QR code and copy the command from the guide. R$15 per active number per month, no per-message fee.