Errors
All errors follow a standard format:
{
"error": {
"type": "not_found",
"message": "Agent not found",
"code": 404,
"request_id": "req_abc123",
"doc_url": "https://www.m8tes.ai/docs/api-errors#error-types"
}
}doc_url links to the docs page for that error type. Include request_id when you contact support.
Branch on error_code, never message text. Older clients can read error.details.error_code; Python exposes it as the exception's .code or .error_code.
Error Types
The four you'll hit while building:
Catch the exception you can recover from:
from m8tes import M8tes, NotFoundError, AuthenticationError, BillingError, RateLimitError
client = M8tes()
try:
agent = client.agents.get(999, user_id="customer_42")
except NotFoundError:
print("Agent does not exist")
except AuthenticationError:
print("Check your API key")
except BillingError as e:
print(e.code, e.details) # machine-readable recovery context
except RateLimitError:
print("Still rate limited after SDK retries. Try again later.")The SDK retries 429 and 500+ responses with exponential backoff for idempotent methods (GET, PUT, DELETE), and for the run-creating POSTs, which carry an idempotency key so a retry returns the original run instead of starting a second one. Any other POST or PATCH is never retried, to prevent duplicate side effects.
All error types and `error.details` conventions
- Errors that carry a machine-readable app code (e.g.
feature_disabled,from_template_conflict,RUN_LIMIT_REACHED) put it inerror.error_code(mirrored aterror.details.error_codefor older clients), with any recovery context (missing,connect_urls,rejected_fields, …) as sibling keys undererror.details. The Python SDK surfaces it as the exception's.code(and.error_codesince 4.8.0). 403 TEAMS_MODEL_LOCKED: a Teams account set a model other thandeepseek-v4-1-flashon an agent, task, or run. Omitmodel(or use that one), connect your own provider, or ask about Enterprise for every model.400 MODEL_NOT_RUNNABLE: the chosen model has no connected provider or gateway route that can fund it for this write's scope. Prefer arunnable: truerow fromGET /api/v2/modelsfor account-scoped pins (the list is account-scoped); an end-user-scoped write may still succeed when that tenant's prepaid/gateway lane can fund a row that isfalseon the list. Connect the matching provider, or leave Default.- Query params are strict: an unknown query parameter is rejected with
422(error.details.error_code = "unknown_query_parameter", with a did-you-mean hint). Analytics params (utm_*,gclid,fbclid,_) are ignored. Unknown fields in request bodies are silently ignored.
Billing Error Codes
For 402, read error.error_code and error.details (BillingError.code and .details in Python):
from m8tes import BillingError
try:
run = client.runs.create_and_wait(agent_id=mate.id, message="...", user_id="customer_42")
except BillingError as e:
if e.code == "RUN_LIMIT_REACHED" and e.details.get("overage_available"):
client.billing.set_overage(enabled=True, monthly_cap_cents=5000) # keep running
elif e.code == "TOKEN_BALANCE_DEPLETED":
checkout_url = client.billing.topup(amount_cents=5000) # add $50 of credit
print("Top up to continue:", checkout_url)
else:
print(e.code, e.details)All billing codes, plus the two 429 guards
Two more guards return 429 rate_limit_error (not 402):
END_USER_RATE_LIMITED: an end-user burst past yourper_end_user_rate_per_minutesetting.detailscarryend_user_id,limit_per_minute,retry_after, plus aRetry-Afterheader. Configure all three sub-caps viaclient.settings.update(...); see Users.SANDBOX_CONCURRENCY_LIMIT: too many runs in flight at once.details.scopesays which ceiling:account(your own burst;detailscarry yourconcurrent_runsandlimit, plusupgrade_urlwhen a bigger plan runs more at once) orplatform(overall capacity, no counts). Wait for in-flight runs to finish and retry after theRetry-Afterheader. The cap depends on your plan (Concurrent runs); upgrade or contact support for more.
Run-Level Failures
The exceptions above cover problems reaching the API. A run can also fail upstream: an expired Claude credential, an exhausted quota, a model rate limit. The HTTP request succeeds (no exception), but the run ends with status "failed", the message in run.output, and a machine-readable class in run.error_code. Always check it before trusting output:
run = client.runs.create_and_wait(agent_id=mate.id, message="...", user_id="customer_42")
if run.error_code:
print(f"run failed upstream: {run.error_code}: {run.output}")
else:
print(run.output)Run `error_code` reference
Transient codes are retry-safe; credential codes need re-auth first.
Interrupted task runs continue their saved session, preserving the run ID and conversation.
A non-null next_retry_at means automatic recovery is queued; keep polling the run or
rejoin its stream when its status returns to running. Recovery checks every minute
starting three minutes after server startup, waits for any platform update to finish,
and uses the shared three-attempt limit. A newer run of the task, cancellation, or
unavailable funding can stop recovery; next_retry_at then clears and manual recovery
is available. Guarded coding runs cannot resume their discarded sandbox session and
require manual recovery. Scheduled failures without a saved session retain their existing fresh
retry policy, which refuses to replay work after external writes.
Cancel pending recovery with POST /api/v2/runs/{run_id}/cancel or
client.runs.cancel(run_id). A queued failed run becomes cancelled and its
next_retry_at clears, preventing either recovery worker from starting it.
A run that has not started
When your connected model subscription has stated when its usage limit resets, a new run that has nowhere else to go is not started just to be refused again. Nowhere else means no second account of that provider with room, no other connected provider outside its cooldown, and no credits. The run is held:
It has no sandbox and does not count toward your concurrent runs. It is not a failure:
no run.failed webhook fires, and polling sees paused, then running.
- Start it now: reply to it.
Continuestarts it with its first message and sends nothing after it. Any other message starts it and is delivered as its next turn. A reply with files or per-reply overrides answers 409. A reply in its Slack thread starts it too. - No run slot free:
Continueanswers 409 and can be sent again. Any other reply is queued, and the run starts as soon as a slot frees up. - Stop it:
POST /api/v2/runs/{run_id}/cancel. It becomescancelled. - Reconnect the subscription: the run starts within 30 seconds.
- A newer run of the same schedule: only the newest held run of a schedule starts.
Older ones become
cancelled. - Archive it: an archived run is not started. At its reset it becomes
cancelled. Unarchive it before then and it starts.
A run is only held on a reset time the provider stated, for the same account, credential and model, and never past that time.
For oauth_revoked, subscription_quota_exhausted, model_unavailable, and an
own-subscription rate_limited failure, manual recovery is the exception to model
pinning: retrying a task or replying to a chat resolves the account's current
connected/preferred provider instead of replaying the inaccessible concrete model.
Connect or select another provider first to switch; a chat reply resends only that
failed follow-up on the same run. Other failure retries keep the original model.
Account gates include EMAIL_VERIFICATION_REQUIRED, ACCOUNT_SUSPENDED, ACCOUNT_DELETED, and API_WALLET_REQUIRED. Restart a blocked one-off run after resolving the gate.
Next: Limits · Going Live