Requests & responses
The rules that hold for every endpoint, so each reference page can stay short.
Requests#
- Base URL:
https://api.saei.automize.sa/v1. HTTPS only. - Bodies are JSON with
Content-Type: application/json. A body that is not valid JSON is refused withinvalid_jsonrather than read as empty. The one exception is a file upload (POST /media), sent asmultipart/form-data. - Phone numbers come back in E.164:
+966501234567. You may send them as00966…,966…or a local05…too (see Arabic & Saudi specifics). Arabic text and emoji are sent as-is (UTF-8). - Filters on lists are query parameters; booleans are
trueorfalse.
Ids#
Every object has an id that starts with its kind, so a log line says what it names, and a wrong kind of id is answered with a clear 404:
| Prefix | Object | Prefix | Object |
|---|---|---|---|
msg_ | a message you sent | ct_ | contact |
imsg_ | a customer's WhatsApp message | tag_ | tag |
cmsg_ | a campaign recipient's message | fld_ | custom field |
msg_test_ | a test-mode message | conv_ | conversation |
num_ | WhatsApp number | usr_ | team member |
tpl_ | template | cmp_ | campaign |
med_ | uploaded file | flow_ / run_ | flow / flow run |
fres_ | WhatsApp Flow form response | we_ / del_ | webhook endpoint / delivery |
evt_ | event | req_ | request |
Treat ids as opaque strings up to 64 characters: do not parse them or assume they are numbers.
Errors#
Every error has the same shape, and the HTTP status matches its type:
{
"error": {
"type": "invalid_request_error",
"code": "window_closed",
"message": "The 24-hour customer service window is closed. Send an approved template instead.",
"param": "type",
"doc_url": "https://app.saei.automize.sa/docs/errors#window_closed",
"request_id": "req_01j9x3k2m4n5p6q7r8s9t0v1w2"
}
}Branch on code, never on message (messages may be reworded). When several fields are wrong, details lists each one. Every code, with how to fix it, is on the Errors page.
Messages are in English. Send Accept-Language: ar and field validation messages (parameter_missing, parameter_invalid) come back in Arabic; the code is the same in both.
Request ids#
Every response carries a Request-Id header (and errors repeat it in request_id). Log it: support can find the exact request from it, and you can look it up yourself in Developers → Logs.
Pagination#
Lists are newest first and paginated with a cursor:
{ "object": "list", "data": [ ... ], "has_more": true, "next_cursor": "eyJpZCI6..." }Ask for the next page with ?cursor=<next_cursor>, and set the page size with ?limit= (1–100, default 25). A cursor does not skip or repeat items when new ones arrive.
Rate limits#
Each key has a request budget per minute. Every response says where you are:
| Header | Meaning |
|---|---|
RateLimit-Limit | Requests allowed per window |
RateLimit-Remaining | Requests left in this window |
RateLimit-Reset | Seconds until the window resets |
Over the limit you get 429 rate_limited with Retry-After in seconds. Pace with the headers instead of retrying in a tight loop.
Retries and idempotency#
A timeout leaves you not knowing whether a POST landed. Send an Idempotency-Key header (a UUID is ideal) and retrying is always safe:
curl -X POST https://api.saei.automize.sa/v1/messages \
-H "Authorization: Bearer $API_KEY" \
-H "Idempotency-Key: 4f6c1c1e-2d0a-4b53-9d4f-58a1c6e7b1aa" \
-H "Content-Type: application/json" \
-d '{ "to": "+966501234567", "type": "template", "template": { "name": "order_update", "language": "ar", "variables": { "body": ["Sara", "#10482"] } } }'- Same key, same body: you get the first response again, with
Idempotent-Replayed: true. Nothing is sent twice. - Same key, different body:
idempotency_key_reused. - Keys are remembered for 24 hours. Errors from our side (5xx, 429) are not remembered, so a retry tries again.
Retry 429 and 5xx with exponential backoff; do not retry other 4xx without changing the request.