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:

IdentityWhat it isSet by
Your accountThe auth boundary. One m8tes account, one API key.Your API key (m8_...)
Your end-userThe data-isolation boundary inside your account.The user_id field on a resource

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.

Python
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:

Python
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:

JSON
{ "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:

JSON
{ "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

RuleAction
Keep keys on your serverAuthenticate end users there and attach user_id
Separate environmentsName keys for production, staging, and CI
Suspected exposureRotate immediately and store the new secret; it is shown once

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 immediately

Handle these key-management errors:

StatusWhenWhat to do
429You already hold 50 active named keys ("API key limit reached (50); revoke an unused key first.")Revoke an unused key; that frees a slot immediately
403The account was created by agent signup and is still unclaimed (no password set and not verified)Claim it via the emailed link before minting or rotating keys

The 403 blocks minting and rotation only. Listing and revoking stay open on an unclaimed account.

Named-key details: expiry, revocation, listing
Field or operationBehavior
nameRequired; trimmed; 1–100 characters
expires_in_daysOptional; 1–3650 days
activeFalse for revoked or expired keys
list()Newest first; up to 100 rows, including revoked keys
last_used_atUpdated at most once per minute
rotate(key_id)Keeps id, name, expiry; resets last-used timestamp. Revoked key returns 409
revoke(key_id)Idempotent. Another account's id returns 404
Key limit50 active keys; revoked keys free a slot
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:

Python
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 only

client.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_id set on every run/agent/task that belongs to one of your end-users
  • Strict mode on: client.settings.get().require_end_user_id is True
  • per_end_user_run_limit and/or per_end_user_cost_limit_cents set
  • Your code handles 402 (cap reached) and 429 (concurrency / rate limit) gracefully
  • Headless runs either use autonomous or resolve awaiting_approval programmatically
  • 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

Was this page helpful?