Developer Docs

Step-by-step guide to getting connected, sending WhatsApp messages, tracking delivery, and wiring up status webhooks. Looking for the interactive endpoint explorer? Go to /docs. Inside the app, the guided setup flow now walks first-time users through Meta credentials, verification, and their first successful message.

Give these docs to your AI

If you're asking ChatGPT, Claude, Cursor, Copilot, or another coding agent to integrate WhatsAPI for you, do not just send a vague prompt. Give it these three links in this order so it has the API shape, the integration rules, and the product context:

1. https://whatsapi.cc/llms.txt
2. https://whatsapi.cc/openapi.json
3. https://whatsapi.cc/developers
What the AI still needs from you: your WhatsAPI API key, your Meta access token, your Meta phone number ID, your public webhook URL if your product has one, and the template names you want to use in production.
Do not let the AI pretend the integration is complete without Meta setup. WhatsAPI is BYOC. Your AI still needs to guide you through Meta for the phone number, token, webhook subscription, and templates.

How it works — the big picture

Before diving into code, here's the mental model. Understanding this makes everything else click.

Sending is asynchronous. When you call POST /send-message, we don't wait for WhatsApp to confirm delivery before responding. We store the message, put it in a queue, and respond immediately with a 202 Accepted and a message id. Delivery happens in the background a second or two later.

YOUR APP
POST /send-message
your server
WHATSAPI
Store & queue
instant 202 + id
META / WA
Deliver to phone
1–3 seconds
YOUR APP
Webhook or poll
status updates

Use the message id to track delivery. Poll GET /messages/:id, or register a webhook and we'll push status changes to your server automatically.

Every message moves through a lifecycle:

StatusWhat it means
queuedWe received it. The background worker hasn't processed it yet — usually milliseconds.
scheduledYou set a future sendAt time. We're holding it until then.
processingOur worker is calling the WhatsApp API right now.
retryingDelivery failed on a transient error and the worker is retrying automatically.
sentWhatsApp accepted it — it's on their servers but hasn't reached the device yet.
deliveredIt arrived on the recipient's phone (double grey tick).
readThe recipient opened the message (double blue tick).
failedDelivery failed. Check errorMessage on the message object for the reason.

Quickstart — 5 minutes to your first message

Reality check before you call this done: a production integration still needs five concrete inputs from the product owner — an API key, whatsappAccessToken, whatsappPhoneNumberId, the public HTTPS URL in the product that should receive forwarded delivery events, and the Meta template names the product will use in production.

Step 1 — Get an API key

Make one POST request with your name and email. You get back an apiKey — include it on every future request.

# Run this in your terminal
curl -X POST https://whatsapi.cc/auth/register   -H "Content-Type: application/json"   -d '{ "name": "Your Name", "email": "[email protected]" }'
const res = await fetch('https://whatsapi.cc/auth/register', {
  method: 'POST',
  headers: { 'Content-Type': 'application/json' },
  body: JSON.stringify({ name: 'Your Name', email: '[email protected]' }),
});
const { data } = await res.json();
console.log(data.apiKey); // save this — it won't be shown again
import requests
r = requests.post('https://whatsapi.cc/auth/register',
    json={'name': 'Your Name', 'email': '[email protected]'})
api_key = r.json()['data']['apiKey']
print(api_key)  # save this!

Response:

{
  "success": true,
  "data": {
    "apiKey": "sk_live_a1b2c3...",  // shown once here — store it immediately
    "plan": "free"
  }
}
Store your API key as an environment variable (e.g. WHATSAPI_KEY). If you lose it, you'll need to register a new account.

Step 2 — Connect your Meta credentials

For database-backed accounts, real sending only works after you save your own Meta credentials. First call GET /auth/me. If byocConfigured is false, save credentials with PUT /auth/me/credentials and verify them with POST /auth/me/credentials/verify.

Yes, users still need Meta. WhatsAPI does not replace Meta Business Manager. Meta still owns the actual business phone number, access token, webhook subscription, and template approval workflow. WhatsAPI sits on top of that setup and gives you a cleaner product layer.
curl -X PUT https://whatsapi.cc/auth/me/credentials   -H "Authorization: Bearer sk_live_a1b2c3..."   -H "Content-Type: application/json"   -d '{
    "whatsappAccessToken": "EAAc...",
    "whatsappPhoneNumberId": "1234567890"
  }'

curl -X POST https://whatsapi.cc/auth/me/credentials/verify   -H "Authorization: Bearer sk_live_a1b2c3..."

Step 3 — Send your first message

Important — templates vs. free text: Meta only allows free-form text when you're replying to a customer who messaged your number first, within 24 hours. For all outbound notifications — OTPs, order updates, reminders — you must use a Meta-approved template. Every new WhatsApp Business account automatically gets hello_world, so that's what this quickstart uses.
Sandbox test mode vs live mode: if you are still using Meta's test business number, your first test may only work to recipient numbers that you manually add in Meta's API Setup screen. That is only for testing. In live mode, you do not add every customer manually there — you send approved templates to customers who have opted in.

Phone numbers must be in E.164 format: country code + number, digits only, no + sign. So +1 (415) 555-2671 becomes 14155552671.

curl -X POST https://whatsapi.cc/send-message   -H "Authorization: Bearer sk_live_a1b2c3..."   -H "Content-Type: application/json"   -d '{
    "to": "14155552671",
    "template": { "name": "hello_world", "language": "en_US" }
  }'
const res = await fetch('https://whatsapi.cc/send-message', {
  method: 'POST',
  headers: {
    'Authorization': 'Bearer ' + process.env.WHATSAPI_KEY,
    'Content-Type': 'application/json',
  },
  body: JSON.stringify({
    to: '14155552671',
    template: { name: 'hello_world', language: 'en_US' },
  }),
});
const { data } = await res.json();
console.log(data.id); // save — use to check delivery later
import os, requests
r = requests.post('https://whatsapi.cc/send-message',
    headers={'Authorization': 'Bearer ' + os.environ['WHATSAPI_KEY']},
    json={
        'to': '14155552671',
        'template': {'name': 'hello_world', 'language': 'en_US'},
    })
print(r.json()['data']['id'])  # message id

Step 4 — Check it was delivered

Use the id from step 3. Wait a second or two — delivery is nearly instant.

curl https://whatsapi.cc/messages/MESSAGE_ID_HERE   -H "Authorization: Bearer sk_live_a1b2c3..."

You'll see "status": "delivered" once it reaches the device. That's it — you're integrated.

If the first sandbox send fails for only some numbers, the most common cause is Meta test-mode restrictions on allowed recipient numbers — not a WhatsAPI bug.

Authentication

Every request (except POST /auth/register) requires your API key in the Authorization header:

Authorization: Bearer sk_live_your_api_key_here

Keys follow the format sk_live_ + 48 hex characters. They don't expire. Treat them like a password — never commit them to source control.

Store your key as WHATSAPI_KEY in your environment variables. In Node.js: process.env.WHATSAPI_KEY. In Python: os.environ['WHATSAPI_KEY'].

Phone number format

All phone numbers must be E.164 — country code + number, digits only, no +:

CountryNormal formatWhat to send
USA / Canada+1 (415) 555-267114155552671
UK+44 7911 123456447911123456
India+91 98765 43210919876543210
Australia+61 412 345 67861412345678

POST /send-message

Queue a single message. Responds 202 immediately — delivery is asynchronous. Save the returned id to track the message.

When to use message vs. template: Meta only permits free-form text (message) when replying to a customer who messaged your number first, within 24 hours. For all outbound notifications — OTPs, order updates, appointment reminders, marketing — you must use the template field with a Meta-approved template. Using message for business-initiated sends will be rejected by Meta.

Request fields

FieldTypeDescription
tostringrequiredRecipient phone in E.164 without +
messagestringrequired*Free-form text reply (max 4096 chars). Only works within a 24-hour customer service window — i.e. the recipient messaged your number first. For outbound notifications use template.
templateobjectrequired*Meta-approved template for business-initiated messages. Required for all outbound notifications. See templates →
sendAtstringoptionalISO 8601 future datetime to schedule (e.g. "2026-05-01T09:00:00Z")
metadataobjectoptionalAny key/value data stored with the message and returned in webhooks
dry_runbooleanoptionaltrue simulates a send without calling WhatsApp. For testing.
curl -X POST https://whatsapi.cc/send-message   -H "Authorization: Bearer sk_live_..."   -H "Content-Type: application/json"   -H "Idempotency-Key: order-1234-shipped"   -d '{
    "to": "14155552671",
    "message": "Hi Sarah! Your order #1234 has shipped.",
    "metadata": { "orderId": "1234" }
  }'
const res = await fetch('https://whatsapi.cc/send-message', {
  method: 'POST',
  headers: {
    'Authorization': 'Bearer ' + process.env.WHATSAPI_KEY,
    'Content-Type': 'application/json',
    'Idempotency-Key': 'order-1234-shipped',
  },
  body: JSON.stringify({
    to: '14155552671',
    message: 'Hi Sarah! Your order #1234 has shipped.',
    metadata: { orderId: '1234' },
  }),
});
const { data } = await res.json();
console.log(data.id);
import os, requests
r = requests.post('https://whatsapi.cc/send-message',
    headers={
        'Authorization': 'Bearer ' + os.environ['WHATSAPI_KEY'],
        'Idempotency-Key': 'order-1234-shipped',
    },
    json={
        'to': '14155552671',
        'message': 'Hi Sarah! Your order #1234 has shipped.',
        'metadata': {'orderId': '1234'},
    })
print(r.json()['data']['id'])

Response 202

{
  "success": true,
  "data": {
    "id": "9dd15ee1-3d3c-44e1-9dd2-8ec6547e7e20",
    "status": "queued",
    "scheduledFor": null
  }
}

Send a template message

Templates are how you send most messages in practice. Meta requires them for any business-initiated conversation — that is, any time you're reaching out to a customer first, rather than replying to them.

Common template use cases: OTP codes, order confirmations, shipping updates, appointment reminders, payment receipts. You design the message, get it approved by Meta (usually same-day), then call it with variable values filled in.

Use the same POST /send-message endpoint — pass a template object instead of a message string.

You need templates approved in Meta Business Manager first. The hello_world template is available to all new accounts at no approval cost.
curl -X POST https://whatsapi.cc/send-message   -H "Authorization: Bearer sk_live_..."   -H "Content-Type: application/json"   -d '{
    "to": "14155552671",
    "template": { "name": "hello_world", "language": "en_US" }
  }'
# Template body: "Hi {{1}}, your order {{2}} is ready!"
curl -X POST https://whatsapi.cc/send-message   -H "Authorization: Bearer sk_live_..."   -H "Content-Type: application/json"   -d '{
    "to": "14155552671",
    "template": {
      "name": "order_ready",
      "language": "en_US",
      "components": [{
        "type": "body",
        "parameters": [
          { "type": "text", "text": "Sarah" },
          { "type": "text", "text": "#1042" }
        ]
      }]
    }
  }'

POST /messages/bulk

Send multiple messages in a single request. Each item in messages accepts the same fields as a normal send. The response includes a batchId you can use to filter GET /messages?batchId=....

Bulk sending is available on paid plans only (Basic, Pro, Enterprise). The per-request limit is the lower of the user's plan allowance and the deployment cap (MAX_BULK_MESSAGES=1000 by default). Basic caps at 100, Pro at 1,000, Enterprise at 5,000.
curl -X POST https://whatsapi.cc/messages/bulk   -H "Authorization: Bearer sk_live_..."   -H "Content-Type: application/json"   -d '{
    "messages": [
      { "to": "14155552671", "message": "Hi Alice, your code is 1234" },
      { "to": "14155552672", "message": "Hi Bob, your code is 5678" }
    ]
  }'

Response 202

{
  "data": {
    "batchId": "a3f9c2d1-...",
    "totalMessages": 2,
    "queued": 2,
    "failedCount": 0
  }
}

Schedule a message for later

Add a sendAt ISO 8601 timestamp (must be in the future) to any send request — text, template, single, or bulk.

{
  "to": "14155552671",
  "message": "Reminder: your appointment is tomorrow at 9am.",
  "sendAt": "2026-04-15T08:00:00.000Z"
}
Scheduled jobs require Redis to survive server restarts. Without Redis they're held in memory and lost if the server restarts.

GET /messages/:id

Get the current status and full delivery history of a message. Use the UUID from the send response.

curl https://whatsapi.cc/messages/9dd15ee1-...   -H "Authorization: Bearer sk_live_..."

Response 200

{
  "data": {
    "id": "9dd15ee1-...",
    "to": "14155552671",
    "body": "Hi Sarah! Your order #1234 has shipped.",
    "status": "delivered",
    "sentAt": "2026-03-28T10:00:01Z",
    "deliveredAt": "2026-03-28T10:00:04Z",
    "readAt": null,
    "failedAt": null,
    "errorMessage": null,
    "metadata": { "orderId": "1234" },
    "timeline": [
      { "status": "queued",     "at": "2026-03-28T10:00:00Z" },
      { "status": "sent",      "at": "2026-03-28T10:00:01Z" },
      { "status": "delivered", "at": "2026-03-28T10:00:04Z" }
    ]
  }
}

GET /messages

Paginated list of your messages. All query parameters are optional.

Query paramDescription
statusFilter: queued, sent, delivered, failed, etc.
batchIdShow only messages from a specific bulk send
limitResults per page (default 20, max 200)
pagePage number (default 1)
curl "https://whatsapi.cc/messages?status=failed&limit=50"   -H "Authorization: Bearer sk_live_..."

POST /messages/:id/retry

Re-queue a failed message. Only works on messages with status: "failed". Returns 202.

curl -X POST https://whatsapi.cc/messages/9dd15ee1-.../retry   -H "Authorization: Bearer sk_live_..."

GET /analytics/summary

Delivery statistics for your account — total sent, breakdown by status, delivery rate.

curl https://whatsapi.cc/analytics/summary   -H "Authorization: Bearer sk_live_..."
{
  "data": {
    "total": 1042,
    "byStatus": { "delivered": 980, "failed": 52, "sent": 10 },
    "deliveryRate": 0.94,
    "failureRate": 0.05
  }
}

Set up a webhook

There are two different webhook paths in a real WhatsAPI integration:

DirectionPurposeWhat you configure
Meta → WhatsAPIMeta sends delivery receipts and inbound events to this APISet Meta's callback URL to https://whatsapi.cc/webhooks/whatsapp
WhatsAPI → Your appWe forward normalized delivery status events to your productSave your callback URL with PUT /auth/me/webhook

Give us a public HTTPS URL on your server. We'll POST to it every time a message status changes — sent, delivered, read, or failed. This is better than polling in production.

# Register your webhook URL
curl -X PUT https://whatsapi.cc/auth/me/webhook   -H "Authorization: Bearer sk_live_..."   -H "Content-Type: application/json"   -d '{ "url": "https://yourapp.com/webhooks/whatsapi" }'

# Remove your webhook
curl -X DELETE https://whatsapi.cc/auth/me/webhook   -H "Authorization: Bearer sk_live_..."
Registering PUT /auth/me/webhook does not configure Meta for you. You must still set Meta's webhook callback to GET/POST /webhooks/whatsapp on this service.

What we send your webhook

We POST this JSON to your URL when a status changes. Respond with any 2xx within 10 seconds or we'll retry once.

{
  "event": "message.status_updated",
  "timestamp": "2026-03-28T10:00:04Z",
  "data": {
    "messageId": "9dd15ee1-...",
    "status": "delivered",
    "to": "14155552671",
    "deliveredAt": "2026-03-28T10:00:04Z",
    "readAt": null,
    "failedAt": null,
    "errorMessage": null,
    "metadata": { "orderId": "1234" }
  }
}

Example handler (Express / Node.js)

app.post('/webhooks/whatsapi', (req, res) => {
  const { event, data } = req.body;
  if (event === 'message.status_updated') {
    if (data.status === 'delivered') console.log('Delivered to', data.to);
    if (data.status === 'failed')    console.error('Failed:', data.errorMessage);
  }
  res.sendStatus(200); // must respond 2xx or we retry
});

POST /auth/register

Create an account and get an API key. No auth required. Rate-limited to 5 requests/minute per IP.

FieldTypeDescription
namestringrequiredYour name or company name
emailstringrequiredEmail address used for your account

GET /auth/me

Returns your account details. The API key itself is not included in this response.

{
  "data": {
    "name": "Your Name",
    "email": "[email protected]",
    "plan": "free",
    "messagesSent": 42,
    "byocConfigured": false,
    "webhookUrl": null
  }
}

Connect your own WhatsApp number

WhatsAPI is a BYOC (Bring Your Own Credentials) platform. You provide your own Meta WhatsApp Business Account credentials — your Access Token and Phone Number ID from Meta Business Manager. Messages are sent from your number, billed by Meta to your account.

⚠ Sending requires BYOC credentials. Without them, message sends will fail. Save credentials with PUT /auth/me/credentials and verify with POST /auth/me/credentials/verify.
# Set your credentials
curl -X PUT https://whatsapi.cc/auth/me/credentials   -H "Authorization: Bearer sk_live_..."   -H "Content-Type: application/json"   -d '{
    "whatsappAccessToken": "EAAc...",
    "whatsappPhoneNumberId": "1234567890"
  }'

# Remove your credentials
curl -X DELETE https://whatsapi.cc/auth/me/credentials   -H "Authorization: Bearer sk_live_..."

Error codes

All errors follow the same shape. The error string is human-readable. Validation errors also include a details array with per-field messages.

{
  "success": false,
  "error": "Invalid API key.",
  "details": [...]  // only on 400 — lists each failing field
}
CodeMeaning and common causes
400Bad request. Validation failed. Check details — e.g. missing required field, wrong phone format, sendAt is in the past.
401Not authenticated. The Authorization header is missing, malformed, or the API key is wrong. Must be: Bearer sk_live_...
403Forbidden. Your account has been deactivated.
404Not found. The message ID doesn't exist or belongs to another account.
409Conflict. Idempotency key was reused with a different body, or the message isn't in a retryable state.
429Too many requests. Rate limit hit. Check the Retry-After header — it tells you how many seconds to wait.
503Service unavailable. The WhatsApp API has been erroring repeatedly; we've paused sending to protect your account. Resolves automatically.

Rate limits

Limits are per API key per minute. Exceed your limit → 429.

PlanRequests per minute
Free20
Basic100
Pro500
Enterprise2,000

Every response includes headers showing your current usage:

X-RateLimit-Limit: 20        # your plan's limit per window
X-RateLimit-Remaining: 17   # how many you have left this window
X-RateLimit-Reset: 1711619220  # Unix timestamp when the window resets
Retry-After: 45             # only present on 429 — seconds to wait

Preventing duplicate messages

If your server retries a failed HTTP request, you could accidentally send the same message twice. The Idempotency-Key header prevents this.

How it works: attach a unique key to your request. If we see the same key again within 24 hours, we return the original response without sending another message.

Idempotency-Key: user-99-order-1234-shipped
A good key uniquely identifies the specific action: user-{userId}-order-{orderId}-{event}. Keys are scoped to your account — different accounts can use the same key without conflicting.

When a cached response is replayed, the response includes the header Idempotency-Replayed: true.

POST /auth/me/rotate-key

Generate a new API key, invalidating the old one immediately. The new key is shown once in the response — store it right away.

curl -X POST https://whatsapi.cc/auth/me/rotate-key   -H "Authorization: Bearer sk_live_OLD_KEY"

Response 200

{
  "success": true,
  "data": {
    "apiKey": "sk_live_new_key_here..."
  }
}
After rotation, the old key stops working immediately. Update your environment variables before making the next request.

Billing & upgrades

Upgrade your plan via Stripe checkout, or manage your existing subscription through the billing portal.

POST /billing/checkout

Start a Stripe checkout session. Redirect the user to the returned URL to complete payment.

curl -X POST https://whatsapi.cc/billing/checkout   -H "Authorization: Bearer sk_live_..."   -H "Content-Type: application/json"   -d '{ "plan": "pro" }'

Valid self-service plans: basic and pro. Enterprise is custom and handled through a sales-assisted flow. Response includes a data.url pointing to Stripe checkout.

GET /billing/portal

Returns a Stripe billing portal URL for managing subscriptions, invoices, and cancellation.

curl https://whatsapi.cc/billing/portal   -H "Authorization: Bearer sk_live_..."