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
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
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
Verify the key
Call
GET /me. A valid key returns its account, channel and scopes.bashcurl 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
401means the key is wrong, revoked or expired. - 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 +.
bashcurl -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:
201for a new contact,200if it already existed. - 5
Send your first message
Needs the
messages:sendscope.bashcurl -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
Read conversations and replies
List conversations with
conversations:read, then fetch a conversation’s messages withmessages:read— each has itsdirectionand deliverystatus.bashcurl "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
Launch a broadcast (optional)
Send an approved template to up to 1,000 recipients per request. Needs
broadcasts:send.bashcurl -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
202immediately; pollGET /broadcasts/{id}for delivery progress. - 8
Receive events with a webhook
Instead of polling, register an HTTPS endpoint (
webhooks:manage). Save thesecretfrom the response — it is shown once — and verify every signature.bashcurl -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.
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.
Scopes
A key can do only what its scopes allow. Grant the minimum.
| Scope | Allows |
|---|---|
messages:send | Send WhatsApp messages |
messages:read | Read messages and delivery status |
contacts:read | List and read contacts |
contacts:write | Create and update contacts |
conversations:read | List and read conversations |
broadcasts:send | Launch and read broadcast campaigns |
webhooks:manage | Register 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.
{ "data": { … } }{ "error": { "code": "forbidden", "message": "This API key is missing the 'messages:send' scope" } }| Status | code | Meaning |
|---|---|---|
| 400 | bad_request | Malformed input (e.g. phone without a leading +) |
| 400 | whatsapp_not_configured | The channel has no working WhatsApp connection |
| 401 | unauthorized | Missing, malformed, unknown, revoked or expired key |
| 403 | forbidden | Valid key, but missing the required scope |
| 404 | not_found | No such resource (or it belongs to another channel) |
| 429 | rate_limited | Per-key rate limit exceeded |
| 502 | meta_error | The request reached Meta and Meta rejected it |
| 500 | internal | Server error — retry, then contact support |
Rate limits
Limits apply per key and depend on your plan.
| Plan | Requests / minute |
|---|---|
| Pulse (and Spark with the API add-on) | 120 |
| Orbit | 600 |
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.
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 pagelimit 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.
/api/v1/meReturns 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.
/api/v1/messagesmessages:sendSend a text, template or media message.
tomust start with+and the country code. Spaces and dashes are fine; a number without+is rejected.type:text(default),template, orimage/video/document/audio. Media needsmedia_url(optionalfilename);textbecomes the caption.reply_to_message_id(optional) must be a message in the same conversation.
{
"to": "+14155550123",
"type": "template",
"template": { "name": "order_update", "language": "en_US", "params": ["A123"] }
}{
"data": {
"message_id": "…",
"whatsapp_message_id": "wamid.…",
"conversation_id": "…",
"contact_id": "…",
"contact_created": true
}
}Contacts
Contacts belong to the key's channel.
/api/v1/contactscontacts:readList contacts, newest first. Filters: search (name or phone), tag (tag id).
/api/v1/contactscontacts:writeFind-or-create by phone. 201 when created, 200 when it already existed.
/api/v1/contacts/{id}contacts:readRead one contact.
/api/v1/contacts/{id}contacts:writeUpdate only the fields you send. Pass tags (names) to replace the contact's tags.
{
"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.
/api/v1/conversationsconversations:readList conversations, newest first. Filters: status (open | pending | closed), contact_id.
/api/v1/conversations/{id}conversations:readRead one conversation with its contact and tags.
/api/v1/conversations/{id}/messagesmessages:readList a conversation's messages, newest first, with direction, status and content.
Broadcasts
Template campaigns, sent in the background.
/api/v1/broadcastsbroadcasts:sendLaunch a template broadcast to up to 1,000 recipients per request.
/api/v1/broadcasts/{id}broadcasts:sendStatus 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.
/api/v1/webhookswebhooks:manageRegister an endpoint. The response includes the signing secret exactly once.
/api/v1/webhookswebhooks:manageList endpoints (the secret is never returned again).
/api/v1/webhooks/{id}webhooks:manageRead one endpoint.
/api/v1/webhooks/{id}webhooks:manageUpdate url, events or is_active. Re-enabling resets the failure counter.
/api/v1/webhooks/{id}webhooks:manageRemove an endpoint.
Events
| Event | Fires when |
|---|---|
message.received | An inbound message arrives from a contact |
message.status_updated | A message you sent changed delivery status (sent / delivered / read) |
conversation.created | A new conversation is opened for a contact |
Delivery payload
{
"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.
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
2xxquickly. Timeouts,5xx,408and429are 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 withPATCH. - Events can repeat or arrive out of order. Dedupe on
idand reconcile with the read endpoints when it matters.
Troubleshooting
| Symptom | Likely cause |
|---|---|
| 401 unauthorized | Wrong or revoked key, or the header is not Authorization: Bearer … |
| 403 forbidden | The key lacks the scope in the endpoint's badge — create a new key with it |
| 400 on to | Number missing the leading + and country code |
| 404 on a resource you can see in the app | It belongs to a different WhatsApp number than the key |
| Text message accepted but not delivered | Outside the 24-hour window — use a template |
| 429 rate_limited | Respect Retry-After, or move up a plan for a higher limit |
| Webhook stopped arriving | Endpoint was auto-disabled after failures — PATCH is_active to true |