Skip to main content

Provisioning API Endpoint Reference

Every route the Agent Provisioning API serves. Paths are relative to your regional base URL (see Agent Provisioning API); in the US, prefix each one with https://us-api.outermind.ai/api/.

Every route requires an Authorization: Bearer om_live_... header. Errors share one envelope, documented in the Error Reference.

Route table

MethodRouteRequired scope
POSTapp/provisioning/agent-packages/applyderived per declared section
POSTapp/provisioning/knowledge-packages/applyknowledge.indexes.write and/or knowledge.sources.write
GETapp/provisioning/knowledge-packagespackages.read
GETapp/provisioning/knowledge-packages/{name}packages.read
DELETEapp/provisioning/knowledge-packages/{name}packages.prune
GETapp/provisioning/agent-packagespackages.read
GETapp/provisioning/agent-packages/{name}packages.read
GETapp/provisioning/agent-packages/{name}/statuspackages.read
DELETEapp/provisioning/agent-packages/{name}packages.prune
GETapp/provisioning/operations/{operationId}packages.read
GETapp/provisioning/capabilitiesnone beyond a valid key
GETapp/provisioning/catalog/toolscatalog.read
GETapp/provisioning/catalog/knowledge-basescatalog.read
GETapp/provisioning/catalog/package-schemanone beyond a valid key
POSTapp/knowledge-indexes/{indexId}/sources/{sourceId}/syncknowledge.sync
GETapp/knowledge-indexes/{indexId}/sources/{sourceId}/sync/{runId}knowledge.sync

The shape of the table above is load bearing: it is parsed by an automated check that asserts this reference documents exactly the routes the API serves. A new route must be added as a row here, with its method and route string each in backticks and nothing else in those cells. The final Required scope column is parsed the same way, against the API's scope vocabulary, so every scope must appear in backticks in the scope column of the call that needs it - a scope mentioned only in prose does not count as documented.

Every scope named in that table is read by a route that is live today. None of the three knowledge.* scopes confers deletion - tearing a knowledge index down is refused through this API by design and stays a console action, so no scope maps to knowledge:delete. Deletion is a separate authority: packages.prune is what gates the two DELETE routes above.

Who may grant a scope

Scopes bound what a key may do. A second, separate bound applies to the human minting it: an admin may only grant a scope whose underlying console permissions their own role already carries. A mint that asks for more is refused with 403 and the machine code scope_exceeds_minter_permissions; the body names the scope and the exact permission keys the minter is missing.

{
"error": "Forbidden",
"code": "scope_exceeds_minter_permissions",
"message": "You cannot grant scope(s) you do not hold: agents.tools (missing agents:write).",
"violations": [{ "scope": "agents.tools", "missingPermissions": ["agents:write"] }]
}
Permissions the minting admin must hold (all of them)Required scope
agents:writeagents.provision
agents:writeagents.tools
mailboxes:writemailboxes.bind
agents:write, knowledge:readknowledge.manage
escalation:configescalation.configure
agents:readpackages.read
agents:deletepackages.prune
tools:read, knowledge:readcatalog.read
knowledge:write, index-knowledge:settings:writeknowledge.indexes.write
knowledge:write, sharepoint:writeknowledge.sources.write
sharepoint:writeknowledge.sync

The check runs once, at mint time, and never again. That is deliberate: a key outlives the role of the admin who minted it, and re-checking on every request would revoke live credentials as a side effect of an unrelated role edit and put a permission query on the hot path of every call. A key minted by an admin who later loses a permission keeps working; revocation and expiry, not a runtime re-check, are how that key is retired. Nothing about this changes an existing key - scope arrays are never re-evaluated, downgraded or revoked by it.

POST app/provisioning/agent-packages/apply

Applies an AgentPackage document. This is the route that creates and updates agents.

Required scope: derived from the document. The API reads which sections your document declares and requires one scope per declared section, so a document with no mailbox never needs mailboxes.bind:

Declared in the documentScope required
spec.agent (always present)agents.provision
spec.tools.grant with at least one entryagents.tools
spec.mailboxmailboxes.bind
spec.knowledgeSources with at least one entryknowledge.manage
spec.escalation.reviewPolicy set to anything other than noneescalation.configure
prune=true on the query stringpackages.prune

Scopes are checked after the document is parsed (the required set is derived from it) and before the ceiling check, so a scope failure never reveals anything about your ceiling and vice versa.

Query parameters

ParameterValuesDefaultMeaning
dryRuntrueoffRun every gate and produce the plan, write nothing
prunetrueoffRemove resources this package owns but no longer declares

Only the exact string true enables either flag. dryRun=yes or dryRun=1 is off, so a typo can never quietly turn a rehearsal into a real apply, or quietly turn a prune on.

Request body

An AgentPackage document. See The AgentPackage Document.

Responses

200 OK, only when dryRun=true. The plan, with nothing written:

{
"plan": [
{ "action": "update", "kind": "agent", "name": "billing-triage", "changes": { "instructions": "modified" } },
{ "action": "create", "kind": "toolGrant", "name": "search_knowledge_base" },
{ "action": "noop", "kind": "toolGrant", "name": "send_email" }
],
"orphaned": [
{ "kind": "toolGrant", "name": "web_search", "ownership": "ledger", "hint": "pass prune=true to remove" }
],
"driftedInConsole": [
{ "kind": "agent", "name": "billing-triage", "fields": ["friendlyName"] }
]
}
FieldMeaning
plan[].actioncreate, update, noop or delete
plan[].kindagent, toolGrant, mailboxBinding, knowledgeSource or escalationConfig on an AgentPackage; knowledgeIndex on a KnowledgePackage
plan[].nameThe resource's name within its kind
plan[].changesPresent on single-resource kinds only. A map of field path to added or modified
plan[].addressPresent on mailboxBinding entries only. The mailbox address
orphaned[]Resources not declared by this document. ownership: "ledger" means this package created it and prune=true would remove it, except for knowledgeIndex, whose teardown is refused through this API and which is reported retained instead; ownership: "unowned" means something else created it and prune will never touch it
driftedInConsole[]Resources whose live state was edited in the console since this package last applied, and which this apply is about to overwrite. fields[] names what differs

delete entries appear in plan[] only when prune=true, and only for ownership: "ledger" orphans.

202 Accepted, the normal apply response. Always 202, even when the work could have finished inline, so a consumer never has to special case a contract that varies by timing:

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

pollUrl is route relative. Prepend your regional base URL.

Errors

400 invalid_document, 400 unknown_field, 400 tenant_id_not_permitted_in_body, 400 unsupported_api_version, 400 unsupported_kind, 401 invalid_key, 401 key_revoked, 401 key_expired, 403 scope_not_granted, 409 package_exceeds_key_ceiling, 409 name_conflict, 409 operation_in_flight, 422 preflight_failed, 429 rate_limited, 500 internal_error.

unsupported_kind means the document's root kind is one this API does not serve. It is checked immediately after apiVersion and before anything else, so a document declaring an unknown kind is never parsed on a best-effort basis; fields[0].path is kind and reason lists the kinds that are served. Today those are AgentPackage and KnowledgePackage. Each apply route serves exactly one of them, so posting a KnowledgePackage here is also unsupported_kind, naming the kind this route does serve.

Every one of them is a zero-write rejection. A request refused at any gate leaves the tenant byte identical, so iterating on a document never leaves debris behind.

POST app/provisioning/knowledge-packages/apply

Applies a KnowledgePackage document. This is the route that creates and updates grounded knowledge indexes.

Required scopes. This route derives its scopes per declared section, so a document that only adds sources to an index you already have needs the narrower key.

  • knowledge.sources.write is required whenever any index stanza carries a sources property. That is presence, not non emptiness: an explicitly empty sources: [] is the array that makes every live source of that index an orphan, so it needs the same authority adding one does.
  • knowledge.indexes.write is required whenever any index stanza declares something about the index ITSELF - that is, any of description, retrievalReasoningEffort or outputMode. A stanza that carries a non empty sources array and none of those is a source only edit and does not need this scope. name and retrieverType are the index identity rather than a change to it, so declaring them does not make a stanza an index edit.
  • knowledge.indexes.write is re checked after preflight for any declared index name that does not resolve to a live index. Creating an index is index authority however the document is shaped, so a key holding only knowledge.sources.write cannot bring a new index into existence as a side effect of declaring a source in it. The refusal is a 403 naming knowledge.indexes.write, and it happens before anything is planned or written - ?dryRun=true is gated identically.

The check is on the resulting desired state rather than on the difference from live state, so converging an index you already have needs the same scope creating it did; otherwise a two step apply would reach past a bound a one step apply could not. ?prune=true additionally requires packages.prune, exactly as on the agent-packages route.

The request shape, the query parameters and the response bodies are the sibling route's. ?dryRun=true returns the same 200 plan envelope and writes nothing; a real apply returns the same 202 with an operationId you poll at app/provisioning/operations/{operationId}. The error codes are the sibling route's with one exception, called out under Errors below.

{
"apiVersion": "provisioning.outermind.ai/v1",
"kind": "KnowledgePackage",
"metadata": { "name": "sales-knowledge", "version": "1.0.0" },
"spec": {
"indexes": [
{
"name": "Sales Operations",
"retrieverType": "sharepoint",
"description": "Playbooks, pricing and territory documents.",
"retrievalReasoningEffort": "medium",
"outputMode": "answerSynthesis"
}
]
}
}

plan[].kind is knowledgeIndex for every entry this route produces.

What a package may not declare

IndexId, ToolId, KnowledgeBaseName and every Azure resource name are derived, never settable: declaring one is a 400 unknown_field. So are openAIProviderId and searchProviderId - both resolve from the tenant's active Azure OpenAI and Azure AI Search providers at apply time, exactly as the console does. A package naming provider identifiers would not be portable between tenants, which is the whole point of a package. A tenant with no active provider of either kind is refused 422 preflight_failed carrying knowledge_provider_not_configured, before anything is created.

retrieverType is immutable once an index exists: it decides the Azure index schema and every derived resource name, so changing it would strand the live index. A document that changes it is refused 422 with knowledge_index_retriever_type_immutable, and the live value is never rewritten.

Omitting retrievalReasoningEffort or outputMode keeps whatever the index already has; on a first apply the platform default applies (medium and answerSynthesis). Index names must be unique within one document, and a duplicate is a 400 at the gate rather than a failure partway through an apply.

Deletion is refused by design

Dropping an index from the document, with or without ?prune=true, never deletes it. It appears in orphaned[] with a hint saying so, and the operation reports it as retained. Tearing an index down destroys its Azure search index, knowledge source and knowledge base along with everything indexed in them, so it stays a deliberate console action; no scope grants it through this API.

Errors

The same set as the agent-packages route, minus 409 name_conflict, which cannot occur here: package identity is scoped by kind, so a KnowledgePackage never collides with an agent name and this route's prerequisite failures are always 422. Plus these 422 preflight_failed codes in unmet[].code:

CodeMeaning
knowledge_provider_not_configuredThe tenant has no active Azure OpenAI provider, no active Azure AI Search provider, or neither
knowledge_index_retriever_type_immutableA declared index exists with a different retrieverType
knowledge_index_retriever_type_unsupportedThe declared retrieverType has no index schema in this build and cannot be provisioned
knowledge_index_name_ambiguousMore than one index in the tenant carries the declared name and none is owned by this package

GET app/provisioning/knowledge-packages

Lists the KnowledgePackages this tenant has applied. Agent packages are a separate family on a separate route, so the two never appear in one another's list even when they share a name.

Required scope: packages.read.

curl -s -H "Authorization: Bearer $OM_KEY" \
"https://us-api.outermind.ai/api/app/provisioning/knowledge-packages" | jq .

200 OK:

{
"packages": [
{
"name": "sales-knowledge",
"version": "1.0.0",
"resourceCount": 2,
"createdAt": "2026-08-02T11:40:19.000Z",
"lastAppliedAt": "2026-08-21T08:05:44.000Z"
}
]
}

The body shape is the agent-packages list's, field for field. resourceCount counts the indexes and sources the package owns.

A tenant with no knowledge packages gets 200 and an empty list, never a 404.

Errors

401 invalid_key, 401 key_revoked, 401 key_expired, 403 scope_not_granted, 429 rate_limited, 500 internal_error.

GET app/provisioning/knowledge-packages/{name}

Exports the named package as a KnowledgePackage document, rendered from current live state.

Required scope: packages.read.

{name} is the package's metadata.name.

curl -s -H "Authorization: Bearer $OM_KEY" \
"https://us-api.outermind.ai/api/app/provisioning/knowledge-packages/sales-knowledge" | jq .

200 OK: the KnowledgePackage document itself, not wrapped in an envelope. It is a valid apply body, so an exported document round trips: you can export from one tenant, review it, and apply it to another.

{
"apiVersion": "provisioning.outermind.ai/v1",
"kind": "KnowledgePackage",
"metadata": {
"name": "sales-knowledge",
"version": "1.0.0",
"lineage": { "origin": "exported", "sourceTenantId": null }
},
"spec": {
"indexes": [
{
"name": "Sales Operations",
"retrieverType": "sharepoint",
"description": "Playbooks, pricing and territory documents.",
"retrievalReasoningEffort": "medium",
"outputMode": "answerSynthesis",
"sources": [
{
"kind": "sharepoint",
"name": "Sales Playbooks",
"sites": [
{ "siteUrl": "https://contoso.sharepoint.com/sites/Sales", "libraries": ["Documents"] }
],
"syncMode": "daily",
"fileTypes": ["docx", "xlsx", "pdf", "pptx", "txt", "md"],
"maxFileSizeBytes": 5242880,
"generateSummaries": true
}
]
}
]
}
}

Every optional field is written out explicitly with the value the index or source actually holds, rather than left to a default. That is what makes the export a fleet replication tool: applying it elsewhere produces the same configuration, not the same configuration plus whatever the defaults happen to be that day.

Nothing derived is ever included: no indexId, sourceId, siteId, driveId, libraryId or toolId, and lineage.sourceTenantId is always null.

If live state has drifted from what the package last applied, the exported document carries metadata.exportedWithDrift: true. The flag appears only when there is drift; it is never present as false. It is accepted on apply, so the round trip stays byte compatible. Drift here means an index or source was renamed, edited or removed in the console since the package last applied.

Errors

401 invalid_key, 401 key_revoked, 401 key_expired, 403 scope_not_granted, 404 package_not_found, 429 rate_limited, 500 internal_error.

A package belonging to another tenant returns 404, never 403. A 403 would confirm it exists.

DELETE app/provisioning/knowledge-packages/{name}

Removes the knowledge sources the named package owns, and reports every index it owns as retained.

Required scope: packages.prune. The scope is checked before any read, so a key without it learns nothing about whether the package exists.

curl -s -X DELETE -H "Authorization: Bearer $OM_KEY" \
"https://us-api.outermind.ai/api/app/provisioning/knowledge-packages/sales-knowledge" | jq .

202 Accepted, with the same body shape apply returns, polled the same way:

{
"operationId": "b71c33d0-0f92-4a11-9ac0-6b7f22c4e5aa",
"status": "pending",
"packageName": "sales-knowledge",
"pollUrl": "app/provisioning/operations/b71c33d0-0f92-4a11-9ac0-6b7f22c4e5aa"
}

This route never tears an index down

Indexes are reported, not removed. Polling the operation shows each one as status: "retained" with reason: "teardown_refused", and no action field, because no teardown was attempted:

{
"resources": [
{ "kind": "knowledgeIndexSource", "name": "Sales Operations/Sales Playbooks", "action": "delete", "status": "deleted" },
{ "kind": "knowledgeIndex", "name": "Sales Operations", "status": "retained", "reason": "teardown_refused" }
]
}

A retained entry counts as neither a deletion nor a failure, so a DELETE whose only remaining resources are indexes still reports succeeded.

Tearing an index down destroys its Azure search index, knowledge source and knowledge base along with everything indexed in them, and where a tenant holds more than one index of the same retrieverType those Azure resources are shared. It stays a deliberate console action; no scope, verb or query parameter grants it through this API.

Because the index is retained, so is its ownership record: the package remains in GET app/provisioning/knowledge-packages after a DELETE, with a smaller resourceCount. That record is the only proof the package owns the index, so it is kept deliberately. Re-applying the same metadata.name adopts those indexes again rather than creating duplicates.

Only resources this package owns are touched. An index or source created outside the package is left alone.

Errors

401 invalid_key, 401 key_revoked, 401 key_expired, 403 scope_not_granted, 404 package_not_found, 409 operation_in_flight, 429 rate_limited, 500 internal_error.

GET app/provisioning/agent-packages

Lists the packages this tenant has applied.

Required scope: packages.read.

curl -s -H "Authorization: Bearer $OM_KEY" \
"https://us-api.outermind.ai/api/app/provisioning/agent-packages" | jq .

200 OK:

{
"packages": [
{
"name": "billing-triage",
"version": "1.2.0",
"resourceCount": 4,
"createdAt": "2026-07-02T11:40:19.000Z",
"lastAppliedAt": "2026-08-13T08:05:44.000Z"
}
]
}

A tenant with no packages gets 200 and an empty list, never a 404. The collection exists, it is simply empty.

Errors

401 invalid_key, 401 key_revoked, 401 key_expired, 403 scope_not_granted, 429 rate_limited, 500 internal_error.

GET app/provisioning/agent-packages/{name}

Exports the named package as an AgentPackage document, rendered from current live state.

Required scope: packages.read.

{name} is the package's metadata.name.

curl -s -H "Authorization: Bearer $OM_KEY" \
"https://us-api.outermind.ai/api/app/provisioning/agent-packages/billing-triage" | jq .

200 OK: the AgentPackage document itself, not wrapped in an envelope. It is a valid apply body, so an exported document round trips: you can export from one tenant, review it, and apply it to another.

If live state has drifted from what the package last applied, the exported document carries metadata.exportedWithDrift: true. That flag is accepted on apply, so the round trip stays byte compatible.

Errors

401 invalid_key, 401 key_revoked, 401 key_expired, 403 scope_not_granted, 404 package_not_found, 429 rate_limited, 500 internal_error.

A package belonging to another tenant returns 404, never 403. A 403 would confirm it exists.

GET app/provisioning/agent-packages/{name}/status

Reports the runtime state of the agent the named package owns: whether it is active, when it last did anything, what it has been doing over a bounded window, and how its spend stands against the daily and monthly cost ceilings the package declares.

Every other route on this page reports on the apply. This one reports on the agent afterwards, so automation that provisions a fleet can also watch it.

Required scope: packages.read.

The agent is resolved through the package's ownership records, never by name, so this route can only ever report on an agent the package actually created.

Query parameters

ParameterValuesDefaultMeaning
days1 to 307Size of the activity window, in the tenant's local calendar days, ending today

A value outside the range is clamped; a non-numeric value falls back to the default. days affects the activity block only. The spend block's windows are fixed by the ceilings themselves (today, and month to date), because those are the windows the platform actually enforces against.

curl -s -H "Authorization: Bearer $OM_KEY" \
"https://us-api.outermind.ai/api/app/provisioning/agent-packages/billing-triage/status?days=7" | jq .

200 OK:

{
"packageName": "billing-triage",
"timezone": "America/Denver",
"agent": { "isActive": true, "isDeleted": false },
"window": { "days": 7, "from": "2026-08-11", "to": "2026-08-17" },
"activity": {
"executions": 128,
"toolCalls": 512,
"successfulToolCalls": 498,
"failedToolCalls": 14,
"llmCalls": 260,
"lastActivityAt": "2026-08-17T09:14:22.000Z"
},
"spend": {
"currency": "USD",
"daily": { "spent": 3.12, "limit": 25, "limitReached": false },
"monthly": { "spent": 41.87, "limit": 500, "limitReached": false }
}
}
FieldMeaning
timezoneThe timezone this response was read in. Normally the tenant's configured one; UTC if that setting is unreadable or is not a usable IANA zone. It buckets both the activity window and the spend day and month, so from, to, "today" and "this month" are all read in whatever this field reports
agent.isActiveWhether the agent is switched on. An inactive agent runs nothing
agent.isDeletedWhether the agent has been removed. A deleted agent's history is still reported
window.daysThe window actually applied, after clamping
window.from, window.toThe inclusive local-date bounds of the window
activity.executionsExecutions started in the window
activity.toolCalls, successfulToolCalls, failedToolCallsTool call counts in the window. failedToolCalls counts TOOL CALLS, not failed executions - a single execution can make many calls and survive a failed one
activity.llmCallsModel calls in the window
activity.lastActivityAtWhen execution statistics for this agent were last written, in UTC. This is a daily rollup timestamp, not a precise "last execution finished at". It is deliberately NOT windowed: an agent that was busy last month and idle this week still reports last month's timestamp rather than null
spend.daily.spentSpend so far in the tenant's current local day, in USD
spend.monthly.spentSpend so far in the tenant's current local month, in USD
spend.*.limitThe ceiling this agent is enforced against, from spec.agent.limits.maxDailyUsageCost / maxMonthlyUsageCost. null when no positive ceiling is set
spend.*.limitReachedtrue once spent reaches limit, which is exactly when the platform starts refusing the agent's executions. null when there is no ceiling to compare against

spend is read from the same records the cost ceilings are enforced against, so the number here and the number the platform gates on cannot disagree.

agent is null, with activity zeroed and spend amounts null, when the package exists but owns no agent (a partial apply) or when its agent row has since been removed outright. That is still a 200: the package demonstrably exists, so a 404 would be wrong.

No instruction text, mailbox address, supervisor address, tool name, internal identifier or key material appears anywhere in this response.

Errors

401 invalid_key, 401 key_revoked, 401 key_expired, 403 scope_not_granted, 404 package_not_found, 429 rate_limited, 500 internal_error.

A package belonging to another tenant returns 404, never 403. A 403 would confirm it exists.

DELETE app/provisioning/agent-packages/{name}

Removes everything the named package owns: the agent, its tool grants, its mailbox binding and its knowledge-source attachments.

Required scope: packages.prune. The scope is checked before any read, so a key without it learns nothing about whether the package exists.

curl -s -X DELETE -H "Authorization: Bearer $OM_KEY" \
"https://us-api.outermind.ai/api/app/provisioning/agent-packages/billing-triage" | jq .

202 Accepted, with the same body shape apply returns. The teardown makes live calls to Microsoft 365, so it runs on the queue and is polled the same way:

{
"operationId": "b71c33d0-0f92-4a11-9ac0-6b7f22c4e5aa",
"status": "pending",
"packageName": "billing-triage",
"pollUrl": "app/provisioning/operations/b71c33d0-0f92-4a11-9ac0-6b7f22c4e5aa"
}

Only resources this package owns are removed. Anything created outside the package is left alone, even if the agent uses it.

Errors

401 invalid_key, 401 key_revoked, 401 key_expired, 403 scope_not_granted, 404 package_not_found, 409 operation_in_flight, 429 rate_limited, 500 internal_error.

GET app/provisioning/operations/{operationId}

Reads one provisioning operation. This is the poll target for the pollUrl apply and delete return.

Required scope: packages.read, or the key that created the operation. A key may always read back an operation it started, so a key minted only to apply can follow the pollUrl it was handed without also holding packages.read. Any other operation needs packages.read; without it the answer is 403 scope_not_granted whether or not the operation exists.

curl -s -H "Authorization: Bearer $OM_KEY" \
"https://us-api.outermind.ai/api/app/provisioning/operations/6f2a9c14-2f1e-4a7b-8f0a-1e2d3c4b5a69" | jq .

200 OK:

{
"operationId": "6f2a9c14-2f1e-4a7b-8f0a-1e2d3c4b5a69",
"status": "partially_succeeded",
"packageName": "billing-triage",
"startedAt": "2026-08-14T09:12:04.000Z",
"completedAt": "2026-08-14T09:12:19.000Z",
"resources": [
{ "kind": "agent", "name": "billing-triage", "status": "applied" },
{ "kind": "toolGrant", "name": "search_knowledge_base", "status": "applied" },
{
"kind": "knowledgeSource",
"name": "Billing Policies",
"status": "failed",
"error": { "code": "reconcile_failed", "message": "knowledge source could not be attached" }
}
]
}

status is one of pending, running, succeeded, partially_succeeded or failed. The first two are non terminal; keep polling.

resources[] is empty until the operation reaches a terminal status. Entries come in two shapes:

Converge entries, from an apply:

FieldValues
kindagent, toolGrant, mailboxBinding, knowledgeSource, escalationConfig, knowledgeIndex
nameThe resource's name within its kind: the package name for agent, the tool name for toolGrant, the mailbox address for mailboxBinding, the knowledge base's name for knowledgeSource, the policy value for escalationConfig, and the index's declared name for knowledgeIndex
statusapplied, noop, skipped or failed
reasonPresent on skipped only. prerequisite_failed
errorPresent on failed only. { "code": "reconcile_failed", "message": "..." }

Prune entries, from prune=true or a DELETE. Distinguished by a status outside the converge vocabulary, and, whenever a teardown was actually attempted, by carrying action:

FieldValues
kindAs above
nameThe resource name
actiondelete whenever a teardown was attempted. Absent on retained, where none was
statusdeleted, failed or retained
reasonPresent on retained only. teardown_refused
errorPresent on failed only. { "code": "prune_failed", "message": "..." }

retained is reported for a knowledgeIndex orphan and only for that kind: the orphan is named so you can see it, and is deliberately left alone. It counts as neither a deletion nor a failure, so a prune whose only orphans are retained still reports succeeded.

One further entry shape appears when an operation exhausts its retries rather than completing: kind is operation, name is the operation id, status is failed, and error.code is either apply_failed or operation_in_flight. Both are retryable by re-applying the same document.

Errors

401 invalid_key, 401 key_revoked, 401 key_expired, 403 scope_not_granted, 404 operation_not_found, 429 rate_limited, 500 internal_error.

An operation belonging to another tenant, and a malformed operation id, both return 404 operation_not_found.

GET app/provisioning/capabilities

Reports what the calling key may do. No scope is required beyond a valid key: a credential that cannot read its own boundaries has to discover them through production failures.

curl -s -H "Authorization: Bearer $OM_KEY" \
"https://us-api.outermind.ai/api/app/provisioning/capabilities" | jq .

200 OK:

{
"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
}
}
}
FieldMeaning
keyPrefixThe 12-character display prefix, so an automation holding several keys can tell which one it presented
principalTypetenant
scopes[]Every scope granted to this key
ceiling.toolCeiling[]Tool names this key may grant. Entries may contain a * wildcard; a document may not
ceiling.capabilities.mailboxBindingWhether a document may declare spec.mailbox at all
ceiling.capabilities.smeAgentsWhether a document may declare spec.agent.sme at all
ceiling.capabilities.outboundEmail, outboundTeams, outboundSlackThe furthest reach permitted on each channel. none, internal or any, each permitting itself and everything narrower
ceiling.capabilities.maxDailyUsageCost, maxMonthlyUsageCostUpper bounds on the matching spec.agent.limits values
ceiling.capabilities.autonomyLevels[]Permitted values for spec.agent.autonomy.level. An empty list means no autonomy dial may be declared
ceiling.capabilities.reviewPolicies[]Permitted values for spec.escalation.reviewPolicy. none is always permitted regardless of this list
ceiling.capabilities.knowledgeSourceKinds[]Permitted values for spec.knowledgeSources[].kind. An empty list means no knowledge source may be attached

Errors

401 invalid_key, 401 key_revoked, 401 key_expired, 429 rate_limited, 500 internal_error.

GET app/provisioning/catalog/tools

Lists every tool this tenant could grant to an agent, annotated with whether the calling key's ceiling permits it.

Required scope: catalog.read.

curl -s -H "Authorization: Bearer $OM_KEY" \
"https://us-api.outermind.ai/api/app/provisioning/catalog/tools" | jq .

200 OK:

{
"tools": [
{
"name": "search_knowledge_base",
"friendlyName": "Search Knowledge Base",
"category": "Knowledge",
"description": "Search the tenant's grounded knowledge indexes.",
"source": "platform",
"impact": "read",
"withinToolCeiling": true
}
]
}
FieldMeaning
nameThe exact value to put in spec.tools.grant[]
friendlyNameDisplay name, or null
categoryThe tool's group, or null
descriptionWhat the tool does, or null
sourceplatform for a tool every tenant has, tenant for one this tenant defined
impactHow consequential invoking the tool is: read, write, act, destroy, or unclassified when the tool is tenant defined and carries no classification
withinToolCeilingWhether the calling key's toolCeiling covers this name

The row set is the same availability test the apply route's tenant preflight applies, so a name this catalog returns will not be refused as unavailable against the same tenant state. Filtering on withinToolCeiling before writing a document is the cheapest way to avoid a ceiling refusal.

Errors

401 invalid_key, 401 key_revoked, 401 key_expired, 403 scope_not_granted, 429 rate_limited, 500 internal_error.

GET app/provisioning/catalog/knowledge-bases

Lists the knowledge bases this tenant has, which are the only values spec.knowledgeSources[].name may take.

Required scope: catalog.read.

curl -s -H "Authorization: Bearer $OM_KEY" \
"https://us-api.outermind.ai/api/app/provisioning/catalog/knowledge-bases" | jq .

200 OK:

{
"knowledgeBases": [
{
"name": "Billing Policies",
"description": "Current billing and refund policy documents.",
"toolName": "search_billing_policies"
}
]
}

name is exactly what a document must carry. The API never creates a knowledge base; attaching one means granting the agent access to a knowledge base an admin already built in the console.

Errors

401 invalid_key, 401 key_revoked, 401 key_expired, 403 scope_not_granted, 429 rate_limited, 500 internal_error.

GET app/provisioning/catalog/package-schema

Serves the document contract itself: the published JSON Schema, the defaults the API materialises, and the closed vocabularies. No scope is required beyond a valid key, for the same reason capabilities needs none.

curl -s -H "Authorization: Bearer $OM_KEY" \
"https://us-api.outermind.ai/api/app/provisioning/catalog/package-schema" | jq .

200 OK:

{
"apiVersion": "provisioning.outermind.ai/v1",
"schema": {
"$schema": "http://json-schema.org/draft-07/schema#",
"title": "AgentPackage (provisioning.outermind.ai/v1)",
"type": "object",
"required": ["apiVersion", "kind", "metadata", "spec"]
},
"defaults": {
"agent": {
"agentType": "standard",
"isActive": true,
"outbound": { "email": "internal", "teams": "internal", "slack": "internal" },
"limits": { "maxDailyUsageCost": 25, "maxMonthlyUsageCost": 500 },
"smeAutonomyLevel": "auto_low"
},
"mailbox": { "mailboxType": "shared", "autoReplyEnabled": false }
},
"vocabulary": {
"autonomyLevels": ["investigate_only", "approve_all", "auto_low", "auto_medium", "full"],
"reviewPolicies": ["request_human_input", "approval_queue", "mailbox_draft", "none"],
"knowledgeSourceKinds": ["existingRef"],
"outboundAccessLevels": ["none", "internal", "any"]
}
}

schema is the full JSON Schema document, abbreviated above. Validate your documents against this rather than against a copy: it is the version that ships with the API you are calling, so it cannot drift from what the apply route actually enforces.

Note the difference between this and capabilities. vocabulary is what the document format permits; ceiling.capabilities is what your key permits. A value can be in the vocabulary and still be refused by your ceiling.

Errors

401 invalid_key, 401 key_revoked, 401 key_expired, 429 rate_limited, 500 internal_error.

POST app/knowledge-indexes/{indexId}/sources/{sourceId}/sync

Starts a sync of one SharePoint knowledge source now, and returns immediately.

Required scope: knowledge.sync.

This route sits outside app/provisioning/ on purpose. Syncing is an operation on a resource that already exists, not a provisioning verb, so it takes no document and does not participate in apply.

It is not idempotent. Two calls mean two syncs, not "converge twice". That is why there is no syncNow field in a package document: such a field would re-fire on every re-apply of an unchanged document.

A source whose sync mode is manual can be synced through this route. Manual mode means "no timer", and this route is how such a source is ever synced at all.

curl -s -X POST -H "Authorization: Bearer $OM_KEY" \
"https://us-api.outermind.ai/api/app/knowledge-indexes/0a1b2c3d-4e5f-4a6b-8c7d-9e0f1a2b3c4d/sources/7d8e9f01-2a3b-4c5d-8e6f-0a1b2c3d4e5f/sync" | jq .

202 Accepted:

{
"runId": "3c4d5e6f-7a8b-4c9d-8e0f-1a2b3c4d5e6f",
"status": "running",
"sourceId": "7d8e9f01-2a3b-4c5d-8e6f-0a1b2c3d4e5f",
"pollUrl": "app/knowledge-indexes/0a1b2c3d-4e5f-4a6b-8c7d-9e0f1a2b3c4d/sources/7d8e9f01-2a3b-4c5d-8e6f-0a1b2c3d4e5f/sync/3c4d5e6f-7a8b-4c9d-8e0f-1a2b3c4d5e6f"
}

runId identifies the run. Poll pollUrl for its progress; the status above is the same value that route reports, so the two never disagree. pollUrl is route relative, like the apply route's. Prepend your regional base URL.

409 sync_already_running when a sync is already in flight for the source. The body carries the in-flight run's id, so a caller that raced itself can poll the run that is actually going rather than starting a second crawl:

{
"error": "sync_already_running",
"message": "A sync is already running for this knowledge source.",
"runId": "3c4d5e6f-7a8b-4c9d-8e0f-1a2b3c4d5e6f"
}

422 preflight_failed when the source is paused or being deleted. Resume it in the console; nothing about the key can change this answer:

{
"error": "preflight_failed",
"message": "The knowledge source is not in a syncable state.",
"unmet": [{ "code": "knowledge_source_not_syncable", "sourceStatus": "paused" }]
}

Errors

401 invalid_key, 401 key_revoked, 401 key_expired, 403 scope_not_granted, 404 source_not_found, 409 sync_already_running, 422 preflight_failed, 429 rate_limited, 500 internal_error.

A source belonging to another tenant, a source under a different index, a source of a kind this route does not serve, and a malformed index or source id all return 404 source_not_found. The route serves SharePoint knowledge sources only, because knowledge.sync grants only the SharePoint write permission.

GET app/knowledge-indexes/{indexId}/sources/{sourceId}/sync/{runId}

Reads one sync run. This is the poll target for the pollUrl the POST above returns.

Required scope: knowledge.sync.

curl -s -H "Authorization: Bearer $OM_KEY" \
"https://us-api.outermind.ai/api/app/knowledge-indexes/0a1b2c3d-4e5f-4a6b-8c7d-9e0f1a2b3c4d/sources/7d8e9f01-2a3b-4c5d-8e6f-0a1b2c3d4e5f/sync/3c4d5e6f-7a8b-4c9d-8e0f-1a2b3c4d5e6f" | jq .

200 OK:

{
"runId": "3c4d5e6f-7a8b-4c9d-8e0f-1a2b3c4d5e6f",
"sourceId": "7d8e9f01-2a3b-4c5d-8e6f-0a1b2c3d4e5f",
"status": "success",
"syncType": "delta",
"triggeredBy": "manual",
"startedAt": "2026-08-24T09:12:04.000Z",
"completedAt": "2026-08-24T09:14:41.000Z",
"durationMs": 157000,
"documents": { "processed": 412, "added": 6, "updated": 11, "deleted": 2, "failed": 0, "skipped": 393 },
"libraries": { "processed": 3, "succeeded": 3, "failed": 0 },
"error": null
}

status is one of waiting, running, success, partial, failed or cancelled. The first two are non terminal; keep polling. error is set on failed and may be set on partial.

Errors

401 invalid_key, 401 key_revoked, 401 key_expired, 403 scope_not_granted, 404 sync_run_not_found, 429 rate_limited, 500 internal_error.

A run belonging to another tenant, a run belonging to a different source or index, and a malformed id all return 404 sync_run_not_found.