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
| Item | Value |
|---|---|
| Base URL | https://server.waplify.io |
| Auth Header | Authorization: Bearer wapl_your_api_key |
| Content-Type | application/json |
| Rate Limit | 100 requests/minute per API key |
| Phone Format | Country 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"
}
| Field | Required | Description |
|---|---|---|
template_name | Yes | Template name (case-sensitive) |
contact_phone | Yes | With country code, no + |
contact_name | No | Defaults to "User #random" |
body_data | No | Template body variables (required if template has {{1}} etc.) |
header_data | No | For TEXT header variables only |
media_url | Conditional | Required if template has IMAGE/VIDEO/DOCUMENT header |
filename | No | For document templates |
waba_phone_id | No | Defaults 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.failedwebhook - 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"
}
| Field | Required | Description |
|---|---|---|
contact_phone | Yes | With country code, no + |
contact_name | No | Defaults to "User #random" |
message_type | Yes | text, image, video, audio, or document |
message | For text | Text message body (max 4,096 chars) |
media_url | For media | Required for image/video/audio/document |
caption | No | Caption for image/video/document |
filename | No | For document type |
waba_phone_id | No | Defaults to user's primary number |
buttons | No | Interactive 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
}
| Field | Required | Description |
|---|---|---|
name | Yes | Lowercase alphanumeric + underscores only |
category | Yes | MARKETING, UTILITY, or AUTHENTICATION |
language | No | Defaults to en_US |
waba_phone_id | No | Target WABA phone ID. Falls back to the first number linked to your account. |
header | No | { format, text?, text_example? }. Formats: TEXT/IMAGE/VIDEO/DOCUMENT/LOCATION |
media_url | Conditional | Required if header format is IMAGE/VIDEO/DOCUMENT — public URL, downloaded and uploaded to Meta |
body | Conditional | Required for MARKETING/UTILITY. { text, examples? }. Max 1024 chars. |
footer | No | { text } (max 60 chars, no variables) |
buttons | No | Up to 10. Types: QUICK_REPLY (max 10), URL (max 2), PHONE_NUMBER (max 1), COPY_CODE (max 1), FLOW |
limited_time_offer | No | MARKETING-only. Requires offer_code + COPY_CODE button at index 0, no footer, body ≤ 600 chars |
offer_code | Conditional | Required for LTO templates (max 15 chars) |
add_security_recommendation | No | AUTHENTICATION only — adds security text |
code_expiration_minutes | No | AUTHENTICATION only (1–90) |
message_send_ttl_seconds | No | TTL 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)
5. Create Carousel Template
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"] }
]
}
| Field | Required | Description |
|---|---|---|
name | Yes | Campaign name |
template_name | Yes | Must be APPROVED and owned by the user |
contacts | Yes | 1–10,000 entries { phone, name? }. Phones normalized + upserted. |
body_data | No | Applied to every recipient (same format as Send Template) |
header_data | No | TEXT-header variables |
media_url | Conditional | Required if template has IMAGE/VIDEO/DOCUMENT header |
filename | No | For DOCUMENT media |
waba_phone_id | No | Falls back to template's, then user's first |
scheduled_at | No | ISO 8601. Omit to create as draft. |
scheduled_timezone | No | IANA string, e.g. Asia/Kolkata |
expiration_time_ms | Conditional | Required for limited-time offer templates |
carousel_data | Conditional | Required 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) orscheduled(auto-sends atscheduled_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"
}
| Field | Meaning |
|---|---|
campaign_status | draft, scheduled, running, completed, cancelled, failed |
sent_count | WhatsApp accepted the send |
delivered_count | Reached the contact's phone |
read_count | Contact opened the message |
clicked_count | Contact clicked a URL / quick-reply button |
failed_count | Send/delivery failed at any stage |
replied_count | Contact sent a reply back |
success_count | total_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
| Param | Default | Description |
|---|---|---|
page | 1 | Page number |
limit | 20 | Per 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.
Webhooks
Configuration
Register endpoints at Developers > Webhooks in the dashboard.
| Setting | Default | Description |
|---|---|---|
url | — | HTTPS URL to receive notifications |
secret | — | Optional HMAC-SHA256 signing secret |
subscribed_events | all | Which events to receive |
retry_count | 3 | Max retries (0–10) |
timeout | 5s | Request timeout (1–30s) |
Events
| Event | When |
|---|---|
message.sent | Message accepted by WhatsApp |
message.delivered | Delivered to contact's phone |
message.read | Read by contact |
message.failed | Delivery failed |
button.clicked | Button interaction |
message.received | New inbound message |
message.reply | Reply to your outbound message |
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"
}
}
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)