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
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.
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:
| Status | What it means |
|---|---|
| queued | We received it. The background worker hasn't processed it yet — usually milliseconds. |
| scheduled | You set a future sendAt time. We're holding it until then. |
| processing | Our worker is calling the WhatsApp API right now. |
| retrying | Delivery failed on a transient error and the worker is retrying automatically. |
| sent | WhatsApp accepted it — it's on their servers but hasn't reached the device yet. |
| delivered | It arrived on the recipient's phone (double grey tick). |
| read | The recipient opened the message (double blue tick). |
| failed | Delivery failed. Check errorMessage on the message object for the reason. |
Quickstart — 5 minutes to your first message
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"
}
}
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.
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
hello_world, so that's what this quickstart uses.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.
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.
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 +:
| Country | Normal format | What to send |
|---|---|---|
| USA / Canada | +1 (415) 555-2671 | 14155552671 |
| UK | +44 7911 123456 | 447911123456 |
| India | +91 98765 43210 | 919876543210 |
| Australia | +61 412 345 678 | 61412345678 |
POST /send-message
Queue a single message. Responds 202 immediately — delivery is asynchronous. Save the returned id to track the message.
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
| Field | Type | Description | |
|---|---|---|---|
| to | string | required | Recipient phone in E.164 without + |
| message | string | required* | 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. |
| template | object | required* | Meta-approved template for business-initiated messages. Required for all outbound notifications. See templates → |
| sendAt | string | optional | ISO 8601 future datetime to schedule (e.g. "2026-05-01T09:00:00Z") |
| metadata | object | optional | Any key/value data stored with the message and returned in webhooks |
| dry_run | boolean | optional | true 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.
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=....
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"
}
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 param | Description |
|---|---|
| status | Filter: queued, sent, delivered, failed, etc. |
| batchId | Show only messages from a specific bulk send |
| limit | Results per page (default 20, max 200) |
| page | Page 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:
| Direction | Purpose | What you configure |
|---|---|---|
| Meta → WhatsAPI | Meta sends delivery receipts and inbound events to this API | Set Meta's callback URL to https://whatsapi.cc/webhooks/whatsapp |
| WhatsAPI → Your app | We forward normalized delivery status events to your product | Save 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_..."
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.
| Field | Type | Description | |
|---|---|---|---|
| name | string | required | Your name or company name |
| string | required | Email 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.
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
}
| Code | Meaning and common causes |
|---|---|
| 400 | Bad request. Validation failed. Check details — e.g. missing required field, wrong phone format, sendAt is in the past. |
| 401 | Not authenticated. The Authorization header is missing, malformed, or the API key is wrong. Must be: Bearer sk_live_... |
| 403 | Forbidden. Your account has been deactivated. |
| 404 | Not found. The message ID doesn't exist or belongs to another account. |
| 409 | Conflict. Idempotency key was reused with a different body, or the message isn't in a retryable state. |
| 429 | Too many requests. Rate limit hit. Check the Retry-After header — it tells you how many seconds to wait. |
| 503 | Service 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.
| Plan | Requests per minute |
|---|---|
| Free | 20 |
| Basic | 100 |
| Pro | 500 |
| Enterprise | 2,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
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..."
}
}
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_..."