Errors & Rate Limits
Error envelopes and codes for MCP and the REST APIs, rate-limit buckets, and retry behavior.
The two REST APIs use different error envelopes
The Public API wraps errors in {"error": {"code", "message"}} with lowercase snake_case codes.
The Distributor API returns a flat {"status": "error", "errorCode", "message"} object with SCREAMING_SNAKE_CASE codes.
Parsers written for one will silently fail on the other.
Public API errors
Every non-2xx response from /api/v1 has this shape:
{
"error": {
"code": "not_found",
"message": "Not found"
}
}| Code | HTTP | Meaning | Retry? |
|---|---|---|---|
validation_failed | 400 | Request body or query parameters failed validation | No - fix the request. Check details |
unauthorized | 401 | Missing or invalid API key | No - fix the key |
subscription_required | 402 | A feature required by this endpoint is not enabled for the organization | No - upgrade the plan |
forbidden | 403 | The API key's role lacks permission for this action | No - use a key with a broader role |
trial_exhausted | 403 | Usage access refused: the trial minutes are used up. Assistant creation and knowledge-item and prompt writes are refused; existing data stays readable | No - start a subscription |
subscription_required | 403 | Usage access refused: the organization has no active subscription, so assistant creation and knowledge-item and prompt writes are refused. The 402 form instead means a plan feature is missing | No - activate a subscription |
organization_access_required | 403 | Usage access refused: the organization is being deleted or no longer available, or was blocked while the request ran | No - contact support |
organization_blocked | 403 | The API key's organization is blocked; every request with its keys is refused | No - contact support |
not_found | 404 | Resource does not exist (or belongs to another organization) | No - check the ID |
conflict | 409 | Resource is in use or the operation would violate an invariant | No - resolve the conflict first |
outbound_target_blocked | 409 | The exact outbound target is blocked for the selected assistant. Manage lists and assignments in Dashboard → Settings → Blocked callers; configuration is not exposed through the Public API | No - choose another target or change the Dashboard policy |
rate_limit_exceeded | 429 | Rate limit exceeded, an outbound assistant is in cooldown, or the limiter is unavailable | Yes - use Retry-After when present; otherwise back off |
internal_error | 500 | Unexpected server error; the operation did not complete | Reads: yes. Mutations: check resource state first |
Validation details
validation_failed responses carry an optional details array. Each entry has a
path (array of field names and array indices) and a message:
{
"error": {
"code": "validation_failed",
"message": "Validation failed",
"details": [
{ "path": ["first_name"], "message": "\"first_name\" is required" },
{ "path": ["phone_numbers", 0], "message": "\"phone_numbers[0]\" must be a string" }
]
}
}Per-endpoint error responses are listed in the Public API reference.
MCP errors
Authentication and entitlement failures happen before MCP dispatch and are
normal HTTP errors. 401 means the credential is missing or invalid; OAuth
clients use its WWW-Authenticate metadata to start authorization. 402 subscription_required means the organization is not entitled - reconnecting
OAuth cannot fix it. 403 means the user or organization lifecycle blocks the
request (a blocked organization gets ORGANIZATION_BLOCKED for OAuth and API-key
credentials alike), 429 means retry after backoff, and 503 means OAuth configuration or
the current organization authorization context cannot be loaded.
Authentication failures have the body { "error": { "code", "errorCode", "message" } }. Branch on errorCode:
| errorCode | HTTP | code | Meaning / action |
|---|---|---|---|
TOKEN_MISSING | 401 | unauthorized | No bearer credential. Send one |
TOKEN_INVALID | 401 | unauthorized | The access token or API key is invalid or expired, or the token belongs to another host. Authorize again or replace the key |
TOKEN_INCOMPLETE | 401 | unauthorized | The access token lacks its user, organization or instance. Reconnect the app |
USER_INACTIVE | 403 | forbidden | The connected user no longer exists or is not active |
ACCOUNT_DELETION_IN_PROGRESS | 403 | forbidden | The connected user account is being deleted |
ORGANIZATION_NOT_FOUND | 403 | forbidden | The organization chosen at consent no longer exists. Reconnect and choose another |
ORGANIZATION_BLOCKED | 403 | forbidden | The organization is blocked. Contact support |
ORGANIZATION_DELETION_IN_PROGRESS | 403 | forbidden | The organization chosen at consent is being deleted |
INSTANCE_NOT_FOUND | 403 | forbidden | The token's instance cannot be found for this user. Reconnect the app |
OIDC_NOT_CONFIGURED | 503 | temporarily_unavailable | OAuth is not configured on this deployment. API keys keep working |
CONNECTOR_ORIGIN_UNAVAILABLE | 503 | temporarily_unavailable | The OAuth connector for this host could not be resolved. Retry shortly |
ORGANIZATION_AUTHORIZATION_INVALID | 503 | temporarily_unavailable | The user's role in the organization cannot be loaded: a temporary failure, or the user is no longer a member. Retry; if it persists, reconnect instead of retrying forever |
Once dispatch starts, tool validation and service errors are MCP tool results
with isError: true and the Public API { "error": { "code", "message", "details?" } } body. Branch on code, not localized message. A tool whose
product feature is not enabled for the organization returns
feature_not_available, where the Public API answers 402 subscription_required; a scope or role refusal returns forbidden. See
MCP setup and access rules.
Distributor API errors
Every non-2xx response from /api/distributor/v1 has this shape:
{
"status": "error",
"errorCode": "RATE_LIMITED",
"message": "Too many requests for this API key."
}Match on errorCode, not on message - messages can change. Common
integration-relevant codes:
| errorCode | HTTP | Meaning / action |
|---|---|---|
MISSING_API_KEY, INVALID_API_KEY | 401 | No/bad bearer key. Fix the key |
API_KEY_EXPIRED | 401 | Key past its expiry (default 365 days). Request a replacement from SMAO |
API_VERSION_NOT_ALLOWED | 403 | The key metadata does not authorize Distributor API v1. Request a valid v1 key |
MISSING_PERMISSION | 403 | Key lacks the required permission. Permission sets are immutable - you need a different key |
VALIDATION_ERROR | 400 | Bad request body or parameters. Fix and resend |
INVALID_DATE_RANGE, DATE_RANGE_TOO_LARGE | 400 | The date window is invalid, ends after today (UTC), or is too long. Fix and resend |
INVALID_ID | 400 | Not returned in practice: malformed IDs are refused earlier with VALIDATION_ERROR |
NOT_FOUND | 404 | Resource not found in this distributor's scope |
RATE_LIMITED | 429 | Rate limit exceeded. Wait Retry-After seconds, then retry |
RATE_LIMIT_UNAVAILABLE | 503 | Rate-limit store unavailable (fail-closed). Retry with backoff |
INTERNAL_ERROR | 500 | Unexpected server error. There is no idempotency-key header - check state before retrying a mutation |
Partner and organization codes
| errorCode | HTTP | Meaning / action |
|---|---|---|
DUPLICATE_EXTERNAL_PARTNER_ID | 409 | externalPartnerId already exists. Fetch and reconcile the existing partner |
DUPLICATE_EXTERNAL_CUSTOMER_ID | 409 | externalCustomerId already exists. Fetch and reconcile the existing customer |
PARTNER_NOT_FOUND | 404 | The requested active v1 partner was not found in this distributor's scope |
PARTNER_HAS_ORGANIZATIONS | 409 | The partner still has linked organizations, including its internal MSP organization; v1 does not cascade-delete them |
INVALID_PLAN, PLAN_NOT_FOUND | 400/404 | The requested assignable plan is invalid, or no subscription exists for billing |
ORG_CREATION_FAILED | 500 | The partner's internal organization could not be provisioned |
ORG_DELETION_BLOCKED | 409 | The organization cannot be deleted right now: calls are active, jobs are processing, or it is busy with another change. A refused attempt normally removes nothing and leaves the organization as it was; retry later. Only when a call starts after the deletion has begun removing data does it stay unusable until a retry succeeds |
The full enums and per-endpoint responses are in the Distributor v1 reference.
Rate limits
The REST APIs limit per API key. For the Public API, matched operations return the
three headers once authentication and the feature gate succeed and the limiter
has evaluated the request. Authentication failures, the 402 feature gate, and
unmatched routes do not carry the complete header set:
curl -sD - -o /dev/null "https://api2.smao.ai/api/v1/assistants" \
-H "Authorization: Bearer $SMAO_API_KEY"X-RateLimit-Limit: 1000
X-RateLimit-Remaining: 997
X-RateLimit-Reset: 1776902940X-RateLimit-Reset is a Unix timestamp (seconds) of the window reset.
| API | Bucket | Limit |
|---|---|---|
| Public | read - authenticated documented GET operations | 1,000 / min default |
| Public | write - authenticated documented POST / PATCH / DELETE operations except POST /calls | 200 / min default |
| Public | outbound_trigger - POST /calls | 60 / hour default, plus one outbound call per assistant per 60 seconds |
| MCP | read - every message except calls of write tools or unknown tool names, including initialize, tools/list and notifications; per organization and OAuth user or API key | 1,000 / min default |
| MCP | write - calls of write tools or unknown tool names; per organization and OAuth user or API key | 200 / min default |
| Distributor | all authenticated endpoints (/health is unlimited) | 60 / min (default; per-key configurable) |
MCP counts every JSON-RPC message. A batch of N messages costs N units and is
charged to the write bucket when any message calls a write tool or an unknown
tool. A batch that does not fit is rejected whole with 429, and none of its
messages run. A batch of more than 100 messages is rejected with 400 and
JSON-RPC error -32600 after its units have been charged, so send at most 100
messages per batch.
Distributor responses that pass authentication include Retry-After (seconds)
for the current rate-limit window, even before the bucket is exhausted. On
429, sleep that long before retrying; X-RateLimit-Reset carries the same
information as an absolute Unix timestamp. A Public API 429 caused by the per-assistant
outbound cooldown, including a storage failure while reserving it, does not include Retry-After; back off for at least 60
seconds.
All three surfaces fail closed. If the rate-limit store cannot be reached, the
Public API rejects with 429 rate_limit_exceeded plus the rate-limit headers and
Retry-After: 30, the Distributor API rejects with 503 RATE_LIMIT_UNAVAILABLE,
and MCP rejects with a 429 JSON-RPC error plus Retry-After. Retry with
exponential backoff; do not treat it as a hard failure. If the Public API
limiter itself fails before it can evaluate the bucket, for example because of
a misconfiguration, the 429 may carry neither the rate-limit headers nor
Retry-After.