API Reference · v1
WapiSnap REST API
Send WhatsApp messages from any system over plain HTTP. Per-workspace API keys, JSON request/response, async send with status polling. Base URL:https://api.wapisnap.com.
Authentication
Every request to /v1/* must include an Authorization: Bearer header. Keys are scoped to a single workspace and are issued from Settings → API Keys in the dashboard. The plaintext value appears only at creation time; we store a bcrypt hash + the last 8 chars for display.
Authorization: Bearer wapi_live_a1B2c3D4e5F6g7H8i9J0kLmNoPqRsTuVEvery authenticated request lands in the workspace's request log, viewable at Settings → API Keys → View logs. Useful for confirming a call arrived as expected, debugging non-2xx responses, and correlating the request_id from an error response back to its origin.
Send a message
POST /v1/messages: enqueues a send and returns immediately with a message id. The actual Meta call happens out of band; poll GET /v1/messages/{id} for the final status. Three body shapes are supported.
Text message
POST /v1/messages HTTP/1.1
Host: api.wapisnap.com
Authorization: Bearer wapi_live_...
Content-Type: application/json
{
"to": "+919876543210",
"type": "text",
"text": "Your appointment is confirmed for tomorrow at 11 AM."
}202 with status: "queued", and the rejection only shows up when you poll GET /v1/messages/{id} and see status: "failed". To message a contact outside the window, use an approved template message instead.Interactive message
Three subtypes, matching the dashboard flow builder's Send Interactive node: button (1-3 reply buttons), list (up to 10 rows total across sections), and cta_url (a single button that opens a URL). header and footer are optional plain text on all three. Like text messages, this is a free-form send and subject to the same 24-hour window above.
POST /v1/messages HTTP/1.1
Host: api.wapisnap.com
Authorization: Bearer wapi_live_...
Content-Type: application/json
{
"to": "+919876543210",
"type": "interactive",
"interactive": {
"type": "button",
"body": "Confirm your order?",
"buttons": [
{ "id": "yes", "title": "Yes" },
{ "id": "no", "title": "No" }
]
}
}POST /v1/messages HTTP/1.1
Host: api.wapisnap.com
Authorization: Bearer wapi_live_...
Content-Type: application/json
{
"to": "+919876543210",
"type": "interactive",
"interactive": {
"type": "list",
"body": "Pick a delivery slot",
"button_label": "View slots",
"sections": [
{
"title": "Morning",
"rows": [
{ "id": "slot-1", "title": "9 AM", "description": "First slot" }
]
}
]
}
}POST /v1/messages HTTP/1.1
Host: api.wapisnap.com
Authorization: Bearer wapi_live_...
Content-Type: application/json
{
"to": "+919876543210",
"type": "interactive",
"interactive": {
"type": "cta_url",
"body": "Track your order",
"cta": { "display_text": "Track order", "url": "https://example.com/track/123" }
}
}Template message
The template must be APPROVED on Meta's side. Variables follow the grouped shape: body, header, button_url, button_otp. For templates containing a FLOW button, WapiSnap mints the flow_token server-side, you do not pass it.
POST /v1/messages HTTP/1.1
Host: api.wapisnap.com
Authorization: Bearer wapi_live_...
Content-Type: application/json
{
"to": "+919876543210",
"type": "template",
"template": {
"name": "application_followup_new_lead",
"language": "en",
"variables": {
"body": { "1": "Sam" }
}
}
}Image, video and document headers
If the template's header is an image, video or document (its HEADER component has format IMAGE, VIDEO or DOCUMENT in GET /v1/templates), pass the file for this send as header_media_url. The header variable group is only for text headers.
POST /v1/messages HTTP/1.1
Host: api.wapisnap.com
Authorization: Bearer wapi_live_...
Content-Type: application/json
{
"to": "+919876543210",
"type": "template",
"template": {
"name": "diwali_offer",
"language": "en",
"variables": {
"body": { "1": "Sam" }
},
"header_media_url": "https://cdn.example.com/diwali.jpg"
}
}https link: Meta downloads the file when the message is sent, so expiring signed URLs can fail on retries. Without a usable URL the request returns 400 invalid_request with param: "template.header_media_url" and nothing is queued. Document headers show the file name from the URL.Authentication (OTP) templates
Templates in the AUTHENTICATION category have a Meta-generated body (no custom header/footer text) and, if the template has a Copy Code button, require the same code in two places: the body {{1}} and a separate button_otp group, index-keyed like button_url. Only COPY_CODE-type buttons are supported today.
POST /v1/messages HTTP/1.1
Host: api.wapisnap.com
Authorization: Bearer wapi_live_...
Content-Type: application/json
{
"to": "+919876543210",
"type": "template",
"template": {
"name": "login_otp",
"language": "en",
"variables": {
"body": { "1": "482913" },
"button_otp": { "0": "482913" }
}
}
}button_otp entirely; only body is required.Response
HTTP/1.1 202 Accepted
Content-Type: application/json
{
"id": "cmqzr2pqj8009pyxcwhwz",
"status": "queued",
"to": "+919876543210",
"type": "template",
"conversation_id": "cmqzr1a5g0002pyxc9k2jz1x",
"meta_message_id": null,
"error": null,
"created_at": "2026-06-29T15:30:00.000Z"
}Request body
| Field | Type | Description |
|---|---|---|
to Required | string | Recipient phone in E.164 format (e.g. +919876543210). |
type Required | "text" | "template" | "interactive" | Message type. Determines which of the body fields below is read. |
text Conditional | string | Required when type="text". 1–4096 chars. |
template.name Conditional | string | Required when type="template". Must match an APPROVED template. |
template.language Conditional | string | Language code (e.g. en, en_US). Defaults to en. |
template.variables Optional | object | Grouped map. Keys: body, header, button_url, button_otp. Each holds a positional (or, for buttons, 0-based index) map: { "1": "Sam" }. |
template.header_media_url Conditional | string | Required when the template has an image, video or document header. Public https URL of the file to send, up to 2000 chars. Ignored for other templates. |
interactive.type Conditional | "button" | "list" | "cta_url" | Required when type="interactive". Picks which of the fields below apply. |
interactive.body Conditional | string | Required when type="interactive". 1-1024 chars. |
interactive.header / .footer Optional | string | Plain text, all subtypes. 1-60 chars each. |
interactive.buttons Conditional | array | Required for button. 1-3 items, each { "id", "title" } (title up to 20 chars). |
interactive.button_label / .sections Conditional | string / array | Required for list. sections is [{ "title", "rows": [{ "id", "title", "description"? }] }], up to 10 rows total across all sections. |
interactive.cta Conditional | object | Required for cta_url. { "display_text", "url" }. |
Get message status
GET /v1/messages/{id}: returns the current status of a previously-sent message.
HTTP/1.1 200 OK
Content-Type: application/json
{
"id": "cmqzr2pqj8009pyxcwhwz",
"status": "delivered",
"to": "+919876543210",
"type": "template",
"conversation_id": "cmqzr1a5g0002pyxc9k2jz1x",
"meta_message_id": "wamid.HBgMOTE5OTQ5MDIyODIzFQ...",
"error": null,
"created_at": "2026-06-29T15:30:00.000Z"
}Status values
| Value | Meaning |
|---|---|
| queued | Accepted by WapiSnap, not yet sent to Meta. |
| sent | Meta acknowledged the send. |
| delivered | Delivered to the recipient handset. |
| read | Read by the recipient. |
| failed | Send failed; see the `error` field on the message. |
List templates
GET /v1/templates: returns every message template on the workspace's WhatsApp Business Account. Both query parameters are optional; omit them to list everything.
Query parameters
| Name | Values | Description |
|---|---|---|
| status | APPROVED, PENDING, REJECTED, PAUSED, DISABLED | Filter by template lifecycle status. Only APPROVED templates can be sent. |
| category | UTILITY, MARKETING, AUTHENTICATION | Filter by Meta's classification. Meta may reclassify at any time. |
GET /v1/templates?status=APPROVED&category=UTILITY HTTP/1.1
Host: api.wapisnap.com
Authorization: Bearer wapi_live_...HTTP/1.1 200 OK
Content-Type: application/json
{
"data": [
{
"template_id": "1570724521119439",
"name": "service_down_alert_v1",
"language": "en_US",
"status": "APPROVED",
"category": "UTILITY",
"components": [
{ "type": "HEADER", "format": "TEXT", "text": "Service outage detected" },
{
"type": "BODY",
"text": "A service you monitor stopped responding at {{1}}.\n\nEndpoint: {{2}}\nStatus: {{3}}\nDuration so far: {{4}}\n\nThe on-call team has been notified."
},
{ "type": "FOOTER", "text": "Automated alert" },
{
"type": "BUTTONS",
"buttons": [
{ "type": "URL", "text": "View incident details", "url": "https://app.example.com/incidents/{{1}}" }
]
}
],
"quality_score": "GREEN"
}
]
}Meta is the source of truth: each call fans out to the Graph API. If you poll, cache aggressively on your side; the per-workspace rate limit is shared with message sends.
Track an event
POST /v1/events: record something a customer did. Events show on the customer's timeline and can start, advance or end journeys. The call returns 202 once the event is stored; identity resolution and journeys run asynchronously, usually within seconds.
POST /v1/events HTTP/1.1
Host: api.wapisnap.com
Authorization: Bearer wapi_live_...
Content-Type: application/json
{
"event": "order_placed",
"event_id": "order_placed:INK-1042",
"occurred_at": "2026-09-24T10:15:00+05:30",
"phone": "9876543210",
"email": "asha@example.com",
"user_id": "cust_881",
"traits": { "first_name": "Asha", "city": "Thane" },
"consent": { "whatsapp": "opted_in" },
"properties": {
"order_id": "INK-1042",
"total": 1249,
"currency": "INR",
"payment_method": "COD",
"items": [{ "sku": "PEN-SET-12", "qty": 1, "price": 1249 }]
}
}HTTP/1.1 202 Accepted
{ "id": "cmuf4byfp000ipmu00p0vup0y", "duplicate": false, "status": "accepted" }| Field | Rules |
|---|---|
| event | Required. lower_snake_case, up to 64 characters. message_received and button_clicked are reserved. |
| event_id | Recommended. Your unique id for this occurrence, up to 128 characters. Repeating an event_id is ignored, so retries are safe. |
| occurred_at | ISO 8601. Defaults to now; at most 5 minutes ahead and 1 year back. Events older than 24 hours are stored but never start journeys. |
| phone / email / user_id | At least one. Phones without a country code are read as the workspace's default country. |
| properties | JSON object up to 16 KB, keys in lower_snake_case, nested up to 5 levels. |
| traits | Facts about the customer: flat object, up to 50 keys and 8 KB. |
| consent | { "whatsapp" | "email": "opted_in" | "opted_out" }. Stored per phone and per address. |
consent.whatsapp = "opted_in" only when the customer actually agreed to receive WhatsApp messages. Marketing-category templates in journeys are sent only to opted-in numbers by default.Identify a customer
POST /v1/profiles: attach identifiers, traits and consent to a customer without recording an event. Same identity fields as events. Identifiers sent together in one call belong to one person; if they currently belong to different profiles, those profiles are merged. Profiles with different user_ids are never merged, and a merge that would leave a profile with more than three phones or emails is not applied (the response reports conflict).
POST /v1/profiles HTTP/1.1
Host: api.wapisnap.com
Authorization: Bearer wapi_live_...
Content-Type: application/json
{ "phone": "9876543210", "consent": { "whatsapp": "opted_out" } }HTTP/1.1 200 OK
{ "id": "clx...", "created": false, "merged_profile_ids": [], "conflict": null }Errors
Every error response carries the same envelope. request_id is the value to share with us when reporting an issue, it correlates to server-side logs.
HTTP/1.1 401 Unauthorized
Content-Type: application/json
{
"error": {
"code": "unauthorized",
"message": "Invalid or revoked API key.",
"type": "authentication_error",
"request_id": "req_a1b2c3d4e5f6g7h8"
}
}HTTP/1.1 400 Bad Request
Content-Type: application/json
{
"error": {
"code": "invalid_request",
"message": "Field 'to' must be in E.164 format (e.g. +919876543210).",
"type": "validation_error",
"param": "to",
"request_id": "req_a1b2c3d4e5f6g7h8"
}
}Common codes
| Code | HTTP | Meaning |
|---|---|---|
| unauthorized | 401 | Missing, invalid, or revoked API key. |
| forbidden | 403 | The key is valid but lacks permission for this operation. |
| not_found | 404 | The referenced resource does not exist or belongs to a different workspace. |
| invalid_request | 400 | Payload failed validation; check the `param` field for the offending field. |
| rate_limited | 429 | Workspace exceeded the per-second cap. Honor the Retry-After header. |
| template_not_found | 404 | No approved template with that name + language exists in this workspace. |
| template_not_approved | 400 | The template exists but is in PENDING or REJECTED state. |
| no_wa_number | 400 | The workspace has no WhatsApp number configured. |
| contact_marketing_throttled | 409 | Smart-retry has temporarily blocked marketing sends to this contact (Meta 131049 protection). |
| internal_error | 500 | Server-side failure. The `request_id` lets us trace it in logs. |
Rate limits
The default cap is 20 requests per second sustained, with bursts up to 30, per workspace. Requests beyond the burst budget receive 429 rate_limited with a Retry-After header in seconds. Need higher? Get in touch. Events have their own, higher cap of 100 requests per second per workspace, and /v1/profiles allows 30 per second.
Versioning
The current version is v1, locked under the /v1 URL prefix. We will never make breaking changes to v1; additive fields (new optional request fields, new response fields) may land at any time. Future major versions will live alongside v1 under /v2 with at least 6 months of overlap.
Roadmap
- v1.1: Idempotency keys (client-supplied), outbound webhooks for status + inbound replies.
- v1.2: Media attachments (image, video, document, audio).
- v1.3: Dedicated
POST /v1/flows/{id}/triggerfor standalone flow sends. - v2.x: Conversation list / inbox read endpoints.