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.

WAPit WhatsApp messaging platform

Base URL

All API requests are made to:

https://wowresult-174418289148.europe-west1.run.app

For local development: http://localhost:8080

Authentication

All API endpoints (except /health and /auth/login) require a valid JWT token.

POST/auth/login

Get 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.

FieldTypeRequiredDescription
message_idstringYesYour unique batch/request tracking ID
client_idstringYesYour tenant/client identifier
campaign_idstringYesCampaign slug (must match a campaign in the system)
wa_number_idstringYesWhich WA number to send from (e.g. "1" or "2")
callback_urlstringYesURL where delivery statuses and replies will be POSTed
recipientsarrayYesArray of recipient objects (see below)
contentobjectYesMessage content (varies per endpoint)

Recipient Object

FieldTypeRequiredDescription
recipient_idstringYesYour unique ID for this recipient
phonestringYesPhone number (SA format auto-detected: 082... → 2782...)
namestringNoRecipient 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

POST/api/v1/send/text

Single 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"
    }
  ]
}
POST/api/v1/send/text

Multiple 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

POST/api/v1/send/image

Send an Image Message

FieldTypeRequiredDescription
content.image_urlstringYesPublic URL of the image (JPEG, PNG)
content.captionstringNoOptional 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

POST/api/v1/send/video

Send a Video Message

FieldTypeRequiredDescription
content.video_urlstringYesPublic URL of the video (MP4, max 16MB)
content.captionstringNoOptional 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

POST/api/v1/send/document

Send a Document

FieldTypeRequiredDescription
content.document_urlstringYesPublic URL of the document (PDF, DOCX, etc.)
content.filenamestringNoDisplay filename for the recipient
content.captionstringNoOptional 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

POST/api/v1/send/audio

Send an Audio/Voice Message

FieldTypeRequiredDescription
content.audio_urlstringYesPublic 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

POST/api/v1/send/location

Send a Location Pin

FieldTypeRequiredDescription
content.latitudenumberYesLatitude coordinate
content.longitudenumberYesLongitude coordinate
content.namestringNoLocation name (displayed as title)
content.addressstringNoFull 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.

POST/api/v1/send/interactive

Reply Buttons (max 3)

FieldTypeRequiredDescription
content.typestringYesMust be "buttons"
content.bodystringYesMessage text above the buttons
content.buttonsarrayYes1-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.

POST/api/v1/send/interactive

List with Sections

FieldTypeRequiredDescription
content.typestringYesMust be "list"
content.bodystringYesMessage text above the list
content.button_textstringYesText on the button that opens the list
content.sectionsarrayYesArray 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)

FieldMax length / count
content.body1,024 characters
content.buttons[].title20 characters (max 3 buttons)
content.buttons[].id256 characters
content.button_text20 characters (list menu open button)
content.sections[].title24 characters (max 10 sections)
content.sections[].rows[].title24 characters
content.sections[].rows[].description72 characters
content.sections[].rows[].id200 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.

POST/api/v1/send/template

Template with Variables

FieldTypeRequiredDescription
content.template_namestringYesMeta-approved template name
content.language_codestringYesTemplate language (e.g. "en_US", "en")
content.componentsarrayNoShared template components (header, body vars)
recipients[].componentsarrayNoPer-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.

GET/api/v1/contacts/lists?campaign_id=X

List 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
}
POST/api/v1/contacts/lists

Create Contact List

FieldTypeRequiredDescription
company_idstringYesCompany identifier
campaign_idstringYesCampaign slug
namestringYesList display name
descriptionstringNoOptional 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"
  }
}
GET/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"
  }
}
PUT/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"
  }
}
DELETE/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"
}
GET/api/v1/contacts/lists/{id}/contacts?limit=50&offset=0&search=term

List Contacts in a List

FieldTypeRequiredDescription
limitintegerNoPage size (default: 50)
offsetintegerNoPagination offset (default: 0)
searchstringNoFilter 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
}
POST/api/v1/contacts/lists/{id}/contacts

Add 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
}
POST/api/v1/contacts/lists/{id}/contacts/import

Import 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": []
}
GET/api/v1/contacts/lists/{id}/contacts/export

Export 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
PUT/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"
  }
}
DELETE/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.

GET/api/v1/saved-messages?customer_profile_id=X&type=Y&include_shared=true

List Saved Messages

Optional type filter. Pass include_shared=true (default) to include platform templates with scope='shared'.

FieldTypeRequiredDescription
customer_profile_idstringYesCustomer profile ID (query parameter)
typestringNoFilter 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
}
POST/api/v1/saved-messages

Create Saved Message

FieldTypeRequiredDescription
customer_profile_idstringYesCustomer profile ID
company_idstringNoCompany identifier
campaign_idstringNoCampaign slug
namestringYesTemplate display name
categorystringNoCategory label (default: general)
typestringYesMessage type (see list endpoint)
bodystringYesMessage body or description
content_jsonstringNoType-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"
  }
}
GET/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"
  }
}
PUT/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"
  }
}
DELETE/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

Typecontent_json
textnull (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.

GET/api/v1/scheduled-messages?company_id=X&status=pending

List 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
}
POST/api/v1/scheduled-messages

Schedule 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"
  }
}
GET/api/v1/scheduled-messages/{id}

Get Scheduled Message

GET /api/v1/scheduled-messages/sched_001
Authorization: Bearer <token>
PUT/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"
}
POST/api/v1/scheduled-messages/process

Process Due Scheduled Messages

POST /api/v1/scheduled-messages/process
Authorization: Bearer <token>

Response (200 OK)

{
  "success": true,
  "processed": 3,
  "sent": 145,
  "failed": 2
}
DELETE/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.

GET/api/v1/analytics/summary?company_id=X&period=7d

Analytics 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
  }
}
GET/api/v1/analytics/batches?company_id=X&campaign_id=Y

List 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
}
GET/api/v1/analytics/batches/{id}

Get Message Batch

GET /api/v1/analytics/batches/batch_001
Authorization: Bearer <token>
POST/api/v1/analytics/batches

Create 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" }
}
PUT/api/v1/analytics/batches/{id}/counts

Update 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.

GET/api/v1/admin/shared-templates?type=X

List 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
}
POST/api/v1/admin/shared-templates

Create 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" }
}
GET/api/v1/admin/shared-templates/{id}

Get Shared Template

GET /api/v1/admin/shared-templates/tmpl_shared_001
Authorization: Bearer <token>
PUT/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\"}"
}
DELETE/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.

GET/api/v1/tags?company_id=X

List 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
}
POST/api/v1/tags

Create 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" }
}
PUT/api/v1/tags/{id}

Update Tag

PUT /api/v1/tags/tag_001
Authorization: Bearer <token>
Content-Type: application/json

{
  "name": "VIP Premium",
  "color": "#059669"
}
DELETE/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"
}
GET/api/v1/contacts/{contact_id}/tags

Get 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" }
  ]
}
POST/api/v1/contacts/{contact_id}/tags

Assign 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
}
GET/api/v1/tags/{id}/contacts?company_id=X

List Contacts by Tag

GET /api/v1/tags/tag_001/contacts?company_id=comp_abc
Authorization: Bearer <token>
DELETE/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.

POST/api/v1/contacts/duplicates/check

Check 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
}
GET/api/v1/contacts/duplicates?company_id=X

Find 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.

POST/api/v1/send/reaction

React to a Message

FieldTypeRequiredDescription
campaign_idstringYesCampaign slug
wa_number_idnumberYesWA number to send from
message_idstringYesThe wa_message_id of the message to react to
emojistringYesEmoji 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).

POST/api/v1/messages/read

Mark Message as Read

FieldTypeRequiredDescription
campaign_idstringYesCampaign slug
wa_number_idnumberYesWA number that received the message
message_idstringYesThe 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.

GET/api/v1/templates

List Templates

FieldTypeRequiredDescription
campaign_idstringYesCampaign slug (query parameter)
wa_number_idnumberYesWA 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
}
POST/api/v1/templates

Create Template

Submit a new template to Meta for approval. Templates typically take 24-48 hours to be reviewed. Status updates are received via webhook.

FieldTypeRequiredDescription
campaign_idstringYesCampaign slug
wa_number_idnumberYesWA number ID
namestringYesTemplate name (lowercase, underscores only)
categorystringYesUTILITY, MARKETING, or AUTHENTICATION
languagestringNoLanguage code (default: en_US)
componentsarrayYesTemplate 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"
}
PUT/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."
}
POST/api/v1/templates/sync

Sync 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"
}
DELETE/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.

POST/api/v1/media/upload

Upload Media File

FieldTypeRequiredDescription
filebinaryYesThe media file (multipart/form-data)
campaign_idstringYesCampaign slug (form field)
wa_number_idstringYesWA 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.

GET/api/v1/profile

Get Business Profile

FieldTypeRequiredDescription
campaign_idstringYesCampaign slug (query parameter)
wa_number_idnumberYesWA 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://..."
  }
}
PUT/api/v1/profile

Update Business Profile

FieldTypeRequiredDescription
campaign_idstringYesCampaign slug
wa_number_idnumberYesWA number ID
aboutstringNoShort business description (139 chars max)
addressstringNoBusiness address
descriptionstringNoFull business description (512 chars max)
emailstringNoBusiness email
verticalstringNoBusiness category (AUTO, BEAUTY, RETAIL, etc.)
websitesarrayNoUp 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

HTTPCodeMeaning
400VALIDATION_ERRORMissing or invalid fields
400JSON_SYNTAX_ERRORMalformed JSON body
401AUTH_MISSING_TOKENNo Authorization header
401AUTH_INVALID_TOKENToken is malformed or tampered
401AUTH_TOKEN_EXPIREDToken expired — re-authenticate
401AUTH_INVALID_CREDENTIALSWrong username or password
404WA_NUMBER_NOT_FOUNDcampaign_id + wa_number_id doesn't match a credential
400WA_NUMBER_INACTIVEWA 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.

TierDaily Limit
Initial250 conversations
Tier 11,000 conversations
Tier 210,000 conversations
Tier 3100,000 conversations
Tier 4Unlimited

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

MethodEndpointAuth
GET/healthNo
POST/auth/loginNo
POST/api/v1/send/textBearer
POST/api/v1/send/imageBearer
POST/api/v1/send/videoBearer
POST/api/v1/send/documentBearer
POST/api/v1/send/audioBearer
POST/api/v1/send/locationBearer
POST/api/v1/send/interactiveBearer
POST/api/v1/send/templateBearer
POST/api/v1/send/reactionBearer
POST/api/v1/messages/readBearer
GET/api/v1/templatesBearer
POST/api/v1/templatesBearer
POST/api/v1/templates/syncBearer
PUT/api/v1/templates/{template_id}Bearer
DELETE/api/v1/templates/{name}Bearer
POST/api/v1/media/uploadBearer
GET/api/v1/profileBearer
PUT/api/v1/profileBearer
GET/api/v1/companiesBearer
GET/api/v1/campaignsBearer
GET/api/v1/contacts/listsBearer
POST/api/v1/contacts/listsBearer
GET/api/v1/contacts/lists/{id}Bearer
PUT/api/v1/contacts/lists/{id}Bearer
DELETE/api/v1/contacts/lists/{id}Bearer
GET/api/v1/contacts/lists/{id}/contactsBearer
POST/api/v1/contacts/lists/{id}/contactsBearer
POST/api/v1/contacts/lists/{id}/contacts/importBearer
GET/api/v1/contacts/lists/{id}/contacts/exportBearer
PUT/api/v1/contacts/lists/{id}/contacts/{contact_id}Bearer
DELETE/api/v1/contacts/lists/{id}/contacts/{contact_id}Bearer
GET/api/v1/saved-messagesBearer
POST/api/v1/saved-messagesBearer
GET/api/v1/saved-messages/{id}Bearer
PUT/api/v1/saved-messages/{id}Bearer
DELETE/api/v1/saved-messages/{id}Bearer
GET/api/v1/scheduled-messagesBearer
POST/api/v1/scheduled-messagesBearer
GET/api/v1/scheduled-messages/{id}Bearer
PUT/api/v1/scheduled-messages/{id}Bearer
DELETE/api/v1/scheduled-messages/{id}Bearer
POST/api/v1/scheduled-messages/processBearer
GET/api/v1/analytics/summaryBearer
GET/api/v1/analytics/batchesBearer
POST/api/v1/analytics/batchesBearer
GET/api/v1/analytics/batches/{id}Bearer
PUT/api/v1/analytics/batches/{id}/countsBearer
GET/api/v1/tagsBearer
POST/api/v1/tagsBearer
PUT/api/v1/tags/{id}Bearer
DELETE/api/v1/tags/{id}Bearer
GET/api/v1/tags/{id}/contactsBearer
GET/api/v1/contacts/{contact_id}/tagsBearer
POST/api/v1/contacts/{contact_id}/tagsBearer
DELETE/api/v1/contacts/{contact_id}/tags/{tag_id}Bearer
POST/api/v1/contacts/duplicates/checkBearer
GET/api/v1/contacts/duplicatesBearer
GET/api/v1/client/messagesBearer
GET/api/v1/client/incomingBearer
GET/api/v1/admin/shared-templatesBearer
POST/api/v1/admin/shared-templatesBearer
GET/api/v1/admin/shared-templates/{id}Bearer
PUT/api/v1/admin/shared-templates/{id}Bearer
DELETE/api/v1/admin/shared-templates/{id}Bearer
GET/webhook/metaToken*
POST/webhook/metaNo**

* 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.