Guides
Errors and limits
Every error has the same shape, limits are predictable, and writes can be retried without duplicating anything.
Error format#
Every 4xx or 5xx response carries an error object with a stable code (for your code) and a readable message (for your logs). Some errors add details:
{
"error": {
"code": "validation_error",
"message": "Request validation failed",
"details": {
"formErrors": [],
"fieldErrors": { "channel": ["Invalid enum value"] }
}
}
}Status codes#
| Status | When | What to do |
|---|---|---|
| 400 | Invalid JSON (invalid_json) or failed validation (validation_error, with per-field details). | Fix the request. Do not retry it as is. |
| 401 | The API key is missing, invalid or revoked (unauthorized). | Check the header and the key. |
| 401 | The key expired (api_key_expired, with details.expired_at). A key can have an expiration date; 7 days before, we email the owners and admins. | Create a new key or rotate this one before the date. |
| 402 | A plan limit was reached (plan_limit_exceeded). | See details.upgrade_url>. |
| 402 | The period's messages ran out on Developer Free (message_quota_exceeded). Incoming messages are still stored. | Read fix: wait for the next period or upgrade to Developer. See Pricing |
| 403 | Missing scope, IP not allowed or workspace disabled. | Add the scope to the key or check its IPs. |
| 404 | The resource does not exist in this workspace. | Check the id. |
| 409 | Conflict, such as an Idempotency-Key still in progress. | Retry in a few seconds. |
| 422 | The request is valid but cannot be processed (for example no_channel_connection, or invalid_sms_message and sms_sender_mismatch when sending an SMS). | Read message: something usually needs to be set up in the workspace. |
| 429 | You went over the rate limit (rate_limited). | Wait and retry with backoff. |
| 5xx | heysteff error. | Retry with backoff and the same Idempotency-Key. |
Rate limit#
Each API key has a request cap per 60-second window that depends on the workspace plan:
| Plan | Requests per minute, per key |
|---|---|
| Developer Free | 100 |
| Developer | 2,000 |
| Developer Platform | 4,000 |
Every response carries the limit state in three headers:
X-RateLimit-Limit: your key's cap.X-RateLimit-Remaining: what's left in the current window.X-RateLimit-Reset: Unix seconds when the next window starts.
Past the cap, the API answers 429 rate_limited with Retry-After (seconds) until the next window starts. Retry after that time, with exponential backoff and some jitter (for example 1, 2, 4 and 8 seconds).
Workspace plan limits, such as messages per month, are separate. They answer 402 plan_limit_exceeded and retrying does not help.
Idempotency#
Operations that create something accept the Idempotency-Key header. If you repeat a request with the same key, you get exactly the same response as the first time (same status, same body) and nothing new is created.
| Endpoint | Key |
|---|---|
POST /v1/messages, POST /v1/campaigns, POST /v1/automation-flows/{id}/trigger | Idempotency-Key header, optional. |
POST /v1/charges, POST /v1/refunds | Idempotency-Key header, required. |
POST /v1/events | idempotency_key body field. |
POST /v1/conversions | external_ref body field. |
- Use one key per logical operation, like
order-1042-shipped, not one per attempt. - If the first request is still in progress, the second gets
409 idempotency_conflict: retry in a few seconds.
Pagination#
List endpoints return pages. limit goes from 1 to 200 (50 by default).
| Endpoint | How to move forward |
|---|---|
GET /v1/conversations, GET /v1/conversations/{id}/messages, GET /v1/contacts | Pass cursor=<cursor.next> while cursor.has_more is true. |
GET /v1/events | Pass after_seq=<cursor.next_after_seq>. |
GET /v1/reviews | Use limit and offset. |
Amounts and dates#
- Amounts are integers in minor units:
1990is 19.90 USD. In currencies without decimals, like the Chilean peso,19990is $19,990. - Dates are ISO 8601 with a time zone:
2026-10-31T14:02:11Z. - Phone numbers use international format:
+15551234567.