SocialMate Local API
Responses & errors
The v1.1 { data } / { error } envelope, pagination, status codes, and the error catalogue.
The API speaks JSON in and JSON out. As of v1.1 every response is wrapped in a small, predictable envelope so a client always parses the same two shapes.
Success — the data envelope
Every 2xx response carries its payload under data:
{ "data": { "sent": true, "messageId": "3EB0…" } }
The shape of the payload itself doesn't change — only its position under data.
A 202 auto-queue response, for instance, is
{ "data": { "queued": true, "itemId": "…" } }.
Errors — the error envelope
Every 4xx/5xx response carries an error object with a
stable code, a human message, and any extra diagnostic fields the
route provides (reason, retryAfterMs, required, …):
{
"error": {
"code": "rate_limited",
"message": "Per-key request rate exceeded",
"retryAfterMs": 1200
}
}
Pagination
List endpoints that page (contacts, messages, media) return their rows under
data alongside a pagination block. Use limit (max 500)
and offset query params; the total tells you when to stop.
The queue endpoints are the exception: they return
{ "data": { "items": [ … ], "total": n, "hasMore": bool } } — no separate
pagination block. See Queue.
{
"data": [ /* rows */ ],
"pagination": { "limit": 100, "offset": 0, "total": 1283 }
}
Legacy (pre-1.1) shape
If you built against the older bare shapes, pin them per request with a header — the payload is then returned unwrapped. The legacy envelope is supported for back-compat and will be removed in a future v2.
curl http://localhost:3456/v1/accounts \
-H "x-api-key: YOUR_KEY" \
-H "X-SocialMate-Envelope: v1"
Binary responses (the media file and thumbnail streams) and the
/docs spec routes are never wrapped.
HTTP status codes
| Code | Meaning |
|---|---|
200 | OK. |
202 | Accepted — the send was auto-queued (Pro) because anti-ban blocked an immediate send. |
206 | Partial content — a ranged media file stream. |
400 | Validation failed (bad/missing field, malformed body). |
401 | Missing or invalid x-api-key. |
402 | The feature requires Pro — the body names the missing feature. |
403 | The key lacks the required scope — error.code insufficient_scope, with required and have naming the gap. |
404 | Account, contact, group, media, key, or webhook not found. |
409 | Conflict — e.g. the account isn't connected, or media isn't downloaded yet. |
422 | Unprocessable — well-formed but semantically rejected (error.code unprocessable). |
429 | Rate-limited (per-key bucket) or anti-ban blocked the send. Honour Retry-After. |
500 | Server error. |
502 | The send/operation reached WhatsApp but failed downstream. |
503 | Service temporarily unavailable (error.code unavailable). |
The error.code catalogue
Most responses carry a status-derived error.code, and routes attach extra descriptive
fields (reason, required, have, feature,
tier, retryAfterMs) for the detail. The status-derived set:
| code | Status | When |
|---|---|---|
bad_request | 400 | Validation failed (bad/missing field, malformed body). |
unauthorized | 401 | Missing or invalid x-api-key. |
license_required | 402 | Pro feature — see feature / tier. |
insufficient_scope | 403 | Scope too narrow — see required / have. |
not_found | 404 | No such account, contact, group, media, key, or webhook. |
conflict | 409 | Account not connected, or media not downloaded yet. |
unprocessable | 422 | Well-formed but semantically rejected. |
rate_limited | 429 | Per-key bucket exceeded, or anti-ban blocked the send — see retryAfterMs and (anti-ban only) reason. |
upstream_error | 502 | Reached WhatsApp but failed downstream. |
unavailable | 503 | Service temporarily unavailable. |
server_error | 500 | Unexpected server error. |
Some routes return a more specific machine code instead of the status-derived one,
so don't assume the status alone tells you the code. These survive the envelope:
insufficient_scope, account_scope_mismatch,
account_suspended_free, invalid_url, invalid_contact_id,
bulk_import_disabled, context_exists, and the anti-ban family
ANTI_BAN_<REASON> (e.g. ANTI_BAN_SIGNAL_RATE_LIMIT). When you need to
branch on an anti-ban block, use error.reason — it is stable across every route.
Anti-ban blocks return 429 with an error.reason naming the
specific block — rate_limit, warming, night_mode,
risk_critical, paused_global, paused_account,
banned, unknown_account, duplicate_text, or
signal_rate_limit (see
Anti-ban status).
Scope errors return 403 insufficient_scope with
required / have (see
Authentication).