API Reference

ZAPI WhatsApp Business API

The ZAPI Public API lets your backend send WhatsApp messages, manage contacts and templates, receive real-time events via webhooks, and assign conversations to agents — all with API keys and scoped permissions.

Server-side only. API keys must never be embedded in websites, mobile apps, or browser JavaScript. Create keys in Settings → API and call the API from your server.

Get started

Before you begin

  • A ZAPI workspace with WhatsApp connected Number.
  • At least one approved message template for outbound messages.
  • Basic HTTP/REST knowledge and a server that can make HTTPS requests.

Steps

  1. Sign up or log in to your workspace.
  2. Go to Settings → API → New key. Name the key and select scopes (start with messages:send and templates:read).
  3. Copy the key when shown — format zapi_live_<32 hex chars>. It is shown only once.
  4. Send a test request from your server using the examples below.

Your first request

curl
curl -X POST "https://server.zapi.co.in/api/v1/messages" \
  -H "Authorization: Bearer zapi_live_YOUR_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "to": "919876543210",
    "type": "template",
    "template": {
      "name": "hello_world",
      "language": "en",
      "components": []
    }
  }'

Success response:

json
{
  "status": "success",
  "data": {
    "message_id": "wamid.HBgM...",
    "recipient": "919876543210",
    "conversation_id": "550e8400-e29b-41d4-a716-446655440000",
    "internal_message_id": "660e8400-e29b-41d4-a716-446655440001"
  }
}

Base URL

All v1 endpoints are relative to:

text
https://server.zapi.co.in/api/v1

Health check (no authentication): https://server.zapi.co.in/api/v1/health

Example full path: https://server.zapi.co.in/api/v1/messages, https://server.zapi.co.in/api/v1/contacts

Authentication

Include your API key on every request (except /health):

http
Authorization: Bearer zapi_live_<your_key>

Each key has one or more scopes. If the key is valid but lacks permission for an endpoint, you receive 403 FORBIDDEN.

Keys can be limited to specific WhatsApp phone numbers when created in the dashboard. Restricted keys may only send messages, upload media, and manage templates for those numbers. Use connectionPhone on POST /messages with a number the key is allowed to use. If the key is tied to a single number, you may omit connectionPhone. Check allowed numbers via GET /account/me.

Validate a key at startup with GET /account/me — works with any valid key (no special scope required). Returns workspace info and granted scopes.

bash
curl "https://server.zapi.co.in/api/v1/account/me" \
  -H "Authorization: Bearer zapi_live_YOUR_KEY"
json
{
  "status": "success",
  "data": {
    "valid": true,
    "workspace_id": "8361cfa9-702d-42d5-b2dc-81a367c7faeb",
    "workspace_name": "My Business",
    "api_key": {
      "id": "438b3afe-7486-4d2f-8200-f235b481a503",
      "name": "Production integration",
      "prefix": "zapi_live_a22ee8",
      "scopes": ["messages:send", "templates:read", "templates:write"],
      "expires_at": null,
      "allowed_connection_ids": ["conn-uuid-1"],
      "allowed_connections": [
        {
          "id": "conn-uuid-1",
          "display_phone_number": "918089276988",
          "phone_number_id": "313981541791317",
          "waba_id": "123456789",
          "is_primary": true
        }
      ]
    }
  }
}

Scopes

ScopeAllows
messages:sendSend messages, bulk send
messages:readList messages, bulk job status
templates:readList and get templates
templates:writeCreate, delete, sync templates
contacts:readList and get contacts
contacts:writeCreate, update, opt-in/out contacts
media:readDownload media by ID
media:writeUpload media files
webhooks:manageCreate and manage outbound webhooks
account:readPlan usage and quotas
conversations:writeAssign operator to conversation

Message flow

WhatsApp Business API follows Meta's conversation rules:

  1. Opt-in — You must have permission to message a customer (especially for marketing).
  2. Template first — Business-initiated messages outside an active session must use an approved template.
  3. 24-hour session — When the customer replies, you can send free-form text, media, and interactive messages for 24 hours.
  4. Session refresh — Each inbound message from the customer opens/resets the 24-hour window.
  5. Re-engage — After the window closes, send another approved template to start a new conversation.

Phone number format

Use E.164 digits without + or 00 prefix:

  • India: 919876543210
  • UAE: 971501234567
  • 8–15 digits after normalization

Messages

Conversation (session) messages

Use POST /messages to send messages inside an open 24-hour session. Unlike template messages, session messages have no category restriction once the customer has replied or messaged you first.

  • Requires scope messages:send.
  • If the session has expired, you will get SESSION_EXPIRED — send an approved template first (see Templates).
  • Media files sent via public URL must be HTTPS and at most 100 MB.
  • Text supports WhatsApp formatting: *bold*, _italic_, ~strikethrough~, and emojis (up to 4096 characters).

POST /messages

Send one message to a single recipient.

ParameterTypeDescriptionRequired
tostringRecipient phone (E.164 digits, no +)Yes
typestringtext, image, video, audio, document, template, interactiveYes
connectionPhonestringSend from a specific connected WhatsApp numberNo

For template messages (business-initiated / outside session), see the Templates section.

POST Text message

ParameterTypeDescriptionRequired
typestringMust be textYes
text.bodystringMessage content (up to 4096 chars)Yes
text.preview_urlbooleanShow link preview for URLs in body (default false)No
bash
curl -X POST "https://server.zapi.co.in/api/v1/messages" \
  -H "Authorization: Bearer zapi_live_YOUR_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "to": "919876543210",
    "type": "text",
    "text": {
      "body": "Thanks for your order! It ships tomorrow.",
      "preview_url": false
    }
  }'

POST Image message

ParameterTypeDescriptionRequired
typestringMust be imageYes
image.linkstringPublic HTTPS URL of the image (JPEG, PNG, WebP)Yes
image.captionstringOptional caption under the imageNo
json
{
  "to": "919876543210",
  "type": "image",
  "image": {
    "link": "https://example.com/photo.jpg",
    "caption": "Your delivery photo"
  }
}

POST Video message

ParameterTypeDescriptionRequired
typestringMust be videoYes
video.linkstringPublic HTTPS URL (MP4 recommended)Yes
video.captionstringOptional captionNo
json
{
  "to": "919876543210",
  "type": "video",
  "video": {
    "link": "https://example.com/demo.mp4",
    "caption": "Product walkthrough"
  }
}

POST Document message

ParameterTypeDescriptionRequired
typestringMust be documentYes
document.linkstringPublic HTTPS URL of the fileYes
document.filenamestringDisplay filename (e.g. invoice.pdf)No
document.captionstringOptional captionNo
json
{
  "to": "919876543210",
  "type": "document",
  "document": {
    "link": "https://example.com/invoice.pdf",
    "filename": "invoice-jan-2026.pdf",
    "caption": "Your invoice"
  }
}

POST Audio message

ParameterTypeDescriptionRequired
typestringMust be audioYes
audio.linkstringPublic HTTPS URL (MP3, OGG, AAC)Yes
json
{
  "to": "919876543210",
  "type": "audio",
  "audio": {
    "link": "https://example.com/voice-note.mp3"
  }
}

POST Interactive buttons (quick replies)

Up to 3 reply buttons. Customer taps a button to respond.

json
{
  "to": "919876543210",
  "type": "interactive",
  "interactive": {
    "type": "button",
    "body": { "text": "How can we help you today?" },
    "footer": { "text": "Choose an option" },
    "action": {
      "buttons": [
        { "type": "reply", "reply": { "id": "sales", "title": "Sales" } },
        { "type": "reply", "reply": { "id": "support", "title": "Support" } },
        { "type": "reply", "reply": { "id": "billing", "title": "Billing" } }
      ]
    }
  }
}

POST Interactive list message

Show a menu of options grouped in sections (e.g. delivery addresses, product catalog).

json
{
  "to": "919876543210",
  "type": "interactive",
  "interactive": {
    "type": "list",
    "header": { "type": "text", "text": "Delivery address" },
    "body": { "text": "Thanks for your order! Select your delivery address." },
    "footer": { "text": "Please choose an option" },
    "action": {
      "button": "Select",
      "sections": [
        {
          "title": "Saved addresses",
          "rows": [
            { "id": "home", "title": "Home", "description": "123 Main St" },
            { "id": "office", "title": "Office", "description": "456 Business Park" }
          ]
        }
      ]
    }
  }
}

POST Interactive CTA URL

A message with a single button that opens an external URL.

json
{
  "to": "919876543210",
  "type": "interactive",
  "interactive": {
    "type": "cta_url",
    "header": { "type": "text", "text": "ZAPI" },
    "body": { "text": "Learn more about our WhatsApp API." },
    "footer": { "text": "Tap the button below" },
    "action": {
      "name": "cta_url",
      "parameters": {
        "display_text": "Visit website",
        "url": "https://zapi.co.in"
      }
    }
  }
}

POST Request location

Ask the customer to share their current location.

json
{
  "to": "919876543210",
  "type": "interactive",
  "interactive": {
    "type": "location_request_message",
    "body": { "text": "Please share your delivery location." },
    "action": { "name": "send_location" }
  }
}

Response, idempotency & errors

Success response

json
{
  "status": "success",
  "data": {
    "message_id": "wamid.HBgM...",
    "recipient": "919876543210",
    "conversation_id": "550e8400-e29b-41d4-a716-446655440000",
    "internal_message_id": "660e8400-e29b-41d4-a716-446655440001"
  }
}

Idempotency (optional header)

Send Idempotency-Key: unique-value to safely retry. The same key within 24 hours returns the cached response.

bash
curl -X POST "https://server.zapi.co.in/api/v1/messages" \
  -H "Authorization: Bearer zapi_live_YOUR_KEY" \
  -H "Content-Type: application/json" \
  -H "Idempotency-Key: order-12345-send-1" \
  -d '{"to":"919876543210","type":"text","text":{"body":"Hello"}}'

Common errors

ParameterTypeDescriptionRequired
SESSION_EXPIRED40024-hour window closed — send a template to re-openNo
VALIDATION_ERROR400Missing to, type, or required body/media fieldsNo
UNAUTHORIZED401Invalid or missing API keyNo
FORBIDDEN403Key lacks messages:send scopeNo

GET /messages

List message history for a contact or conversation. Scope: messages:read.

ParameterTypeDescriptionRequired
phonestringContact phone numberNo
conversation_iduuidConversation ID (alternative to phone)No
limitnumberMax results (default 50)No
beforestringMessage ID for paginationNo
bash
curl "https://server.zapi.co.in/api/v1/messages?phone=919876543210&limit=50" \
  -H "Authorization: Bearer zapi_live_YOUR_KEY"

POST /messages/bulk

Send a template to many recipients asynchronously. Returns 202 Accepted with a job_id. Only opted-in contacts receive marketing templates.

json
{
  "template": { "name": "promo_offer", "language": "en" },
  "recipients": [
    { "phone": "919876543210", "variables": ["John"] },
    { "phone": "919123456789", "variables": ["Jane"] }
  ],
  "marketing_opt_in": true
}

Poll status: GET https://server.zapi.co.in/api/v1/messages/bulk/JOB_ID

CSV upload: POST https://server.zapi.co.in/api/v1/messages/bulk/csv (multipart, field file)

Templates

Template messages overview

WhatsApp has four message categories for business-initiated conversations: Marketing, Utility, Authentication, and Service (user-initiated). Pricing and review rules depend on category and destination country.

  • You must start (or re-open) a conversation with an approved template message.
  • After the customer replies, a 24-hour session window opens — you can then send free-form text, media, and interactive messages via POST /messages.
  • When the session expires, send another approved template to re-engage.
  • Marketing templates require the contact to be opted in to marketing messages.

Template categories

ParameterTypeDescriptionRequired
MARKETINGenumPromotions, offers, re-engagement campaignsNo
UTILITYenumTransactional updates: orders, appointments, account alertsNo
AUTHENTICATIONenumOne-time passwords and verification codes (Copy Code button)No

Manage templates with templates:read / templates:write. Send approved templates with messages:send via POST /messages.

GET /templates

List templates in your workspace. Filter by status, category, or WABA.

ParameterTypeDescriptionRequired
statusstringPENDING, APPROVED, or REJECTEDNo
categorystringMARKETING, UTILITY, or AUTHENTICATIONNo
wabaIdstringFilter by WhatsApp Business Account IDNo
limitnumberMax results (default 50)No
bash
curl "https://server.zapi.co.in/api/v1/templates?status=APPROVED&limit=50" \
  -H "Authorization: Bearer zapi_live_YOUR_KEY"
json
{
  "status": "success",
  "data": {
    "count": 1,
    "templates": [
      {
        "id": "07fdd6c7-799b-47ba-a7e6-b71500bd2697",
        "name": "zapi_appt_confirmed",
        "category": "UTILITY",
        "language": "en",
        "status": "APPROVED",
        "whatsappTemplateId": "1072819278764966",
        "content": "Your appointment on {{1}} is confirmed.",
        "components": [],
        "wabaId": "123456789",
        "createdAt": "2026-01-01T00:00:00.000Z",
        "updatedAt": "2026-01-02T00:00:00.000Z"
      }
    ]
  }
}

GET /templates/:idOrName

Get a single template by internal UUID, template name, or Meta whatsappTemplateId.

bash
curl "https://server.zapi.co.in/api/v1/templates/zapi_appt_confirmed" \
  -H "Authorization: Bearer zapi_live_YOUR_KEY"

POST /templates

Submit a new template to Meta for review. Subject to plan template limits.

ParameterTypeDescriptionRequired
namestringUnique lowercase name with underscores (e.g. zapi_appt_confirmed)Yes
categorystringMARKETING, UTILITY, or AUTHENTICATIONYes
languagestringLanguage code (default en)No
componentsarrayMeta-format BODY, HEADER, FOOTER, BUTTONS componentsYes
wabaIdstringTarget WABA when workspace has multiple connectionsNo
header_media_urlstringPublic URL for IMAGE/VIDEO/DOCUMENT header on createNo

Utility template (body variable)

json
{
  "name": "zapi_appt_confirmed",
  "category": "UTILITY",
  "language": "en",
  "components": [
    {
      "type": "BODY",
      "text": "Your appointment on {{1}} is confirmed.",
      "example": {
        "body_text": [
          ["Monday 12 January at 3:00 PM"]
        ]
      }
    }
  ]
}

Template with header, footer, and buttons

json
{
  "name": "zapi_order_receipt",
  "category": "UTILITY",
  "language": "en",
  "components": [
    {
      "type": "HEADER",
      "format": "TEXT",
      "text": "Order receipt"
    },
    {
      "type": "BODY",
      "text": "Hi {{1}}, your order {{2}} has been confirmed.",
      "example": {
        "body_text": [
          ["John", "ORD-9281"]
        ]
      }
    },
    {
      "type": "FOOTER",
      "text": "Thank you for shopping with us."
    },
    {
      "type": "BUTTONS",
      "buttons": [
        {
          "type": "URL",
          "text": "Track order",
          "url": "https://example.com/track/{{1}}",
          "example": ["ORD-9281"]
        }
      ]
    }
  ]
}

Use a new unique name on every submit — Meta locks each name to its original category. Body variables require example.body_text. Media headers on create require header_media_url.

POST /templates/:idOrName/sync

Pull the latest approval status and components from Meta. Also supports GET /templates/:idOrName/sync. Accepts UUID, template name, or Meta template ID.

bash
curl -X POST "https://server.zapi.co.in/api/v1/templates/zapi_appt_confirmed/sync" \
  -H "Authorization: Bearer zapi_live_YOUR_KEY"
json
{
  "status": "success",
  "data": {
    "template": { "name": "zapi_appt_confirmed", "status": "APPROVED" },
    "metaStatus": "APPROVED",
    "message": "Template status synced from Meta"
  }
}

DELETE /templates/:idOrName

Delete from ZAPI and Meta (when connected).

bash
curl -X DELETE "https://server.zapi.co.in/api/v1/templates/zapi_appt_confirmed" \
  -H "Authorization: Bearer zapi_live_YOUR_KEY"

Send template messages

Approved templates are sent via POST /messages (scope messages:send), not the /templates endpoints.

Multiple WhatsApp numbers: Templates are owned by the WhatsApp Business Account (wabaId) — use wabaId on GET/POST /templates to manage templates for a specific WABA. To choose which phone number sends the template, pass connectionPhone on POST /messages (display number or Meta phoneNumberId). If omitted, ZAPI uses your primary connection, then the first connected number. When two numbers share the same WABA, they share the same template catalog — only connectionPhone distinguishes the sender.

POST Send static text template

For approved templates with no variables.

ParameterTypeDescriptionRequired
tostringRecipient phone (E.164 digits, no +)Yes
typestringMust be templateYes
connectionPhonestringSend from a specific connected number (display phone or phoneNumberId). Omit for primary connection.No
template.namestringApproved template name in your accountYes
template.languagestringLanguage code (e.g. en, en_US)Yes
bash
curl -X POST "https://server.zapi.co.in/api/v1/messages" \
  -H "Authorization: Bearer zapi_live_YOUR_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "to": "919876543210",
    "type": "template",
    "connectionPhone": "919111111111",
    "template": {
      "name": "hello_world",
      "language": "en"
    }
  }'
json
{
  "status": "success",
  "data": {
    "message_id": "wamid.HBgM...",
    "recipient": "919876543210",
    "conversation_id": "550e8400-e29b-41d4-a716-446655440000",
    "internal_message_id": "660e8400-e29b-41d4-a716-446655440001"
  }
}

POST Send dynamic template (body variables)

Replace {{1}}, {{2}}, etc. with runtime values in template.components. Optional connectionPhone — see Send template messages.

json
{
  "to": "919876543210",
  "type": "template",
  "template": {
    "name": "zapi_appt_confirmed",
    "language": "en",
    "components": [
      {
        "type": "body",
        "parameters": [
          { "type": "text", "text": "Monday 12 January at 3:00 PM" }
        ]
      }
    ]
  }
}

POST Send template with image / video / document header

For templates with a media header. Provide a public HTTPS URL in the header parameter. Max media size via URL: 100 MB.

Image header

json
{
  "to": "919876543210",
  "type": "template",
  "template": {
    "name": "promo_with_image",
    "language": "en",
    "components": [
      {
        "type": "header",
        "parameters": [
          {
            "type": "image",
            "image": { "link": "https://example.com/banner.png" }
          }
        ]
      },
      {
        "type": "body",
        "parameters": [
          { "type": "text", "text": "John" }
        ]
      }
    ]
  }
}

Video header

json
{
  "to": "919876543210",
  "type": "template",
  "template": {
    "name": "product_demo",
    "language": "en",
    "components": [
      {
        "type": "header",
        "parameters": [
          {
            "type": "video",
            "video": { "link": "https://example.com/demo.mp4" }
          }
        ]
      }
    ]
  }
}

Document header

json
{
  "to": "919876543210",
  "type": "template",
  "template": {
    "name": "invoice_delivery",
    "language": "en",
    "components": [
      {
        "type": "header",
        "parameters": [
          {
            "type": "document",
            "document": {
              "link": "https://example.com/invoice.pdf",
              "filename": "invoice-jan-2026.pdf"
            }
          }
        ]
      }
    ]
  }
}

POST Send authentication (OTP) template

For AUTHENTICATION category templates with a Copy Code button. Provide the OTP in both the body and the URL button parameter (index 0).

json
{
  "to": "919876543210",
  "type": "template",
  "template": {
    "name": "test_authen",
    "language": "en",
    "components": [
      {
        "type": "body",
        "parameters": [
          { "type": "text", "text": "123456" }
        ]
      },
      {
        "type": "button",
        "sub_type": "url",
        "index": "0",
        "parameters": [
          { "type": "text", "text": "123456" }
        ]
      }
    ]
  }
}

Common template errors

ParameterTypeDescriptionRequired
TEMPLATE_NOT_FOUND404Template name/ID not found in workspace or not approvedNo
VALIDATION_ERROR400Missing required fields, category mismatch, or Meta rejected the payloadNo
132001metaTemplate name does not exist for the given language codeNo
132000metaNumber of parameters does not match the templateNo
131008metaRequired parameter missing (e.g. OTP button URL param)No
SESSION_EXPIRED400Outside 24h window — send a template to re-open the conversationNo

Meta approval webhook: configure template.status in Settings → Webhooks to receive approve/reject events automatically.

Contacts

Scopes: contacts:read, contacts:write

GET /contacts

bash
curl "https://server.zapi.co.in/api/v1/contacts?page=1&limit=50&search=john" \
  -H "Authorization: Bearer zapi_live_YOUR_KEY"

POST /contacts

json
{
  "name": "Jane Doe",
  "phone": "919876543210",
  "source": "api",
  "marketingOptIn": true,
  "customFields": {
    "company": "Acme Corp",
    "customer_id": "CRM-9981"
  }
}

Opt-in: POST /contacts/:id/opt-in · Opt-out: POST /contacts/:id/opt-out with body {"reason":"user_request"}

POST /conversations/assign

Assign a workspace user to handle a conversation. Scope: conversations:write or contacts:write. The conversation must already exist.

json
{
  "phone": "919876543210",
  "user_id": "WORKSPACE_USER_UUID"
}

Or use conversation_id. Set user_id: null to unassign.

Media

Scopes: media:write (upload), media:read (download)

POST /media

bash
curl -X POST "https://server.zapi.co.in/api/v1/media" \
  -H "Authorization: Bearer zapi_live_YOUR_KEY" \
  -F "file=@/path/to/image.jpg"

Response includes media_id — use in send requests.

GET /media/:mediaId

bash
curl "https://server.zapi.co.in/api/v1/media/MEDIA_ID" \
  -H "Authorization: Bearer zapi_live_YOUR_KEY" \
  --output downloaded.jpg

Webhooks

Scope: webhooks:manage. ZAPI POSTs JSON to your HTTPS endpoint when events occur. Configure in the dashboard (Settings → Webhooks) or via API.

Create webhook

json
{
  "url": "https://your-app.com/zapi/webhook",
  "events": ["message.inbound", "message.status"],
  "secret": "your-signing-secret-min-16-chars"
}

Events

  • message.inbound — Customer sent a WhatsApp message
  • message.status — Delivered, read, or failed
  • template.status — Template approved or rejected on Meta
  • contact.created — Contact created via public API

Verify signatures

Each delivery includes header:

http
X-Zapi-Signature: sha256=<hex>

Compute HMAC-SHA256 of the raw request body using your webhook secret and compare with the header value (after sha256=).

javascript
const crypto = require('crypto');

function verifyWebhook(rawBody, signatureHeader, secret) {
  const expected = 'sha256=' + crypto
    .createHmac('sha256', secret)
    .update(rawBody, 'utf8')
    .digest('hex');
  return crypto.timingSafeEqual(
    Buffer.from(expected),
    Buffer.from(signatureHeader)
  );
}

Account

GET /account/me

Validate an API key and return workspace identity plus granted scopes. No special scope required — any valid key can call this endpoint.

bash
curl "https://server.zapi.co.in/api/v1/account/me" \
  -H "Authorization: Bearer zapi_live_YOUR_KEY"

Returns 401 UNAUTHORIZED for missing, invalid, or expired keys.

GET /account/usage

Scope: account:read

bash
curl "https://server.zapi.co.in/api/v1/account/usage" \
  -H "Authorization: Bearer zapi_live_YOUR_KEY"

Returns plan name, contact/template/broadcast limits, current usage counts, and WhatsApp connection status.

Error codes

All errors use this shape:

json
{
  "status": "error",
  "error": {
    "code": "VALIDATION_ERROR",
    "message": "Human-readable explanation"
  }
}
CodeHTTPMeaning
UNAUTHORIZED401Missing or invalid API key
FORBIDDEN403Key valid but missing required scope
PLAN_LIMIT_EXCEEDED402Plan quota exceeded — upgrade or reduce usage
VALIDATION_ERROR400Invalid request body or parameters
NOT_FOUND404Resource not found
INVALID_RECIPIENT404Phone invalid or contact could not be resolved
TEMPLATE_NOT_FOUND400Template missing or not approved
SESSION_EXPIRED400Outside 24h window — send a template
WHATSAPP_NOT_CONFIGURED400No WhatsApp number connected
RATE_LIMIT_EXCEEDED429Too many requests — check Retry-After header

Rate limits

Default: 120 requests per minute per API key. When exceeded, the API returns 429 RATE_LIMIT_EXCEEDED with a Retry-After header (seconds).

Plan limits (contacts, templates, broadcasts, media storage) return 402 PLAN_LIMIT_EXCEEDED.

Ready to integrate?

Create an API key in your dashboard and start with a template message.