Errors

HTTP status semantics, scope failures, and why masking is not an error

StatusMeaning
401Missing, malformed, or revoked API key.
403The 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.
400Bad 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 scope

Scopes 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.

SurfaceResponse
GraphQLAn error with code PLAN_FEATURE_NOT_ALLOWED and the missing feature: apiAccess, mcp or webhooks
MCP403 with {"error":"plan_feature_not_allowed","feature":"mcp"}
Inbound webhooks400 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:

SurfaceResponse
GraphQLextensions.code = "TENANT_CONNECTION_UNAVAILABLE"
HTTP / REST503 with { statusCode, code, which, message }
MCPThe 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: true with 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.

On this page