Errors
HTTP status semantics, scope failures, and why masking is not an error
| Status | Meaning |
|---|---|
| 401 | Missing, malformed, or revoked API key. |
| 403 | The key lacks the operation's scope; or X-Wexio-Org/orgId isn't a client you manage; or a chat, media or reply target isn't owned by the acting org; or a key tried a dashboard-only operation. |
| 400 | Bad input: text and media together, an empty message, a missing or unapproved WhatsApp template, a channel that can't start conversations, an invalid media ID, or a role a key may not assign. |
Rate Limits
Over a limit, GraphQL returns extensions.code = "RATE_LIMITED" with extensions.retryAfterMs; HTTP and /mcp return 429 with Retry-After. Buckets, the page clamp, expensive operations and the WhatsApp template cap are all in Rate limits.
Scope Failures
A missing scope is a 403, and the message names the scope - so you can fix the key rather than guess:
Forbidden: this API key lacks the MESSAGES_SEND scopeScopes are fixed at mint time. The fix is a new key with the right set, not a retry. See API keys.
Masking Is Not an Error
A key without PII_READ gets a successful response with identity fields null and participant lists empty. Nothing fails, nothing is logged as an error.
This means a masked read is indistinguishable from genuinely absent data. Before concluding a contact has no phone number, check whether the key holds PII_READ. The same applies to internal notes, which are simply absent without NOTES_WRITE.
Plan Feature Not Allowed
The API, MCP and webhooks each need a plan that includes them. The check runs on every call, so a downgrade stops a working integration without revoking anything.
| Surface | Response |
|---|---|
| GraphQL | An error with code PLAN_FEATURE_NOT_ALLOWED and the missing feature: apiAccess, mcp or webhooks |
| MCP | 403 with {"error":"plan_feature_not_allowed","feature":"mcp"} |
| Inbound webhooks | 400 WEBHOOK_UNAVAILABLE |
This is not an authentication problem and the key is still valid - do not rotate it, and do not treat it as a permanent failure. It clears by itself when the plan is restored. See what happens on a downgrade.
Tenant Connection Unavailable
An organisation can run on its own database and Redis rather than shared infrastructure. When that infrastructure is unreachable, the request is refused:
| Surface | Response |
|---|---|
| GraphQL | extensions.code = "TENANT_CONNECTION_UNAVAILABLE" |
| HTTP / REST | 503 with { statusCode, code, which, message } |
| MCP | The text Tenant connection unavailable |
which is "database" or "redis", and it is included only for an API key belonging to that organisation - a provider reading a child does not get it.
The request is never served from shared infrastructure as a fallback. An org that brought its own database gets its own database or an error, never someone else's storage. That is the point of the isolation, so treat a 503 here as "retry later", not as data loss or an empty result.
Check the organisation's tenantConnection field to see which side is down and when it was last checked - see client orgs.
No Existence Oracle
When a chat, media item, or reply target isn't owned by the org you are acting on, the response is the same uniform "unavailable" 403 whether the resource exists elsewhere or not. You can't use error responses to probe for other organizations' IDs.
The same holds for identity: a key without PII_READ cannot filter or search on a phone number or name and infer from the result that such a person exists.
Error Shapes
- GraphQL errors use the standard
errors[]array. - MCP errors return
isError: truewith a message.
A provider-side delivery failure is not an error. The mutation succeeds and the message comes back with deliveryStatus: FAILED - check it on every send. See Channel constraints → Delivery failures.