API Reference

Agents API

REST endpoints and MCP tools for the portable-agent registry: read, author, version, and export an agent.

The portable-agent registry is reachable two ways: as REST endpoints on the airlock API, and as tools on your organization's MCP endpoint. They share most of their implementation and all of their permission rules, so an agent looks the same from either side.

They are not the same contract, though, and the differences matter if you are porting a client between them:

  • Listing. REST paginates and filters; the list_agents tool takes no arguments and returns everything you can see.
  • Writes. A REST write executes directly. An MCP management write goes through execute_tool, so your organization's policy applies to it and it can require approval before it runs.
  • Rolling back. Both sides have it: POST .../agents/{name}/versions over REST, the create_agent_version tool over MCP. They run the same operation.
  • Errors. REST answers with HTTP status codes; a tool call answers with a result carrying isError. Authentication and protocol failures are the exception: those fail before a tool result exists.

Use REST from a script that holds a Cognito JWT; use the MCP tools from an agent, or from anything authenticating with a service token.

For what a portable agent is, and how the definition is shaped, start with the Portable Agents guide.

Base URL and authentication

https://api.air-lock.ai

Every request needs a bearer credential:

Authorization: Bearer <token>

This is a Cognito JWT, and only a Cognito JWT. The alk_pat_… personal tokens and alk_svc_… service tokens belong to the MCP endpoint: the REST API sits behind a JWT authorizer that rejects an opaque token before any agent handler runs. A CI script that authenticates with a service token has to reach the registry through the MCP tools below instead. See Authentication.

{orgSlug} in every path is your organization's slug. Callers must be an active member of that organization.

Endpoints at a glance

MethodPathMCP equivalent
GET/v1/orgs/{orgSlug}/agentslist_agents, airlock-management/search_agents
POST/v1/orgs/{orgSlug}/agentsairlock-management/create_agent
GET/v1/orgs/{orgSlug}/agents/{name}describe_agent
PATCH/v1/orgs/{orgSlug}/agents/{name}airlock-management/update_agent
DELETE/v1/orgs/{orgSlug}/agents/{name}airlock-management/delete_agent
GET/v1/orgs/{orgSlug}/agents/{name}/versions/{n}describe_agent with version
POST/v1/orgs/{orgSlug}/agents/{name}/versionsairlock-management/create_agent_version
GET/v1/orgs/{orgSlug}/agents/{name}/exportexport_agent
GET/v1/orgs/{orgSlug}/model-catalogairlock-management/get_model_catalog
GET/v1/orgs/{orgSlug}/agents/{name}/deploymentsairlock-management/list_deployments
POST/v1/orgs/{orgSlug}/agents/{name}/deploymentsairlock-management/create_deployment
GET/v1/orgs/{orgSlug}/deploymentsNone
GET/v1/orgs/{orgSlug}/deployments/{id}airlock-management/get_deployment
PATCH/v1/orgs/{orgSlug}/deployments/{id}None
DELETE/v1/orgs/{orgSlug}/deployments/{id}None
POST/v1/orgs/{orgSlug}/deployments/{id}/pushairlock-management/push_deployment
POST/v1/orgs/{orgSlug}/deployments/{id}/suspendairlock-management/suspend_deployment
POST/v1/orgs/{orgSlug}/deployments/{id}/resumeairlock-management/resume_deployment
GET, PUT, DELETE/v1/orgs/{orgSlug}/deploy-targets/{target}/credentialNone

Restoring an earlier version is a write of its own on both sides, and the Versions tab in the Control Room offers it as a button. Before that route existed, the REST way to roll back was to read the version you wanted and PATCH its spec back; that still works, but the dedicated route refuses a no-op and records the restore as one.

The envelope

The envelope is the document an agent's definition travels as. A write accepts one as its request body, and the two single-agent reads return one under an envelope key. It is not the shape of every response on this page: the list returns summaries plus page metadata, an export returns install instructions, and a delete returns no body at all.

{
  "agent": {
    "name": "triage",
    "description": "Triage incoming GitHub issues. Trigger phrases: \"triage this issue\".",
    "tags": ["support"],
    "license": "MIT"
  },
  "spec": {
    "schemaVersion": "1.0",
    "systemPrompt": "You triage incoming issues…",
    "modelPreference": [
      { "family": "claude", "model": "sonnet-4-6" },
      { "family": "gpt", "model": "gpt-5.4" }
    ],
    "vendorHints": { "vercel": { "maxSteps": 16 } }
  }
}

agent is mutable registry metadata and spec is the immutable per-version payload. name, description, schemaVersion, systemPrompt and a non-empty modelPreference are required; tags, license and vendorHints are optional and are omitted from a response rather than returned as null.

There is no ownership field. It was retired: who can use an agent is decided by access grants, on the agent's Access tab, and can change at any time. An envelope that still carries one imports cleanly and simply arrives without it, exactly as the retired spec.budget and spec.approvalMode do.

A definition carries no skills list, and sending one has no effect: unknown keys are stripped rather than rejected, so a spec.skills you send is silently dropped. Which skills an agent can reach is decided at run time by the identity it authenticates as, never by the definition.

vendorHints is opaque to airlock and always advisory. Nothing in it is enforced: vercel.maxSteps, for example, is a step cap the Vercel AI SDK install instructions ask your code to honor.

On a read, each modelPreference entry may carry an extra resolved field naming the concrete route airlock picked for it, plus a resolvedCandidates list when the model is reachable more than one way. Both are decoration added on the way out by the two GET routes and by describe_agent, never something you send, and a create or update response does not carry them. An entry comes back undecorated when airlock could not resolve it, which means "use your own mapping".

A body that fails validation comes back as 400 with every failed check listed, each message prefixed by the path it applies to:

{
  "error": "Invalid agent envelope",
  "details": [
    "spec.systemPrompt: must not be empty",
    "spec.modelPreference[0].family: must not be empty"
  ]
}

Unknown keys are stripped rather than rejected, so a field the schema does not know about is silently dropped instead of failing the write.

List agents

GET /v1/orgs/{orgSlug}/agents

Returns the agents visible to you: the ones you authored, plus any you hold an access grant for. Organization admins see every agent.

Query parameterEffect
limit, cursorPage through the library, newest first
tagsNarrow to agents carrying these tags. Repeat the parameter once per tag (?tags=support&tags=triage), matched exactly and combined with OR
modelNarrow to agents whose model preference names this family, in any position, not only the first
searchScored keyword search over name and description. Returns a relevance-ordered result rather than a page, so no cursor comes back
facetstrue adds the facet option lists over your whole visible library
{
  "agents": [
    {
      "id": "a7e1c9d4-5b32-4f8e-9c06-3b1d8f27ac54",
      "name": "triage",
      "description": "Triage incoming GitHub issues…",
      "tags": ["support"],
      "versionCount": 4,
      "modelPreference": [{ "family": "claude", "model": "sonnet-4-6" }],
      "createdBy": "…",
      "createdAt": "2026-07-02T09:12:44.001Z",
      "updatedAt": "2026-08-11T15:03:20.884Z"
    }
  ],
  "nextCursor": "…",
  "hasMore": true
}

versionCount is the current version number, which is also the total number of versions, because versions run 1 to N with no gaps.

A returned page can be smaller than limit while hasMore is still true: rows you cannot see are filtered after the page is read. Follow the cursor rather than treating a short page as the end.

Create an agent

POST /v1/orgs/{orgSlug}/agents

The body is an envelope. Any active member of the organization may create an agent. A new one is visible to its author and nobody else until it is granted; the agent and its first version are written together, so an agent is never readable without a version.

Returns 201 with:

{ "envelope": { "agent": { … }, "spec": { … } }, "version": 1 }
StatusWhen
400The envelope failed validation, or names a model family the catalog does not carry
409The name is already taken in this organization

Read an agent

GET /v1/orgs/{orgSlug}/agents/{name}
{
  "envelope": { "agent": { … }, "spec": { … } },
  "version": 4,
  "id": "a7e1c9d4-5b32-4f8e-9c06-3b1d8f27ac54"
}

id is the agent's immutable registry id. It is not part of the envelope, and it is what you store when you need a reference that survives a rename. It is also what access grants key on.

An agent you have no grant for answers 404, not 403, so nobody can probe for the existence of agents they cannot see.

Update an agent

PATCH /v1/orgs/{orgSlug}/agents/{name}

The body is a whole envelope, not a partial one. What happens to it depends on which half changed:

  • A change under agent (name, description, tags, license) applies in place.
  • Any change under spec writes a new version and makes it current.
  • Both at once does both, in one request.
  • Neither is a no-op that still returns the current state.

Returns 200 with { envelope, version }, where version is the current version after the update.

StatusWhen
400Validation failed
403You can see this agent but did not write it. Being granted an agent lets you use it, never rewrite it for everybody else who holds a grant
404No agent of that name is visible to you. Deliberately the same answer as a name nobody holds, so a name is never an existence probe
409The new name is already taken

A name is unique within your organization, so it addresses exactly one agent: reads and writes resolve to the same one, and there is no ambiguity to disambiguate.

Delete an agent

DELETE /v1/orgs/{orgSlug}/agents/{name}

Removes the agent, every one of its versions, and its access grants. Returns 204.

An agent with any deployment cannot be deleted, because the deployment pins a version the delete would take away. That comes back as 409 naming the deployments that blocked it:

{
  "error": "This agent still has deployments, including: \"prod\" (production). Delete its deployments first.",
  "deployments": [{ "id": "…", "name": "prod", "environment": "production" }]
}

Treat that list as indicative rather than exhaustive. A deployment created moments earlier may not appear in it yet.

Read a historical version

GET /v1/orgs/{orgSlug}/agents/{name}/versions/{n}

Versions are numbered from 1. Returns { envelope, version } with that version's authored spec, projected through the current schema. Registry metadata is not versioned, so the agent half is always current.

That projection matters for old versions: fields the schema has since dropped are stripped on read, and family and model names are trimmed. So a version stored a while ago comes back as what it means today, not byte-for-byte as it was written.

With one deliberate exception: a stored spec that no longer satisfies the schema at all is returned raw, unprojected, rather than being refused. So a very old or hand-edited version can still come back carrying fields the schema no longer knows. Treat the projection as the normal case, not a guarantee.

The response is not byte-stable across catalog changes. The resolved / resolvedCandidates decoration on each modelPreference entry is computed against the catalog as it stands now, not as it stood at that version, so the same historical read can answer differently after a catalog publish. Only the authored fields are genuinely historical. A client pinning a version should compare those, not the whole document.

400 for a version number that is not a positive integer, 404 for one that does not exist.

Restore an earlier version

POST /v1/orgs/{orgSlug}/agents/{name}/versions
{ "fromVersion": 2, "changeMessage": "Back to the calmer prompt" }

Copies the spec of fromVersion into a new version and makes it current. Nothing is rewritten: the history keeps every version, including the one you restored from, so the restore can itself be undone by restoring again. changeMessage is optional and defaults to Restored from v2.

Registry metadata is not versioned, so the agent half (name, description, tags, license) is left exactly as it is. A rename made after the version you are restoring stays.

The restored spec is projected through the current schema first, the same projection a historical read applies, so a version stored before a field was dropped comes back without it.

Returns 201 with { envelope, version, restoredFromVersion }, where version is the number of the new current version.

StatusWhen
400fromVersion is not a positive integer, the stored version no longer satisfies the schema, or the old spec names a model family the catalog has since retired and the current version does not also name it. The last case carries a details array naming each family
403You can see this agent but did not write it. Same gate as PATCH
404No agent of that name is visible to you, or no version carries that number
409That spec is already the current one. The check compares the spec, not the number, so restoring v1 twice in a row is refused the second time rather than appending an identical copy

Export an agent for a host

GET /v1/orgs/{orgSlug}/agents/{name}/export?adapter=claude-code
Query parameterRequiredValue
adapterYesclaude-code, cursor, claude-sdk, openai, bedrock, gemini, vercel
versionNoExport a historical version instead of the current one
{
  "format": "claude-code",
  "agentName": "triage",
  "artifacts": [],
  "installInstructions": "There is NO file to install. Claude Code reaches this agent over MCP…"
}

agentName is the name the export was rendered under. On this route it always equals the name in the path, because the path is what resolves the agent. It exists for parity with export_agent, which can address an agent by immutable id and therefore can return a name that differs from the one you asked for.

artifacts is always empty, for every target. No host is handed a file: each one connects to your MCP endpoint and fetches the definition per run, so the whole answer is in installInstructions. An empty list is a valid, successful export, not a failure. The field remains on the response so existing callers keep parsing it.

Exports fail with 400 in three cases worth knowing:

  • The adapter is not one of the values above. The message lists them.
  • The agent's modelPreference names nothing this host can run. That is deliberate: the target's model table is as real a constraint as the wire format, and a silent substitution would be worse than a refusal.
  • The agent's name is not safe to render into generated instructions, a pasted shell command, or the describe_agent arguments. Names are only required to be non-blank at the registry, so this is checked at export time instead.

Deployments

A deployment is the record of one agent version running on one platform as one service account. The Deploy tab in the Control Room creates and pushes them; these routes are what it calls. See Pushing an agent to ElevenLabs and Pushing an agent to Anthropic Managed Agents for what a push does on each platform.

Every read is open to any member of the organization. Every write is admin-only and answers 403 for anybody else.

List and read deployments

GET /v1/orgs/{orgSlug}/agents/{name}/deployments
GET /v1/orgs/{orgSlug}/deployments
GET /v1/orgs/{orgSlug}/deployments/{id}

The first lists one agent's deployments (404 for an agent you cannot see), the second every deployment in the organization, narrowed to one service account with ?serviceAccountId=. Both return { "deployments": [ … ] }; the third returns { "deployment": { … } }.

A deployment carries its id, name, agentId, the pinned agentVersion, environment (test or production), status (active, suspended or deleting), mode, serviceAccountId, autoPush, and a runtime block. runtime.target is the platform (elevenlabs or anthropic_managed_agents), and runtime.push holds the push state: pushedVersion is the version live on the platform, and lastAutoPush is what the auto-push setting last did. No response ever carries a token or a key.

Create a deployment

POST /v1/orgs/{orgSlug}/agents/{name}/deployments
{
  "name": "triage on ElevenLabs",
  "environment": "test",
  "mode": "unattended",
  "serviceAccountId": "…",
  "runtime": { "target": "elevenlabs" }
}

name, environment and mode are required. A deployment pushed to a platform is unattended and needs an ACTIVE service account. agentVersion pins a version and defaults to the current one. Creating a deployment contacts nothing; the push does. Returns 201 with { deployment }.

Push a deployment

POST /v1/orgs/{orgSlug}/deployments/{id}/push

Creates the agent on the deployment's platform on the first push and updates it on every later one. Every field is optional:

FieldEffect
versionThe agent version to push. Defaults to the current one. A version other than the pinned one repoints the deployment first
voiceElevenLabs only. { voiceId, language, firstMessage }, used when the agent is first created. An update push leaves them alone
nameCallerElevenLabs only. true sends the conversation's user_email variable as the caller on every tool call. Absent keeps the current setting

There is no model field: the model comes from the agent's model preference, and a body naming one is refused. A push mints a token bound to the deployment when it needs one and delivers it to the platform, never to the caller. Returns 200 with { deployment, created, tokenReminted }. A push that stopped part-way resumes from where it got to when you call it again.

Change a deployment

PATCH /v1/orgs/{orgSlug}/deployments/{id}

Takes name, runtime, or autoPush. autoPush is the New agent versions setting, one of off, propose (Ask for approval) or push (Push automatically), and must be sent in a request of its own, without name or runtime. The admin who sends it becomes the one the setting acts as. Returns 200 with { deployment }; 409 when the deployment changed under the write, so re-read and retry.

Suspend, resume and delete

POST   /v1/orgs/{orgSlug}/deployments/{id}/suspend
POST   /v1/orgs/{orgSlug}/deployments/{id}/resume
DELETE /v1/orgs/{orgSlug}/deployments/{id}

Suspending stops the deployment's token working on its next call. Resuming does not restore that token, so push again afterwards to deliver a fresh one. Deleting revokes the deployment's tokens and returns 204; it does not remove the agent from the platform, which you do there.

Platform keys

GET    /v1/orgs/{orgSlug}/deploy-targets/{target}/credential
PUT    /v1/orgs/{orgSlug}/deploy-targets/{target}/credential
DELETE /v1/orgs/{orgSlug}/deploy-targets/{target}/credential

The organization's API key for a platform, where {target} is elevenlabs or anthropic_managed_agents. GET tells any member whether a key is configured, and adds its last four characters (hint) for an admin. PUT takes { "value": "<key>" }, checks it with the platform first and stores nothing when the platform refuses it (400). No route ever returns the key itself.

The MCP tools

Three of the agent tools are meta-tools on the org endpoint, callable directly. The rest are management tools under the airlock-management namespace, which you invoke through execute_tool.

Meta-tools

ToolPurposeWho can call it
list_agentsEvery agent visible to you, each with its current version number and registry id. Takes no argumentsAny member
describe_agentThe full envelope. Takes agent (name), id, or both, plus an optional versionAny member who can see the agent
export_agentInstall guidance for a host: how to wire the endpoint and fetch the definition per run. Takes agent / id, a required adapter, and an optional versionAny member who can see the agent

describe_agent and export_agent address an agent three ways, and the difference matters:

  • id alone is the durable form. Prefer it for anything you store.
  • agent alone works, but a name is mutable and reusable, so a stored name-keyed reference can be orphaned by a rename, or silently re-bound when a new agent takes the freed name.
  • Both together resolves on the id and compares the name. When they disagree, the result carries a warning telling you the agent was renamed, so a caller that stored the old name can correct it.

An id that does not resolve never falls back to the name.

Management tools

Invoke these through execute_tool with the namespaced name, for example airlock-management/create_agent.

That namespace is the management project's slug, which is airlock-management by default but is not a constant. If a call comes back tool-not-found, read the authoritative name from list_services rather than assuming the default.

ToolPurposeWho can call it
search_agentsKeyword search over name, description and tags. Takes query and an optional limit. The tag half is the deliberate difference from the REST ?search=, which scores name and description only: a REST caller has the tags facet to reach a tag exactly, and an agent driving this tool does notAny member
create_agentCreate an agent and its first version from an envelopeAny member
update_agentUpdate an agent by name with a whole envelope. Registry edits apply in place, spec edits bump a versionIts author, or an admin
create_agent_versionRestore an earlier version: copies the spec of from_version into a new current version, with an optional change_summary. Append-only, so the restore can itself be undoneSame as update_agent
delete_agentDelete an agent by name, along with all of its versions and its access grants. An agent with a deployment is refused, and the refusal names the deployments that blocked itSame as update_agent, after approval its caller can give
get_model_catalogThe families and models a modelPreference may declare, and the concrete routes each one can run on. Fetch at boot instead of maintaining your own mapping, and re-fetch when the catalog version changesAny member
get_model_credentialRelease one of the organization's stored model-provider API keys by credentialName. Default-deny: released only to a service account holding an explicit grant for that credential name, so human users, admins included, are always refusedA granted service account
list_deploymentsOne agent's deployments, by agentName: target, pinned version, status, service account and push stateAn admin
get_deploymentOne deployment by deploymentId, plus deployKey: { target, configured }, whether the organization has stored a key for its platform. The key itself is never returnedAn admin
create_deploymentCreate a deployment of agentName for a target (elevenlabs or anthropic_managed_agents), running as serviceAccountId. Optional agentVersion, name and environment (test by default). Contacts nothing; push it nextAn admin, after approval
push_deploymentPush deploymentId to its platform. target must repeat the deployment's own platform, so an approver sees where the push goes; a mismatch is refused. Optional version, and for ElevenLabs voice and nameCaller. onlyIfNewer refuses, with nothing pushed, when the platform already runs that version or a newer one, or the agent has a newer version; the automatic proposals always set it. Safe to retry. The minted token goes to the platform and never into the resultAn admin, after approval
suspend_deploymentSuspend deploymentId. Its token stops working on the next callAn admin, after approval
resume_deploymentResume deploymentId. Push it again afterwards, because suspending invalidated the token the platform heldAn admin, after approval

"After approval" means the default policy: these tools, and the service-account writes below, ship with a REQUIRE_APPROVAL rule in front of your saved management policy, unless that policy names the tool itself or already denies it. An admin can change that rule like any other. "After approval its caller can give" is the same rule with self-approval on: the person whose agent made the call confirms it themselves, so a member deleting their own agent is not waiting on an admin.

A version bump tells you what it left behind. When update_agent writes a new version, or create_agent_version restores one, the result carries deploymentsBehind: each active pushed deployment still running an older version, with the version live on its platform, what its autoPush setting will do with the new one, and a suggestedCall holding the push_deployment arguments that would bring it current. A deploymentsNote beside it says what to ask the user, and tells a non-admin to ask an admin. Both fields are absent when nothing is behind. create_agent answers with a deploymentsNote naming create_deployment and push_deployment as the way to run the new agent on a platform.

Two notes on permissions that matter if you are automating against these.

A tool being visible is not a tool being allowed. Admin-only management tools are hidden from a non-admin's tool list, but the authorization decision is made by the handler on every call, not by that filter.

Authorship drives the write gate, not the grant. update_agent, create_agent_version and delete_agent are available to an agent's author and to organization admins, and to nobody else. Being granted an agent lets you use it, never rewrite it for everybody else who holds a grant. Exactly how the REST routes behave.

Service-account tools

A deployment runs as a service account, so the same namespace can manage them. All four are admin-only.

ToolPurposeApproval by default
list_service_accountsEvery service account in the organization, with the id that grant_access and create_deployment takeNo
create_service_accountCreate one from a name and an optional description. Creates no token: a deployment's push mints its own. At most 25 per organization, unless airlock raised the limit for itYes
update_service_accountChange name, description, status (ACTIVE or DISABLED, which stops every token the account holds) or allowedIpCidrsYes
delete_service_accountDelete one, revoking its tokens and removing its credentials, policies and access grants. Refused while any deployment runs as it; the refusal names those deploymentsYes

Errors

StatusMeaning
400Validation failed, or a required parameter is missing
401The bearer credential is missing or not valid
403Authenticated, but not permitted: you are not a member of the named organization, your account is not active, you are writing an agent you can see but did not author, or you are writing a deployment or platform key without being an admin
404No such agent, no such version, or anything hidden from you. 404 rather than 403 for anything you cannot see, so existence is never disclosed. An agent you CAN see but did not write answers 403 on a write, because pretending it does not exist would deny something you can see in your own list
409A name collision, or a delete blocked by an existing deployment

Over MCP the same conditions come back as a tool result with isError set and the message in the text content, rather than as an HTTP status.