REST API · v1

Developer API Documentation

Connect your own software to your WhatsApp Chat CRM account. Send WhatsApp messages and approved templates, create and update customers, tag them from your billing or support systems, read conversations, and receive incoming messages by webhook — over plain HTTPS and JSON.

Base URLhttps://YOUR_DOMAIN/api/v1
Quick start AI prompt builder

Overview

The API is organised around a small number of predictable HTTP endpoints. Every request and response body is JSON. You authenticate with a secret API key that belongs to one WhatsApp Chat CRM account (a tenant), so a key can only ever read and write that account's data.

Send
Text messages inside the 24-hour window, and approved templates any time — one at a time or in bulk.
Sync customers
Create/update customers with your own external_id, tag them, and read their conversations.
Receive
Every incoming customer message is forwarded as JSON to a webhook URL you configure.
Server-to-server only. Your API key can message your customers. Never put it in a web page, mobile app or public repository — call the API from your own backend.

Quick start

  1. Sign in and open Settings → Developer API, then click Generate Key. Copy the key — it starts with whsec_. (Owners can do this; a manager needs the Manage API Keys permission.)
  2. Make sure WhatsApp is connected under Meta Settings — message endpoints need it.
  3. Send your first request:

A successful call returns 201 (created) or 200 (already existed and was updated):

Response 201
{
  "success": true,
  "isNewContact": true,
  "contactId": "329cfdf6-a303-44b9-bffc-fc3336caafb5"
}

Authentication

Send your key in the x-api-key header on every request.

Header
x-api-key: whsec_your_secret_key_here
Situation Result
Header missing 401 Unauthorized: Missing x-api-key header.
Wrong or revoked key 403 Forbidden: Invalid or revoked API key.
Subscription expired 402 SUBSCRIPTION_EXPIRED — every endpoint pauses until the owner recharges

A key identifies the account, not a person, and has full access to every endpoint on this page. Create one key per integration so you can revoke a single one without downtime. Revocation is immediate.

Conventions & limits

Topic Rule
Format JSON in and out. Send Content-Type: application/json. UTF-8.
Phone numbers Digits only, including the country code, no +, no leading zeros: 919876543210. Spaces, dashes and + are stripped, but a missing country code is not guessed.
Timestamps ISO-8601 in UTC, e.g. 2026-09-18T12:44:24.000Z.
Rate limit 300 requests per minute per account, shared by all keys. Over the limit you get 429 with standard RateLimit-* headers.
Body size Max 100 KB per request. Split big bulk batches (about 100 recipients per call is a good size).
Timeouts Sends are made to WhatsApp inline: roughly 0.3–2 s each. Bulk calls take about that × recipients — use a client timeout of 60 s or more.
24-hour window WhatsApp allows free-text only within 24 h of the customer's last message. Outside it, or for brand-new customers, use a template.

Data types

Shared shapes used across endpoints, written as TypeScript so they can be pasted into your project.

TypeScript
type PhoneNumber       = string;  // digits incl. country code: "919876543210"
type Timestamp         = string;  // ISO-8601 UTC
type WhatsAppMessageId = string;  // Meta "wamid", e.g. "wamid.HBgMOTE5ODc2NTQz..."

interface ErrorResponse {
  error: string;                  // code or human-readable message
  message?: string;               // extra explanation on some errors
  [extra: string]: unknown;       // e.g. useTemplateEndpoint, expires_at
}

interface BulkResultItem {
  phone: PhoneNumber;
  success: boolean;
  messageId?: WhatsAppMessageId;  // when success = true
  error?: string;                 // when success = false
}

interface Customer {
  id: string;                     // UUID
  name: string | null;
  phone_number: PhoneNumber;
  last_interacted_at: Timestamp | null;
  created_at: Timestamp;
}

interface Message {
  id: string;
  wamid: WhatsAppMessageId | null;
  context_wamid: WhatsAppMessageId | null;   // set when replying to another message
  direction: "inbound" | "outbound";
  message_type: "text" | "image" | "video" | "document" | "audio" | "template" | "interactive" | string;
  body: string | null;            // JSON *string* when message_type = "template"
  media_url: string | null;       // signed relative link, valid 10 minutes
  status: "sent" | "delivered" | "read" | "failed" | "received" | string;
  is_read: 0 | 1;
  created_at: Timestamp;
}
POST/api/v1/messages/send

Send a message

Sends a plain-text WhatsApp message. Works only while the customer's 24-hour window is open (they messaged you in the last 24 h). Otherwise you get 409 WINDOW_CLOSED — fall back to Send a template.

Request body

Field Type Description
to string required Recipient phone number, e.g. 919876543210.
message string required Message text (WhatsApp allows up to about 4096 characters).

Responses

Success 200
{ "success": true, "messageId": "wamid.HBgMOTE5ODc2NTQzMjEwFQIAERgS..." }
Window closed 409
{
  "error": "WINDOW_CLOSED",
  "message": "This customer's 24-hour session window is closed. Send an approved template via /v1/messages/send-template instead, or wait for the customer to message first.",
  "useTemplateEndpoint": "/v1/messages/send-template"
}
Status When
400 to or message missing; or WhatsApp isn't connected for this account.
409 24-hour window closed, or a brand-new customer. Use a template.
500 WhatsApp rejected it (invalid number, not on WhatsApp…). error carries Meta's own message.
Not idempotent. If a request times out the message may still have been sent. Store the returned messageId and check before retrying, or you may deliver a duplicate.
POST/api/v1/messages/send-template

Send a template

Sends a Meta-approved message template. Templates work at any time, including to customers who have never written to you — this is how you start a conversation. Create and approve templates in the dashboard first.

Request body

Field Type Description
to string required Recipient phone number.
templateName string required Exact approved template name, e.g. order_update.
languageCode string optional Default en_US. Must match the template's approved language (en, hi, en_GB…).
variables string[] optional Values for the body placeholders in order: variables[0] → {{1}}, variables[1] → {{2}}… The count must equal the template's placeholders.

Responses

Success 200
{ "success": true, "messageId": "wamid.HBgMOTE5ODc2NTQzMjEwFQIAERgS..." }
Status When
400 to / templateName missing, or WhatsApp not connected.
500 Meta rejected it — typically template does not exist, not approved yet, or parameter count mismatch.
Only body text variables are supported. Templates that need header media/variables or dynamic button URLs can't be fully parameterised through this API yet.
POST/api/v1/messages/bulk-send

Bulk send messages

Sends the same text to many recipients. The 24-hour rule is applied per recipient: anyone whose window is closed is skipped and reported as a failed item — they never fail the whole batch. Recipients are processed one after another.

Request body

Field Type Description
recipients string[] required Non-empty array of phone numbers.
message string required Text sent to everyone.

Response

Always 200 — check every item
{
  "total": 2,
  "results": [
    { "phone": "919876543210", "success": true,  "messageId": "wamid.HBg..." },
    { "phone": "919876543211", "success": false,
      "error": "WINDOW_CLOSED: 24-hour session window is closed for this recipient — use /v1/messages/bulk-template instead." }
  ]
}
HTTP 200 does not mean every message was sent. Always loop over results[] and read success per recipient. A 400 is returned only if recipients/message are missing or WhatsApp isn't connected.
POST/api/v1/messages/bulk-template

Bulk send templates

Sends one template to many recipients, each with its own variables — ideal for renewal reminders and order updates. Works regardless of the 24-hour window.

Request body

Field Type Description
templateName string required Approved template name.
languageCode string optional Default en_US.
batch { to: string; variables?: string[] }[] required Non-empty. Each item: recipient to and that recipient's variables.

Response

Always 200 — check every item
{
  "total": 2,
  "results": [
    { "phone": "919876543210", "success": true,  "messageId": "wamid.HBg..." },
    { "phone": "919876543211", "success": false, "error": "(#132000) Number of parameters does not match the expected number of params" }
  ]
}
GET/api/v1/customers

List customers

Returns every customer of the account, most recently active first. No query parameters.

Response

Success 200
{
  "count": 1,
  "customers": [
    {
      "id": "329cfdf6-a303-44b9-bffc-fc3336caafb5",
      "name": "Asha Verma",
      "phone_number": "919876543210",
      "last_interacted_at": "2026-09-18T12:44:24.000Z",
      "created_at": "2026-09-10T08:01:00.000Z"
    }
  ]
}
There is no pagination, search or filtering. On accounts with many customers, cache the result instead of polling it.
POST/api/v1/customers

Create or update a customer

An upsert: adds a customer, or updates the one that already exists. Use it from lead forms, landing pages, billing systems — anything that discovers a customer.

How a customer is matched

  1. external_id equals an existing customer's → that customer is updated.
  2. Otherwise phone_number equals an existing customer → updated, and the external_id is attached if they had none.
  3. Otherwise a new customer is created.

Request body

Field Type Description
phone_number string required Digits incl. country code.
full_name string optional Max 100 characters.
alternate_phone_number string optional A second number, digits only.
email string optional Max 191 characters. Not format-validated.
address string optional Max 5000 characters.
external_id string optional Your own id for this customer (subscription id, account number…). Max 191 chars, must be unique per account. Lets later calls target the customer without their phone number.
field_data { name: string; value: string }[] optional Free-form extra details (same shape as Meta Lead Ads). Name ≤ 100, value ≤ 2000 chars. Entries without a name are ignored. When sent, it replaces the previous custom fields as a whole.

Responses

Created 201  /  Updated 200
{ "success": true, "isNewContact": true, "contactId": "329cfdf6-a303-44b9-bffc-fc3336caafb5" }
Status Error
400 Parameter "phone_number" is required.
400 A valid phone number is required. — the value contained no digits.
external_id wins over phone_number. If you send an existing external_id with a different phone_number, the existing customer is updated and their phone number is not changed — this endpoint never overwrites the phone number of an existing customer.
Safe to retry. Repeating the same call updates the same customer again — it never creates a duplicate. Only the fields you send are changed; a missing or blank field never erases an existing value. Updates never change which staff member owns the customer. A brand-new customer is flagged New, shown at the top of the CRM, and triggers a push notification to the team.
GET/api/v1/customers/:phone/messages

Message history

The full conversation with one customer, oldest first. Put the phone number in the URL path, e.g. /api/v1/customers/919876543210/messages.

Path parameter

Name Type Description
:phone string Customer phone number with country code. Non-digits are ignored.

Response

Success 200
{
  "customer": { "id": "329cfdf6-...", "name": "Asha Verma", "phone_number": "919876543210" },
  "totalMessages": 2,
  "messages": [
    {
      "id": "b1f0...", "wamid": "wamid.HBg...", "context_wamid": null,
      "direction": "inbound", "message_type": "text", "body": "Hi, is my order ready?",
      "media_url": null, "status": "received", "is_read": 1,
      "created_at": "2026-09-18T12:40:02.000Z"
    },
    {
      "id": "c9a2...", "wamid": "wamid.HBg...", "context_wamid": null,
      "direction": "outbound", "message_type": "template",
      "body": "{\"is_template\":true,\"template_name\":\"order_update\",\"header\":\"\",\"body\":\"Hi Asha, order #4821 ships Friday.\",\"footer\":\"\",\"buttons\":[]}",
      "media_url": null, "status": "delivered", "is_read": 1,
      "created_at": "2026-09-18T12:44:24.000Z"
    }
  ]
}
  • Template messages: body is a JSON string — JSON.parse it. Variables are already substituted.
  • Media: media_url is a relative, self-authenticating link such as /api/media/abc123.jpg?exp=…&sig=…. Prefix your base URL. It needs no API key and expires after 10 minutes — download immediately; don't store the link.
  • 404 Customer not found. if that number isn't a customer. No pagination: the whole history is returned.
POST/api/v1/customers/tags

Add or remove tags

Lets your other systems flag things on a customer. Example: your billing app sees a plan expiring in 2 days and tags the customer 2 D Exp, so your support team sees it in the CRM and reaches out.

Request body

Field Type Description
external_id string one of two Preferred way to identify the customer (tried first).
phone_number string one of two Fallback identifier.
tags string[] optional* Tag names to add, all at once, on top of existing tags. Unknown names are created automatically. Case-insensitive (vip = VIP). Max 50 chars each.
remove_tags string[] optional* Tag names to remove. Names the customer doesn't have are ignored.

* At least one of tags / remove_tags is required. Adds are applied first, then removals.

Responses

Success 200
{
  "success": true,
  "customer": { "id": "329cfdf6-...", "name": "Asha Verma", "phone_number": "919876543210" },
  "appliedTags": ["2 D Exp", "VIP"],
  "removedTags": ["Trial"]
}
Status Error
400 Provide either "external_id" or "phone_number" to identify the customer.
400 Provide at least one tag in "tags" or "remove_tags".
404 Customer not found. — create them first with Create / update customer.
Safe to retry — re-applying a tag the customer already has does nothing.

Errors

Every error is a JSON object with an error field (some add message and extras).

HTTP Meaning Retry?
400 Missing or invalid parameter, or WhatsApp not connected for this account. No — fix the request
401 x-api-key header missing. No
402 SUBSCRIPTION_EXPIRED — response also includes message and expires_at. No — owner must recharge
403 Invalid or revoked API key. No
404 Customer not found. No
409 WINDOW_CLOSED — free-text not allowed outside the 24-hour window. No — send a template
429 Rate limit (300 requests/min/account) exceeded. Yes — back off; see RateLimit-Reset
500 WhatsApp/Meta rejected a message (error holds Meta's text), or an internal error. Also returned for a body over 100 KB or malformed JSON (Internal Server Error). Only generic errors. A Meta rejection will fail again
Example 402
{
  "error": "SUBSCRIPTION_EXPIRED",
  "message": "Your subscription plan has expired. All inbound/outbound services and APIs are halted.",
  "expires_at": "2026-09-01T00:00:00.000Z",
  "rechargeEndpoint": "/api/billing/overview"
}

Incoming-message webhooks

The endpoints above are you → CRM. To be told when a customer writes to you (CRM → you), open Settings → Integrations and add a destination URL. Every incoming customer message is forwarded there as Meta's raw, unmodified WhatsApp Cloud API webhook JSON, so any existing "WhatsApp Cloud API" node, parser or library works unchanged.

Setting Details
URL Any http(s) URL you control (HTTPS strongly recommended).
Method POST (recommended — JSON body) or GET (payload sent as one ?payload=<url-encoded JSON> query parameter, for receivers that only accept GET).
Authentication None; Bearer (Authorization: Bearer <token>); or an API-key header (X-API-Key: <token> — header name is configurable). Plus any number of extra static headers. Check this header on your side.
Timeout 8 seconds. Reply 200 immediately and process asynchronously.
Retries None. A failed delivery is logged (status/error visible on the Integrations screen) but not re-sent. Be highly available, or reconcile using Message history.
What's sent Only events containing a customer message (value.messages[]). Delivery/read status updates are not forwarded — read a sent message's status from Message history.
Ordering / duplicates No ordering guarantee. De-duplicate on the message id (wamid).
Example payload your URL receives
{
  "object": "whatsapp_business_account",
  "entry": [{
    "id": "<WABA_ID>",
    "changes": [{
      "field": "messages",
      "value": {
        "messaging_product": "whatsapp",
        "metadata": { "display_phone_number": "918602505909", "phone_number_id": "<PHONE_NUMBER_ID>" },
        "contacts": [{ "profile": { "name": "Asha Verma" }, "wa_id": "919876543210" }],
        "messages": [{
          "from": "919876543210",
          "id": "wamid.HBgM...",
          "timestamp": "1789755000",
          "type": "text",
          "text": { "body": "Hi, is my order ready?" }
        }]
      }
    }]
  }]
}

Use the Test button on an integration to send a sample event to your URL while you build.

Integration recipes

Billing app → tag expiring customers

Pseudo-code — run nightly
for sub in subscriptions.expiring_within(days=5):
    POST /api/v1/customers       { phone_number: sub.phone, external_id: sub.id, full_name: sub.name }   # ensure they exist
    POST /api/v1/customers/tags  { external_id: sub.id, tags: ["2 D Exp"] }

for sub in subscriptions.renewed_today():
    POST /api/v1/customers/tags  { external_id: sub.id, remove_tags: ["2 D Exp"] }

Lead form → CRM, then a welcome template

Pseudo-code
POST /api/v1/customers { phone_number, full_name, email, field_data: [ ...all other form answers... ] }
POST /api/v1/messages/send-template { to: phone_number, templateName: "welcome", variables: [full_name] }
# A new customer has no open window, so free text would return 409 — always start with a template.

Send a message with automatic fallback

Pseudo-code
r = POST /api/v1/messages/send { to, message }
if r.status == 409 and r.body.error == "WINDOW_CLOSED":
    POST /api/v1/messages/send-template { to, templateName: "generic_update", variables: [message] }

Retry helper (Node.js)

Best practices

  • Keep keys server-side and out of source control. Use environment variables or a secrets manager.
  • One key per integration, named after it. Rotate by creating a new key, deploying, then revoking the old one.
  • Use HTTPS and set sensible client timeouts (60 s for bulk).
  • Retry only what is safe. Retry 429 and generic 5xx with exponential back-off + jitter. Customer and tag calls are idempotent; message sends are not — check before resending.
  • Always inspect bulk results[] — HTTP 200 doesn't mean every message went out.
  • Store your own external_id on every customer you create so you never depend on phone numbers alone.
  • Authenticate your webhook receiver with the Bearer/API-key header option, and respond fast.
  • Don't log full phone numbers or message bodies to shared systems without need.

Known limitations

  • No pagination or filtering on List customers and Message history.
  • Bulk sends run one after another inside a single HTTP request — there is no queue or job id.
  • Templates support body variables only.
  • A key has access to every endpoint; there are no per-endpoint scopes.
  • No endpoints yet for tickets, tasks, or assigning a customer to a staff member.
  • Outgoing webhooks have no automatic retries, no signature header, and don't include delivery-status events.

Changelog

Date Change
2026-09-26 Added external_id to Create / update customer; added Add / remove tags.
Earlier Send message, send template, bulk send, bulk template, list customers, message history.