Introduction
The Soosh API lets your own systems do what your team does in the app: send WhatsApp messages and templates, keep contacts in sync, run broadcasts, and hear about each change with webhooks.
- Base URL
https://app.soosh.io/v1- For AI assistants
- openapi.yaml · llms.txt · every page as Markdown
Authentication
Section titled “Authentication”Send a workspace key as Authorization: Bearer soosh_live_… on every request. Create an API key in Settings.
A key can only do what its scopes allow. Anything else gets 403 scope_missing. Cookies are ignored. Browsers get no CORS access to /v1, so call the API from your server, never from a web page.
curl https://app.soosh.io/v1/numbers \
-H "Authorization: Bearer $SOOSH_API_KEY"Requests and responses
Section titled “Requests and responses”| Topic | Rule |
|---|---|
| Format | JSON, except POST /v1/media, which takes multipart/form-data. |
| IDs | UUIDs. |
| Times | RFC 3339 in UTC, with milliseconds in responses. |
| Phone numbers | Send the number with its country code. Spaces, dashes and brackets are fine. You get it back as + and digits. |
| Unknown request fields | Refused with 400 invalid_request. |
| New fields and values | Ignore a field you don’t know. Where a field says more values may be added, handle one you don’t know. |
| Versioning | v1 only gains things. A breaking change becomes /v2, with at least 6 months of overlap. |
curl -X POST https://app.soosh.io/v1/contacts \
-H "Authorization: Bearer $SOOSH_API_KEY" \
-H "Content-Type: application/json" \
-d '{ "phone": "+919876543210", "name": "Priya Sharma" }'Pagination
Section titled “Pagination”A list returns {"data": [...], "next_cursor": "…" | null}. Pass next_cursor back as cursor to get the next page. A null cursor means you’ve reached the end. There’s no total count.
| Parameter | Meaning |
|---|---|
limit |
1 to 100 items. The default is 25. |
cursor |
The next_cursor of the previous page, unchanged. |
sort |
created_at (the default) lists newest first. updated_at lists oldest change first. |
A cursor that was changed, or made for a different sort, is 400 invalid_request. Only the lists of contacts, messages and templates take sort.
To copy changes into your own system, list with sort=updated_at. A change shows up about 30 seconds after it is saved. Resume from the last next_cursor, or pass updated_after set to a minute before your last sync, and skip rows you already have.
{
"data": [
…
],
"next_cursor": "eyJjIjoiMjAyNi0xMC0x…"
}Retrying safely
Section titled “Retrying safely”Send an Idempotency-Key header on any POST, so a retry never repeats it. The key is 1 to 255 printable ASCII characters.
Soosh keeps the key for 24 hours. A retry must be the same request: the same method, path and body, from the same API key or one rotated from it.
A repeat of a finished request returns the stored response with Idempotent-Replayed: true. Original-Request-ID names the request that made it. The body is the first response, byte for byte, so an error body’s request_id is the first request’s.
This includes 4xx responses: a window_closed stays window_closed for 24 hours under that key. A 5xx isn’t stored, so a retry runs again.
The same key on a different request, or from another of your API keys, is 422 idempotency_mismatch. A request still running under the key is 409 idempotency_in_progress.
curl -X POST https://app.soosh.io/v1/messages \
-H "Authorization: Bearer $SOOSH_API_KEY" \
-H "Idempotency-Key: order-1042-shipped" \
-d '{ … }'Rate limits
Section titled “Rate limits”Every authenticated response carries X-RateLimit-Limit, X-RateLimit-Remaining and X-RateLimit-Reset for your workspace. X-RateLimit-Reset is the seconds until the limit resets. A 429 also carries Retry-After.
| Limit | Window |
|---|---|
| All requests | A minute |
| Messages sent | A second |
| Heavy requests | 10 a minute |
Your plan sets the first two limits (api_requests_per_minute and api_messages_per_second); heavy requests are POST /v1/broadcasts, POST /v1/templates, POST /v1/media and GET /v1/usage, counted together.
X-Request-ID: req_8Yq2LhT4mWc9Rk1Zp6Ns
X-RateLimit-Limit: 600
X-RateLimit-Remaining: 588
X-RateLimit-Reset: 42Errors
Section titled “Errors”Every error has the same body, with a code to act on. Codes never change meaning, though new ones may be added: handle one you don’t know by its HTTP status.
Every response, including errors, has an X-Request-ID header: req_ followed by 20 letters and digits. Quote it when you contact support. Errors lists every status and code.
{
"error": {
"code": "window_closed",
"message": "This contact hasn’t written in the last 24 hours, so only a template message can be sent.",
"request_id": "req_8Yq2LhT4mWc9Rk1Zp6Ns"
}
}