Errors
Every error from the Mojeeb Public API uses the same envelope. Branch on code (stable, forever-additive) — never on message (human-readable, may be reworded without notice).
What does an error response look like?
{
"error": {
"type": "validation_error",
"code": "invalid_from_phone",
"message": "The 'from' field must be an E.164 phone number...",
"param": "from",
"correlation_id": "req_01KQ..."
}
}
| Field | Always present? | Purpose |
|---|---|---|
type | Yes | Stable category for catch blocks |
code | Yes | Stable, specific reason — branch on this |
message | Yes | Human-readable; never use as a key |
correlation_id | Yes | Trace id for our logs — quote in support tickets |
| Extra fields | Sometimes | Per-error context (e.g. param, available_phones, original_correlation_id) |
What are the error types?
A small, stable set of categories. Forever-additive — new types may appear, none will be removed:
type | Typical HTTP |
|---|---|
authentication_error | 401 |
permission_error | 403 |
validation_error | 422 |
idempotency_error | 409 / 422 |
rate_limit_error | 429 |
quota_error | 402 |
not_found_error | 404 |
upstream_error | 503 |
internal_error | 500 |
What's the full code catalog?
Codes are a stable enum. New codes appear without notice; nothing existing will be renamed or repurposed.
code | HTTP | Meaning |
|---|---|---|
invalid_api_key | 401 | Missing, malformed, unknown, or revoked key. Fix your Authorization header. |
revoked_api_key | 401 | Key was revoked from the dashboard. Create a new one. |
expired_api_key | 401 | Key past its expiration date. Create a new one. |
insufficient_scope | 403 | Key lacks the required scope (response includes required_scope). |
agent_not_authorized | 403 | Key is restricted to specific agents and this one isn't allowlisted. |
invalid_agent_id | 422 | agent_id isn't a valid UUID. |
invalid_phone_number | 422 | to isn't a valid E.164 number. |
invalid_from_phone | 422 | from isn't a valid E.164 number. |
from_phone_not_found_for_agent | 422 | from is well-formed but doesn't match any active WhatsApp connection on the agent (response includes available_phones). |
invalid_template_name | 422 | template.name is missing or blank. |
invalid_request_body | 422 | A required field is missing or has the wrong type (response includes param naming the offending field). |
idempotency_key_in_progress | 409 | Same key + body still processing. Retry after Retry-After. |
idempotency_key_in_use_with_different_params | 422 | Same key was used previously with a different body (response includes original_correlation_id). |
rate_limit_exceeded | 429 | Per-key rate limit hit. Backoff and retry after Retry-After. |
message_limit_exceeded | 402 | (Future) Subscription's message limit reached. |
message_not_found | 404 | GET /v1/whatsapp/messages/{id} — id doesn't exist or belongs to another organization. |
endpoint_not_found | 404 | The path doesn't match any /v1/ endpoint. Check for typos. |
whatsapp_unavailable | 503 | Meta's WhatsApp Cloud API is unreachable. Retry with backoff. |
internal_server_error | 500 | Our problem. Retry with exponential backoff; if it persists, send us the correlation_id. |
What HTTP statuses can the API return?
| Status | Meaning |
|---|---|
200 | Synchronous success (e.g. status lookup) |
201 | Resource created |
202 | Accepted for async processing (e.g. message queued for send) |
400 | Malformed request (invalid JSON) |
401 | Authentication failed |
402 | (Future) Quota exceeded |
403 | Authorization failed (scope, agent allowlist) |
404 | Resource not found |
409 | Conflict (idempotency key in progress) |
422 | Validation error |
429 | Rate limit exceeded |
500 | Internal error |
503 | Upstream platform unavailable |
How do I use the correlation_id?
Save it on every error. When you contact support, paste the correlation_id from the failing response — we can trace the entire request lifecycle in our logs in seconds.
You can also send your own trace id and we'll honor it:
curl ... -H "X-Correlation-ID: my-app-trace-abc123"
Max 128 chars, alphanumeric plus _-. We reflect it back in X-Correlation-ID on the response and embed it in any error body. If you don't send one, we generate req_<26-char ULID>.
Common questions
Why does invalid_api_key cover so many sub-cases?
A single code keeps client logic simple — your code only needs one branch for "auth failed." The message field disambiguates for humans debugging, but you should never branch on text. Treat any invalid_api_key as "fix the key and retry."
Will you add new error codes later?
Yes. New code values can appear without notice as we add features. Keep your switch/match statements default-safe — fall back to "unknown error, log and surface to user."
What about field-level errors with multiple invalid fields?
Currently the API returns the first validation error encountered. We may add a richer multi-error shape later as an additive change (a new optional field on the envelope, never replacing existing fields). For now, fix one error at a time.
What's original_correlation_id?
Returned on idempotency_key_in_use_with_different_params. It points to the trace id of the request that originally claimed the idempotency key, so you can find that earlier request in your own application logs.
What's available_phones?
Returned on from_phone_not_found_for_agent. Lists every active WhatsApp number on the agent in E.164 format, so you can self-correct your from value without a second roundtrip to discover what was valid.