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 header
Authorization: Bearer wapi_live_a1B2c3D4e5F6g7H8i9J0kLmNoPqRsTuV
Treat API keys like passwords. Anyone holding the key can send messages from your workspace. Rotate by issuing a new key and revoking the old one; revocation is instant.

Every 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

Request
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."
}
Free-form text only delivers inside WhatsApp's 24-hour customer service window, i.e. within 24 hours of the contact's last inbound message. Outside that window Meta rejects the send. WapiSnap does not pre-check this: the request still returns 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.

Button request
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" }
    ]
  }
}
List request
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" }
        ]
      }
    ]
  }
}
CTA URL request
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.

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

Request
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"
  }
}
The URL must be a public 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.

Request
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" }
    }
  }
}
If the template has no button, omit button_otp entirely; only body is required.

Response

202 Accepted
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

FieldTypeDescription
to
Required
stringRecipient 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
stringRequired when type="text". 1–4096 chars.
template.name
Conditional
stringRequired when type="template". Must match an APPROVED template.
template.language
Conditional
stringLanguage code (e.g. en, en_US). Defaults to en.
template.variables
Optional
objectGrouped 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
stringRequired 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
stringRequired when type="interactive". 1-1024 chars.
interactive.header / .footer
Optional
stringPlain text, all subtypes. 1-60 chars each.
interactive.buttons
Conditional
arrayRequired for button. 1-3 items, each { "id", "title" } (title up to 20 chars).
interactive.button_label / .sections
Conditional
string / arrayRequired for list. sections is [{ "title", "rows": [{ "id", "title", "description"? }] }], up to 10 rows total across all sections.
interactive.cta
Conditional
objectRequired for cta_url. { "display_text", "url" }.

Get message status

GET /v1/messages/{id}: returns the current status of a previously-sent message.

200 OK
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

ValueMeaning
queuedAccepted by WapiSnap, not yet sent to Meta.
sentMeta acknowledged the send.
deliveredDelivered to the recipient handset.
readRead by the recipient.
failedSend 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

NameValuesDescription
statusAPPROVED, PENDING, REJECTED, PAUSED, DISABLEDFilter by template lifecycle status. Only APPROVED templates can be sent.
categoryUTILITY, MARKETING, AUTHENTICATIONFilter by Meta's classification. Meta may reclassify at any time.
Request
GET /v1/templates?status=APPROVED&category=UTILITY HTTP/1.1
Host: api.wapisnap.com
Authorization: Bearer wapi_live_...
200 OK
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.

Request
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 }]
  }
}
Response
HTTP/1.1 202 Accepted

{ "id": "cmuf4byfp000ipmu00p0vup0y", "duplicate": false, "status": "accepted" }
FieldRules
eventRequired. lower_snake_case, up to 64 characters. message_received and button_clicked are reserved.
event_idRecommended. Your unique id for this occurrence, up to 128 characters. Repeating an event_id is ignored, so retries are safe.
occurred_atISO 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_idAt least one. Phones without a country code are read as the workspace's default country.
propertiesJSON object up to 16 KB, keys in lower_snake_case, nested up to 5 levels.
traitsFacts 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.
Send 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).

Request
POST /v1/profiles HTTP/1.1
Host: api.wapisnap.com
Authorization: Bearer wapi_live_...
Content-Type: application/json

{ "phone": "9876543210", "consent": { "whatsapp": "opted_out" } }
Response
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.

401 Unauthorized
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"
  }
}
400 Bad Request
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

CodeHTTPMeaning
unauthorized401Missing, invalid, or revoked API key.
forbidden403The key is valid but lacks permission for this operation.
not_found404The referenced resource does not exist or belongs to a different workspace.
invalid_request400Payload failed validation; check the `param` field for the offending field.
rate_limited429Workspace exceeded the per-second cap. Honor the Retry-After header.
template_not_found404No approved template with that name + language exists in this workspace.
template_not_approved400The template exists but is in PENDING or REJECTED state.
no_wa_number400The workspace has no WhatsApp number configured.
contact_marketing_throttled409Smart-retry has temporarily blocked marketing sends to this contact (Meta 131049 protection).
internal_error500Server-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}/trigger for standalone flow sends.
  • v2.x: Conversation list / inbox read endpoints.