Provisioning API Error Reference
Every failure from the Agent Provisioning API is machine classifiable. This page is the contract an automation should be written against: which code means retry, which means edit the document, which means change a setting in the console, and which means page a human.
The error envelope
{
"error": "package_exceeds_key_ceiling",
"message": "The document requests capabilities beyond this key's ceiling.",
"violations": [
{
"path": "spec.agent.outbound.email",
"requested": "any",
"ceiling": "internal",
"reason": "exceeds_ordinal_ceiling"
}
]
}
| Field | Always present | Notes |
|---|---|---|
error | yes | The stable, contractual code. Branch on this |
message | yes | Human-readable prose. Not contractual; it may be reworded at any time, so never branch on it |
fields[] | 400 only | What was wrong with the document's structure |
violations[] | 409 package_exceeds_key_ceiling only | Which requests exceeded the key's ceiling |
unmet[] | 422 preflight_failed and 409 name_conflict only | Which tenant prerequisites are not satisfied |
A response carries at most one of fields[], violations[] and unmet[].
Detail lists are complete rather than first-failure. One call tells you everything that is wrong at that stage, so you are not fixing one field per round trip.
Status codes
| Status | error | What it means | Your move |
|---|---|---|---|
| 400 | invalid_document | The document failed structural validation | Fix the document; read fields[] |
| 400 | unknown_field | The document carries fields this API does not recognise | Fix the spelling; read fields[] |
| 400 | tenant_id_not_permitted_in_body | A tenant identifier appeared in the body | Remove it. The key already carries the tenant |
| 400 | unsupported_api_version | apiVersion is not a version this API serves | Set it to provisioning.outermind.ai/v1 |
| 400 | unsupported_kind | The document's root kind is not one this API serves | Set it to AgentPackage. fields[0].reason lists the kinds that are served |
| 401 | invalid_key | No key, a malformed key, or a key that does not exist | Check the Authorization header |
| 401 | key_revoked | The key was revoked in the console | Mint a replacement |
| 401 | key_expired | The key passed its expiry date | Mint a replacement |
| 403 | scope_not_granted | The key lacks a scope this request needs | Mint a key with the scope. The response names it in a scope field |
| 404 | package_not_found | No such package for this tenant | Check the name |
| 404 | operation_not_found | No such operation for this tenant | Check the operation id |
| 404 | source_not_found | No such knowledge source under that index for this tenant. A malformed id, a source under a different index, and a source of a connector kind the sync route does not serve all answer this | Check the index id and the source id |
| 404 | sync_run_not_found | No such sync run for that tenant, index and source | Check the run id, and that it belongs to the source you are polling |
| 409 | package_exceeds_key_ceiling | The document asked for more than this key may grant | Narrow the document, or mint a wider key |
| 409 | name_conflict | A resource this package does not own already holds the name | Rename the package, or remove the conflicting agent |
| 409 | operation_in_flight | Another operation for this package is pending or running | Poll that one, or retry shortly |
| 409 | sync_already_running | A sync for this knowledge source is already in flight | Poll the run the body's runId names, or retry shortly |
| 422 | preflight_failed | The tenant is not ready for what the document asks | Configure the tenant; read unmet[] |
| 429 | rate_limited | The key exceeded its per-minute or per-day limit | Back off for the seconds given in the Retry-After header |
| 500 | internal_error | Something failed on our side | Retry. Apply is idempotent, so retrying is safe. The sync route is deliberately not idempotent, but retrying a 500 from it is still safe: the retry either starts a fresh run or answers 409 sync_already_running, never a second crawl alongside the first |
Every 400, 403, 409 and 422 is a zero-write rejection. The request is refused before anything is written, so the tenant is byte identical afterwards and iterating on a document never leaves debris behind.
409 versus 422: which human do you page
This is the single most important distinction in the API, and it exists so that an automation never has to guess.
409 means the document asked for more than this key may grant. That is a policy decision. Nothing about the tenant is wrong. Someone with console access has to decide either to narrow the document, or to mint a key with a wider ceiling. Because a ceiling is fixed at mint time, "widening the key" always means minting a new one.
422 means the tenant is not ready. That is an environmental fact. The document may be perfectly reasonable, and the key may be perfectly adequate. Something has to be created or configured in the tenant first: a mailbox that does not exist, an authorization that was never completed, a knowledge base that has not been built.
Collapsing them into one code would leave every automation guessing which of two very different people to notify. Branch on the code, not on the message.
409 operation_in_flight is the one 409 that is neither. It is a timing conflict, resolved by waiting rather than by editing anything, and it is the only 409 that carries neither violations[] nor unmet[]. Retry it and page nobody.
409 name_conflict sits with the 422s in spirit but is a 409 by status, because a non-owned resource holding the name is a harder stop than an unmet prerequisite. It carries its finding in unmet[], not violations[]. When a document has both a name conflict and other unmet prerequisites, the response is 409 and unmet[] still lists everything, so you still learn it all in one call.
400: fields[]
Each entry describes one structural problem.
{
"error": "invalid_document",
"message": "Document failed structural validation.",
"fields": [
{ "path": "spec.agent.friendlyName", "reason": "value must be a string" },
{ "path": "metadata.version", "reason": "value must match the required pattern", "received": "<string, 3 chars>" }
]
}
| Field | Notes |
|---|---|
path | Dotted path into the document, with array indices |
reason | What was wrong at that path |
received | Optional. A bounded description of what you sent, never the value itself |
received is deliberately a shape rather than a value: sending back the content of spec.agent.instructions in an error body would put your own text somewhere it does not belong.
unknown_field versus invalid_document. When every problem is an unrecognised field, the code is unknown_field. When there is any other kind of problem in the mix, the code is invalid_document and the unknown fields still appear in fields[]. So a document that is wrong in two ways is never reported as merely misspelled.
409: violations[]
Each entry names one place the document exceeded the key's ceiling.
{
"error": "package_exceeds_key_ceiling",
"message": "The document requests capabilities beyond this key's ceiling.",
"violations": [
{ "path": "spec.tools.grant[0]", "requested": "onedrive_delete", "ceiling": "not granted", "reason": "tool_not_in_ceiling" },
{ "path": "spec.agent.limits.maxDailyUsageCost", "requested": 100, "ceiling": 25, "reason": "exceeds_numeric_ceiling" }
]
}
| Field | Notes |
|---|---|
path | Where in the document the request was made |
requested | What the document asked for, or a bounded description of it |
ceiling | What the key permits at that path |
reason | One of the eight codes below |
reason | Meaning |
|---|---|
exceeds_ordinal_ceiling | An outbound channel asked for more reach than the key permits. The order is none, internal, any, and a ceiling permits its own level and everything narrower |
exceeds_numeric_ceiling | A cost limit is above the key's cap, or is not a number |
not_in_enum_ceiling | A value is not in the key's permitted list for that field, such as an autonomy level or a review policy |
tool_not_in_ceiling | A granted tool name is not covered by the key's tool ceiling |
wildcard_not_allowed_in_package | A grant contains a *. Ceilings may use wildcards; documents may not |
capability_not_granted | The document declared a section the key may not use at all, such as spec.mailbox on a key without mailbox binding, or spec.agent.sme on a key without SME provisioning |
capability_not_resolved | A value stood where the document format expects an object that gates a capability, so the gate could not work out what was being asked for. Denied rather than assumed harmless |
unknown_field_not_covered_by_ceiling | The document contains a path the ceiling grammar does not recognise, so no bound could be applied to it |
Remember that omitted fields with defaults are checked as though you had written the default. A document with no outbound section still requests internal on all three channels, and will violate a none ceiling.
422: unmet[]
Each entry names one tenant prerequisite that is not satisfied.
{
"error": "preflight_failed",
"message": "The target tenant does not satisfy this document's prerequisites.",
"unmet": [
{
"code": "mailbox_not_found",
"detail": "No mailbox found in Microsoft 365 for 'billing@contoso.com'.",
"remediation": "Create the mailbox in Microsoft 365, then re-apply. The provisioning API never creates mailboxes.",
"blocks": ["spec.mailbox.address"]
}
]
}
| Field | Notes |
|---|---|
code | The stable code. Branch on this. Always present |
detail | What specifically was not satisfied. Present on entries a submitted document produced |
remediation | Optional. The suggested fix, in prose |
blocks[] | The document paths this prerequisite blocks. Present on entries a submitted document produced |
sourceStatus | knowledge_source_not_syncable only. The knowledge source's current status |
Only code is guaranteed. The sync route submits no document, so its one entry carries code and sourceStatus and neither detail nor blocks[] - branch on code and read the rest defensively.
code | Meaning and fix |
|---|---|
mailbox_not_found | spec.mailbox.address does not resolve to a mailbox in Microsoft 365, or resolves to a distribution list. Create a shared or individual mailbox, then re-apply. The API never creates mailboxes |
exchange_authorization_missing | The tenant has not completed Exchange authorization, so no mailbox can be bound. Run the Exchange Authorization wizard in the console once |
knowledge_source_not_found | A spec.knowledgeSources[].name does not match a knowledge base this tenant has. Check GET app/provisioning/catalog/knowledge-bases |
supervisor_not_resolvable | spec.agent.supervisorEmail does not resolve to a mailbox or mail-enabled group in this tenant |
tool_not_available | A granted tool, or a tool the chosen review policy needs, is not enabled for this tenant. Enable it in the console, or change the document. Check GET app/provisioning/catalog/tools |
escalation_router_not_configured | The chosen review policy needs somewhere for an escalation to go, and the tenant has neither a monitored mailbox nor a configured Escalation Router |
name_conflict | An agent with this name already exists and is not owned by this package. Rename the package, or remove the conflicting agent. This one is reported with status 409, not 422 |
instructions_rejected | spec.agent.instructions was rejected by instruction-safety screening. Revise the instructions, and the design evidence if present |
instruction_screening_unavailable | Instructions could not be screened because the tenant has no usable LLM provider. Configure one, then re-apply. This fails closed on purpose: unscreened instructions are never accepted |
sme_supervisor_required | An SME package must declare spec.agent.supervisorEmail |
sme_description_too_short | An SME's spec.agent.description must be at least 80 characters, because that description is how other agents discover it |
sme_conversion_not_supported | The named agent already exists as a non-SME agent, and this API will not convert it. Use a new package name, or convert it in the console |
sme_design_evidence_required | Creating a new SME requires a spec.designEvidence block |
sme_escalation_contact_not_resolvable | An SME escalation contact address does not resolve to a recipient in this tenant. This is checked for both escalationContact and alternateEscalationContact |
knowledge_source_not_syncable | POST app/knowledge-indexes/{indexId}/sources/{sourceId}/sync named a source whose status is paused or deleting, which the sync processor skips. Resume the source in the console, then retry. Carries sourceStatus; it is the one code not raised by preflight, so it has no blocks[] |
Partial success is not an error
An operation that reaches partially_succeeded did so after a 202. It is a terminal operation status, not an HTTP error, and it does not mean the apply is unrecoverable.
{
"operationId": "6f2a9c14-2f1e-4a7b-8f0a-1e2d3c4b5a69",
"status": "partially_succeeded",
"packageName": "billing-triage",
"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" }
}
]
}
The remedy is to re-apply the same document. Apply is idempotent: the resources that already converged report noop, and only the failed ones are attempted again. Do not build a compensating "undo" path and do not construct a second, smaller document; both are more likely to cause damage than the retry is.
Read resources[] to see which resource failed and why. An entry with status: "skipped" and reason: "prerequisite_failed" was not attempted because something it depended on failed first; it will be attempted on the next apply once the blocker clears.
status: "failed" on the whole operation means nothing converged, and is handled the same way: re-apply.
Writing an automation against this
A practical branch table:
| Response | Automated handling |
|---|---|
429 rate_limited | Sleep for Retry-After seconds, retry |
500 internal_error | Retry with backoff, a bounded number of times |
409 operation_in_flight | Poll the existing operation, or retry shortly. Never an alert |
409 sync_already_running | Poll the run the body's runId names, or retry shortly. Never an alert |
operation status partially_succeeded or failed | Re-apply the same document once, then alert if it recurs |
422 preflight_failed | Alert the tenant's operator, quoting unmet[].remediation |
409 package_exceeds_key_ceiling | Alert whoever owns the key, quoting violations[] |
409 name_conflict | Alert a human; this needs a naming decision |
403 scope_not_granted | Alert whoever owns the key, quoting the scope field |
401 key_revoked or key_expired | Alert whoever owns the key. Do not retry |
400 of any kind | A bug in whatever generated the document. Fail the pipeline, do not retry |