Skip to content

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

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.

Every request carries your key
curl https://app.soosh.io/v1/numbers \
  -H "Authorization: Bearer $SOOSH_API_KEY"
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.
A request with a body
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" }'

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.

A page of contacts
{
  "data": [
    …
  ],
  "next_cursor": "eyJjIjoiMjAyNi0xMC0x…"
}

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.

A request you can send again
curl -X POST https://app.soosh.io/v1/messages \
  -H "Authorization: Bearer $SOOSH_API_KEY" \
  -H "Idempotency-Key: order-1042-shipped" \
  -d '{ … }'

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.

On every response
X-Request-ID: req_8Yq2LhT4mWc9Rk1Zp6Ns
X-RateLimit-Limit: 600
X-RateLimit-Remaining: 588
X-RateLimit-Reset: 42

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.

Every status and code

A 422 answer
{
  "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"
  }
}