Skip to main content
Read this page before writing code against /v1/teams, and read it against ONE tenant. 32 operations are mounted and addressable, and what each one does depends on which runtime that tenant is on — a single column, company_containers.sidecar_url.
  • a durable tenant is served 22; 10 refuse.
  • a hermes tenant is served 13; 19 refuse.
A tenant is a durable tenant only if naive up placed it there, so read the second line unless you know otherwise. registerVettaRuntime() is the only function that points company_containers.sidecar_url at the durable runtime, and it has exactly one production caller — services/placement.ts, reached from the apply route when a teams: block declares runtime.durable(...) and the operator supplies NAIVE_DURABLE_CREDENTIAL_<TEAM> out of band. It is still reachable from no route a tenant can call, because registering overwrites the tenant’s legacy coordinates and the way back is not symmetric; placement refuses outright if the tenant already has a live Hermes container. POST …/migrate is one of the 10 that refuse. Placement is an operator act, not a request.A refusal is never a “coming soon” placeholder that returns an empty object. It names the missing dependency on the wire, and it names the runtime it is about: error.details.runtime is "durable" or "hermes", and for a durable tenant error.details.missing additionally names which verb the runtime already implements and which control-plane translation is absent. Every success carries provider. Nothing on this surface fabricates a value.

What this surface is

/v1/teams addresses work by a pair: a team and a tenant.
Every operation except GET /v1/teams is scoped to that pair. tenantUserId is a tenant_users.id — the same id POST /v1/users returns and the same id the legacy /v1/users/{user_id}/… routes take. {team} is validated as a routing segment and echoed back in the response. It is not used as a storage key in this build, because a declared team’s name has no storage this API may read. Where that changes an answer, the response says so.
One credential, one host. These routes use the same Authorization: Bearer nv_sk_... key and the same base URL as every other endpoint. There is no separate runtime credential and no second hostname.

Authentication and gating

  • Auth: session cookie or API key (requireSessionOrAuth).
  • Idempotency: the mount carries the idempotency middleware. Read GET /v1/limits before relying on it — the store is an in-process Map with no cross-replica sharing and no body fingerprint, and the API prints that fact rather than advertising a guarantee it does not have.
  • AccountKit primitive gate: the tasks primitive, applied inside the router. There is no gatePrimitive(...) on the mount, and there is no registered teams primitive to gate on — registering one would oblige three mirrors, and gating on an unregistered slug produces an inert gate, which reads like a real one and enforces nothing. So the check is made where the tenant is resolved instead: every operation addressed to a (team, tenant) pair calls assertPrimitiveEnabled(kit, "tasks") — the same predicate gatePrimitive applies first, on the primitive whose data this surface serves. A kit that disables tasks is refused here exactly as it is at /v1/users/{id}/tasks:
    403
    The one exception is GET /v1/teams, which is company-scoped, resolves no tenant and therefore reads no kit. It answers 501 regardless.

Runtime provider is reported on every answer

A tenant runs on one of two runtimes, and mixed mode is the normal state. Every response that could be aggregated carries provider: An aggregate that omits provider lets a reader average a durable tenant and a hermes tenant into one number. None of these responses omit it.

The 32 operations

Legend, and it has three values now because a binary one could not be true:
  • live — serves real rows on BOTH runtimes (from different stores where the store differs: the runtime’s own board for a durable tenant, the legacy mirror for a hermes one).
  • durable — served for a durable tenant, 501 for a hermes one.
  • 501 — refused on both, with error.details.missing naming the absent dependency for that tenant’s runtime.

Company-scoped

The team at a tenant

The board

Runs

Approvals, cost, sessions, effects

How a refusal reads

501
501 is the code these use on purpose. 404 would say the address is wrong, 403 would say you are not allowed, and 200 with an empty list would say you have none — a claim about your tenant rather than about the build. Only 501 says “the request is well-formed and the server does not implement it”.

Fields that are null on purpose

Several responses carry a null beside a sibling *_unavailable_because string. That pairing is the surface’s core convention: no field on this API is filled with a plausible-looking placeholder. A 0, an [] or a synthesised digest in any of these places would be indistinguishable downstream from a real one. That is the failure the convention exists to prevent.