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.sentmessage.deliveredmessage.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: replyreply_buttons[] of { id, title } (1–3 items; title ≤ 20 chars, unique). cta_urlurl_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)
searchSearch 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)
roleFilter: owner, admin, manager, agent
statusactiveFilter: active or deactivated
searchMatch 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
urlHTTPS URL to receive notifications
secretOptional 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)