Skip to main content

Agent Provisioning API

The Agent Provisioning API turns a working agent into a file. You describe the agent you want in one declarative JSON document - an AgentPackage - and POST it. Outermind creates the agent, grants its tools, binds its mailbox, attaches its knowledge sources and configures its escalation policy, then reports what it did.

The same document applied twice produces the same result. That is what makes it useful: an agent built through this API is repeatable, reviewable and diffable, and an MSP standing up the same agent in forty tenants applies one file forty times instead of clicking through the console forty times.

This page covers the base URL, authentication, the ceiling, the asynchronous model, and an end to end quickstart. The companion pages go deeper:

Base URL

The API is served from your tenant's regional API host, under the /api prefix. Pick the row that matches the region your tenant is in:

RegionBase URL
United Stateshttps://us-api.outermind.ai/api
United Kingdomhttps://uk-api.outermind.ai/api
Europehttps://eu-api.outermind.ai/api
Australiahttps://au-api.outermind.ai/api

If you are unsure which region you are in, look at the URL of the admin console you sign in to. A console at https://us.outermind.ai pairs with the API host https://us-api.outermind.ai.

Every route path in this documentation is written relative to that base. So app/provisioning/capabilities in the US is:

https://us-api.outermind.ai/api/app/provisioning/capabilities

There is no cross-region routing. A key minted in one region works only against that region's host.

Authentication

Every request carries a provisioning API key in an Authorization header:

Authorization: Bearer om_live_a1b2c3d4e5f6g7h8i9j0k1l2m3n4o5p6q7r8s9t0

A key looks like om_live_ followed by 32 random alphanumeric characters. Keys are minted from the admin console under Administration > Account > Provisioning API Keys; see Provisioning API Keys. The plaintext is shown exactly once, at mint time. Outermind stores only a hash of it plus the first 12 characters as a display prefix, so a lost key is replaced rather than recovered.

The key carries the tenant, and nothing else may

No route accepts a tenant identifier. Not in the URL, not in a query parameter, and not in the request body. The tenant is resolved from the key itself, and that is the only source the API will read.

This surprises people arriving from multi-tenant APIs where you name the tenant on every call. Here, a tenantId key anywhere in the document - at any nesting depth, under any capitalisation - is a hard 400 tenant_id_not_permitted_in_body, and it is rejected even when the value matches the key's own tenant. There is no permissive reading of a tenant identifier in a body, because a field the API sometimes ignores is a field a caller eventually trusts.

Scopes

A key carries an explicit list of scopes. There is no hierarchy, no wildcard, and no scope that implies another: a key that can provision agents cannot delete them unless packages.prune was granted separately.

ScopeGrants
agents.provisionCreate and update the agent itself
agents.toolsGrant tools to the agent
mailboxes.bindBind the agent to a monitored mailbox
knowledge.manageAttach knowledge sources to the agent
escalation.configureSet the agent's human-review policy
packages.readList packages, export a package, poll an operation
packages.pruneRemove resources the package no longer declares, and delete a package
catalog.readRead the tool and knowledge-base catalogs
knowledge.indexes.writeCreate and configure grounded knowledge indexes
knowledge.sources.writeRegister and update the knowledge sources that feed an index
knowledge.syncTrigger a sync of an existing knowledge source

All three knowledge scopes are live. knowledge.sync authorises the two knowledge-source sync routes; knowledge.indexes.write and knowledge.sources.write authorise POST app/provisioning/knowledge-packages/apply, which requires one or both depending on what your document declares - see the Endpoint Reference.

Apply is the only route whose required scopes are derived rather than fixed. It needs one scope per section your document actually declares, so a package with no spec.mailbox never needs mailboxes.bind. See Endpoint Reference.

A key missing a scope it needs gets 403 scope_not_granted, and the response names the missing scope in a scope field.

A second, separate bound applies to the admin minting the key rather than to the key itself: they can only grant a scope whose underlying console permissions their own role already carries, and a mint that asks for more is refused 403 scope_exceeds_minter_permissions. See Who may grant a scope.

The ceiling

Scopes say which kinds of change a key may make. The ceiling says how far each one may go. It bounds the tool names the key may grant, whether it may bind a mailbox at all, whether it may create a Subject Matter Expert, how far outbound email, Teams and Slack access may reach, which autonomy levels and review policies are permitted, which knowledge-source kinds are allowed, and the daily and monthly cost caps.

Three properties matter to an automation author:

  1. A new key starts at the narrowest ceiling. No tools, no mailbox binding, no outbound access, no SME, conservative cost caps. Widening any of it is a deliberate act by a human in the console. That act, not a per-request approval prompt, is the human checkpoint for this whole API.
  2. A ceiling is fixed at mint time. It cannot be edited afterwards. To change what a key may do, revoke it and mint a replacement.
  3. A document that asks for more than the ceiling permits is refused whole, with 409 package_exceeds_key_ceiling and a violations[] list naming every path that exceeded it. Nothing partial is applied. See the 409 versus 422 split.

GET app/provisioning/capabilities returns the calling key's scopes and ceiling verbatim, so an automation can check what it is holding before it builds a document.

Rate limiting

Requests are rate limited per key, per minute and per day. Exceeding a limit returns 429 rate_limited with a Retry-After response header giving the number of seconds to wait. Back off on that header rather than on a guess.

The asynchronous model

Apply and delete are asynchronous. Both return 202 Accepted with an operation id, and the actual work runs on a queue behind it:

{
"operationId": "6f2a9c14-2f1e-4a7b-8f0a-1e2d3c4b5a69",
"status": "pending",
"packageName": "billing-triage",
"pollUrl": "app/provisioning/operations/6f2a9c14-2f1e-4a7b-8f0a-1e2d3c4b5a69"
}

Two things to note about pollUrl. It is route-relative: no scheme, no host, and no /api prefix. Prepend your regional base URL, exactly as you would for any other route in this documentation. And the 202 carries no agent id, deliberately. Nothing has been created at that point, so there is no identifier that could be recorded for a resource which might still fail to materialise.

Polling

Poll GET app/provisioning/operations/{operationId} until status is terminal:

OPERATION_ID=6f2a9c14-2f1e-4a7b-8f0a-1e2d3c4b5a69

while true; do
BODY=$(curl -s \
-H "Authorization: Bearer $OM_KEY" \
"https://us-api.outermind.ai/api/app/provisioning/operations/$OPERATION_ID")
STATUS=$(echo "$BODY" | jq -r .status)
case "$STATUS" in
succeeded|partially_succeeded|failed) echo "$BODY" | jq .; break ;;
*) sleep 3 ;;
esac
done
statusTerminalMeaning
pendingnoAccepted and queued; nothing has run yet
runningnoThe reconcile is in progress
succeededyesEvery resource converged
partially_succeededyesSome resources converged and some did not; see resources[]
failedyesNo resource converged

A poll interval of a few seconds is appropriate. There are no webhooks or callbacks in this version, so polling is the only completion signal.

Retrying is safe

Apply is idempotent. Re-applying the same document converges the same resources and reports noop for everything that already matches, so retrying after a 500 internal_error, a network failure, or a partially_succeeded operation is the documented repair path rather than a risk of duplication.

One request at a time per package, though: while an operation for a package is pending or running, another apply or delete for that same package is refused with 409 operation_in_flight. That is a timing conflict, not a policy failure. Poll the operation already in flight, or retry once it settles.

Quickstart

This walks a fresh key from nothing to a running agent. It assumes a US tenant; substitute your own regional host.

1. Mint a key

In the admin console, go to Administration > Account > Provisioning API Keys and mint a key. For this quickstart, grant it agents.provision, agents.tools, packages.read and catalog.read. Widen the tool ceiling to cover the tools you intend to grant, and permit the autonomy level the document below asks for.

catalog.read is easy to miss. Without it, steps 3 and 4 below return 403 scope_not_granted, and you have no way to discover which tool names your tenant actually has. Neither a scope nor a ceiling can be added later, because both are fixed at mint time; a key that is short one is replaced, not edited.

Copy the plaintext key and store it in your secrets manager. Then export it:

export OM_KEY='om_live_...'
export OM_API='https://us-api.outermind.ai/api'

2. Confirm what the key may do

curl -s -H "Authorization: Bearer $OM_KEY" "$OM_API/app/provisioning/capabilities" | jq .
{
"keyPrefix": "om_live_a1b2",
"principalType": "tenant",
"scopes": ["agents.provision", "agents.tools", "packages.read", "catalog.read"],
"ceiling": {
"toolCeiling": ["search_*", "send_email"],
"capabilities": {
"mailboxBinding": false,
"outboundEmail": "internal",
"outboundTeams": "none",
"outboundSlack": "none",
"maxDailyUsageCost": 25,
"maxMonthlyUsageCost": 500,
"knowledgeSourceKinds": [],
"reviewPolicies": ["request_human_input"],
"autonomyLevels": ["investigate_only", "auto_low"],
"smeAgents": false,
"smeInvocationPolicyModes": []
}
}
}

This route needs no scope beyond a valid key, so it also doubles as a credential check.

3. Read the document contract

curl -s -H "Authorization: Bearer $OM_KEY" "$OM_API/app/provisioning/catalog/package-schema" | jq .

This returns the published JSON Schema for the AgentPackage document, the defaults the API materialises for fields you omit, and the closed vocabularies for autonomy levels, review policies, knowledge-source kinds and outbound access levels. It is machine readable, and it is the version that ships with the API you are actually calling, so validate against it rather than against a copy.

4. Find real tool names

curl -s -H "Authorization: Bearer $OM_KEY" "$OM_API/app/provisioning/catalog/tools" | jq '.tools[] | select(.withinToolCeiling)'

Each entry carries a withinToolCeiling flag computed against your own key, so you can filter to the tools this key could actually grant before you write a document that would be refused.

5. Rehearse with dryRun

curl -s -X POST "$OM_API/app/provisioning/agent-packages/apply?dryRun=true" \
-H "Authorization: Bearer $OM_KEY" \
-H "Content-Type: application/json" \
-d '{
"apiVersion": "provisioning.outermind.ai/v1",
"kind": "AgentPackage",
"metadata": { "name": "billing-triage", "version": "1.0.0" },
"spec": {
"agent": {
"friendlyName": "Billing Triage",
"description": "Reads inbound billing questions and drafts a first response.",
"instructions": "You triage billing questions. Summarise the ask, identify the account, and draft a reply for review.",
"autonomy": { "level": "auto_low" }
},
"tools": { "grant": ["search_knowledge_base"] }
}
}' | jq .

A dryRun is a full rehearsal, not just a diff. Authentication, document validation, scope, ceiling and tenant preflight all run, and nothing is written. If a document is going to be refused, dryRun refuses it with the same status code and the same body the real apply would return.

On success you get 200 and the plan:

{
"plan": [
{ "action": "create", "kind": "agent", "name": "billing-triage", "changes": { "friendlyName": "added" } },
{ "action": "create", "kind": "toolGrant", "name": "search_knowledge_base" }
],
"orphaned": [],
"driftedInConsole": []
}

6. Apply for real

Drop ?dryRun=true and repeat the request. You get 202 with an operationId.

7. Poll to completion

Use the poll loop above. A finished operation looks like this:

{
"operationId": "6f2a9c14-2f1e-4a7b-8f0a-1e2d3c4b5a69",
"status": "succeeded",
"packageName": "billing-triage",
"startedAt": "2026-08-14T09:12:04.000Z",
"completedAt": "2026-08-14T09:12:11.000Z",
"resources": [
{ "kind": "agent", "name": "billing-triage", "status": "applied" },
{ "kind": "toolGrant", "name": "search_knowledge_base", "status": "applied" }
]
}

The agent now exists in the console alongside agents built by hand. Editing it there is allowed; the next apply of this package reports any console edit it is about to overwrite under driftedInConsole.

Where to go next