API
Documentation
Complete reference for integrating with the WAPit WhatsApp messaging gateway. Send text, media, audio, location, templates, and interactive messages. Manage templates, upload media, and control your business profile via a simple REST API.

Contents
Base URL
All API requests are made to:
https://wowresult-174418289148.europe-west1.run.appFor local development: http://localhost:8080
Authentication
All API endpoints (except /health and /auth/login) require a valid JWT token.
/auth/loginGet Authentication Token
Exchange your API credentials for a JWT token. Tokens are valid for 24 hours.
Request
POST /auth/login
Content-Type: application/json
{
"username": "carleads_api",
"password": "your_secure_password"
}Response (200 OK)
{
"success": true,
"token": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...",
"expires_in": 86400,
"expires_at": "2026-07-04T14:30:00Z"
}Using the Token
Include the token in all subsequent requests:
Authorization: Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...Common Request Fields
All send endpoints share these fields. The content object varies per message type.
| Field | Type | Required | Description |
|---|---|---|---|
| message_id | string | Yes | Your unique batch/request tracking ID |
| client_id | string | Yes | Your tenant/client identifier |
| campaign_id | string | Yes | Campaign slug (must match a campaign in the system) |
| wa_number_id | string | Yes | Which WA number to send from (e.g. "1" or "2") |
| callback_url | string | Yes | URL where delivery statuses and replies will be POSTed |
| recipients | array | Yes | Array of recipient objects (see below) |
| content | object | Yes | Message content (varies per endpoint) |
Recipient Object
| Field | Type | Required | Description |
|---|---|---|---|
| recipient_id | string | Yes | Your unique ID for this recipient |
| phone | string | Yes | Phone number (SA format auto-detected: 082... → 2782...) |
| name | string | No | Recipient name (replaces {{name}} in message body) |
Phone normalization: South African numbers are automatically converted (e.g. 0825551234 → 27825551234). International numbers should include the country code without the + prefix.
Personalization: Use {{name}}in message body/caption fields. It will be replaced with each recipient's name field.
Contact lists: Fetch contacts from GET /api/v1/contacts/lists/{id}/contacts and map each row to a recipient object. You can combine list contacts with manually entered recipients in the same recipients array — deduplicate by phone before sending.
Send Text Message
/api/v1/send/textSingle Recipient
POST /api/v1/send/text
Authorization: Bearer <token>
Content-Type: application/json
{
"message_id": "order_confirm_001",
"client_id": "carleads",
"campaign_id": "carleads_main",
"wa_number_id": "1",
"callback_url": "https://your-app.com/webhooks/wa",
"recipients": [
{
"recipient_id": "lead_5001",
"phone": "0825551234",
"name": "John"
}
],
"content": {
"body": "Hi {{name}}, your order has been confirmed! We'll notify you when it ships."
}
}Response (200 OK)
{
"success": true,
"message_id": "order_confirm_001",
"timestamp": "2026-07-03T14:30:00Z",
"results": [
{
"recipient_id": "lead_5001",
"phone": "27825551234",
"wa_message_id": "wamid.HBgNMjc4MjU1NTEyMzQVAgA...",
"status": "sent"
}
]
}/api/v1/send/textMultiple Recipients (Batch)
Send the same message to multiple recipients in one API call. Each recipient gets a personalized message and an individual status in the response.
POST /api/v1/send/text
Authorization: Bearer <token>
Content-Type: application/json
{
"message_id": "promo_batch_042",
"client_id": "carleads",
"campaign_id": "carleads_main",
"wa_number_id": "1",
"callback_url": "https://your-app.com/webhooks/wa",
"recipients": [
{ "recipient_id": "lead_101", "phone": "0821111111", "name": "Alice" },
{ "recipient_id": "lead_102", "phone": "0822222222", "name": "Bob" },
{ "recipient_id": "lead_103", "phone": "0823333333", "name": "Carol" }
],
"content": {
"body": "Hey {{name}}! Don't miss our special offer this weekend. Reply YES to learn more."
}
}Response (200 OK)
{
"success": true,
"message_id": "promo_batch_042",
"timestamp": "2026-07-03T14:30:00Z",
"results": [
{
"recipient_id": "lead_101",
"phone": "27821111111",
"wa_message_id": "wamid.HBgNMjc4MjExMTExMTEVAgA...",
"status": "sent"
},
{
"recipient_id": "lead_102",
"phone": "27822222222",
"wa_message_id": "wamid.HBgNMjc4MjIyMjIyMjIVAgA...",
"status": "sent"
},
{
"recipient_id": "lead_103",
"phone": "27823333333",
"status": "queued",
"error": "Daily conversation limit reached for tier. Scheduled for 2026-07-04"
}
]
}Note: Each recipient is processed individually. Some may succeed while others are queued or fail. A 100ms delay is applied between recipients.
Send Image
/api/v1/send/imageSend an Image Message
| Field | Type | Required | Description |
|---|---|---|---|
| content.image_url | string | Yes | Public URL of the image (JPEG, PNG) |
| content.caption | string | No | Optional caption (supports {{name}}) |
POST /api/v1/send/image
Authorization: Bearer <token>
Content-Type: application/json
{
"message_id": "img_promo_007",
"client_id": "carleads",
"campaign_id": "carleads_main",
"wa_number_id": "1",
"callback_url": "https://your-app.com/webhooks/wa",
"recipients": [
{ "recipient_id": "lead_201", "phone": "0825551234", "name": "Sarah" }
],
"content": {
"image_url": "https://storage.googleapis.com/your-bucket/promo-banner.jpg",
"caption": "Hi {{name}}, check out our latest deals!"
}
}Response (200 OK)
{
"success": true,
"message_id": "img_promo_007",
"timestamp": "2026-07-03T14:35:00Z",
"results": [
{
"recipient_id": "lead_201",
"phone": "27825551234",
"wa_message_id": "wamid.HBgNMjc4MjU1NTEyMzQVAgASGBI...",
"status": "sent"
}
]
}Send Video
/api/v1/send/videoSend a Video Message
| Field | Type | Required | Description |
|---|---|---|---|
| content.video_url | string | Yes | Public URL of the video (MP4, max 16MB) |
| content.caption | string | No | Optional caption (supports {{name}}) |
POST /api/v1/send/video
Authorization: Bearer <token>
Content-Type: application/json
{
"message_id": "video_demo_003",
"client_id": "carleads",
"campaign_id": "carleads_main",
"wa_number_id": "1",
"callback_url": "https://your-app.com/webhooks/wa",
"recipients": [
{ "recipient_id": "lead_301", "phone": "0825551234", "name": "Mike" }
],
"content": {
"video_url": "https://storage.googleapis.com/your-bucket/demo-video.mp4",
"caption": "{{name}}, here's a quick walkthrough of the new features"
}
}Response (200 OK)
{
"success": true,
"message_id": "video_demo_003",
"timestamp": "2026-07-03T14:38:00Z",
"results": [
{
"recipient_id": "lead_301",
"phone": "27825551234",
"wa_message_id": "wamid.HBgNMjc4MjU1NTEyMzQVAgASGBI...",
"status": "sent"
}
]
}Send Document
/api/v1/send/documentSend a Document
| Field | Type | Required | Description |
|---|---|---|---|
| content.document_url | string | Yes | Public URL of the document (PDF, DOCX, etc.) |
| content.filename | string | No | Display filename for the recipient |
| content.caption | string | No | Optional caption |
POST /api/v1/send/document
Authorization: Bearer <token>
Content-Type: application/json
{
"message_id": "invoice_send_099",
"client_id": "carleads",
"campaign_id": "carleads_main",
"wa_number_id": "1",
"callback_url": "https://your-app.com/webhooks/wa",
"recipients": [
{ "recipient_id": "customer_401", "phone": "0825551234", "name": "Lisa" }
],
"content": {
"document_url": "https://storage.googleapis.com/your-bucket/invoice-2026-099.pdf",
"filename": "Invoice-2026-099.pdf",
"caption": "Hi {{name}}, please find your invoice attached."
}
}Response (200 OK)
{
"success": true,
"message_id": "invoice_send_099",
"timestamp": "2026-07-03T14:42:00Z",
"results": [
{
"recipient_id": "customer_401",
"phone": "27825551234",
"wa_message_id": "wamid.HBgNMjc4MjU1NTEyMzQVAgASGBI...",
"status": "sent"
}
]
}Send Audio
/api/v1/send/audioSend an Audio/Voice Message
| Field | Type | Required | Description |
|---|---|---|---|
| content.audio_url | string | Yes | Public URL of the audio file (MP3, OGG, AMR) |
POST /api/v1/send/audio
Authorization: Bearer <token>
Content-Type: application/json
{
"message_id": "audio_msg_001",
"client_id": "carleads",
"campaign_id": "carleads_main",
"wa_number_id": "1",
"callback_url": "https://your-app.com/webhooks/wa",
"recipients": [
{ "recipient_id": "lead_801", "phone": "0825551234", "name": "James" }
],
"content": {
"audio_url": "https://storage.googleapis.com/your-bucket/greeting.mp3"
}
}Response (200 OK)
{
"success": true,
"message_id": "audio_msg_001",
"timestamp": "2026-07-03T15:00:00Z",
"results": [
{
"recipient_id": "lead_801",
"phone": "27825551234",
"wa_message_id": "wamid.HBgNMjc4MjU1NTEyMzQVAgA...",
"status": "sent"
}
]
}Send Location
/api/v1/send/locationSend a Location Pin
| Field | Type | Required | Description |
|---|---|---|---|
| content.latitude | number | Yes | Latitude coordinate |
| content.longitude | number | Yes | Longitude coordinate |
| content.name | string | No | Location name (displayed as title) |
| content.address | string | No | Full address text |
POST /api/v1/send/location
Authorization: Bearer <token>
Content-Type: application/json
{
"message_id": "location_msg_001",
"client_id": "carleads",
"campaign_id": "carleads_main",
"wa_number_id": "1",
"callback_url": "https://your-app.com/webhooks/wa",
"recipients": [
{ "recipient_id": "cust_901", "phone": "0825551234", "name": "Anna" }
],
"content": {
"latitude": -33.9249,
"longitude": 18.4241,
"name": "Cape Town Showroom",
"address": "123 Main Road, Cape Town, 8001"
}
}Response (200 OK)
{
"success": true,
"message_id": "location_msg_001",
"timestamp": "2026-07-03T15:05:00Z",
"results": [
{
"recipient_id": "cust_901",
"phone": "27825551234",
"wa_message_id": "wamid.HBgNMjc4MjU1NTEyMzQVAgA...",
"status": "sent"
}
]
}Send Interactive: Buttons
Send a message with up to 3 reply buttons. When the user taps a button, you'll receive a callback with the button ID.
/api/v1/send/interactiveReply Buttons (max 3)
| Field | Type | Required | Description |
|---|---|---|---|
| content.type | string | Yes | Must be "buttons" |
| content.body | string | Yes | Message text above the buttons |
| content.buttons | array | Yes | 1-3 button objects |
POST /api/v1/send/interactive
Authorization: Bearer <token>
Content-Type: application/json
{
"message_id": "confirm_appt_055",
"client_id": "carleads",
"campaign_id": "carleads_main",
"wa_number_id": "1",
"callback_url": "https://your-app.com/webhooks/wa",
"recipients": [
{ "recipient_id": "patient_501", "phone": "0825551234", "name": "David" }
],
"content": {
"type": "buttons",
"body": "Hi {{name}}, you have an appointment tomorrow at 10:00 AM. Can you confirm?",
"buttons": [
{ "type": "reply", "reply": { "id": "btn_confirm", "title": "Confirm" } },
{ "type": "reply", "reply": { "id": "btn_reschedule", "title": "Reschedule" } },
{ "type": "reply", "reply": { "id": "btn_cancel", "title": "Cancel" } }
]
}
}Response (200 OK)
{
"success": true,
"message_id": "confirm_appt_055",
"timestamp": "2026-07-03T14:40:00Z",
"results": [
{
"recipient_id": "patient_501",
"phone": "27825551234",
"wa_message_id": "wamid.HBgNMjc4MjU1NTEyMzQVAgASGBQ...",
"status": "sent"
}
]
}Send Interactive: List
Send a list menu with sections and selectable items. The user taps a button to expand the list, then selects an option.
/api/v1/send/interactiveList with Sections
| Field | Type | Required | Description |
|---|---|---|---|
| content.type | string | Yes | Must be "list" |
| content.body | string | Yes | Message text above the list |
| content.button_text | string | Yes | Text on the button that opens the list |
| content.sections | array | Yes | Array of section objects with rows |
POST /api/v1/send/interactive
Authorization: Bearer <token>
Content-Type: application/json
{
"message_id": "dept_select_012",
"client_id": "carleads",
"campaign_id": "carleads_main",
"wa_number_id": "1",
"callback_url": "https://your-app.com/webhooks/wa",
"recipients": [
{ "recipient_id": "customer_601", "phone": "0825551234", "name": "Emma" }
],
"content": {
"type": "list",
"body": "Hi {{name}}, how can we help you today? Please select a department:",
"button_text": "Choose Department",
"sections": [
{
"title": "Sales",
"rows": [
{ "id": "dept_new_car", "title": "New Vehicles", "description": "Enquire about new cars" },
{ "id": "dept_used_car", "title": "Used Vehicles", "description": "Browse pre-owned stock" }
]
},
{
"title": "Support",
"rows": [
{ "id": "dept_service", "title": "Service & Repairs", "description": "Book a service" },
{ "id": "dept_finance", "title": "Finance", "description": "Payment queries" }
]
}
]
}
}Response (200 OK)
{
"success": true,
"message_id": "dept_select_012",
"timestamp": "2026-07-03T14:45:00Z",
"results": [
{
"recipient_id": "customer_601",
"phone": "27825551234",
"wa_message_id": "wamid.HBgNMjc4MjU1NTEyMzQVAgASGBQ...",
"status": "sent"
}
]
}Interactive Message Limits
WAPit validates interactive payloads server-side before calling Meta. Requests that exceed these WhatsApp Cloud API limits return HTTP 400 with a VALIDATION_ERROR and field-specific details (e.g. content.buttons[0].title). If Meta still rejects a payload, error code 131009 is mapped to a friendly character-limit message.
Character & count limits (enforced on /api/v1/send/interactive)
| Field | Max length / count |
|---|---|
| content.body | 1,024 characters |
| content.buttons[].title | 20 characters (max 3 buttons) |
| content.buttons[].id | 256 characters |
| content.button_text | 20 characters (list menu open button) |
| content.sections[].title | 24 characters (max 10 sections) |
| content.sections[].rows[].title | 24 characters |
| content.sections[].rows[].description | 72 characters |
| content.sections[].rows[].id | 200 characters (max 10 rows total across all sections) |
Validation error example
{
"success": false,
"error": {
"code": "VALIDATION_ERROR",
"message": "Request validation failed",
"details": [
{
"field": "content.buttons[0].title",
"issue": "Button title must be 20 characters or fewer (got 28)"
},
{
"field": "content.sections[0].options[1].description",
"issue": "List row description must be 72 characters or fewer (got 95)"
}
]
}
}Send Template Message
Send a pre-approved message template. Templates must be approved by Meta before use. Per-recipient components override the shared content.components.
/api/v1/send/templateTemplate with Variables
| Field | Type | Required | Description |
|---|---|---|---|
| content.template_name | string | Yes | Meta-approved template name |
| content.language_code | string | Yes | Template language (e.g. "en_US", "en") |
| content.components | array | No | Shared template components (header, body vars) |
| recipients[].components | array | No | Per-recipient overrides (takes precedence) |
POST /api/v1/send/template
Authorization: Bearer <token>
Content-Type: application/json
{
"message_id": "reminder_batch_077",
"client_id": "carleads",
"campaign_id": "carleads_main",
"wa_number_id": "1",
"callback_url": "https://your-app.com/webhooks/wa",
"recipients": [
{
"recipient_id": "patient_701",
"phone": "0825551234",
"name": "John",
"components": [
{
"type": "body",
"parameters": [
{ "type": "text", "text": "John" },
{ "type": "text", "text": "Dr. Smith" },
{ "type": "text", "text": "10:00 AM, 5 July 2026" }
]
}
]
},
{
"recipient_id": "patient_702",
"phone": "0826661234",
"name": "Sarah",
"components": [
{
"type": "body",
"parameters": [
{ "type": "text", "text": "Sarah" },
{ "type": "text", "text": "Dr. Jones" },
{ "type": "text", "text": "2:30 PM, 5 July 2026" }
]
}
]
}
],
"content": {
"template_name": "appointment_reminder",
"language_code": "en"
}
}Response (200 OK)
{
"success": true,
"message_id": "reminder_batch_077",
"timestamp": "2026-07-03T14:50:00Z",
"results": [
{
"recipient_id": "patient_701",
"phone": "27825551234",
"wa_message_id": "wamid.HBgNMjc4MjU1NTEyMzQVAgA...",
"status": "sent"
},
{
"recipient_id": "patient_702",
"phone": "27826661234",
"wa_message_id": "wamid.HBgNMjc4MjY2NjEyMzQVAgA...",
"status": "sent"
}
]
}Contact List Management
Create and manage contact lists scoped to a campaign. Use lists to store recipient phone numbers for bulk messaging. Contacts are normalized to international format on insert.
/api/v1/contacts/lists?campaign_id=XList Contact Lists
GET /api/v1/contacts/lists?campaign_id=carleads_main
Authorization: Bearer <token>Response (200 OK)
{
"success": true,
"lists": [
{
"id": "a1b2c3d4-e5f6-7890-abcd-ef1234567890",
"company_id": "comp_carleads_abc123",
"campaign_id": "carleads_main",
"name": "Newsletter Subscribers",
"description": "Monthly promo list",
"contact_count": 150,
"is_active": true,
"created_at": "2026-07-01T10:00:00Z",
"updated_at": "2026-07-03T12:00:00Z"
}
],
"count": 1
}/api/v1/contacts/listsCreate Contact List
| Field | Type | Required | Description |
|---|---|---|---|
| company_id | string | Yes | Company identifier |
| campaign_id | string | Yes | Campaign slug |
| name | string | Yes | List display name |
| description | string | No | Optional description |
POST /api/v1/contacts/lists
Authorization: Bearer <token>
Content-Type: application/json
{
"company_id": "comp_carleads_abc123",
"campaign_id": "carleads_main",
"name": "VIP Customers",
"description": "High-value repeat buyers"
}Response (201 Created)
{
"success": true,
"list": {
"id": "b2c3d4e5-f6a7-8901-bcde-f12345678901",
"company_id": "comp_carleads_abc123",
"campaign_id": "carleads_main",
"name": "VIP Customers",
"description": "High-value repeat buyers",
"contact_count": 0,
"is_active": true,
"created_at": "2026-07-03T15:00:00Z",
"updated_at": "2026-07-03T15:00:00Z"
}
}/api/v1/contacts/lists/{id}Get Contact List
GET /api/v1/contacts/lists/b2c3d4e5-f6a7-8901-bcde-f12345678901
Authorization: Bearer <token>Response (200 OK)
{
"success": true,
"list": {
"id": "b2c3d4e5-f6a7-8901-bcde-f12345678901",
"company_id": "comp_carleads_abc123",
"campaign_id": "carleads_main",
"name": "VIP Customers",
"description": "High-value repeat buyers",
"contact_count": 42,
"is_active": true,
"created_at": "2026-07-03T15:00:00Z",
"updated_at": "2026-07-03T16:00:00Z"
}
}/api/v1/contacts/lists/{id}Update Contact List
PUT /api/v1/contacts/lists/b2c3d4e5-f6a7-8901-bcde-f12345678901
Authorization: Bearer <token>
Content-Type: application/json
{
"name": "VIP Customers (Updated)",
"description": "Premium tier subscribers"
}Response (200 OK)
{
"success": true,
"list": {
"id": "b2c3d4e5-f6a7-8901-bcde-f12345678901",
"name": "VIP Customers (Updated)",
"description": "Premium tier subscribers",
"contact_count": 42,
"updated_at": "2026-07-03T16:30:00Z"
}
}/api/v1/contacts/lists/{id}Delete Contact List
Permanently deletes the list and all contacts (CASCADE).
DELETE /api/v1/contacts/lists/b2c3d4e5-f6a7-8901-bcde-f12345678901
Authorization: Bearer <token>Response (200 OK)
{
"success": true,
"message": "Contact list deleted"
}/api/v1/contacts/lists/{id}/contacts?limit=50&offset=0&search=termList Contacts in a List
| Field | Type | Required | Description |
|---|---|---|---|
| limit | integer | No | Page size (default: 50) |
| offset | integer | No | Pagination offset (default: 0) |
| search | string | No | Filter by phone or name (ILIKE) |
GET /api/v1/contacts/lists/b2c3d4e5-f6a7-8901-bcde-f12345678901/contacts?limit=50&offset=0&search=john
Authorization: Bearer <token>Response (200 OK)
{
"success": true,
"contacts": [
{
"id": "c3d4e5f6-a7b8-9012-cdef-123456789012",
"contact_list_id": "b2c3d4e5-f6a7-8901-bcde-f12345678901",
"phone": "27821234567",
"name": "John Smith",
"email": "john@example.com",
"is_active": true,
"created_at": "2026-07-03T15:10:00Z",
"updated_at": "2026-07-03T15:10:00Z"
}
],
"total": 42
}/api/v1/contacts/lists/{id}/contactsAdd Contacts (Batch)
Batch insert contacts. Optional duplicate_mode: warn (409 with duplicates), skip, or proceed. Duplicate phones within the same list are upserted.
POST /api/v1/contacts/lists/b2c3d4e5-f6a7-8901-bcde-f12345678901/contacts
Authorization: Bearer <token>
Content-Type: application/json
{
"campaign_id": "carleads_main",
"duplicate_mode": "warn",
"contacts": [
{ "phone": "0821234567", "name": "John Smith", "email": "john@example.com" },
{ "phone": "27821234568", "name": "Jane Doe", "email": "jane@example.com" }
]
}Response (200 OK)
{
"success": true,
"added": 1,
"updated": 1
}/api/v1/contacts/lists/{id}/contacts/importImport Contacts from CSV
Import contacts from CSV data. Expected columns: phone,name,email. Optional duplicate_mode: warn, skip, or proceed.
POST /api/v1/contacts/lists/b2c3d4e5-f6a7-8901-bcde-f12345678901/contacts/import
Authorization: Bearer <token>
Content-Type: application/json
{
"csv_data": "phone,name,email\n27821234567,John Smith,john@example.com\n27821234568,Jane Doe,jane@example.com",
"duplicate_mode": "skip"
}Response (200 OK)
{
"success": true,
"imported": 2,
"skipped": 0,
"errors": []
}/api/v1/contacts/lists/{id}/contacts/exportExport Contacts to CSV
Download all contacts in a list as CSV (phone,name,email).
GET /api/v1/contacts/lists/b2c3d4e5-f6a7-8901-bcde-f12345678901/contacts/export
Authorization: Bearer <token>Response (200 OK)
Returns text/csv with header row and one contact per line.
phone,name,email
27821234567,John Smith,john@example.com
27821234568,Jane Doe,jane@example.com/api/v1/contacts/lists/{id}/contacts/{contact_id}Update Contact
PUT /api/v1/contacts/lists/b2c3d4e5-f6a7-8901-bcde-f12345678901/contacts/c3d4e5f6-a7b8-9012-cdef-123456789012
Authorization: Bearer <token>
Content-Type: application/json
{
"phone": "27821234567",
"name": "John Smith Jr.",
"email": "john.jr@example.com",
"metadata_json": "{\"source\": \"import\"}"
}Response (200 OK)
{
"success": true,
"contact": {
"id": "c3d4e5f6-a7b8-9012-cdef-123456789012",
"phone": "27821234567",
"name": "John Smith Jr.",
"email": "john.jr@example.com",
"updated_at": "2026-07-03T16:45:00Z"
}
}/api/v1/contacts/lists/{id}/contacts/{contact_id}Delete Contact
DELETE /api/v1/contacts/lists/b2c3d4e5-f6a7-8901-bcde-f12345678901/contacts/c3d4e5f6-a7b8-9012-cdef-123456789012
Authorization: Bearer <token>Response (200 OK)
{
"success": true,
"message": "Contact deleted"
}Saved Message Templates
Store reusable message templates locally (not Meta WABA templates). Templates support all message types: text, buttons, list, image, video, document, audio, location, and WhatsApp template payloads. The content_json field stores type-specific payload data matching the send endpoint format.
/api/v1/saved-messages?customer_profile_id=X&type=Y&include_shared=trueList Saved Messages
Optional type filter. Pass include_shared=true (default) to include platform templates with scope='shared'.
| Field | Type | Required | Description |
|---|---|---|---|
| customer_profile_id | string | Yes | Customer profile ID (query parameter) |
| type | string | No | Filter by type: text, buttons, list, image, video, document, audio, location, wa_template |
GET /api/v1/saved-messages?customer_profile_id=clxyz123&type=buttons
Authorization: Bearer <token>Response (200 OK)
{
"success": true,
"messages": [
{
"id": "msg_abc123",
"customer_profile_id": "clxyz123",
"company_id": "comp_carleads_abc123",
"campaign_id": "carleads_main",
"name": "Appointment Confirm",
"category": "utility",
"type": "buttons",
"body": "Hi {{name}}, can you confirm your appointment?",
"content_json": "{\"type\":\"buttons\",\"buttons\":[{\"id\":\"confirm\",\"title\":\"Confirm\"}]}",
"is_active": true,
"created_at": "2026-07-01T10:00:00Z",
"updated_at": "2026-07-03T12:00:00Z"
}
],
"count": 1
}/api/v1/saved-messagesCreate Saved Message
| Field | Type | Required | Description |
|---|---|---|---|
| customer_profile_id | string | Yes | Customer profile ID |
| company_id | string | No | Company identifier |
| campaign_id | string | No | Campaign slug |
| name | string | Yes | Template display name |
| category | string | No | Category label (default: general) |
| type | string | Yes | Message type (see list endpoint) |
| body | string | Yes | Message body or description |
| content_json | string | No | Type-specific JSON payload |
POST /api/v1/saved-messages
Authorization: Bearer <token>
Content-Type: application/json
{
"customer_profile_id": "clxyz123",
"company_id": "comp_carleads_abc123",
"campaign_id": "carleads_main",
"name": "Store Location",
"category": "utility",
"type": "location",
"body": "Visit our showroom",
"content_json": "{\"latitude\":-33.9249,\"longitude\":18.4241,\"name\":\"Cape Town Showroom\",\"address\":\"123 Main Road, Cape Town\"}"
}Response (201 Created)
{
"success": true,
"message": {
"id": "msg_def456",
"customer_profile_id": "clxyz123",
"name": "Store Location",
"type": "location",
"body": "Visit our showroom",
"is_active": true,
"created_at": "2026-07-03T15:00:00Z"
}
}/api/v1/saved-messages/{id}Get Saved Message
GET /api/v1/saved-messages/msg_def456
Authorization: Bearer <token>Response (200 OK)
{
"success": true,
"message": {
"id": "msg_def456",
"customer_profile_id": "clxyz123",
"name": "Store Location",
"type": "location",
"body": "Visit our showroom",
"content_json": "{\"latitude\":-33.9249,\"longitude\":18.4241,\"name\":\"Cape Town Showroom\",\"address\":\"123 Main Road\"}",
"is_active": true,
"created_at": "2026-07-03T15:00:00Z",
"updated_at": "2026-07-03T15:00:00Z"
}
}/api/v1/saved-messages/{id}Update Saved Message
PUT /api/v1/saved-messages/msg_def456
Authorization: Bearer <token>
Content-Type: application/json
{
"name": "Store Location (Updated)",
"category": "marketing",
"type": "location",
"body": "Find us here",
"content_json": "{\"latitude\":-33.9249,\"longitude\":18.4241,\"name\":\"Cape Town Showroom\",\"address\":\"456 New Street\"}"
}Response (200 OK)
{
"success": true,
"message": {
"id": "msg_def456",
"name": "Store Location (Updated)",
"body": "Find us here",
"updated_at": "2026-07-03T16:00:00Z"
}
}/api/v1/saved-messages/{id}Delete Saved Message
Soft-deletes the template (sets is_active to false).
DELETE /api/v1/saved-messages/msg_def456
Authorization: Bearer <token>Response (200 OK)
{
"success": true,
"message": "Saved message deleted"
}content_json Structure by Type
| Type | content_json |
|---|---|
| text | null (body field is sufficient) |
| buttons | {"type":"buttons","body":"...","buttons":[{"type":"reply","reply":{"id":"x","title":"Y"}}]} |
| list | {"type":"list","body":"...","button_text":"View","sections":[{"title":"S","rows":[{"id":"x","title":"Y","description":"Z"}]}]} |
| image | {"image_url":"https://...","caption":"text"} |
| video | {"video_url":"https://...","caption":"text"} |
| document | {"document_url":"https://...","filename":"file.pdf","caption":"text"} |
| audio | {"audio_url":"https://..."} |
| location | {"latitude":-33.9,"longitude":18.4,"name":"Place","address":"123 St"} |
| wa_template | {"template_name":"opt_in","language_code":"en","components":[...]} |
Message Scheduling
Schedule messages to send at a future time. Target a contact list, explicit phone numbers, or both. Call POST /scheduled-messages/process to dispatch all due pending messages.
/api/v1/scheduled-messages?company_id=X&status=pendingList Scheduled Messages
GET /api/v1/scheduled-messages?company_id=comp_abc&status=pending
Authorization: Bearer <token>Response (200 OK)
{
"success": true,
"scheduled_messages": [
{
"id": "sched_001",
"company_id": "comp_abc",
"campaign_id": "carleads_main",
"wa_credential_id": "cred_123",
"contact_list_id": "list_uuid",
"message_type": "text",
"body": "Hi {{name}}, reminder for tomorrow!",
"scheduled_at": "2026-07-06T09:00:00Z",
"status": "pending",
"sent_count": 0,
"failed_count": 0
}
],
"count": 1
}/api/v1/scheduled-messagesSchedule a Message
POST /api/v1/scheduled-messages
Authorization: Bearer <token>
Content-Type: application/json
{
"company_id": "comp_abc",
"campaign_id": "carleads_main",
"wa_credential_id": "cred_123",
"customer_profile_id": "clxyz123",
"contact_list_id": "list_uuid",
"message_type": "text",
"body": "Hi {{name}}, your appointment is tomorrow at 10am.",
"scheduled_at": "2026-07-06T09:00:00Z"
}Response (201 Created)
{
"success": true,
"scheduled_message": {
"id": "sched_001",
"status": "pending",
"scheduled_at": "2026-07-06T09:00:00Z"
}
}/api/v1/scheduled-messages/{id}Get Scheduled Message
GET /api/v1/scheduled-messages/sched_001
Authorization: Bearer <token>/api/v1/scheduled-messages/{id}Update Scheduled Message
Update a pending scheduled message. Only messages with status pending can be modified.
PUT /api/v1/scheduled-messages/sched_001
Authorization: Bearer <token>
Content-Type: application/json
{
"scheduled_at": "2026-07-07T09:00:00Z",
"body": "Hi {{name}}, updated reminder for tomorrow!",
"contact_list_id": "list_uuid"
}/api/v1/scheduled-messages/processProcess Due Scheduled Messages
POST /api/v1/scheduled-messages/process
Authorization: Bearer <token>Response (200 OK)
{
"success": true,
"processed": 3,
"sent": 145,
"failed": 2
}/api/v1/scheduled-messages/{id}Cancel Scheduled Message
DELETE /api/v1/scheduled-messages/sched_001
Authorization: Bearer <token>Response (200 OK)
{
"success": true,
"message": "Scheduled message cancelled successfully"
}Campaign Analytics
Track message batches and aggregate delivery metrics. Link individual sends to a batch via batch_id on message_log.
/api/v1/analytics/summary?company_id=X&period=7dAnalytics Summary
Period: 7d, 30d, or 90d.
GET /api/v1/analytics/summary?company_id=comp_abc&period=30d
Authorization: Bearer <token>Response (200 OK)
{
"success": true,
"summary": {
"total_batches": 12,
"total_messages": 4500,
"total_delivered": 4200,
"total_read": 3100,
"total_failed": 45,
"delivery_rate": 93.3,
"read_rate": 73.8
}
}/api/v1/analytics/batches?company_id=X&campaign_id=YList Message Batches
GET /api/v1/analytics/batches?company_id=comp_abc&campaign_id=carleads_main
Authorization: Bearer <token>Response (200 OK)
{
"success": true,
"batches": [
{
"id": "batch_001",
"name": "July Promo",
"message_type": "text",
"total_recipients": 500,
"sent_count": 500,
"delivered_count": 480,
"read_count": 320,
"failed_count": 5,
"status": "completed"
}
],
"count": 1
}/api/v1/analytics/batches/{id}Get Message Batch
GET /api/v1/analytics/batches/batch_001
Authorization: Bearer <token>/api/v1/analytics/batchesCreate Message Batch
POST /api/v1/analytics/batches
Authorization: Bearer <token>
Content-Type: application/json
{
"company_id": "comp_abc",
"campaign_id": "carleads_main",
"wa_credential_id": "cred_123",
"name": "July Promo",
"message_type": "text",
"total_recipients": 500
}Response (201 Created)
{
"success": true,
"batch_id": "batch_001",
"batch": { "id": "batch_001", "status": "in_progress" }
}/api/v1/analytics/batches/{id}/countsUpdate Batch Counts
PUT /api/v1/analytics/batches/batch_001/counts
Authorization: Bearer <token>
Content-Type: application/json
{
"sent_count": 500,
"delivered_count": 480,
"read_count": 320,
"failed_count": 5,
"status": "completed"
}Shared Templates
Platform-wide templates (scope='shared') managed by admins. Customers see them alongside their own templates via GET /saved-messages?include_shared=true.
/api/v1/admin/shared-templates?type=XList Shared Templates
GET /api/v1/admin/shared-templates?type=buttons
Authorization: Bearer <token>Response (200 OK)
{
"success": true,
"templates": [
{
"id": "tmpl_shared_001",
"name": "Welcome Message",
"type": "text",
"scope": "shared",
"body": "Welcome to our platform!"
}
],
"count": 1
}/api/v1/admin/shared-templatesCreate Shared Template
POST /api/v1/admin/shared-templates
Authorization: Bearer <token>
Content-Type: application/json
{
"name": "Appointment Reminder",
"type": "buttons",
"category": "utility",
"body": "Hi {{name}}, please confirm your appointment.",
"content_json": "{\"type\":\"buttons\",\"buttons\":[{\"type\":\"reply\",\"reply\":{\"id\":\"confirm\",\"title\":\"Confirm\"}}]}"
}Response (201 Created)
{
"success": true,
"template": { "id": "tmpl_shared_002", "scope": "shared", "name": "Appointment Reminder" }
}/api/v1/admin/shared-templates/{id}Get Shared Template
GET /api/v1/admin/shared-templates/tmpl_shared_001
Authorization: Bearer <token>/api/v1/admin/shared-templates/{id}Update Shared Template
PUT /api/v1/admin/shared-templates/tmpl_shared_001
Authorization: Bearer <token>
Content-Type: application/json
{
"name": "Welcome Message (Updated)",
"body": "Welcome! We're glad you're here.",
"content_json": "{\"type\":\"text\"}"
}/api/v1/admin/shared-templates/{id}Delete Shared Template
DELETE /api/v1/admin/shared-templates/tmpl_shared_001
Authorization: Bearer <token>Contact Tags
Tag contacts for segmentation. Tags are scoped to a company. Assign tags to individual contacts for filtering and targeted sends.
/api/v1/tags?company_id=XList Tags
GET /api/v1/tags?company_id=comp_abc
Authorization: Bearer <token>Response (200 OK)
{
"success": true,
"tags": [
{
"id": "tag_001",
"company_id": "comp_abc",
"name": "VIP",
"color": "#10B981",
"contact_count": 42
}
],
"count": 1
}/api/v1/tagsCreate Tag
POST /api/v1/tags
Authorization: Bearer <token>
Content-Type: application/json
{
"company_id": "comp_abc",
"name": "VIP",
"color": "#10B981"
}Response (201 Created)
{
"success": true,
"tag": { "id": "tag_001", "name": "VIP", "color": "#10B981" }
}/api/v1/tags/{id}Update Tag
PUT /api/v1/tags/tag_001
Authorization: Bearer <token>
Content-Type: application/json
{
"name": "VIP Premium",
"color": "#059669"
}/api/v1/tags/{id}Delete Tag
DELETE /api/v1/tags/tag_001?company_id=comp_abc
Authorization: Bearer <token>Response (200 OK)
{
"success": true,
"message": "Tag deleted"
}/api/v1/contacts/{contact_id}/tagsGet Contact Tags
GET /api/v1/contacts/contact_uuid/tags
Authorization: Bearer <token>Response (200 OK)
{
"success": true,
"tags": [
{ "id": "tag_001", "name": "VIP", "color": "#10B981" }
]
}/api/v1/contacts/{contact_id}/tagsAssign Tags to Contact
POST /api/v1/contacts/contact_uuid/tags
Authorization: Bearer <token>
Content-Type: application/json
{
"tag_ids": ["tag_001", "tag_002"]
}Response (200 OK)
{
"success": true,
"message": "Tags assigned successfully",
"assigned": 2
}/api/v1/tags/{id}/contacts?company_id=XList Contacts by Tag
GET /api/v1/tags/tag_001/contacts?company_id=comp_abc
Authorization: Bearer <token>/api/v1/contacts/{contact_id}/tags/{tag_id}Remove Tag from Contact
DELETE /api/v1/contacts/contact_uuid/tags/tag_001
Authorization: Bearer <token>Response (200 OK)
{
"success": true,
"message": "Tag removed from contact"
}Duplicate Detection
Detect phone numbers that already exist in other contact lists within the same company. Use duplicate_mode on add/import: warn (409 with duplicates), skip, or proceed.
/api/v1/contacts/duplicates/checkCheck Duplicates Before Import
POST /api/v1/contacts/duplicates/check
Authorization: Bearer <token>
Content-Type: application/json
{
"company_id": "comp_abc",
"target_list_id": "list_uuid",
"phones": ["0825551234", "0826667890"]
}Response (200 OK)
{
"success": true,
"duplicates": [
{
"id": "contact_uuid",
"phone": "27825551234",
"name": "John",
"list_id": "other_list_uuid",
"list_name": "Newsletter Subscribers"
}
],
"total": 1
}/api/v1/contacts/duplicates?company_id=XFind All Duplicates
GET /api/v1/contacts/duplicates?company_id=comp_abc
Authorization: Bearer <token>Response (200 OK)
{
"success": true,
"duplicates": [
{
"phone": "27825551234",
"name": "John",
"list_count": 2,
"lists": ["VIP Customers", "Newsletter Subscribers"]
}
],
"total": 1
}Send Reaction
React to a specific message with an emoji. This does not use the standard recipient/content format — it targets a specific WhatsApp message by its wa_message_id.
/api/v1/send/reactionReact to a Message
| Field | Type | Required | Description |
|---|---|---|---|
| campaign_id | string | Yes | Campaign slug |
| wa_number_id | number | Yes | WA number to send from |
| message_id | string | Yes | The wa_message_id of the message to react to |
| emoji | string | Yes | Emoji character (e.g. "👍", "❤️") |
POST /api/v1/send/reaction
Authorization: Bearer <token>
Content-Type: application/json
{
"campaign_id": "carleads_main",
"wa_number_id": 1,
"message_id": "wamid.HBgNMjc4MjU1NTEyMzQVAgA...",
"emoji": "👍"
}Response (200 OK)
{
"success": true,
"message": "Reaction sent"
}Mark as Read
Send a read receipt for a specific message. This shows the sender that you've read their message (blue ticks).
/api/v1/messages/readMark Message as Read
| Field | Type | Required | Description |
|---|---|---|---|
| campaign_id | string | Yes | Campaign slug |
| wa_number_id | number | Yes | WA number that received the message |
| message_id | string | Yes | The wa_message_id to mark as read |
POST /api/v1/messages/read
Authorization: Bearer <token>
Content-Type: application/json
{
"campaign_id": "carleads_main",
"wa_number_id": 1,
"message_id": "wamid.HBgNMjc4MjU1NTEyMzQVAgA..."
}Response (200 OK)
{
"success": true,
"message": "Message marked as read"
}Template Management
Manage your Meta WABA message templates. Templates must be submitted to Meta for approval before they can be used with /send/template. These endpoints interact directly with the Meta Business API.
/api/v1/templatesList Templates
| Field | Type | Required | Description |
|---|---|---|---|
| campaign_id | string | Yes | Campaign slug (query parameter) |
| wa_number_id | number | Yes | WA number ID (query parameter) |
GET /api/v1/templates?campaign_id=carleads_main&wa_number_id=1
Authorization: Bearer <token>Response (200 OK)
{
"success": true,
"templates": [
{
"id": "local-uuid-001",
"meta_template_id": "123456789",
"name": "appointment_reminder",
"status": "APPROVED",
"category": "UTILITY",
"language": "en_US",
"components": [
{ "type": "BODY", "text": "Hi {{1}}, your appointment with {{2}} is at {{3}}." }
],
"synced_at": "2026-07-03T14:00:00Z"
},
{
"id": "local-uuid-002",
"meta_template_id": "987654321",
"name": "order_confirmation",
"status": "PENDING",
"category": "UTILITY",
"language": "en_US",
"components": [
{ "type": "BODY", "text": "Hi {{1}}, your order {{2}} has been confirmed." }
],
"synced_at": "2026-07-03T14:00:00Z"
}
],
"count": 2
}/api/v1/templatesCreate Template
Submit a new template to Meta for approval. Templates typically take 24-48 hours to be reviewed. Status updates are received via webhook.
| Field | Type | Required | Description |
|---|---|---|---|
| campaign_id | string | Yes | Campaign slug |
| wa_number_id | number | Yes | WA number ID |
| name | string | Yes | Template name (lowercase, underscores only) |
| category | string | Yes | UTILITY, MARKETING, or AUTHENTICATION |
| language | string | No | Language code (default: en_US) |
| components | array | Yes | Template components (HEADER, BODY, FOOTER, BUTTONS) |
POST /api/v1/templates
Authorization: Bearer <token>
Content-Type: application/json
{
"campaign_id": "carleads_main",
"wa_number_id": 1,
"name": "order_confirmation",
"category": "UTILITY",
"language": "en_US",
"components": [
{
"type": "BODY",
"text": "Hi {{1}}, your order {{2}} has been confirmed. Expected delivery: {{3}}."
}
]
}Response (201 Created)
{
"success": true,
"template": {
"id": "123456789",
"name": "order_confirmation",
"status": "PENDING",
"category": "UTILITY"
},
"message": "Template submitted for approval"
}/api/v1/templates/{template_id}Edit Template
Update an existing Meta template by its meta_template_id. Edited templates return to PENDING status until Meta re-reviews them.
PUT /api/v1/templates/123456789
Authorization: Bearer <token>
Content-Type: application/json
{
"campaign_id": "carleads_main",
"wa_number_id": "1",
"category": "UTILITY",
"components": [
{
"type": "BODY",
"text": "Hi {{1}}, your order {{2}} has shipped. Track it here: {{3}}."
}
]
}Response (200 OK)
{
"success": true,
"meta_template_id": "123456789",
"status": "PENDING",
"message": "Template updated. It will be re-reviewed by Meta."
}/api/v1/templates/syncSync Templates from Meta
Force-sync all templates from Meta to the local cache. Useful after making changes directly in Meta Business Manager.
POST /api/v1/templates/sync?campaign_id=carleads_main&wa_number_id=1
Authorization: Bearer <token>Response (200 OK)
{
"success": true,
"synced": 5,
"message": "Templates synced successfully"
}/api/v1/templates/{name}Delete Template
Delete a template from Meta. This permanently removes it and it cannot be recovered.
DELETE /api/v1/templates/order_confirmation?campaign_id=carleads_main&wa_number_id=1
Authorization: Bearer <token>Response (200 OK)
{
"success": true,
"message": "Template deleted"
}Media Upload
Upload media files to Meta for use in messages. Returns a media_id that can be used as an alternative to public URLs in send/image, send/video, send/document, and send/audio endpoints.
/api/v1/media/uploadUpload Media File
| Field | Type | Required | Description |
|---|---|---|---|
| file | binary | Yes | The media file (multipart/form-data) |
| campaign_id | string | Yes | Campaign slug (form field) |
| wa_number_id | string | Yes | WA number ID (form field) |
Supported formats: JPEG, PNG, MP4, PDF, DOCX, MP3, OGG, AMR (max 16MB for video/audio, 100MB for documents)
POST /api/v1/media/upload
Authorization: Bearer <token>
Content-Type: multipart/form-data
------boundary
Content-Disposition: form-data; name="campaign_id"
carleads_main
------boundary
Content-Disposition: form-data; name="wa_number_id"
1
------boundary
Content-Disposition: form-data; name="file"; filename="invoice.pdf"
Content-Type: application/pdf
<binary file data>
------boundary--Response (200 OK)
{
"success": true,
"media_id": "1234567890",
"message": "Media uploaded successfully"
}Usage: Use the returned media_id in your send requests instead of a public URL. Media IDs are valid for 30 days.
Business Profile
View and update your WhatsApp Business profile information. This controls what users see when they view your business details on WhatsApp.
/api/v1/profileGet Business Profile
| Field | Type | Required | Description |
|---|---|---|---|
| campaign_id | string | Yes | Campaign slug (query parameter) |
| wa_number_id | number | Yes | WA number ID (query parameter) |
GET /api/v1/profile?campaign_id=carleads_main&wa_number_id=1
Authorization: Bearer <token>Response (200 OK)
{
"success": true,
"profile": {
"about": "We sell premium vehicles",
"address": "123 Main Road, Cape Town",
"description": "Trusted car dealership since 1990",
"email": "info@carleads.co.za",
"vertical": "AUTO",
"websites": ["https://carleads.co.za"],
"profile_picture_url": "https://..."
}
}/api/v1/profileUpdate Business Profile
| Field | Type | Required | Description |
|---|---|---|---|
| campaign_id | string | Yes | Campaign slug |
| wa_number_id | number | Yes | WA number ID |
| about | string | No | Short business description (139 chars max) |
| address | string | No | Business address |
| description | string | No | Full business description (512 chars max) |
| string | No | Business email | |
| vertical | string | No | Business category (AUTO, BEAUTY, RETAIL, etc.) |
| websites | array | No | Up to 2 website URLs |
PUT /api/v1/profile
Authorization: Bearer <token>
Content-Type: application/json
{
"campaign_id": "carleads_main",
"wa_number_id": 1,
"about": "SA's #1 car dealership",
"address": "456 New Street, Cape Town, 8001",
"description": "Premium new and pre-owned vehicles with nationwide delivery",
"email": "hello@carleads.co.za",
"vertical": "AUTO",
"websites": ["https://carleads.co.za"]
}Response (200 OK)
{
"success": true,
"message": "Profile updated successfully"
}Callbacks & Webhooks
WAPit sends POST requests to your callback_url for every status change and incoming reply. Each callback includes the original message_id and recipient_id you sent, so you can link events back to your records.
Your endpoint must respond within 10 seconds with any 2xx status code. Failed callbacks are logged but not retried.
Outbound: Delivery Status Updates
Meta sends delivery events to WAPit; WAPit forwards them to your callback_url as event_type: status_update with statuses sent, delivered, read, or failed.
Inbound: User Replies & Incoming Messages
When a recipient replies (text, button tap, list selection, or media), WAPit POSTs event_type: user_reply to your callback. Media replies send an immediate callback with media_status: processing, then a second event_type: media_ready callback once the file is stored in cloud storage. You can also poll GET /api/v1/client/incoming for message history.
Status Update: Sent
Received immediately after the message is accepted by Meta.
POST https://your-app.com/webhooks/wa
Content-Type: application/json
{
"event_type": "status_update",
"message_id": "order_confirm_001",
"client_id": "carleads",
"campaign_id": "carleads_main",
"recipient_id": "lead_5001",
"recipient_phone": "27825551234",
"wa_message_id": "wamid.HBgNMjc4MjU1NTEyMzQVAgA...",
"status": "sent",
"timestamp": "2026-07-03T14:30:01Z"
}Status Update: Delivered
The message reached the recipient's device.
{
"event_type": "status_update",
"message_id": "order_confirm_001",
"client_id": "carleads",
"campaign_id": "carleads_main",
"recipient_id": "lead_5001",
"recipient_phone": "27825551234",
"wa_message_id": "wamid.HBgNMjc4MjU1NTEyMzQVAgA...",
"status": "delivered",
"timestamp": "2026-07-03T14:30:05Z"
}Status Update: Read
The recipient opened and read the message (blue ticks).
{
"event_type": "status_update",
"message_id": "order_confirm_001",
"client_id": "carleads",
"campaign_id": "carleads_main",
"recipient_id": "lead_5001",
"recipient_phone": "27825551234",
"wa_message_id": "wamid.HBgNMjc4MjU1NTEyMzQVAgA...",
"status": "read",
"timestamp": "2026-07-03T14:31:22Z"
}Status Update: Failed
The message could not be delivered. Includes error details from Meta.
{
"event_type": "status_update",
"message_id": "promo_batch_042",
"client_id": "carleads",
"campaign_id": "carleads_main",
"recipient_id": "lead_103",
"recipient_phone": "27823333333",
"wa_message_id": "wamid.HBgNMjc4MjMzMzMzMzMVAgA...",
"status": "failed",
"timestamp": "2026-07-03T14:30:12Z",
"error": {
"code": 131026,
"title": "Message undeliverable",
"message": "The recipient phone number is not a valid WhatsApp number"
}
}User Reply: Text
The recipient typed a text reply to your message.
{
"event_type": "user_reply",
"incoming_message_id": "f47ac10b-58cc-4372-a567-0e02b2c3d479",
"original_message_id": "uuid-of-message-log-row",
"client_id": "carleads",
"campaign_id": "carleads_main",
"recipient_id": "lead_5001",
"sender_phone": "27825551234",
"sender_name": "John",
"wa_message_id": "wamid.inbound_abc123",
"timestamp": "2026-07-03T14:35:00Z",
"reply": {
"type": "text",
"body": "Yes, I'd like to know more about the offer"
}
}User Reply: Button Tap
The recipient tapped one of the reply buttons you sent.
{
"event_type": "user_reply",
"incoming_message_id": "a1b2c3d4-e5f6-7890-abcd-ef1234567890",
"original_message_id": "uuid-of-message-log-row",
"client_id": "carleads",
"campaign_id": "carleads_main",
"recipient_id": "patient_501",
"sender_phone": "27825551234",
"sender_name": "David",
"wa_message_id": "wamid.inbound_btn456",
"timestamp": "2026-07-03T14:42:00Z",
"reply": {
"type": "button",
"button_id": "btn_confirm",
"button_title": "Confirm"
}
}User Reply: List Selection
The recipient selected an item from a list message.
{
"event_type": "user_reply",
"incoming_message_id": "b2c3d4e5-f6a7-8901-bcde-f12345678901",
"original_message_id": "uuid-of-message-log-row",
"client_id": "carleads",
"campaign_id": "carleads_main",
"recipient_id": "customer_601",
"sender_phone": "27825551234",
"sender_name": "Emma",
"wa_message_id": "wamid.inbound_list789",
"timestamp": "2026-07-03T14:45:00Z",
"reply": {
"type": "list_selection",
"selected_id": "dept_new_car",
"selected_title": "New Vehicles"
}
}User Reply: Media (Processing)
The recipient sent an image/video/document. You'll receive this immediately (media still uploading to cloud storage).
{
"event_type": "user_reply",
"incoming_message_id": "c3d4e5f6-a7b8-9012-cdef-123456789012",
"original_message_id": "uuid-of-message-log-row",
"client_id": "carleads",
"campaign_id": "carleads_main",
"recipient_id": "lead_5001",
"sender_phone": "27825551234",
"sender_name": "John",
"wa_message_id": "wamid.inbound_img101",
"timestamp": "2026-07-03T14:50:00Z",
"media_status": "processing",
"reply": {
"type": "image",
"media_id": "1234567890",
"mime_type": "image/jpeg",
"media_url": ""
}
}Media Ready
Sent after the media file has been downloaded from Meta and uploaded to cloud storage. The media_url is now a permanent link.
{
"event_type": "media_ready",
"incoming_message_id": "c3d4e5f6-a7b8-9012-cdef-123456789012",
"original_message_id": "uuid-of-message-log-row",
"client_id": "carleads",
"campaign_id": "carleads_main",
"recipient_id": "lead_5001",
"sender_phone": "27825551234",
"sender_name": "John",
"wa_message_id": "wamid.inbound_img101",
"timestamp": "2026-07-03T14:50:08Z",
"media_status": "ready",
"reply": {
"type": "image",
"media_id": "1234567890",
"mime_type": "image/jpeg",
"media_url": "https://storage.googleapis.com/wowresults.appspot.com/filestorage/1234567890.jpg"
}
}Message Queued (Tier Limit)
Your daily conversation limit has been reached. The message is queued and will be sent the next day.
{
"event_type": "message_queued_tier_limit",
"message_id": "promo_batch_042",
"recipient_id": "lead_103",
"phone": "27823333333",
"reason": "Daily conversation limit reached for company tier",
"tier": "tier_1",
"daily_limit": 1000,
"current_count": 1000,
"scheduled_for": "2026-07-04",
"timestamp": "2026-07-03T14:30:10Z"
}Error Handling
All errors follow a consistent format with a machine-readable code, human-readable message, and optional field-level details.
{
"success": false,
"error": {
"code": "VALIDATION_ERROR",
"message": "Request validation failed",
"details": [
{
"field": "campaign_id",
"issue": "Field is required but was not provided",
"expected": "The campaign identifier",
"examples": ["carleads_main"]
},
{
"field": "recipients",
"issue": "At least one recipient is required",
"expected": "Array with at least 1 recipient object"
}
]
}
}Common Error Codes
| HTTP | Code | Meaning |
|---|---|---|
| 400 | VALIDATION_ERROR | Missing or invalid fields |
| 400 | JSON_SYNTAX_ERROR | Malformed JSON body |
| 401 | AUTH_MISSING_TOKEN | No Authorization header |
| 401 | AUTH_INVALID_TOKEN | Token is malformed or tampered |
| 401 | AUTH_TOKEN_EXPIRED | Token expired — re-authenticate |
| 401 | AUTH_INVALID_CREDENTIALS | Wrong username or password |
| 404 | WA_NUMBER_NOT_FOUND | campaign_id + wa_number_id doesn't match a credential |
| 400 | WA_NUMBER_INACTIVE | WA number is disabled |
| 202 | (quarantined) | Circuit breaker open — messages quarantined |
Field-Level Validation Details
Validation errors include a details array. Each entry has field (JSON path), issue (human-readable explanation), and optionally expected / examples. Interactive message limits return paths like content.buttons[0].title or content.sections[0].options[1].description.
{
"success": false,
"error": {
"code": "VALIDATION_ERROR",
"message": "Request validation failed",
"details": [
{
"field": "content.buttons[0].title",
"issue": "Button title must be 20 characters or fewer (got 28)"
}
]
}
}Rate Limiting
Three layers of rate limiting protect your WhatsApp numbers and ensure compliance with Meta's policies:
1. Company Tier Limit (Daily)
Limits unique conversations per company per day.
| Tier | Daily Limit |
|---|---|
| Initial | 250 conversations |
| Tier 1 | 1,000 conversations |
| Tier 2 | 10,000 conversations |
| Tier 3 | 100,000 conversations |
| Tier 4 | Unlimited |
Exceeded messages are queued for the next day. A message_queued_tier_limit callback is sent.
2. WABA Hourly Throttle
Maximum 4,900 messages per hour per WhatsApp number (Meta allows 5,000 — we reserve 100 for buffer). Exceeded messages are queued and sent when capacity frees up. Status: "queued".
3. Circuit Breaker (Number Protection)
If a WhatsApp number shows high error rates (spam errors), the circuit breaker opens and all messages are quarantined to prevent Meta from banning the number. Recovery is automatic with escalating cooldowns: 4h → 14h → 28h → 32h.
Quarantined messages return HTTP 202 with status "quarantined".
4. Inter-Recipient Delay
A 100ms delayis applied between recipients in a batch to avoid bursting Meta's API. A batch of 10 recipients takes approximately 1 second.
All Endpoints
| Method | Endpoint | Auth | Description |
|---|---|---|---|
| GET | /health | No | Service health check |
| POST | /auth/login | No | Get JWT token |
| POST | /api/v1/send/text | Bearer | Send text message |
| POST | /api/v1/send/image | Bearer | Send image message |
| POST | /api/v1/send/video | Bearer | Send video message |
| POST | /api/v1/send/document | Bearer | Send document message |
| POST | /api/v1/send/audio | Bearer | Send audio/voice message |
| POST | /api/v1/send/location | Bearer | Send location pin |
| POST | /api/v1/send/interactive | Bearer | Send buttons, list, or carousel |
| POST | /api/v1/send/template | Bearer | Send template message |
| POST | /api/v1/send/reaction | Bearer | React to a message with emoji |
| POST | /api/v1/messages/read | Bearer | Mark message as read |
| GET | /api/v1/templates | Bearer | List Meta WABA templates |
| POST | /api/v1/templates | Bearer | Create template on Meta |
| POST | /api/v1/templates/sync | Bearer | Sync templates from Meta |
| PUT | /api/v1/templates/{template_id} | Bearer | Edit Meta WABA template |
| DELETE | /api/v1/templates/{name} | Bearer | Delete template from Meta |
| POST | /api/v1/media/upload | Bearer | Upload media to Meta |
| GET | /api/v1/profile | Bearer | Get business profile |
| PUT | /api/v1/profile | Bearer | Update business profile |
| GET | /api/v1/companies | Bearer | List your companies |
| GET | /api/v1/campaigns | Bearer | List your campaigns |
| GET | /api/v1/contacts/lists | Bearer | List contact lists for a campaign |
| POST | /api/v1/contacts/lists | Bearer | Create contact list |
| GET | /api/v1/contacts/lists/{id} | Bearer | Get contact list |
| PUT | /api/v1/contacts/lists/{id} | Bearer | Update contact list |
| DELETE | /api/v1/contacts/lists/{id} | Bearer | Delete contact list |
| GET | /api/v1/contacts/lists/{id}/contacts | Bearer | List contacts in a list |
| POST | /api/v1/contacts/lists/{id}/contacts | Bearer | Add contacts (batch) |
| POST | /api/v1/contacts/lists/{id}/contacts/import | Bearer | Import contacts from CSV |
| GET | /api/v1/contacts/lists/{id}/contacts/export | Bearer | Export contacts to CSV |
| PUT | /api/v1/contacts/lists/{id}/contacts/{contact_id} | Bearer | Update contact |
| DELETE | /api/v1/contacts/lists/{id}/contacts/{contact_id} | Bearer | Delete contact |
| GET | /api/v1/saved-messages | Bearer | List saved message templates |
| POST | /api/v1/saved-messages | Bearer | Create saved message template |
| GET | /api/v1/saved-messages/{id} | Bearer | Get saved message template |
| PUT | /api/v1/saved-messages/{id} | Bearer | Update saved message template |
| DELETE | /api/v1/saved-messages/{id} | Bearer | Delete saved message template |
| GET | /api/v1/scheduled-messages | Bearer | List scheduled messages |
| POST | /api/v1/scheduled-messages | Bearer | Schedule message for later |
| GET | /api/v1/scheduled-messages/{id} | Bearer | Get scheduled message |
| PUT | /api/v1/scheduled-messages/{id} | Bearer | Update scheduled message |
| DELETE | /api/v1/scheduled-messages/{id} | Bearer | Cancel scheduled message |
| POST | /api/v1/scheduled-messages/process | Bearer | Process due scheduled messages |
| GET | /api/v1/analytics/summary | Bearer | Campaign analytics summary |
| GET | /api/v1/analytics/batches | Bearer | List message batches |
| POST | /api/v1/analytics/batches | Bearer | Create message batch |
| GET | /api/v1/analytics/batches/{id} | Bearer | Get message batch |
| PUT | /api/v1/analytics/batches/{id}/counts | Bearer | Update batch delivery counts |
| GET | /api/v1/tags | Bearer | List contact tags |
| POST | /api/v1/tags | Bearer | Create contact tag |
| PUT | /api/v1/tags/{id} | Bearer | Update contact tag |
| DELETE | /api/v1/tags/{id} | Bearer | Delete contact tag |
| GET | /api/v1/tags/{id}/contacts | Bearer | List contacts by tag |
| GET | /api/v1/contacts/{contact_id}/tags | Bearer | Get tags for a contact |
| POST | /api/v1/contacts/{contact_id}/tags | Bearer | Assign tags to contact |
| DELETE | /api/v1/contacts/{contact_id}/tags/{tag_id} | Bearer | Remove tag from contact |
| POST | /api/v1/contacts/duplicates/check | Bearer | Check for duplicate contacts |
| GET | /api/v1/contacts/duplicates | Bearer | Find all duplicate contacts |
| GET | /api/v1/client/messages | Bearer | Poll outbound message delivery log |
| GET | /api/v1/client/incoming | Bearer | Poll incoming messages / replies |
| GET | /api/v1/admin/shared-templates | Bearer | List platform shared templates |
| POST | /api/v1/admin/shared-templates | Bearer | Create shared template |
| GET | /api/v1/admin/shared-templates/{id} | Bearer | Get shared template |
| PUT | /api/v1/admin/shared-templates/{id} | Bearer | Update shared template |
| DELETE | /api/v1/admin/shared-templates/{id} | Bearer | Delete shared template |
| GET | /webhook/meta | Token* | Meta webhook verification |
| POST | /webhook/meta | No** | Meta webhook events |
* Meta verification uses hub.verify_token query parameter (configured server-side)
** Meta signs webhook payloads; verification is handled internally
Ready to integrate?
Get your API credentials and start sending WhatsApp messages in minutes. Our team will set up your company, campaign, and WA credentials.

