Developers

Engage REST API

Send WhatsApp messages, sync contacts, launch broadcasts and receive signed webhooks from your own code. JSON in, JSON out, one API key per WhatsApp number.

Bearer auth

Authorization: Bearer engage_live_…

Scoped keys

Each key does only what you grant

Signed webhooks

HMAC-SHA256 with retries

Step-by-step guide

From zero to your first WhatsApp message and live webhook in eight steps. Follow them in order.

  1. 1

    Make sure API access is on

    The API and webhooks come with the Pulse and Orbit plans. On Spark, buy the API keys & webhooks add-on from Billing. You also need a WhatsApp number connected under Channels.

  2. 2

    Create an API key

    Go to Channels → your number → API tab → New API key (admins and owners only).

    • Name it after the integration that will use it.
    • Tick only the scopes it needs.
    • Copy the key now. It is shown once; we store only a hash. Lost it? Revoke and create a new one.
    A key is bound to one WhatsApp number. It can only see that number’s contacts, conversations and messages, and messages are sent from that number. Keep it on your server — never in a browser or mobile app.
  3. 3

    Verify the key

    Call GET /me. A valid key returns its account, channel and scopes.

    bash
    curl https://engage.scale360x.com/api/v1/me \
      -H "Authorization: Bearer engage_live_xxx"
    response
    {
      "data": {
        "account": { "id": "…", "name": "Acme Inc" },
        "key": { "id": "…", "scopes": ["messages:send", "contacts:write"] }
      }
    }

    A 401 means the key is wrong, revoked or expired.

  4. 4

    Add a contact (optional)

    Not required — sending a message creates the contact for you. Create one first when you want a name, email, company or tags on it. Phone numbers must be E.164 with a leading +.

    bash
    curl -X POST https://engage.scale360x.com/api/v1/contacts \
      -H "Authorization: Bearer engage_live_xxx" \
      -H "Content-Type: application/json" \
      -d '{ "phone": "+919876543210", "name": "Jane Doe", "tags": ["lead"] }'

    It is find-or-create by phone: 201 for a new contact, 200 if it already existed.

  5. 5

    Send your first message

    Needs the messages:send scope.

    bash
    curl -X POST https://engage.scale360x.com/api/v1/messages \
      -H "Authorization: Bearer engage_live_xxx" \
      -H "Content-Type: application/json" \
      -d '{ "to": "+919876543210", "type": "text", "text": "Hi 👋" }'
    WhatsApp only allows free-form text, images and documents within 24 hours of the customer’s last message. To start a conversation, or after the window closes, send an approved template (see Messages).
  6. 6

    Read conversations and replies

    List conversations with conversations:read, then fetch a conversation’s messages with messages:read — each has its direction and delivery status.

    bash
    curl "https://engage.scale360x.com/api/v1/conversations?status=open&limit=20" \
      -H "Authorization: Bearer engage_live_xxx"
    
    curl https://engage.scale360x.com/api/v1/conversations/<conversation_id>/messages \
      -H "Authorization: Bearer engage_live_xxx"
  7. 7

    Launch a broadcast (optional)

    Send an approved template to up to 1,000 recipients per request. Needs broadcasts:send.

    bash
    curl -X POST https://engage.scale360x.com/api/v1/broadcasts \
      -H "Authorization: Bearer engage_live_xxx" \
      -H "Content-Type: application/json" \
      -d '{
        "name": "July promo",
        "template_name": "promo_july",
        "template_language": "en_US",
        "recipients": [
          { "to": "+919876543210", "params": ["Jane"] },
          { "to": "+919876543211" }
        ]
      }'

    You get 202 immediately; poll GET /broadcasts/{id} for delivery progress.

  8. 8

    Receive events with a webhook

    Instead of polling, register an HTTPS endpoint (webhooks:manage). Save the secret from the response — it is shown once — and verify every signature.

    bash
    curl -X POST https://engage.scale360x.com/api/v1/webhooks \
      -H "Authorization: Bearer engage_live_xxx" \
      -H "Content-Type: application/json" \
      -d '{ "url": "https://example.com/hooks/engage", "events": ["message.received", "message.status_updated"] }'

Authentication

Every request carries an API key as a bearer token.

Request header
Authorization: Bearer engage_live_xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx

Base URL: https://engage.scale360x.com/api/v1. Keys are channel-scoped — they cannot reach other numbers or accounts. Revoke a key any time from Channels → your number → API tab; it stops working on its next request, and revoked keys stay listed as an audit trail.

Never commit a key or ship it to a browser. If one leaks, revoke it immediately.

Scopes

A key can do only what its scopes allow. Grant the minimum.

ScopeAllows
messages:sendSend WhatsApp messages
messages:readRead messages and delivery status
contacts:readList and read contacts
contacts:writeCreate and update contacts
conversations:readList and read conversations
broadcasts:sendLaunch and read broadcast campaigns
webhooks:manageRegister and manage outbound webhooks

A key with no scopes can still call GET /me.

Responses & errors

Two shapes only. Branch on error.code — it is stable; the message may be reworded.

success
{ "data": { … } }
failure
{ "error": { "code": "forbidden", "message": "This API key is missing the 'messages:send' scope" } }
StatuscodeMeaning
400bad_requestMalformed input (e.g. phone without a leading +)
400whatsapp_not_configuredThe channel has no working WhatsApp connection
401unauthorizedMissing, malformed, unknown, revoked or expired key
403forbiddenValid key, but missing the required scope
404not_foundNo such resource (or it belongs to another channel)
429rate_limitedPer-key rate limit exceeded
502meta_errorThe request reached Meta and Meta rejected it
500internalServer error — retry, then contact support

Rate limits

Limits apply per key and depend on your plan.

PlanRequests / minute
Pulse (and Spark with the API add-on)120
Orbit600

On 429, wait for Retry-After seconds. Every response also carries X-RateLimit-Limit, X-RateLimit-Remaining and X-RateLimit-Reset. Message sending speed to WhatsApp is governed separately by your plan’s per-number send speed.

Pagination

Every list endpoint uses stable keyset cursors.

paging
GET /api/v1/contacts?limit=50
→ { "data": [ … ], "meta": { "next_cursor": "eyJ…" } }

GET /api/v1/contacts?limit=50&cursor=eyJ…
→ { "data": [ … ], "meta": { "next_cursor": null } }   // last page

limit defaults to 50 (max 100). Pass the cursor back unchanged; null means you are done.

Account (me)

Verify a key and discover its account, channel and scopes. No scope needed.

GET/api/v1/me

Returns the account and channel the key is bound to, plus its scopes.

Messages

Send to an E.164 number — the contact and conversation are found or created for you.

POST/api/v1/messagesmessages:send

Send a text, template or media message.

  • to must start with + and the country code. Spaces and dashes are fine; a number without + is rejected.
  • type: text (default), template, or image / video / document / audio. Media needs media_url (optional filename); text becomes the caption.
  • reply_to_message_id (optional) must be a message in the same conversation.
template message
{
  "to": "+14155550123",
  "type": "template",
  "template": { "name": "order_update", "language": "en_US", "params": ["A123"] }
}
201 response
{
  "data": {
    "message_id": "…",
    "whatsapp_message_id": "wamid.…",
    "conversation_id": "…",
    "contact_id": "…",
    "contact_created": true
  }
}

Contacts

Contacts belong to the key's channel.

GET/api/v1/contactscontacts:read

List contacts, newest first. Filters: search (name or phone), tag (tag id).

POST/api/v1/contactscontacts:write

Find-or-create by phone. 201 when created, 200 when it already existed.

GET/api/v1/contacts/{id}contacts:read

Read one contact.

PATCH/api/v1/contacts/{id}contacts:write

Update only the fields you send. Pass tags (names) to replace the contact's tags.

contact object
{
  "id": "…", "phone": "+14155550123", "name": "Jane Doe",
  "email": null, "company": "Acme", "avatar_url": null,
  "tags": [{ "id": "…", "name": "vip", "color": "#3b82f6" }],
  "created_at": "…", "updated_at": "…"
}

Conversations

Read-only access to chats and their messages.

GET/api/v1/conversationsconversations:read

List conversations, newest first. Filters: status (open | pending | closed), contact_id.

GET/api/v1/conversations/{id}conversations:read

Read one conversation with its contact and tags.

GET/api/v1/conversations/{id}/messagesmessages:read

List a conversation's messages, newest first, with direction, status and content.

Broadcasts

Template campaigns, sent in the background.

POST/api/v1/broadcastsbroadcasts:send

Launch a template broadcast to up to 1,000 recipients per request.

GET/api/v1/broadcasts/{id}broadcasts:send

Status and counts. delivered_count and read_count keep rising as Meta reports.

Invalid numbers (including any without +) are dropped and counted as rejected; if none are valid the call fails with 400. The 202 response has broadcast_id, status (sending → sent), total_recipients, accepted and rejected. Split larger sends into multiple requests.

Webhooks

Get pushed events instead of polling. Manage endpoints with the webhooks:manage scope.

POST/api/v1/webhookswebhooks:manage

Register an endpoint. The response includes the signing secret exactly once.

GET/api/v1/webhookswebhooks:manage

List endpoints (the secret is never returned again).

GET/api/v1/webhooks/{id}webhooks:manage

Read one endpoint.

PATCH/api/v1/webhooks/{id}webhooks:manage

Update url, events or is_active. Re-enabling resets the failure counter.

DELETE/api/v1/webhooks/{id}webhooks:manage

Remove an endpoint.

Events

EventFires when
message.receivedAn inbound message arrives from a contact
message.status_updatedA message you sent changed delivery status (sent / delivered / read)
conversation.createdA new conversation is opened for a contact

Delivery payload

POST to your url
{
  "id": "8f3c…",                       // unique per delivery — dedupe on this
  "event": "message.received",
  "occurred_at": "2026-07-01T12:00:00.000Z",
  "account_id": "…",
  "data": {
    "conversation_id": "…",
    "contact_id": "…",
    "whatsapp_message_id": "wamid.…",
    "content_type": "text",
    "text": "Hi 👋"
  }
}

message.status_updated data: whatsapp_message_id, conversation_id, status. conversation.created data: conversation_id, contact_id. Headers: X-Engage-Event, X-Engage-Webhook-Id, X-Engage-Signature.

Verify the signature

X-Engage-Signature: t=<unix_seconds>,v1=<hex> where v1 = HMAC-SHA256(secret, "${t}.${rawBody}"). Recompute it over the raw request body, compare in constant time and reject timestamps more than a few minutes old.

node.js
import crypto from 'node:crypto'

export function verify(header, rawBody, secret) {
  const m = header.match(/t=(\d+),v1=([0-9a-f]+)/)
  if (!m) return false
  const [, t, v1] = m
  if (Math.abs(Date.now() / 1000 - Number(t)) > 300) return false // replay guard
  const expected = crypto.createHmac('sha256', secret).update(`${t}.${rawBody}`).digest('hex')
  const a = Buffer.from(expected), b = Buffer.from(v1)
  return a.length === b.length && crypto.timingSafeEqual(a, b)
}

Delivery rules

  • Reply 2xx quickly. Timeouts, 5xx, 408 and 429 are retried with exponential backoff; other responses are not.
  • Redirects are not followed. The URL must be https:// and resolve to a public address — localhost, private ranges and cloud-metadata addresses are refused.
  • After enough consecutive failures an endpoint is auto-disabled (is_active: false); fix it and re-enable with PATCH.
  • Events can repeat or arrive out of order. Dedupe on id and reconcile with the read endpoints when it matters.

Troubleshooting

SymptomLikely cause
401 unauthorizedWrong or revoked key, or the header is not Authorization: Bearer …
403 forbiddenThe key lacks the scope in the endpoint's badge — create a new key with it
400 on toNumber missing the leading + and country code
404 on a resource you can see in the appIt belongs to a different WhatsApp number than the key
Text message accepted but not deliveredOutside the 24-hour window — use a template
429 rate_limitedRespect Retry-After, or move up a plan for a higher limit
Webhook stopped arrivingEndpoint was auto-disabled after failures — PATCH is_active to true
Building with AI tools instead? The workspace also ships an MCP server so assistants can drive it directly. Questions? Email support@scale360x.com.