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.
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
- Sign up or log in to your workspace.
- Go to Settings → API → New key. Name the key and select scopes (start with
messages:sendandtemplates:read). - Copy the key when shown — format
zapi_live_<32 hex chars>. It is shown only once. - Send a test request from your server using the examples below.
Your first request
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:
{
"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:
https://server.zapi.co.in/api/v1Health 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):
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.
curl "https://server.zapi.co.in/api/v1/account/me" \
-H "Authorization: Bearer zapi_live_YOUR_KEY"{
"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
| Scope | Allows |
|---|---|
| messages:send | Send messages, bulk send |
| messages:read | List messages, bulk job status |
| templates:read | List and get templates |
| templates:write | Create, delete, sync templates |
| contacts:read | List and get contacts |
| contacts:write | Create, update, opt-in/out contacts |
| media:read | Download media by ID |
| media:write | Upload media files |
| webhooks:manage | Create and manage outbound webhooks |
| account:read | Plan usage and quotas |
| conversations:write | Assign operator to conversation |
Message flow
WhatsApp Business API follows Meta's conversation rules:
- Opt-in — You must have permission to message a customer (especially for marketing).
- Template first — Business-initiated messages outside an active session must use an approved template.
- 24-hour session — When the customer replies, you can send free-form text, media, and interactive messages for 24 hours.
- Session refresh — Each inbound message from the customer opens/resets the 24-hour window.
- 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.
| Parameter | Type | Description | Required |
|---|---|---|---|
| to | string | Recipient phone (E.164 digits, no +) | Yes |
| type | string | text, image, video, audio, document, template, interactive | Yes |
| connectionPhone | string | Send from a specific connected WhatsApp number | No |
For template messages (business-initiated / outside session), see the Templates section.
POST Text message
| Parameter | Type | Description | Required |
|---|---|---|---|
| type | string | Must be text | Yes |
| text.body | string | Message content (up to 4096 chars) | Yes |
| text.preview_url | boolean | Show link preview for URLs in body (default false) | No |
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
| Parameter | Type | Description | Required |
|---|---|---|---|
| type | string | Must be image | Yes |
| image.link | string | Public HTTPS URL of the image (JPEG, PNG, WebP) | Yes |
| image.caption | string | Optional caption under the image | No |
{
"to": "919876543210",
"type": "image",
"image": {
"link": "https://example.com/photo.jpg",
"caption": "Your delivery photo"
}
}POST Video message
| Parameter | Type | Description | Required |
|---|---|---|---|
| type | string | Must be video | Yes |
| video.link | string | Public HTTPS URL (MP4 recommended) | Yes |
| video.caption | string | Optional caption | No |
{
"to": "919876543210",
"type": "video",
"video": {
"link": "https://example.com/demo.mp4",
"caption": "Product walkthrough"
}
}POST Document message
| Parameter | Type | Description | Required |
|---|---|---|---|
| type | string | Must be document | Yes |
| document.link | string | Public HTTPS URL of the file | Yes |
| document.filename | string | Display filename (e.g. invoice.pdf) | No |
| document.caption | string | Optional caption | No |
{
"to": "919876543210",
"type": "document",
"document": {
"link": "https://example.com/invoice.pdf",
"filename": "invoice-jan-2026.pdf",
"caption": "Your invoice"
}
}POST Audio message
| Parameter | Type | Description | Required |
|---|---|---|---|
| type | string | Must be audio | Yes |
| audio.link | string | Public HTTPS URL (MP3, OGG, AAC) | Yes |
{
"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.
{
"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).
{
"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.
{
"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.
{
"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
{
"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.
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
| Parameter | Type | Description | Required |
|---|---|---|---|
| SESSION_EXPIRED | 400 | 24-hour window closed — send a template to re-open | No |
| VALIDATION_ERROR | 400 | Missing to, type, or required body/media fields | No |
| UNAUTHORIZED | 401 | Invalid or missing API key | No |
| FORBIDDEN | 403 | Key lacks messages:send scope | No |
GET /messages
List message history for a contact or conversation. Scope: messages:read.
| Parameter | Type | Description | Required |
|---|---|---|---|
| phone | string | Contact phone number | No |
| conversation_id | uuid | Conversation ID (alternative to phone) | No |
| limit | number | Max results (default 50) | No |
| before | string | Message ID for pagination | No |
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.
{
"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
| Parameter | Type | Description | Required |
|---|---|---|---|
| MARKETING | enum | Promotions, offers, re-engagement campaigns | No |
| UTILITY | enum | Transactional updates: orders, appointments, account alerts | No |
| AUTHENTICATION | enum | One-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.
| Parameter | Type | Description | Required |
|---|---|---|---|
| status | string | PENDING, APPROVED, or REJECTED | No |
| category | string | MARKETING, UTILITY, or AUTHENTICATION | No |
| wabaId | string | Filter by WhatsApp Business Account ID | No |
| limit | number | Max results (default 50) | No |
curl "https://server.zapi.co.in/api/v1/templates?status=APPROVED&limit=50" \
-H "Authorization: Bearer zapi_live_YOUR_KEY"{
"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.
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.
| Parameter | Type | Description | Required |
|---|---|---|---|
| name | string | Unique lowercase name with underscores (e.g. zapi_appt_confirmed) | Yes |
| category | string | MARKETING, UTILITY, or AUTHENTICATION | Yes |
| language | string | Language code (default en) | No |
| components | array | Meta-format BODY, HEADER, FOOTER, BUTTONS components | Yes |
| wabaId | string | Target WABA when workspace has multiple connections | No |
| header_media_url | string | Public URL for IMAGE/VIDEO/DOCUMENT header on create | No |
Utility template (body variable)
{
"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
{
"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.
curl -X POST "https://server.zapi.co.in/api/v1/templates/zapi_appt_confirmed/sync" \
-H "Authorization: Bearer zapi_live_YOUR_KEY"{
"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).
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.
| Parameter | Type | Description | Required |
|---|---|---|---|
| to | string | Recipient phone (E.164 digits, no +) | Yes |
| type | string | Must be template | Yes |
| connectionPhone | string | Send from a specific connected number (display phone or phoneNumberId). Omit for primary connection. | No |
| template.name | string | Approved template name in your account | Yes |
| template.language | string | Language code (e.g. en, en_US) | Yes |
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"
}
}'{
"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.
{
"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
{
"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
{
"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
{
"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).
{
"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
| Parameter | Type | Description | Required |
|---|---|---|---|
| TEMPLATE_NOT_FOUND | 404 | Template name/ID not found in workspace or not approved | No |
| VALIDATION_ERROR | 400 | Missing required fields, category mismatch, or Meta rejected the payload | No |
| 132001 | meta | Template name does not exist for the given language code | No |
| 132000 | meta | Number of parameters does not match the template | No |
| 131008 | meta | Required parameter missing (e.g. OTP button URL param) | No |
| SESSION_EXPIRED | 400 | Outside 24h window — send a template to re-open the conversation | No |
Meta approval webhook: configure template.status in Settings → Webhooks to receive approve/reject events automatically.
Contacts
Scopes: contacts:read, contacts:write
GET /contacts
curl "https://server.zapi.co.in/api/v1/contacts?page=1&limit=50&search=john" \
-H "Authorization: Bearer zapi_live_YOUR_KEY"POST /contacts
{
"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.
{
"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
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
curl "https://server.zapi.co.in/api/v1/media/MEDIA_ID" \
-H "Authorization: Bearer zapi_live_YOUR_KEY" \
--output downloaded.jpgWebhooks
Scope: webhooks:manage. ZAPI POSTs JSON to your HTTPS endpoint when events occur. Configure in the dashboard (Settings → Webhooks) or via API.
Create webhook
{
"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 messagemessage.status— Delivered, read, or failedtemplate.status— Template approved or rejected on Metacontact.created— Contact created via public API
Verify signatures
Each delivery includes header:
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=).
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.
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
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:
{
"status": "error",
"error": {
"code": "VALIDATION_ERROR",
"message": "Human-readable explanation"
}
}| Code | HTTP | Meaning |
|---|---|---|
| UNAUTHORIZED | 401 | Missing or invalid API key |
| FORBIDDEN | 403 | Key valid but missing required scope |
| PLAN_LIMIT_EXCEEDED | 402 | Plan quota exceeded — upgrade or reduce usage |
| VALIDATION_ERROR | 400 | Invalid request body or parameters |
| NOT_FOUND | 404 | Resource not found |
| INVALID_RECIPIENT | 404 | Phone invalid or contact could not be resolved |
| TEMPLATE_NOT_FOUND | 400 | Template missing or not approved |
| SESSION_EXPIRED | 400 | Outside 24h window — send a template |
| WHATSAPP_NOT_CONFIGURED | 400 | No WhatsApp number connected |
| RATE_LIMIT_EXCEEDED | 429 | Too 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.