API: Projects — group resources, scope credentials, and attribute spend

The /v1/projects endpoints are now in the public API reference. A project groups the resources you create inside a workspace and the spend you incur from them, so one workspace can run several environments or several end customers without splitting into separate accounts.

Every workspace has an implicit Default project: any resource with no project lives there, and nothing you already send changes — the surface is additive and opt-in.

What a project groups:

KindBelongs to a project
Agents, knowledge bases, tools, audio assetsYes, and can be moved later
Phone numbers and SIP trunksYes
API keys and service accountsYes — a pin fixed when the credential is created
Vault credentials and webhook endpointsOne project, or workspace-wide
Conversations, callers, batch calls, test runs, memoriesYes, frozen at creation and never re-attributed
Usage and spendAttributed through the calling credential’s pin
Cloned voicesFrom the pin on the creating credential — except a consent-verified clone, which is always workspace-wide
The public voice and model catalogNo — workspace-wide

Manage the lifecycle with POST/GET/PATCH/DELETE /v1/projects and .../{project_id}, plus archive, unarchive, restore, teardown, stats, audit, promote, and the members sub-tree (grant/revoke access). Move an existing agent between projects from the console.

Filtering: every list endpoint that takes a project accepts a project_id query parameter. Omit it to get everything you can reach, pass a proj_... id for one project, or pass the literal default for the implicit Default project. On lists whose rows can be workspace-wide — credentials, webhook endpoints and cloned voices — that literal is shared instead, because an absent project there means workspace-wide rather than Default.

Names are unique per workspace, case-insensitively; a workspace holds at most 100 live projects, and at the cap the create is refused with 409 project_limit_reached.

A project is a filter and a grouping, not a security boundary — it scopes what a credential may reach, what a scoped member sees, and where spend lands. If one team must be unable to see another’s data at all, use a separate workspace. See Projects.

Per-project capacity ceilings

A project can now carry two optional capacity ceilings alongside its spend limit: max_concurrent_calls (the most voice-agent calls active at once) and max_requests_per_minute (the most API requests per minute from credentials pinned to the project, across every surface — TTS on a pinned credential included). Both are read on the Project, set via PATCH merge-patch, and cleared by sending null. Setting either requires the billing.manage permission.

Each must sit at or below the workspace’s own plan cap; a value above it is refused with 400 validation_failed naming the field and the ceiling. A project can only narrow the workspace’s capacity, never raise it. Both are checked after the workspace’s own caps: a call over the concurrency ceiling is refused with 429 concurrency_limit_reached (an inbound caller hears the busy message) keyed on the project the call’s agent lives in, and a request over the rate ceiling is refused with 429 rate_limited from a per-project bucket — the same codes the workspace limits already answer. Sibling projects keep their own headroom; console sessions and unpinned keys carry no project and are subject to neither.

Error codes

  • 404 project_not_found — no project with that id, or one your credential cannot reach.
  • 409 project_limit_reached — the workspace already holds the maximum live projects its plan allows (up to 100); delete an unused one to free a slot.
  • 409 project_archived — work would start or bill inside an archived project (a session or call dispatch, a batch or test run, a knowledge-base import, or synthesis on a credential pinned to it). Data stays readable and configuration stays editable; unarchive to resume — no balance or ceiling change clears it. The project consulted is the one the work is attributed to, so a workspace-wide key can be refused too.
  • 409 project_not_archived — a destructive teardown was asked for on a live project. Archive is the reversible pending-deletion state; archive first, then purge from there.
  • 402 project_spend_limit_exceeded — the project’s Orb-rated spend this calendar month reached its limit, the middle ceiling between the workspace budget and a key’s cap. Raise that project’s limit, move the work, or wait for the monthly reset. The project charged is the one the spend is attributed to (an agent conversation bills to its agent’s project), so a workspace-wide key can trip it.
  • 409 project_has_scoped_credentials — a delete was refused because API keys or service accounts are still pinned to the project; the listed credentials ride error.details. Revoke or re-mint them elsewhere first — a pin is never silently widened to the whole workspace.
  • 409 cross_project_reference — a move was refused because live references tie the resource to its current project; the blockers ride error.details.referrers so you can move them together. Detaching them always unblocks.
  • 409 project_too_large_to_promote — the project holds more resources than one synchronous promote may copy.
  • 409 project_restore_window_expired — a restore arrived after the purge retention window closed. Distinct from project_not_found on purpose: the project is gone for good, not an id you mistyped.
  • 409 project_not_purged — a restore was asked for on a project that has not been purged. Not a 404: the project exists and you can still see it.
  • 409 project_name_taken — a restore cannot reinstate the project because another has taken its name since the purge (a purge frees the name immediately). Rename the holder, then restore again.

Filtering request logs by project

The request-log and analytics project_id filter adds a third literal beyond the default and shared used on the resource lists above: unattributed narrows to workspace-level traffic that names no project — an unpinned key, or a console session. That is not the Default project, and a project filter cannot select Default, because a key can only ever be pinned to a project you created.