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" }
    }
  }
}

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" }.
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.

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.

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.