Skip to main content

Waplify API — Complete Reference

This page contains every endpoint, request/response schema, and example in one place. Copy it and paste into any AI tool to generate integration code instantly.

AI-ready API reference

Copy this page and paste into ChatGPT, Claude, or Cursor to generate integration code

Setup​

ItemValue
Base URLhttps://server.waplify.io
Auth HeaderAuthorization: Bearer wapl_your_api_key
Content-Typeapplication/json
Rate Limit100 requests/minute per API key
Phone FormatCountry code + number, no + sign (e.g., 911234567890)

1. Send Template Message​

Send a WhatsApp message using an approved template. Works anytime (no 24-hour window restriction).

POST /api/v1/messages/send

Request:

{
"template_name": "order_confirmation",
"contact_phone": "911234567890",
"contact_name": "John Doe",
"body_data": {
"1": "John",
"2": "Premium Plan",
"3": "99"
},
"header_data": {
"1": "Daily Report"
},
"media_url": "https://example.com/image.png",
"filename": "Report.pdf",
"waba_phone_id": "123456789"
}
FieldRequiredDescription
template_nameYesTemplate name (case-sensitive)
contact_phoneYesWith country code, no +
contact_nameNoDefaults to "User #random"
body_dataNoTemplate body variables (required if template has {{1}} etc.)
header_dataNoFor TEXT header variables only
media_urlConditionalRequired if template has IMAGE/VIDEO/DOCUMENT header
filenameNoFor document templates
waba_phone_idNoDefaults to user's primary number

Response (200):

{
"status": "success",
"message": "Message sent successfully",
"message_id": "wamid.HBgNOTE4MDMxMjM0NTY3OA==",
"contact_id": "507f1f77bcf86cd799439012",
"template_name": "order_confirmation",
"timestamp": "2026-06-15T10:00:00Z"
}

Errors: 400 (missing variables, invalid phone, template not approved, media required), 401 (invalid API key), 404 (template not found), 429 (rate limit)

IMPORTANT — "success" means accepted, not delivered:

  • A "status": "success" response means WhatsApp has accepted the message — it does NOT mean it was delivered
  • Actual delivery status arrives later via webhooks: message.sent → message.delivered → message.read
  • If delivery fails, you receive a message.failed webhook
  • You MUST set up webhooks to track actual delivery

Notes:

  • Contact auto-created if phone number doesn't exist
  • Variables can be positional ("1", "2") or named ("name", "amount")
  • Media limits: Image 5MB (JPEG/PNG), Video 16MB (MP4), Document 100MB (PDF/DOC/DOCX/PPT/PPTX/TXT)

2. Send Free-Form Message​

Send a text or media message without a template. Only works within 24-hour window (contact must have messaged you in last 24 hours).

POST /api/v1/messages/send-message

Request:

{
"contact_phone": "911234567890",
"contact_name": "John Doe",
"message_type": "text",
"message": "Hello! How can I help?",
"media_url": "https://example.com/file.pdf",
"caption": "Here is the document",
"filename": "Report.pdf",
"waba_phone_id": "123456789"
}
FieldRequiredDescription
contact_phoneYesWith country code, no +
contact_nameNoDefaults to "User #random"
message_typeYestext, image, video, audio, or document
messageFor textText message body (max 4,096 chars)
media_urlFor mediaRequired for image/video/audio/document
captionNoCaption for image/video/document
filenameNoFor document type
waba_phone_idNoDefaults to user's primary number
buttonsNoInteractive buttons object — text only. See below

Response (200):

{
"status": "success",
"message": "Message sent successfully",
"message_id": "wamid.HBgNOTE4MDMxMjM0NTY3OA==",
"contact_id": "507f1f77bcf86cd799439012",
"message_type": "text",
"timestamp": "2026-06-15T10:00:00Z"
}

Errors: 400 (invalid phone, missing message/media), 401 (invalid API key), 403 (24-hour window closed — use template endpoint instead), 429 (rate limit)

Note: 403 errors use detail field: {"detail": "Cannot send free-form message: ..."}

Media limits: Image 5MB (JPEG/PNG), Video 16MB (MP4), Audio 16MB (AAC/MP3/M4A/AMR/OGG), Document 100MB (PDF/DOC/DOCX/PPT/PPTX/TXT)

Interactive buttons: Add a buttons object to a text message — up to 3 reply buttons OR one website button (never both). Requires message_type: text, message ≤ 1,024 chars, and an open 24-hour window.

{
"contact_phone": "911234567890",
"message_type": "text",
"message": "Did this answer your question?",
"buttons": {
"type": "reply",
"reply_buttons": [
{ "id": "yes", "title": "Yes, thanks" },
{ "id": "no", "title": "No, I need help" }
]
}
}

buttons.type: reply → reply_buttons[] of { id, title } (1–3 items; title ≤ 20 chars, unique). cta_url → url_button of { display_text, url } (display_text ≤ 20 chars; url must start with http(s)://).


3. List Templates​

Get all WhatsApp message templates on the account (approved and otherwise).

GET /api/v1/templates/

Response (200):

{
"status": "success",
"templates": [
{
"id": "507f1f77bcf86cd799439012",
"name": "order_confirmation",
"category": "UTILITY",
"language": "en_US",
"status": "APPROVED",
"header_format": "IMAGE",
"body": "Hello {{1}}, your order {{2}} is confirmed for {{3}}.",
"requires_media": true,
"variable_count": 3,
"parameter_format": "POSITIONAL",
"is_carousel": false,
"created_at": "2026-06-15T10:00:00Z"
}
],
"total": 1
}

Key fields per template: status (APPROVED/PENDING/REJECTED), header_format (NONE/TEXT/IMAGE/VIDEO/DOCUMENT/LOCATION), requires_media (true = need media_url), body (parse {{1}}/{{name}} for variables), parameter_format (POSITIONAL or NAMED), is_carousel (true = use carousel_data when sending), carousel_cards (card definitions), limited_time_offer + offer_code (LTO templates need expiration_time_ms when sending).


4. Create Template​

Create a new text/media-header template and submit it to Meta. Use endpoint 5 for carousel templates.

POST /api/v1/templates/create

Request:

{
"name": "order_update",
"category": "UTILITY",
"language": "en_US",
"waba_phone_id": "123456789012345",
"header": { "format": "TEXT", "text": "Order Update" },
"media_url": "https://example.com/banner.jpg",
"body": {
"text": "Hello {{1}}, your order {{2}} has been shipped!",
"examples": { "1": "John", "2": "ORD-12345" }
},
"footer": { "text": "Thank you" },
"buttons": [
{ "type": "URL", "text": "Track", "url": "https://example.com/{{1}}", "url_example": "ORD-12345" },
{ "type": "QUICK_REPLY", "text": "Contact us" }
],
"limited_time_offer": { "text": "Limited offer", "has_expiration": true },
"offer_code": "FLASH40",
"add_security_recommendation": false,
"code_expiration_minutes": 10,
"message_send_ttl_seconds": 3600
}
FieldRequiredDescription
nameYesLowercase alphanumeric + underscores only
categoryYesMARKETING, UTILITY, or AUTHENTICATION
languageNoDefaults to en_US
waba_phone_idNoTarget WABA phone ID. Falls back to the first number linked to your account.
headerNo{ format, text?, text_example? }. Formats: TEXT/IMAGE/VIDEO/DOCUMENT/LOCATION
media_urlConditionalRequired if header format is IMAGE/VIDEO/DOCUMENT — public URL, downloaded and uploaded to Meta
bodyConditionalRequired for MARKETING/UTILITY. { text, examples? }. Max 1024 chars.
footerNo{ text } (max 60 chars, no variables)
buttonsNoUp to 10. Types: QUICK_REPLY (max 10), URL (max 2), PHONE_NUMBER (max 1), COPY_CODE (max 1), FLOW
limited_time_offerNoMARKETING-only. Requires offer_code + COPY_CODE button at index 0, no footer, body ≤ 600 chars
offer_codeConditionalRequired for LTO templates (max 15 chars)
add_security_recommendationNoAUTHENTICATION only — adds security text
code_expiration_minutesNoAUTHENTICATION only (1–90)
message_send_ttl_secondsNoTTL for messages sent with this template

Response (201):

{
"status": "success",
"message": "Template 'order_update' created and submitted for review",
"template_id": "507f1f77bcf86cd799439012",
"meta_template_id": "1234567890123456",
"meta_status": "PENDING"
}

Constraints:

  • Cannot mix numbered ({{1}}) and named ({{name}}) variables in the same template
  • WhatsApp domains (whatsapp.com, wa.me) are rejected in URL buttons
  • Templates start as PENDING; call endpoint 6 (sync) or endpoint 3 (list) to refresh approval status

Errors: 422 (validation — name format, mixed variables, missing body), 400 (media download fail, duplicate name), 401 (invalid key), 429 (rate limit)


Create a media-card carousel template (always MARKETING, 2–10 cards, same header format and button structure across cards).

POST /api/v1/templates/create-carousel

Request:

{
"name": "product_showcase",
"language": "en_US",
"waba_phone_id": "123456789012345",
"body": { "text": "Hi {{1}}, check out our top picks!", "examples": { "1": "John" } },
"cards": [
{
"header_format": "IMAGE",
"media_url": "https://example.com/product1.jpg",
"body_text": "Product 1 - Amazing.",
"buttons": [
{ "type": "URL", "text": "Shop", "url": "https://example.com/{{1}}", "url_example": "p1" },
{ "type": "QUICK_REPLY", "text": "More like this" }
]
},
{
"header_format": "IMAGE",
"media_url": "https://example.com/product2.jpg",
"body_text": "Product 2 - Best seller.",
"buttons": [
{ "type": "URL", "text": "Shop", "url": "https://example.com/{{1}}", "url_example": "p2" },
{ "type": "QUICK_REPLY", "text": "More like this" }
]
}
],
"message_send_ttl_seconds": 3600
}

Card rules: 2–10 cards; all cards must share the same header_format (IMAGE or VIDEO); all cards must have the same button types in the same order; either all or no cards carry body_text (max 160 chars each); each card needs 1–2 buttons.

Response (201): Same shape as Create Template (status, template_id, meta_template_id, meta_status).


6. Sync Templates with Meta​

Pull the latest template state from Meta into Waplify. Creates missing templates locally, updates changed ones, removes deleted ones. Runs across every WABA phone linked to the account.

POST /api/v1/templates/sync

No request body.

Response (200):

{
"status": "success",
"message": "Template sync completed",
"summary": {
"total_meta_templates": 12,
"synced": 12,
"created": 1,
"updated": 3,
"skipped": 8,
"deleted": 0
}
}

Use after: creating a template (to pick up Meta's APPROVED/REJECTED decision), or editing templates in the Meta Business Manager.


7. Create Campaign​

Create a WhatsApp campaign from a list of phone numbers using an approved template. Phone numbers are normalized + upserted as contacts automatically.

POST /api/v1/campaigns/

Request:

{
"name": "Summer Sale Blast",
"description": "Announce the summer sale",
"template_name": "summer_sale_promo",
"contacts": [
{ "phone": "911234567890", "name": "John Doe" },
{ "phone": "919876543210", "name": "Priya Singh" }
],
"body_data": { "1": "Loyalty member", "2": "40%" },
"header_data": { "1": "Summer" },
"media_url": "https://example.com/banner.jpg",
"filename": "promo.pdf",
"waba_phone_id": "123456789012345",
"scheduled_at": "2026-05-15T09:30:00Z",
"scheduled_timezone": "Asia/Kolkata",
"expiration_time_ms": 1209600000,
"carousel_data": [
{ "media_url": "https://example.com/c1.jpg", "url_button_params": ["p1"], "button_payloads": ["more-like-this"] },
{ "media_url": "https://example.com/c2.jpg", "url_button_params": ["p2"], "button_payloads": ["more-like-this"] }
]
}
FieldRequiredDescription
nameYesCampaign name
template_nameYesMust be APPROVED and owned by the user
contactsYes1–10,000 entries { phone, name? }. Phones normalized + upserted.
body_dataNoApplied to every recipient (same format as Send Template)
header_dataNoTEXT-header variables
media_urlConditionalRequired if template has IMAGE/VIDEO/DOCUMENT header
filenameNoFor DOCUMENT media
waba_phone_idNoFalls back to template's, then user's first
scheduled_atNoISO 8601. Omit to create as draft.
scheduled_timezoneNoIANA string, e.g. Asia/Kolkata
expiration_time_msConditionalRequired for limited-time offer templates
carousel_dataConditionalRequired for carousel templates; one entry per card, in order

Response (200):

{
"status": "success",
"message": "Campaign created successfully",
"campaign_id": "507f1f77bcf86cd799439060",
"name": "Summer Sale Blast",
"status_value": "draft",
"total_contacts": 2,
"invalid_phones": [],
"timestamp": "2026-06-15T10:00:00Z"
}
  • status_value: draft (needs manual send) or scheduled (auto-sends at scheduled_at)
  • invalid_phones: phones that failed normalization and were skipped
  • Duplicate phones are deduplicated automatically

Not supported via this endpoint: ORDER_DETAILS and FLOW buttons; per-contact variable overrides (body_data is applied to every recipient).

Errors: 404 (template not found), 400 (template not APPROVED, missing body variables, media unreachable, no valid phones, LTO needs expiration_time_ms), 403 (no access to WABA phone), 429 (rate limit)


8. Send Campaign​

Run prechecks on a draft campaign and queue it in the background worker. Asynchronous — returns immediately with a task ID.

POST /api/v1/campaigns/{campaign_id}/send

No body.

Response (202):

{
"status": "accepted",
"message": "Campaign send queued and will run in background",
"campaign_id": "507f1f77bcf86cd799439060",
"task_id": "f5a1f9d4-2c5c-4c9f-9d1b-3e2d1a1f9d4f",
"timestamp": "2026-06-15T10:00:00Z"
}

When to call: only for draft campaigns. Scheduled campaigns auto-send at scheduled_at and don't need this endpoint.

Errors: 400 (invalid campaign_id, precheck failed — template no longer approved, insufficient credits, etc.), 404 (campaign not found), 429 (rate limit)

After queueing: track per-message delivery via webhooks (message.sent, message.delivered, message.read, message.failed) and aggregate stats via endpoint 9.


9. Campaign Stats​

Return aggregate per-contact delivery stats for a campaign.

GET /api/v1/campaigns/{campaign_id}/stats

Response (200):

{
"status": "success",
"campaign_id": "507f1f77bcf86cd799439060",
"name": "Summer Sale Blast",
"campaign_status": "completed",
"total_contacts": 2000,
"sent_count": 1998,
"delivered_count": 1932,
"read_count": 1204,
"clicked_count": 312,
"failed_count": 2,
"replied_count": 41,
"success_count": 1998,
"created_at": "2026-06-15T10:00:00Z",
"scheduled_at": null,
"started_at": "2026-06-15T10:05:00Z",
"completed_at": "2026-06-15T10:45:00Z"
}
FieldMeaning
campaign_statusdraft, scheduled, running, completed, cancelled, failed
sent_countWhatsApp accepted the send
delivered_countReached the contact's phone
read_countContact opened the message
clicked_countContact clicked a URL / quick-reply button
failed_countSend/delivery failed at any stage
replied_countContact sent a reply back
success_counttotal_contacts - failed_count

Funnel: total_contacts → sent → delivered → read → clicked. Counters update continuously as the worker progresses.

Errors: 400 (invalid campaign_id), 404 (not found), 429 (rate limit)


10. List Contacts​

GET /api/v1/contacts/?page=1&limit=20&search=john

ParamDefaultDescription
page1Page number
limit20Per page (max 100)
search—Search name, phone, or email

Response (200):

{
"status": "success",
"contacts": [
{
"id": "507f1f77bcf86cd799439012",
"first_name": "John",
"last_name": "Doe",
"phone_number": "911234567890",
"email": "john@example.com",
"company": "Acme Corp",
"tags": ["vip"],
"opted_in": true,
"source": "api",
"created_at": "2026-06-15T10:00:00Z"
}
],
"total": 1,
"page": 1,
"limit": 20
}

11. Create Contact​

POST /api/v1/contacts/

Request:

{
"phone_number": "911234567890",
"first_name": "John",
"last_name": "Doe",
"email": "john@example.com",
"company": "Acme Corp",
"tags": ["vip", "newsletter"]
}

Fields: phone_number (required), first_name (required), last_name (optional), email (optional), company (optional), tags (optional).

Response (201):

{
"status": "success",
"message": "Contact created successfully",
"contact": { /* same fields as list response */ }
}

12. Get Contact​

GET /api/v1/contacts/{contact_id}

Response (200): Same as create contact response.


13. Delete Contact​

DELETE /api/v1/contacts/{contact_id}

Response (200):

{
"status": "success",
"message": "Contact deleted successfully",
"contact_id": "507f1f77bcf86cd799439012"
}

Note: Soft delete — contact deactivated, not permanently removed.


14. List Groups​

GET /api/v1/groups/?page=1&limit=20&search=vip

Response (200):

{
"status": "success",
"groups": [
{
"id": "507f1f77bcf86cd799439050",
"name": "VIP Customers",
"description": "High-value customers",
"tags": ["vip"],
"contact_count": 42,
"created_at": "2026-06-15T10:00:00Z"
}
],
"total": 1,
"page": 1,
"limit": 20
}

15. Create Group​

POST /api/v1/groups/

Request:

{
"name": "VIP Customers",
"description": "High-value customers",
"tags": ["vip"],
"contact_ids": ["507f1f77bcf86cd799439012"]
}

Fields: name (required, unique per account), description (optional), tags (optional), contact_ids (optional — add contacts immediately).

Response (201):

{
"status": "success",
"message": "Contact group created successfully",
"group": { /* same fields as list response */ }
}

16. Get Group​

GET /api/v1/groups/{group_id}


17. Delete Group​

DELETE /api/v1/groups/{group_id}

Note: Contacts are NOT deleted — only removed from the group.


18. Add Contacts to Group​

POST /api/v1/groups/{group_id}/contacts

Request:

{
"contact_ids": ["507f1f77bcf86cd799439012", "507f1f77bcf86cd799439013"]
}

Response (200):

{
"status": "success",
"message": "2 contact(s) added to group",
"group_id": "507f1f77bcf86cd799439050",
"contact_ids": ["507f1f77bcf86cd799439012", "507f1f77bcf86cd799439013"]
}

Duplicates are silently skipped.


19. Remove Contacts from Group​

DELETE /api/v1/groups/{group_id}/contacts

Request:

{
"contact_ids": ["507f1f77bcf86cd799439012"]
}

Contacts are NOT deleted from your account — only removed from this group.


20. List Agents​

List the team members you can assign conversations to. Returns each agent's user_id (used by endpoint 21).

GET /api/v1/agents/ (keep the trailing slash)

ParamDefaultDescription
page1Page number
limit50Per page (max 100)
role—Filter: owner, admin, manager, agent
statusactiveFilter: active or deactivated
search—Match name or email

Response (200):

{
"status": "success",
"agents": [
{
"user_id": "665f1c2e8a1b2c3d4e5f6a7b",
"full_name": "Priya Sharma",
"email": "priya@acme.com",
"role": "agent",
"status": "active"
}
],
"total": 1,
"page": 1,
"limit": 50
}

Errors: 400 (invalid role/status, or no organisation on the key), 401 (invalid key), 429 (rate limit)


21. Assign Conversation​

Assign or reassign the conversation behind a contact to an agent. Assignment belongs to the conversation (your WhatsApp number + the contact's phone), not the contact record.

POST /api/v1/conversations/{contact_phone}/assign

contact_phone is digits only, no +.

Request:

{
"user_id": "665f1c2e8a1b2c3d4e5f6a7b",
"note": "VIP lead from website form",
"waba_phone_id": "123456789012345"
}
FieldRequiredDescription
user_idYesAgent to assign to (from endpoint 20). Must be an active org member.
noteNoHandoff note (max 2000 chars), recorded on reassignment
waba_phone_idNoDisambiguates which WhatsApp number's conversation, for multi-number accounts

Response (200):

{
"status": "success",
"message": "Conversation assigned to Priya Sharma.",
"data": {
"phone_number": "12125551234",
"waba_phone_id": "123456789012345",
"status": null,
"assigned_to": "665f1c2e8a1b2c3d4e5f6a7b",
"assigned_to_name": "Priya Sharma",
"assigned_at": "2026-06-17T09:30:00.123456+00:00",
"is_reassignment": false,
"previous_assignee_id": null,
"previous_assignee_name": null,
"note": null,
"no_op": null
}
}
  • On a reassignment, is_reassignment is true and previous_assignee_id/previous_assignee_name/note are populated.
  • Assigning the same agent again is a no-op: data.no_op: true, nothing changes.
  • data.status is not populated here — use endpoint 24 for the live open/resolved status.
  • Fires a conversation.assigned webhook with actor.source: "api".

Errors: 400 (conversation is resolved — reopen first; invalid phone/user_id; no WhatsApp number configured), 401 (invalid key), 403 (waba_phone_id not in your org), 404 (user_id not an active org member), 422 (missing user_id), 429 (rate limit)


22. Unassign Conversation​

Release a conversation back to the unassigned pool. Idempotent.

POST /api/v1/conversations/{contact_phone}/unassign

Body optional: { "waba_phone_id": "..." }.

Response (200):

{
"status": "success",
"message": "Conversation unassigned.",
"data": {
"phone_number": "12125551234",
"waba_phone_id": "123456789012345",
"status": null,
"assigned_to": null,
"assigned_to_name": null,
"assigned_at": null,
"is_reassignment": null,
"previous_assignee_id": null,
"previous_assignee_name": null,
"note": null,
"no_op": null
}
}

Fires a conversation.unassigned webhook. Errors: 400 (invalid phone, no WhatsApp number), 401, 403 (waba_phone_id not in your org), 429


23. Reopen Conversation​

Reopen a resolved conversation. The API key's account becomes the new owner. Idempotent (reopening an open chat is a no-op, not an error). This is how you clear the 400 from endpoint 21 on a resolved chat.

POST /api/v1/conversations/{contact_phone}/reopen

Body optional: { "waba_phone_id": "..." }.

Response (200):

{
"status": "success",
"message": "Conversation reopened.",
"data": {
"phone_number": "12125551234",
"waba_phone_id": "123456789012345",
"status": "open",
"assigned_to": "701a2b3c4d5e6f7a8b9c0d1e",
"assigned_to_name": "API Key Owner",
"assigned_at": null,
"is_reassignment": null,
"previous_assignee_id": null,
"previous_assignee_name": null,
"note": null,
"no_op": null
}
}

Fires a conversation.reopened webhook. Errors: 400 (invalid phone, no WhatsApp number), 401, 403 (waba_phone_id not in your org), 429


24. Get Assignment State​

Read the current owner and status of a contact's conversation (read-before-write). Never returns 404 — a contact with no chat thread yet returns has_conversation: false.

GET /api/v1/conversations/{contact_phone}/assignment

Optional query param waba_phone_id (a query param here, not a body field).

Response (200):

{
"status": "success",
"data": {
"phone_number": "12125551234",
"waba_phone_id": "123456789012345",
"has_conversation": true,
"status": "open",
"assigned_to": "665f1c2e8a1b2c3d4e5f6a7b",
"assigned_to_name": "Priya Sharma",
"assigned_at": "2026-06-17T09:30:00.123456+00:00"
}
}

When no thread exists yet, has_conversation is false and status/assigned_* are null. Errors: 400 (invalid phone), 401, 403 (waba_phone_id not in your org), 429


Webhooks​

Configuration​

Register endpoints at Developers > Webhooks in the dashboard.

SettingDefaultDescription
url—HTTPS URL to receive notifications
secret—Optional HMAC-SHA256 signing secret
subscribed_eventsallWhich events to receive
retry_count3Max retries (0–10)
timeout5sRequest timeout (1–30s)

Events​

EventWhen
message.sentMessage accepted by WhatsApp
message.deliveredDelivered to contact's phone
message.readRead by contact
message.failedDelivery failed
button.clickedButton interaction
message.receivedNew inbound message
message.replyReply to your outbound message
conversation.assignedChat assigned/reassigned to an agent
conversation.unassignedChat released to the unassigned pool
conversation.resolvedChat marked resolved
conversation.reopenedResolved chat reopened

Payload — Message Status​

{
"event": "message.delivered",
"timestamp": "2026-06-15T10:05:00",
"campaign_id": "507f1f77bcf86cd799439060",
"campaign_name": "Summer Promo",
"message": {
"message_id": "507f1f77bcf86cd799439070",
"contact_phone": "911234567890",
"contact_name": "John Doe",
"status": "delivered",
"previous_status": "sent",
"status_timestamp": "2026-06-15T10:05:00"
}
}

For message.failed, adds: error_message (string), error_code (number).

Payload — Inbound Message​

{
"event": "message.received",
"timestamp": "2026-06-15T10:15:00",
"message": {
"message_id": "wamid.HBgNOTE4MDMxMjM0NTY3OQ==",
"contact_phone": "911234567890",
"contact_name": "John Doe",
"content": "Hi, I have a question",
"message_type": "text",
"timestamp": "2026-06-15T10:15:00",
"waba_phone_id": "123456789012345",
"is_reply": false,
"is_from_ad": false
}
}

Payload — Reply​

{
"event": "message.reply",
"timestamp": "2026-06-15T10:10:00",
"campaign_id": "507f1f77bcf86cd799439060",
"campaign_name": "Summer Promo",
"original_message": {
"message_id": "507f1f77bcf86cd799439070",
"contact_phone": "911234567890",
"contact_name": "John Doe",
"sent_at": "2026-06-15T10:00:00"
},
"reply": {
"message_id": "507f1f77bcf86cd799439071",
"contact_phone": "911234567890",
"contact_name": "John Doe",
"content": "Yes, I'd like to know more!",
"message_type": "text",
"timestamp": "2026-06-15T10:10:00"
}
}

Payload — Conversation Assignment​

Shared by conversation.assigned, conversation.unassigned, conversation.resolved, conversation.reopened. Timestamps are tz-aware (+00:00), unlike the message payloads above.

{
"event": "conversation.assigned",
"timestamp": "2026-06-17T10:05:00.123456+00:00",
"conversation": {
"contact_phone": "911234567890",
"waba_phone_id": "123456789012345",
"status": "open",
"assigned_to": "507f1f77bcf86cd799439011",
"assigned_to_name": "Priya Sharma",
"previous_assignee_id": "507f1f77bcf86cd799439022",
"previous_assignee_name": "Rahul Verma",
"is_reassignment": true,
"note": "Escalated from tier 1",
"reason": null,
"actor": {
"user_id": "507f1f77bcf86cd799439033",
"name": "Anita Desai",
"source": "manual"
}
}
}

actor.source is one of manual (inbox), api (assignment API), auto_reply, auto_inbound (round-robin), auto_reopen (customer reply). For automatic changes actor.user_id is the literal string "system". Use actor.source to avoid echo loops when your CRM writes assignments back via the API.

Signature Verification​

Header: X-Webhook-Signature: sha256=<hex_digest>

Verify: HMAC-SHA256(your_secret, raw_request_body) must match the hex digest after sha256=.

Retry Policy​

  • 2xx → success
  • 4xx (except 429) → not retried
  • 429/5xx/timeout → retried with exponential backoff (1s, 2s, 4s, 8s...)

Status Flow​

pending → accepted → sent → delivered → read → clicked
↘ failed

Delivery Headers​

Content-Type: application/json
User-Agent: Waplify-Webhook/1.0
X-Webhook-Signature: sha256=... (if secret configured)