Going Live
Production checklist: keep one end user from draining your budget, exhausting compute, or seeing another end user's data.
The multi-tenancy boundary
Two identities, not the same thing:
Every agent, run, task, and memory created with a user_id is scoped to that end user. Reads are strict: user_id="alice" sees only alice's data, never bob's, and never account-level data created with no user_id. No fallback. No opt-out.
from m8tes import M8tes
client = M8tes() # reads M8TES_API_KEY
# Each of your users gets isolated runs by passing their id as user_id.
for event in client.runs.create(
agent_id=bot.id,
message="Summarize my open tickets",
user_id="alice", # → isolated to this end-user
):
...Cap each end-user so one can't drain the account
Without a per-end-user cap, one end user can consume the whole account budget. Set sub-caps:
client.settings.update(
per_end_user_run_limit=50, # ≤ 50 runs per end-user per billing period
per_end_user_cost_limit_cents=2000, # ≤ $20.00 metered cost per end-user
)
# Pass None to clear a cap; omit to leave it unchanged.When an end-user hits a cap, the run is rejected with a 402:
{ "error": { "type": "billing_error", "code": 402,
"details": { "error_code": "END_USER_RUN_LIMIT_REACHED" } } }The error code is END_USER_RUN_LIMIT_REACHED or END_USER_COST_LIMIT_REACHED. These caps limit each end user; the shared prepaid balance must also cover the run.
Concurrency
Concurrent in-flight runs are capped per account to protect shared compute. When you're at the cap, new runs are rejected with a 429:
{ "error": { "type": "rate_limit_error", "code": 429,
"details": { "error_code": "SANDBOX_CONCURRENCY_LIMIT" } } }Treat it as back-pressure: wait for in-flight runs to finish, then retry. The SDK already retries 429s with backoff.
API key hygiene
A named key is a labelled, independently revocable key with an optional expiry. Create one per environment:
prod = client.keys.create(name="production")
print(prod.id, prod.api_key) # api_key is shown ONCE; store it now
client.keys.create(name="ci", expires_in_days=30) # expiry is optional; 1 to 3650 days
for k in client.keys.list():
print(k.id, k.name, k.prefix, k.active, k.last_used_at)
client.keys.rotate(prod.id) # same id and name, new secret, old secret dies now
client.keys.revoke(prod.id) # stops authenticating immediatelyHandle these key-management errors:
The 403 blocks minting and rotation only. Listing and revoking stay open on an unclaimed account.
Named-key details: expiry, revocation, listing
The account's default key
Every account also has one legacy default key, managed without an id. It authenticates exactly like a named key and is unaffected by named-key operations:
new = client.keys.rotate() # store new.api_key now; it's shown once
client.keys.info() # { has_key, prefix }, never the secret
client.keys.revoke() # revokes the default key onlyclient.keys.revoke() does not touch named keys, and client.keys.revoke(key_id) does not touch the default key. Rotating the default key is subject to the same 403 on an unclaimed account.
Approvals for headless runs
Runs default to an approval model. For unattended (headless) runs there's no human to approve a risky tool call, so either:
- Run with
permission_mode="autonomous"(the agent acts without per-tool approval; appropriate for trusted, well-scoped agents), or - Keep approvals on and handle the pause: a run that needs approval enters
awaiting_approval; resolve it programmatically with the run's approve / answer endpoints.
See Human-in-the-Loop for the full model.
API stability and versioning
/api/v2 is the supported developer surface. We treat these as non-breaking:
- Adding optional request fields or response fields
- Adding new endpoints or enum values
- Deprecating a field or endpoint in docs while keeping it working
These are breaking and get advance notice when possible:
- Removing or renaming a field or endpoint
- Changing a field's type or a documented error code's meaning
- Tightening validation on an existing field (e.g. shrinking an allowed enum)
Notice: breaking changes ship with a changelog entry and, when the change affects live integrations, email to the account owner. Deprecated fields stay callable for at least one release cycle before removal.
Machine-readable schema: GET https://api.m8tes.ai/api/v2/openapi.json (V2 routes only; no auth).
SDK: pin a semver range (m8tes>=4.8,<5) and read the Python SDK changelog before upgrading.
Going-live checklist
- Server-side API key only; never exposed to end-users or browsers
- A named key per environment via
client.keys.create(name=...), and the one you tested with rotated -
user_idset on every run/agent/task that belongs to one of your end-users - Strict mode on:
client.settings.get().require_end_user_idisTrue -
per_end_user_run_limitand/orper_end_user_cost_limit_centsset - Your code handles
402(cap reached) and429(concurrency / rate limit) gracefully - Headless runs either use
autonomousor resolveawaiting_approvalprogrammatically - Balance won't interrupt production: auto-reload or spend alerts set on the prepaid balance (see Billing & Usage)
- Webhook endpoints verify the signature (see Webhook Events)
- Channel branding decided: use m8tes branding or arrange your own with sales@m8tes.ai
Next: Limits · Data Retention · Users