Errors
Every Appstrate API error is an RFC 9457 problem document with a stable machine-readable code and a request id.
Errors are returned as application/problem+json per RFC 9457, with a few Stripe-style extension members. Branch on the HTTP status and on code. Never branch on title or detail, which are written for humans and can change.
Format
HTTP/1.1 400 Bad Request
Content-Type: application/problem+json
Request-Id: req_6f1c9a52-7b0e-4b7d-9c2e-0d3a8a1f5e44{
"type": "https://docs.appstrate.dev/errors/validation-failed",
"title": "Validation Failed",
"status": 400,
"detail": "name: Too small: expected string to have >=1 characters (+1 more)",
"instance": "urn:appstrate:request:req_6f1c9a52-7b0e-4b7d-9c2e-0d3a8a1f5e44",
"code": "validation_failed",
"request_id": "req_6f1c9a52-7b0e-4b7d-9c2e-0d3a8a1f5e44",
"errors": [
{
"field": "name",
"code": "out_of_range",
"message": "Too small: expected string to have >=1 characters"
},
{
"field": "scopes",
"code": "invalid_type",
"message": "Invalid input: expected array, received string"
}
]
}| Field | Always | Description |
|---|---|---|
type | Yes | An identifier URI for the error class: https://docs.appstrate.dev/errors/ plus the code with underscores turned into hyphens. An identifier, not a link; see Type URIs to find its row |
title | Yes | Short human-readable summary |
status | Yes | The HTTP status, repeated in the body |
detail | Yes | Human-readable explanation of this occurrence |
instance | Yes | urn:appstrate:request: followed by the request id |
code | Yes | Stable machine-readable code in snake_case |
request_id | Yes | The request id, also sent in the Request-Id response header |
param | No | The single request parameter at fault, such as a header name or a query field |
retry_after | No | Seconds to wait before retrying. Mirrored in the Retry-After header |
errors | No | Field-level errors, one entry per offending field, on validation_failed and on some readiness checks |
Some errors carry additional members the client is expected to act on. For example, a run launch blocked by a missing connection returns the connections to choose from or to reconnect (candidate_connections, required_scopes, and similar) so a UI can resolve it without a second round trip. These members are documented on the operation that emits them.
Property names in problem documents are snake_case, like the rest of the wire format. A few identifiers keep their camelCase spelling (id, userId, createdAt, and similar); see the OpenAPI schemas.
Request ids
Every response, success or error, carries a Request-Id header of the form req_<uuid>. Quote it in support requests and log it on your side: the platform's logs and audit records carry the same id. If you send a valid W3C traceparent header, it is echoed back and the trace id is attached to the server logs for that request.
Validation errors
A request body that fails validation returns 400 with code: "validation_failed" and one errors[] entry per offending field, all in the same response. Bodies are strict: an unknown field is an error, not silently dropped, and a malformed JSON body is a 400 rather than a 500.
Field code | Meaning |
|---|---|
required | A required field is missing |
invalid_type | Wrong type |
invalid_format | Wrong format: not an email, not a URL, not an ISO date |
out_of_range | Too short, too long, too small, or too large |
unknown_field | The body contains a field the schema does not accept |
invalid_value | Not one of the allowed values, or a custom rule failed |
invalid_union, invalid_key, invalid_element | A nested union, map key, or array element failed |
invalid_manifest | A field of a package manifest is invalid |
invalid_request | Any other validation failure |
A missing field is required whatever its type, enum and union fields included. Three cases keep the code of the check that failed: a missing discriminator of a discriminated union (invalid_union), a field checked by a rule on the whole object (invalid_value), and a missing numeric field the schema coerces, such as size on POST /api/uploads (invalid_type).
Some operations put a field code of their own in errors[], such as invalid_input (a schedule's input), file_uri_in_prompt, no_tools_selected, scope_not_in_catalog and unrenderable_authorized_uri. Treat a field code you do not recognise like invalid_request.
field is a path: scopes, metadata.plan, or items[0].name for array elements.
A single bad query parameter or header is reported with code: "invalid_request" and a param instead of an errors[] array.
Type URIs
The type member is built from code: https://docs.appstrate.dev/errors/ followed by the code with every underscore replaced by a hyphen.
code: validation_failed
type: https://docs.appstrate.dev/errors/validation-failedThe URI identifies the error class. It is not a link you can rely on: docs.appstrate.dev does not serve it today. To find a code on this page, use its anchor: every row of the catalogues below carries code- followed by the code in lower case with hyphens (#code-validation-failed).
Lower-case before you look up
The substitution replaces underscores and nothing else. It does not lower-case, so the few upper-case codes produce a mixed-case URI:
code: INVALID_URL
type: https://docs.appstrate.dev/errors/INVALID-URLIts row is #code-invalid-url: lower-case the last segment of the type, or the code with its underscores turned into hyphens.
Two catalogues
Two layers raise coded errors: the platform HTTP API (Error code catalog) and the AFPS runtime and CLI (AFPS runtime error codes). Only the API puts a type in a response; the runtime emits none. The runtime's rows take the anchor prefix code-afps-.
They are kept apart because the two catalogues collide. The API's integrity_mismatch (a version already published with different content) and the runtime's INTEGRITY_MISMATCH (stored bytes no longer matching their recorded hash) are unrelated failures. Under one anchor, one of the two readers would get the wrong explanation.
Error code catalog
These are the codes the platform HTTP API returns. The afps CLI and the embeddable runtime raise a separate set, catalogued under AFPS runtime error codes.
Four codes can come back from almost any endpoint: unauthorized, forbidden, internal_error and rate_limited. validation_failed, invalid_request and not_found cover most of the rest. Everything else is specific to a domain.
This list is maintained by hand and is not closed. It is checked against the code, but a release can add a code before this page lists it, optional modules (a billing module, for example) contribute codes of their own, and a self-hosted instance can load modules this page knows nothing about. Branch on status first and on code second, and treat a code you do not recognise as its status class rather than failing hard. Each operation in the OpenAPI document lists the responses it can return.
Codes marked billing module appear only when a billing module such as @appstrate/module-ee is loaded. Codes marked sidecar come from the /internal routes that the run sidecar calls, not from the public API.
| Code | Status | Meaning |
|---|---|---|
agent_in_use | 409 | The agent still has runs in progress, so it cannot be deleted, published, or have a version restored or deleted yet |
agent_not_active_in_space | 404 | The agent is placed in the space but switched off there. Activate it with POST /api/spaces/{spaceId}/packages, or use another space |
agent_not_found | 404 | No agent with that package id is reachable in this space |
archive_required | 415 | An MCP-server package must be uploaded as a multipart .afps or .zip archive |
authentication_required | 401 | Accepting an invitation needs a signed-in user |
bad_gateway | 502 | An upstream dependency of the platform failed or returned an unusable response |
blocked_target | 403 | Credential or model proxy: the upstream resolves to a blocked (private or reserved) address. A Proxy-Status header says the proxy refused, not the upstream |
blocked_url | 400 | A URL you submitted (a webhook endpoint, a proxy URL) resolves to a private or reserved network address |
bootstrap_org_failed | 500 | First-run bootstrap created the account but not its organization. The instance is in a partial state |
bootstrap_owner_email_mismatch | 403 | The instance names its owner in AUTH_BOOTSTRAP_OWNER_EMAIL. Claim it with that address |
bootstrap_redeem_in_progress | 409 | Another bootstrap redemption is already running on this instance |
bootstrap_signup_failed | 500 | Signup failed while redeeming the bootstrap token |
bootstrap_signup_rejected | 400 / 422 | The auth layer rejected the bootstrap signup. 422 when the password policy refused the password |
bootstrap_token_invalid | 401 | The bootstrap token does not match the instance's |
bootstrap_token_unavailable | 410 | No bootstrap token is redeemable: none is configured, or it was already redeemed |
bootstrap_user_exists | 409 | An account already exists for that email; the bootstrap token only creates a new one |
bootstrap_user_lookup_failed | 500 | The bootstrap signup appeared to succeed but the user could not be found |
bundle_conflict | 409 | An imported bundle contains a package owned by another organization, or one deleted during the import |
bundle_integrity_mismatch | 500 | A stored package artifact no longer matches the integrity hash recorded when it was published. Republish the package. This is the HTTP face of the runtime's INTEGRITY_MISMATCH |
bundle_invalid | 422 | A stored bundle cannot be assembled into a runnable package, or an MCP-server package has no activatable or executable version. detail names the runtime code |
bundle_signature_invalid | 422 | A stored bundle failed the AFPS signature policy (AFPS_SIGNATURE_POLICY). Republish it signed by a trusted key |
chat_capacity | 429 | The chat engine is at its session cap. Retry after retry_after seconds |
chat_enforced_not_skill | 400 | Only a skill can be enforced in a space's chat |
checksum_mismatch | 400 | The SHA-256 you declared does not match the bytes the server received |
conflict | 409 | The package's files were modified concurrently. Reload and retry |
connect_run_no_refresh | 409 | Sidecar. A connect run holds no stored credential to refresh |
connect_unavailable | 503 | This connection method is not available on this deployment |
connection_blocked_by_admin | 403 | An administrator disabled personal connections to this integration. Use the shared connection |
connection_label_taken | 409 | Another connection of this integration already has that label |
connection_not_in_org_default | 400 | The connection named in X-Connection-Id is outside the organization default enforced for that integration |
connection_not_in_run | 400 | The connection named is not bound to that integration by this run |
connection_owner_without_access | 409 | The connection's owner no longer has access to the space, so it cannot be shared there |
connection_pinned | 409 | The connection is pinned to agents by an admin, or named by an organization default. Remove it from there first |
content_entry_immovable | 400 | Editing package files: a required content file cannot be removed, renamed or emptied |
credential_exfiltration_refused | 403 | Credential proxy: the call carries a credential but no authorized_uris entry binds the target's host: the list leaves the host to the caller, or a host wildcard is not under a registrable domain of its own, or the target's registrable domain lies outside the wildcard's literal part. The message names the host to list |
credential_in_use | 409 | Models still reference this model-provider credential. Detach them first |
credential_not_found | 404 | Credential proxy: no usable credential was found for the integration |
credential_unusable | 502 | Credential proxy: the connection's credential is unusable once substituted or injected. Reconnect it |
default_space_not_deletable | 409 | The default space cannot be deleted |
delete_failed | 400 | The organization could not be deleted, for example while runs are in progress. detail names the cause |
dependency_unresolved | 422 | A declared dependency does not resolve to a usable published version. Publish it, fix the pin, or pass dependency_overrides. This is the HTTP face of the runtime's DEPENDENCY_UNRESOLVED |
DOWNLOAD_FAILED | 400 | GitHub import: a file of the repository could not be downloaded |
draft_not_writable | 403 | Running a draft requires write authority on it. Run the latest published version instead |
draft_overwrite | 409 | The import would overwrite unpublished draft changes |
draft_with_spec | 400 | stage: "draft" cannot be combined with a version spec: drafts have no published id |
draft_with_version | 400 | ?source=draft cannot be combined with ?version: drafts have no published id |
duplicate_file_name | 400 | Two input files of a run resolve to the same workspace file name |
email_mismatch | 403 | The invitation was issued to another email address than the signed-in user's |
EMPTY_PATH | 400 | GitHub import: no files were found at that path |
empty_prompt | 400 | The agent's prompt is empty, so it cannot run |
end_user_connection_not_shareable | 409 | An end-user's connection cannot be shared with the space (shared_with_org: true) |
enforced_skills_budget | 409 | The skills enforced in a space's chat would exceed the chat's content budget |
enforced_skills_limit | 409 | A space already enforces the maximum number of chat skills |
enforced_skills_unavailable | 503 | The skills a space requires in its conversations could not be loaded |
external_id_taken | 409 | Another end-user in this space already uses that externalId |
file_count_exceeded | 413 | A run would reference or publish more files than RUN_MAX_FILES allows |
file_in_use | 409 | The file is referenced by one or more runs and cannot be deleted |
file_too_large, FILE_TOO_LARGE | 400 / 413 | A file is over its size limit. 413 when editing package files; 400 from a package ZIP import, and in upper case from a GitHub import |
file_unavailable | 409 | One or more input files were deleted before the run could be created |
forbidden | 403 | The credential is valid but lacks the permission, role or ownership the operation needs |
GITHUB_ERROR | 400 | GitHub import: GitHub answered with an error |
header_not_allowed | 400 | A header was sent under an authentication method that cannot honor it (Appstrate-User outside API-key auth) |
home_move_into_personal_space | 409 | A package cannot be re-homed into a personal space. Fork it instead |
idempotency_conflict | 422 | The Idempotency-Key was already used with a different method, URL or body |
idempotency_in_progress | 409 | A request with the same Idempotency-Key is still being processed. Retry shortly |
idempotency_not_supported | 400 | Idempotency-Key was sent with an unsafe method to an operation that does not honor it. It is refused, not ignored |
identity_mismatch | 409 | The reconnected account differs from the one the connection is linked to |
in_use | 409 | Other packages still depend on the package, so it cannot be deleted |
injected_draft_changed | 409 | Chat: the skill draft changed since the conversation turn injected it |
integration_auth_undeclared | 409 | The integration version the run pins does not declare the auth its connection was created against |
integrity_mismatch | 409 | The version already exists with different content. Bump the version, or force-replace. Unrelated to the runtime's INTEGRITY_MISMATCH |
internal_error | 500 | Unexpected server failure. The body never leaks internals |
invalid_api_version | 400 | The Appstrate-Version header is not a YYYY-MM-DD date |
invalid_bundle | 400 | Editing package files: the content is not valid UTF-8 text, or the files fail package validation |
invalid_config | 400 | The integration manifest's identity_claims holds a path that is not valid JSONPath |
invalid_content | 400 | Package ZIP import: a companion file is invalid, such as a SKILL.md without a frontmatter name |
invalid_cron_expression | 400 | The schedule's cron expression is invalid |
invalid_draft_manifest | 400 | The draft manifest is invalid or lacks a scoped name and version |
invalid_end_user | 403 | The Appstrate-User end-user does not exist, or belongs to another space |
invalid_end_user_id | 400 | The Appstrate-User header is not an eu_ end-user id |
invalid_idempotency_key | 400 | The Idempotency-Key header exceeds the maximum length |
invalid_inline_manifest | 400 | The manifest sent with an inline run is not a valid AFPS agent manifest |
invalid_input | 400 | The run input does not satisfy the agent's input schema |
invalid_manifest | 400 | Package ZIP import: manifest.json is not valid JSON or fails AFPS validation. Also a field code under validation_failed |
invalid_path | 400 | Editing package files: a path is not a usable file path |
invalid_request | 400 | Malformed request that does not fit field validation. param names the parameter at fault when there is one |
invalid_signature | 401 | Run event ingestion: webhook-signature does not match the expected value |
invalid_source | 400 | ?source must be draft or published |
invalid_stored_manifest | 500 | The catalog holds a malformed manifest for that package version |
invalid_timestamp | 401 | Run event ingestion: webhook-timestamp is not a valid Unix timestamp |
invalid_timezone | 400 | The schedule's timezone is not a valid time zone |
INVALID_URL | 400 | GitHub import: the URL is not a GitHub URL this endpoint accepts |
invalid_view_as | 400 | The X-View-As value cannot be parsed, or was sent as a header to a Server-Sent-Events route, which takes the view_as query parameter |
invitation_accepted | 410 | The invitation was already accepted |
invitation_already_pending | 409 | A pending invitation already exists for this email. Edit it instead |
invitation_cancelled | 410 | The invitation was cancelled |
invitation_expired | 410 | The invitation has expired |
invitation_failed | 500 | The invitation could not be created or sent |
invitation_not_found | 404 | No invitation matches that token |
last_owner | 409 | An organization must keep at least one owner |
locked_input_field | 400 | The input field is locked on this agent and cannot be set at launch |
locked_required_field_empty | 400 | A required input field cannot be locked without a value |
message_replayed | 409 | Run event ingestion: that webhook-id was already used for this run |
method_not_allowed | 405 | Only from the MCP endpoint, on a method other than POST. An Allow header lists the permitted methods |
missing_content | 400 | Package ZIP import: a required companion file is missing or empty, such as the agent's prompt.md |
missing_integration_connection | 409 | The run cannot start because of its integrations. errors[] holds one entry per integration; see Connection and readiness entries |
missing_manifest | 400 | Package ZIP import: the archive has no manifest.json, and is not a bare skill either |
missing_signature_headers | 401 | Run event ingestion: webhook-id, webhook-timestamp and webhook-signature are all required |
missing_skill | 400 | The agent declares a skill that is not available to it. Publish it, or share it with the agent's home space |
model_already_added | 409 | The model is already added for this credential |
model_credential_missing | 400 | The selected model has no API key on its provider credential |
model_disabled | 409 | A disabled model cannot be the default, and the default model cannot be disabled |
model_needs_reconnection | 409 | The model's provider credential must be reconnected before it can be the default |
model_not_configured | 400 | No model is configured for the run |
model_not_offered | 400 | The provider does not offer that model |
model_provider_unregistered | 409 | The model is bound to a provider this instance does not register |
name_collision | 409 | The package id is taken, or names a system package that cannot be overwritten |
network_error | 502 | Live model search could not reach OpenRouter |
no_active_subscription | 409 | Billing module. The organization has no subscription to change. Start a checkout instead |
no_billing_account | 404 | Billing module. The organization has no billing account |
no_changes | 409 | Nothing changed since the last published version |
no_published_version | 404 / 409 | The package has never been published. 404 when running or exporting an agent (run the draft instead), 409 when enforcing a skill in a space's chat |
not_a_space_member | 403 | The caller holds no role in the requested space |
not_an_agent | 400 | The referenced package exists but is not an agent |
not_cancellable | 409 | The run is no longer pending or running, so it cannot be cancelled |
not_found, NOT_FOUND | 404 / 400 | The resource does not exist or is outside the caller's reach; the two are deliberately indistinguishable. NOT_FOUND (400) is a GitHub import whose repository or branch does not exist |
OAUTH_CONNECTION_NEEDS_RECONNECTION | 410 | Sidecar. The model provider's OAuth credential is flagged for reconnection |
OAUTH_REFRESH_REVOKED | 410 | Sidecar. The provider revoked the refresh token. The credential is flagged for reconnection |
operation_not_allowed | 403 | The operation is not permitted on a built-in package or entity |
org_creation_disabled | 403 | Organization creation is disabled on this instance |
org_deleting | 409 | The organization is being deleted; no new work is admitted |
org_not_found | 404 | The organization of the invitation no longer exists |
org_run_concurrency_exceeded | 429 | The organization reached its concurrent-run cap. Wait for runs to finish |
org_run_rate_limited | 429 | The organization reached its run launch rate. retry_after is set |
organization_has_no_default_space | 409 | The package cannot be re-homed: the organization has no default space |
package_archive_unreadable | 422 | A stored package archive expands past the decompression limit. Republish it |
package_copy_restricted | 403 | The organization restricts copying packages out of their home space |
package_has_no_version | 409 | The package has no published version to offer outside its home space |
package_not_active_in_space | 404 | The package exists but is not active in this space |
package_not_found | 404 | No package with that id in the caller's organization |
package_not_placed | 404 | The package is not placed in this space |
pairing_expired_or_consumed | 410 | The model-provider pairing token expired or was already used |
password_already_set | 409 | The account already has a password. Use the change-password flow |
path_conflict | 400 | Editing package files: a path conflicts with another file or directory, or a move target already exists |
payload_too_large | 413 | The request body or an upload is over its size limit |
payment_service_unavailable | 503 | Billing module. The payment service is unavailable |
personal_space_has_no_members | 409 | A personal space takes no other members. Convert it to a team space first |
personal_space_immutable | 409 | Only the name of a personal space can change |
personal_space_not_deletable | 409 | A personal space is deleted by offboarding its owner, not directly |
personal_space_not_orphaned | 409 | The personal space still has an owner in the organization |
personal_space_takes_no_end_users | 409 | End-users are created in a team space, not a personal one |
personal_space_takes_no_keys | 409 | API keys are created in a team space, not a personal one |
personal_space_takes_no_oauth_clients | 409 | OAuth clients are registered in a team space, not a personal one |
post_install_failed | 400 | The package could not be installed after its archive was accepted |
precondition_failed | 412 | If-Match does not match the current ETag. The response carries the current ETag |
precondition_required | 428 | The operation requires an If-Match header |
provider_error | 502 | Live model search: OpenRouter returned an error |
quota_exceeded | 402 | Billing module. The organization is out of credits |
rate_limited, RATE_LIMITED | 429 / 400 | Too many requests: wait retry_after seconds. See Rate limiting. RATE_LIMITED (400) is a GitHub import that hit GitHub's own rate limit |
reasoning_level_unsupported | 400 | The model does not support the requested reasoning level |
reasoning_unsupported | 400 | The model does not support reasoning |
redundant_space_role | 409 | The member's organization role already covers every space; an explicit space role would grant nothing |
REPO_TOO_LARGE | 400 | GitHub import: the repository is too large to import directly |
rerun_agent_mismatch | 409 | rerun_from names a run of a different agent |
rerun_inline_input_unavailable | 409 | A field of the original run was an inline data: URI, which is not stored and cannot be replayed |
reserved_entry | 400 | Editing package files: the manifest is edited through the package's manifest field, not as a file |
role_in_use | 409 | The role is still held by space members or pending invitations |
role_key_taken | 409 | A role with that key already exists in the organization |
run_agent_deleted | 409 | Sidecar. The agent of the running run was deleted |
run_definition_gone | 409 | Sidecar. The version the run executes is no longer readable |
run_in_progress | 409 | The package still has runs in progress, so its runs cannot be deleted |
run_not_running | 409 | The run is no longer running, so it cannot accept this operation |
run_sink_closed | 410 | The run's event sink is closed. Stop sending events |
run_sink_expired | 410 | The run's event sink expired |
schedule_actor_invalid | 400 | The schedule's actor is not valid for this organization and space |
schedule_modified_concurrently | 409 | The schedule changed while the request was handled. Reload and retry |
share_target_is_home | 409 | The package already lives in that space |
shutting_down | 503 | The instance is draining. Retry shortly; retry_after is set |
signup_domain_not_allowed | 403 | Bootstrap: the owner's email domain is not on the signup allowlist (AUTH_ALLOWED_SIGNUP_DOMAINS). Another signup refusal keeps the code the auth layer gave it |
skill_unchanged | 409 | The skill already exists with identical content |
slug_taken | 400 | The organization slug is already in use |
space_access_changed | 409 | The space's visibility or default role changed while the request was handled. Reload and retry |
space_has_active_runs | 409 | The space cannot be deleted while runs are in progress |
space_homes_packages | 409 | The space cannot be deleted while it is the home of packages |
space_member_exists | 409 | The user already has an explicit space role. Use PATCH to change it |
space_not_personal | 409 | The operation applies only to a personal space; this one is a team space |
starting | 503 | The server is still starting up. Retry shortly |
storage_limit_exceeded | 403 | The write would exceed the organization's storage quota (ORG_STORAGE_QUOTA_BYTES) |
subscription_blocked | 402 | Billing module. The organization's subscription is suspended or cancelled |
subscription_exists | 409 | Billing module. The organization already has a subscription. Change its plan instead of starting a checkout |
temperature_unsupported | 400 | The model does not support a custom temperature |
temperature_with_reasoning_unsupported | 400 | The model does not accept a temperature together with reasoning |
timeout | 504 | Live model search timed out, or a connection attempt did not complete its login in time |
timestamp_out_of_tolerance | 401 | Run event ingestion: webhook-timestamp is outside the accepted window |
TOO_LARGE | 400 | GitHub import: the files exceed the total size limit |
TOO_MANY_FILES | 400 | GitHub import: the path holds more files than the import accepts |
tree_too_large | 413 | Editing package files: the package would exceed its total size limit |
type_mismatch | 400 | The package already exists under another package type |
unauthorized | 401 | No credential, or an invalid, revoked or expired one |
unauthorized_target | 403 | Credential proxy: the target is outside the integration's authorized_uris, or none are declared |
unexpected | 500 | Billing module. The usage check failed, so usage is refused |
unresolved_placeholder | 400 | Credential proxy: the target, a header or the body holds a {{field}} placeholder the connection cannot fill |
unsupported_api_version | 400 | Appstrate-Version names a version the server cannot serve |
upload_already_written | 409 | The staged upload already has its content |
upload_expired | 410 | The staged upload expired, or its reuse window elapsed |
upload_staging_limit_exceeded | 429 | You hold the maximum number of active staged uploads (UPLOAD_MAX_ACTIVE_PER_ACTOR) |
upstream_timeout | 504 | Credential or model proxy: the upstream did not answer in time |
upstream_unreachable | 502 | Credential or model proxy: the upstream could not be reached |
upstream_unresolvable | 502 | Credential or model proxy: the upstream host could not be resolved |
usage_context_required | 400 | A call on a platform-provided model arrived without a valid run context (X-Run-Id) |
usage_not_allowed | Varies | Not a code but a type: a chat turn refused by a module's usage check carries type https://docs.appstrate.dev/errors/usage-not-allowed, the module's own code (such as quota_exceeded) and the status the module chose, 403 when it names none. This body carries no instance or request_id |
validation_failed | 400 | Body validation failed. Every offending field is in errors[] |
version_artifact_unavailable | 422 | The published version exists but its stored artifact cannot be read |
version_exists | 409 | That version is already published and immutable. Bump the version |
version_not_found | 404 | The requested version of the package does not exist |
version_not_higher | 409 | The version is lower than the highest published version |
version_yanked | 410 | The requested version was yanked and can no longer be run |
view_as_forbidden | 403 | Only an organization owner or administrator can preview a role, and only one that grants nothing they lack |
view_as_not_found | 404 | The space, role or organization named by the role preview does not exist |
view_as_unsupported | 400 | The role preview is not supported for this authentication method |
webhook_limit_reached | 400 | The organization or space is at its webhook limit |
wrong_package_type | 409 | The package is not an integration |
zip_bomb | 400 | Package ZIP import: the archive expands past the decompression limit |
zip_invalid | 400 | Package ZIP import: the file is not a readable ZIP archive |
Connection and readiness entries
A run blocked by its integrations answers 409 missing_integration_connection with one errors[] entry per integration (field: integrations.<id>). Each entry carries its own code and, where it helps a client act without parsing detail, extra snake_case members. On the credential proxy, a resolution failure comes back with its entry code as the top-level code of a 409 instead (for example must_choose_connection), with the entry in errors[]. In the chat, a model whose subscription credential died answers 409 needs_reconnection.
Entry code | Meaning | Extra members |
|---|---|---|
not_connected | No connection to this integration is accessible to the caller | auth_key, required_scopes |
must_choose_connection | Several connections match; the caller must pick one | candidate_connections (each with id, label, account_id, owned_by_actor, needs_reconnection) |
needs_reconnection | The connection exists but its credential is dead | connection_id, owned_by_actor, auth_key, required_scopes |
insufficient_scopes | The connection lacks scopes the selected tools need | connection_id, missing_scopes, owned_by_actor, auth_key, required_scopes |
auth_key_mismatch | The agent requires an auth the caller's connections do not use | required_auth_key, available_auth_keys |
auth_key_serves_no_selected_tool | The auth the agent requires exposes none of its selected tools | required_auth_key |
auth_serves_no_selected_tool | A connection of the set uses an auth that exposes none of the selected tools | connection_id |
remote_binds_one_connection | A remote run binds one connection per integration, but the connection choice binds several to this one. Narrow it to one with a member pin, or run the agent on the platform | |
pinned_connection_unavailable | The connection pinned for this agent is gone or unusable | |
override_connection_unavailable | The connection passed as a per-run override is gone or unusable | |
override_outranked | The per-run override falls outside the pin or organization default that governs the integration | |
integration_not_found | The agent declares an integration that does not exist | |
integration_wrong_type | A package declared as an integration is another package type | |
integration_invalid_manifest | A declared integration's manifest fails validation | |
integration_not_active | A declared integration is not active in this space | |
agent_not_active | Only in the readiness read, with field: agent: the space has switched the agent off. A run launch answers 404 agent_not_active_in_space instead |
When the caller opted in to connect offers, an entry that an OAuth connect flow can clear also carries connect_url, expiresAt and packageId: a single-use, short-lived link to open, never to store.
AFPS runtime error codes
These come from @appstrate/afps-runtime, the portable bundle runner behind the standalone afps CLI. They are not platform API errors:
- Calling the HTTP API? You will not see them. The platform translates bundle failures at its boundary:
DEPENDENCY_UNRESOLVEDbecomesdependency_unresolved,INTEGRITY_MISMATCHbecomesbundle_integrity_mismatch, any other bundle code becomesbundle_invalid, and a signature policy failure becomesbundle_signature_invalid. - Running the
afpsCLI, or embedding the runtime? These are the codes you will see. The runtime throws typed errors (BundleError,BundleSignaturePolicyError,ApiCallFailureError,ResolverError), each with acode, a message, and optional structured details. They are not problem documents and carry notype. The CLI prints a failure asmessage [CODE].
Bundle assembly
Raised by BundleError while reading, verifying or building a bundle.
| Code | Meaning |
|---|---|
ARCHIVE_INVALID | The archive cannot be decompressed, or holds an unsafe path (absolute, backslash, traversal) |
BUNDLE_JSON_MISSING | The archive contains no bundle.json |
BUNDLE_JSON_INVALID | bundle.json is not valid JSON, or declares an invalid package identity or path |
RECORD_MISSING | A package in the bundle has no RECORD file |
RECORD_MALFORMED | A RECORD line cannot be parsed |
RECORD_MISMATCH | A file is missing from RECORD, listed but absent, or its size or hash does not match |
INTEGRITY_MISMATCH | Stored bytes no longer hash to the integrity value recorded at publish time: corruption or tampering. Over HTTP it becomes bundle_integrity_mismatch, not the unrelated API code integrity_mismatch |
VERSION_UNSUPPORTED | bundleFormatVersion is outside the versions this runtime reads or writes |
LIMITS_EXCEEDED | The bundle exceeds a configured limit (compressed size, file count, and similar) |
DEPENDENCY_UNRESOLVED | A declared dependency resolves to nothing, or to a version whose bytes cannot be loaded. Over HTTP it becomes dependency_unresolved |
Bundle validation
Reported by bundle validation, which afps verify prints as [CODE] path: message. The last two are warnings and do not make a bundle invalid.
Signatures and trust chains
Raised by BundleSignaturePolicyError under a required signature policy. Under warn, the same reasons are reported to a callback instead of thrown. These codes are lower case.
Outbound calls and resolvers
Raised when an integration's api_call tool is prepared or executed. ApiCallFailureError carries the lower-case codes the platform's credential proxy answers, with the same meaning; details holds the integration, the target as written and the declared allowlist. ResolverError carries the RESOLVER_ codes.
| Code | Meaning |
|---|---|
unauthorized_target | The target, or a redirect hop, is outside the integration's authorized_uris, or the connection does not render the declared list. As the API's unauthorized_target |
blocked_target | The target, or a redirect hop, resolves to a blocked (private or reserved) address. As the API's blocked_target |
credential_exfiltration_refused | The call carries a credential but the authorized_uris do not bind the target's host, a host wildcard included. As the API's credential_exfiltration_refused |
credential_unusable | A header value holding the substituted or injected credential is not a valid HTTP field value. Reconnect the connection |
upstream_unresolvable | The target host could not be resolved |
upstream_unreachable | The target could not be reached |
upstream_timeout | The target did not answer in time |
RESOLVER_MISSING_REQUIRED | A body that reads a workspace file was sent to a resolver that has no workspace |
RESOLVER_BODY_REFERENCE_FORBIDDEN | The body references a workspace file but the resolver does not allow file access |
RESOLVER_BODY_TOO_LARGE | The request body exceeds the size limit |
RESOLVER_BODY_INVALID | The api_call arguments are invalid, the body cannot be built as declared, or a {{field}} placeholder cannot be filled from the connection |
RESOLVER_HEADER_INVALID | A header value the agent sent is not a valid HTTP field value |
RESOLVER_PATH_OUTSIDE_ALLOWED_ROOTS | A referenced file path is not inside the workspace |
RESOLVER_PATH_SYMLINK_REFUSED | The runtime refused to read or write through a symlink |
RESOLVER_PATH_INVALID | A referenced file path is empty or not a string |
Retrying
429or503withretry_after: wait that many seconds (theRetry-Afterheader carries the same value), then retry.rate_limited,org_run_rate_limited,chat_capacityandshutting_downset it.429without it (org_run_concurrency_exceeded,upload_staging_limit_exceeded) and503starting: back off and retry once in-flight work has finished or the server is up.5xxwithout a hint: retry with exponential backoff and jitter, starting around one second and capped at one minute. For aPOST, send anIdempotency-Keyso a retry cannot create a duplicate. Only the operations that declare the header accept it.409idempotency_in_progress: retry shortly with the same key.422idempotency_conflict: do not retry; that key belongs to a different request.- Other
4xx: do not retry unchanged. The request, or the state it depends on, needs to change first. A few409codes (conflict,schedule_modified_concurrently,space_access_changed) ask you to reload the resource and try again.
Examples
Authentication failure
HTTP/1.1 401 Unauthorized
Content-Type: application/problem+json
WWW-Authenticate: Bearer error="invalid_token"{
"type": "https://docs.appstrate.dev/errors/unauthorized",
"title": "Unauthorized",
"status": 401,
"detail": "Invalid or expired API key",
"instance": "urn:appstrate:request:req_…",
"code": "unauthorized",
"request_id": "req_…"
}Rate limited
HTTP/1.1 429 Too Many Requests
Content-Type: application/problem+json
Retry-After: 42
RateLimit: limit=20, remaining=0, reset=42
RateLimit-Policy: 20;w=60{
"type": "https://docs.appstrate.dev/errors/rate-limited",
"title": "Rate Limited",
"status": 429,
"detail": "Too many requests. Please try again shortly.",
"instance": "urn:appstrate:request:req_…",
"code": "rate_limited",
"request_id": "req_…",
"retry_after": 42
}A header the credential cannot honor
{
"type": "https://docs.appstrate.dev/errors/header-not-allowed",
"title": "Header Not Allowed",
"status": 400,
"detail": "Appstrate-User is only supported with api_key auth, not session authentication.",
"instance": "urn:appstrate:request:req_…",
"code": "header_not_allowed",
"request_id": "req_…",
"param": "Appstrate-User"
}Idempotency
Retry the operations that create runs, end-users, webhooks and OAuth clients without creating duplicates, using the Idempotency-Key header.
API Reference
Cross-cutting rules of the Appstrate API: context headers, versioning, rate limits, pagination, long polling, and realtime streams. Per-operation reference is generated from OpenAPI.