Service error codes
Every Air-Cloud service refuses with a code, and the code is the contract: act on the code and its details, never on the message text. These are the client-facing error contracts, one per service domain; the rules aircloud check reports and the release pipeline's codes are on Rules and error codes.
capability
A refusal's body:
{
"error": {
"code": "capability.<snake_case>",
"message": "<human>",
"correlation_id": "<uuid>",
"details": {}
}
}
| code | HTTP | meaning |
|---|---|---|
capability.unauthenticated |
401 | missing/invalid/expired canonical token; unknown iss; wrong aud; missing tenant_id |
capability.forbidden |
403 | token lacks the capability's required_scope |
capability.not_found |
404 | no registry route matches the request path |
capability.validation |
400 | malformed request body / unsupported method / handle not UUID |
capability.credential_not_found |
404 | handle does not resolve within the token's tenant (RLS zero-row — never a cross-tenant existence oracle) |
capability.credential_revoked |
403 | handle resolves but status != active (immediate revocation denial) |
capability.credential_key_invalid |
422 | The hmac credential's stored value does not decode under the key encoding its secret declares (it was set before the declaration changed), so there is no key to sign or verify with. No key is guessed. |
capability.destination_forbidden |
403 | egress URL fails the allowlist or the public-unicast resolution guard |
capability.header_conflict |
400 | client-supplied header collides with the injection target header |
capability.upstream_unsupported |
502 | upstream response body is not valid UTF-8 (v1 text-body contract; body_b64 is the named additive path) |
capability.upstream_error |
502 | egress/flex upstream connection failure, malformed upstream envelope, or response-size cap exceeded |
capability.timeout |
504 | upstream exceeded the egress/proxy timeout |
capability.internal |
500 | unexpected gateway failure (including a failed pre-dial audit write) |
capability.unsupported_media_type |
415 | the request carries a body its capability arm does not declare — a Content-Type outside the arm's bodies in contracts/capability/v1/capabilities.json (the request-media-types names, ADR-273), or any body on an arm that declares none. Refused by the body gate from the framing alone, before the body is read |
capability.payload_too_large |
413 | the request body is larger than its capability arm reads: the bound its upstream reads under its own declared bound (a forwarding arm), else its declared type's ceiling (contracts/api/v1/request-media-types.json). Refused by the body gate before the handler or the upstream sees the rest |
capability.request_timeout |
408 | the request body did not arrive within the body read bound (contracts/api/v1/request-media-types.json body_read, ADR-273 decision 7). Distinct from capability.timeout, which is an UPSTREAM that did not answer |
capability.resolver_unavailable |
503 | the caller's authority could not be resolved: the capability gateway's watch of the authority projection could not prove it is current within the projection bound (contracts/auth/v1/account-lifecycle.json), or the projection could not be read (#384, ADR-293 decision 8: fail-closed, never read as no restriction). A dependency outage, never a bound the caller exceeded; retryable, and no Retry-After because no instant is known at which the projection will be current. The same condition answers unavailable on the control-plane API and flex.resolver_unavailable on Air-Flex |
capability.membership_required |
403 | the request did not select a membership the caller holds: the caller holds several memberships in the tenant and sent none, or the selection (the X-Air-Membership header, or a verified assertion's membership_id) names a membership that is unknown or belongs to another principal (#384, ADR-293 decision 9). Select one of the caller's own memberships; a forged id selects nothing. The same condition answers membership_required on the control-plane API (contracts/entitlement/v1/errors.json) |
capability.org_node_archived |
409 | the node the request acts at, or one of its ancestors, is archived — or has no node key in the authority projection, so it cannot be proven active (an unknown node refuses here, never as no restriction): archive suspends every authority acting at or below it — a session's membership homed there, a cron, route, event or direct invoke — until the node is unarchived (#384, ADR-295 decisions 4 and 7, ADR-293 decision 9). Read from the authority projection's node keys along the acting node chain; nothing was changed. The same condition answers org_node_archived on the control-plane API (contracts/entitlement/v1/errors.json) |
comms
A refusal's body:
{
"error": {
"code": "comms.<snake_case>",
"message": "<human>",
"correlation_id": "<uuid>",
"details": {}
}
}
| code | HTTP | meaning |
|---|---|---|
comms.validation |
400 | the payload violates the CHANNEL's own I1 invariants — details carries the offending field. Mail: missing html/text, empty to, recipient/body cap, malformed addr-spec. WhatsApp: a non-E.164 to, an empty text, a body over the cap. The pipeline additionally refuses with this code any decoded message carrying no recipients or a non-positive billable quantity — a send that cannot be governed is refused rather than dispatched |
comms.recipient_suppressed |
422 | a recipient of the send is on the global suppression list; the whole send is rejected — no partial sends, no bypass (details carries the suppressed addresses). The recipient set is the CHANNEL's own: mail's is to∪cc∪bcc, WhatsApp's is its single recipient. The check is un-bypassable and runs BEFORE the wallet, so a suppressed recipient never costs the tenant units |
comms.identity_unresolved |
422 | no sending identity resolvable for the tenant/channel — no active comms_sending_identities row, and the CHANNEL's identity species either has no computable default at all (WhatsApp: a paired physical device is not computable) or has one whose inputs are absent (mail: no tenant slug for the platform-subdomain default). Fail-closed, no fallback sender |
comms.rail_error |
502 | the CHANNEL's rail refused, timed out, or is unreachable (audit egress row exists; no sent event; no message row) |
comms.quota_exceeded |
429 | wallet quota deny at admission; the units are the CHANNEL's own billed quantity (mail: the to∪cc∪bcc recipient fan-out; WhatsApp: 1 per message). Fail-closed on KV/NATS failure. Retry-After carries the seconds until the refusing wallet level's next funded lot starts and is ABSENT when nothing is scheduled to lift the refusal (ADR-236 Decision item 8) — a hint with no renewal behind it would invite a retry loop that cannot succeed |
comms.custody_unprovisioned |
409 | the tenant has no WhatsApp key custody — no key-encryption key exists in Vault Transit because no active dpa.whatsapp-unofficial agreement was recorded and custody was never enabled (or it was destroyed). Nothing can be written for the tenant, so pairing is refused before any enrollment and no socket is opened (ADR-188 D4: the DPA gate is the KEK's existence, structural rather than a runtime checkpoint). The control plane's custody-enable surface answers the same code when asked to enable without an active agreement. Fail-closed, no fallback key |
comms.recipient_consent_absent |
422 | this TENANT holds no active consent row for the recipient — aircloud.comms_consent, keyed (tenant_id, channel, address), predicate revoked_at IS NULL AND (expires_at IS NULL OR expires_at > now()). Refused BEFORE the rail dial, and before the step-4 wallet debit, by the channel's ValidateRecipient precondition inside the step-3 recipient loop (ADR-193 D2/D5), so an unconsented recipient never costs the tenant units. It is NOT comms.recipient_suppressed and the distinction is the GRAIN, not the position: suppression is GLOBAL (comms_suppression is UNIQUE (channel, address) with no tenant_id key, so one tenant's bounce suppresses an address for everyone), while consent is PER-TENANT — one code for two grains would make the refusal unactionable to the Builder who has to fix it. A store failure on the check is comms.internal (500), never this code: absence and outage are different facts and collapsing them would report an outage as a missing consent the Builder would then go and re-collect |
comms.send_admission_withheld |
409 | the tenant holds no accepted, unrevoked agreement of the kind whose gated_resource is whatsapp-send-admission (contract.whatsapp-risk-tier) — the ADR-193 D4a admission gate, refused at the pairing surface before any enrollment. Distinct from comms.custody_unprovisioned, which answers the ADR-188 D4 DPA gate on the KEK's existence: a tenant can hold key custody and still lack send admission, and the two are separate legal instruments with different revocation effects (operator-destroy-eligible vs withdraw-admission, declared in contracts/agreements/v1/agreement-kinds.json). This code exists because the SEND path can only answer comms.identity_unresolved once admission is withdrawn — literally true and under-informative about a contract — so the admission surface is where a tenant learns why. Fail-closed: absent and revoked are the same refusal |
comms.risk_tier_undeclared |
409 | the risk tier the control plane pushed onto the wa-runtime tier-admission lease route is not declared by the runtime's compiled contract (contracts/agreements/v1/risk-tiers.json). Registry-vs-agreement SKEW, refused at the boundary BEFORE the lease is written: recording a name the runtime cannot honour would defer the refusal to every subsequent admission, where it is a silent per-send deny instead of one loud synchronous answer the pusher can act on. Substituting some other tier's capacity is what the refusal exists to prevent — it would grant an authority nobody assigned (engineering-principles.md § 8.1 gate 1, plan decision D-E's jurisdiction 1). Distinct from comms.send_admission_withheld, which answers a tenant holding NO admission instrument at all: here the instrument exists and names a tier the two sides disagree about, so the operator reconciles the registries rather than recording an agreement. Fail-closed, no fallback tier |
comms.latency_tier_immature |
422 | the send's declared latency tier is FASTER than the sending device's warm-up maturity allows: contracts/comms/v1/pacing.json gives each tier a required maturity (none, resistance or mature) and the two warm-up phase lengths, and the device's warm-up anchor has not reached the phase the tier requires (#308 plan decision D2). Refused at the gateway BEFORE the wallet debit — a predictable refusal never costs the tenant units (ADR-187). details carries {requested, required_maturity, required_days, actual_days}: the tier the Builder asked for, the phase it requires, the anchor age in days that phase is reached at, and the device's current anchor age in days, so the Builder can pick a slower tier now or wait. The wa-runtime re-checks the same rule as the authority and fails a command that slips past with the comms.whatsapp.failed reason latency_tier_immature. Distinct from comms.validation: the tier is a valid member of the vocabulary; it is the device that is not ready for it |
comms.sending_halted |
429 | an active pacing halt covers the sending device — at number, pool or tenant scope, recorded by the wa-runtime's durable kill switch on a ban signal, a tripped ban rate or the response-rate floor (#308 plan decisions D6 and D7). Refused at the gateway BEFORE the wallet debit (ADR-187). Retry-After carries the seconds until the halt's halted_until terminal and is ABSENT when the halt is permanent (an account-locked number, which lifts only on a re-pair request), because a retry hint for a halt that does not expire would invite a retry loop that can never succeed. It is not comms.quota_exceeded: the tenant's quota is intact, and what refuses is the rail's risk control, which no wallet credit lifts |
comms.unauthenticated |
401 | the comms gateway's router boundary received no valid canonical token for the route (absent, malformed, expired or from an issuer it does not accept) — nothing was read or sent |
comms.forbidden |
403 | the token is valid but lacks the scope the comms route requires, or names a tenant other than the one addressed — refused at the router boundary before the pipeline runs |
comms.not_found |
404 | the comms gateway serves no such route or channel — refused at the router boundary |
comms.internal |
500 | the comms gateway failed on its own side of the boundary (a store or dependency it needs is unavailable or answered unexpectedly); the send is refused rather than risked, and nothing is debited |
comms.payload_too_large |
413 | the request body is larger than the ZITADEL notification listener reads (commsgw.MaxZitadelNotificationBytes): the server's MaxBytesReader (ADR-275 decision 8) refused it before the handler read it whole. A notification is a handful of short localized fields; a body past the ceiling is not one. Distinct from comms.validation (a body the handler read and found malformed) and comms.request_timeout (a body that did not arrive in time) |
comms.request_timeout |
408 | the request body did not arrive within the server's body read bound (ADR-275 / ADR-273 decision 7): the read was cut at its deadline. ZITADEL retries per its own policy. Distinct from comms.payload_too_large (a body too big) and comms.validation (a body that arrived and was malformed) |
comms.resolver_unavailable |
503 | the caller's authority could not be resolved: the comms gateway's watch of the authority projection could not prove it is current within the projection bound (contracts/auth/v1/account-lifecycle.json), or the projection could not be read (#384, ADR-293 decision 8: fail-closed, never read as no restriction). A dependency outage, never a bound the caller exceeded; retryable, and no Retry-After because no instant is known at which the projection will be current. The same condition answers unavailable on the control-plane API and flex.resolver_unavailable on Air-Flex |
comms.membership_required |
403 | the request did not select a membership the caller holds: the caller holds several memberships in the tenant and sent none, or the selection (the X-Air-Membership header, or a verified assertion's membership_id) names a membership that is unknown or belongs to another principal (#384, ADR-293 decision 9). Select one of the caller's own memberships; a forged id selects nothing. The same condition answers membership_required on the control-plane API (contracts/entitlement/v1/errors.json) |
comms.org_node_archived |
409 | the node the request acts at, or one of its ancestors, is archived — or has no node key in the authority projection, so it cannot be proven active (an unknown node refuses here, never as no restriction): archive suspends every authority acting at or below it — a session's membership homed there, a cron, route, event or direct invoke — until the node is unarchived (#384, ADR-295 decisions 4 and 7, ADR-293 decision 9). Read from the authority projection's node keys along the acting node chain; nothing was changed. The same condition answers org_node_archived on the control-plane API (contracts/entitlement/v1/errors.json) |
deploy
| code | HTTP | meaning | remedy |
|---|---|---|---|
source_stage_failed |
— | The release's git source could not be staged (the repository or ref could not be fetched and packaged). | Check the repository URL, the ref and the git credential, then create the release again. |
source_entry_refused |
— | The source tree carries an entry that is not a regular file or a directory (a symbolic link, a hard link, a special file). | Remove the entry from the product source. |
source_artifact_invalid |
— | The uploaded source artifact does not carry exactly one product source layer of the expected media type. | Upload the source with aircloud release create. |
source_too_large |
— | The product source exceeds the upload size limit. | Remove what the product does not need from the source. |
source_invalid |
— | The product source is not a readable product tarball. | Upload the source with aircloud release create. |
grammar_violation |
— | The product tree breaks the product grammar; the violations are listed. | Fix what aircloud check reports. |
product_slug_mismatch |
— | The tree declares a different product than the one the release was created for. | Create the release for the product aircloud.toml declares, or fix its slug. |
inflection_pattern_unportable |
— | A product inflection rule uses a pattern the platform's runtimes would read differently. | Rewrite the inflection rule with a portable pattern. |
locale_bundle_uncompilable |
— | The product's locale files do not compile into a locale bundle. | Fix the locale files under config/locales/. |
resource_projection_failed |
— | The product schema does not project into the resource projection. | Fix the schema under db/schema/. |
part_build_failed |
— | A part of the release failed to build; the part and its own error are named. | Fix what the part's error names. |
part_not_buildable |
— | A part the release scheduled has nothing to build (the release declares no such part, or no source is staged). | Create the release again; if it recurs, report it. |
render_failed |
— | The build job of a part could not be prepared from the release. | Create the release again; if it recurs, report it. |
aot_failed |
— | The ahead-of-time compile of a data migration's component failed. | Check the migration builds and runs in its unit tests, then create the release again. |
build_job_lost |
— | A part's build job ended without writing its result. | Create the release again; if it recurs, report it. |
part_build_cancelled |
— | Another part of the release failed to build, so this part's build was cancelled; the release names the part that failed. | Fix the part that failed, then create the release again. |
tenant_deprovisioned |
— | The product was deleted while this release work was in flight, so the work was stopped. | Nothing: the product no longer exists. |
build_failed |
— | A part's build failed outside a named refusal; the message carries the build's reason. | Read the message; fix the build and create the release again. |
build_input_upstream_unhealthy |
— | A container's build could not pull a base image: the platform's registry proxy-cache did not serve it (the message names the proxy, the upstream and the base). The platform caches the bases of every upstream it fronts, and a base it cannot serve is usually an upstream the proxy currently cannot reach, or a tag or digest the upstream does not publish. | Create the release again later; if it recurs, check that the base the message names exists upstream, pin it by digest, and report it. |
build_capacity_exhausted |
— | A build waited on the account's build capacity — the releases it builds at once (deploy.build.concurrent) or the parts of one release that build at once (deploy.build.parallelism) — and the builds holding that capacity ran past their own declared deadlines, so the wait ended (ADR-280). quota names the meter, the grain, the limit and its use. A build that only waits behind others that are still within their deadlines is never refused: its part shows at_capacity while it waits. | Wait for the builds already running to finish, then create the release again; to build more at once, move the account to a plan with a higher build capacity or add a build capacity add-on. |
build_capacity_unavailable |
— | A build waited on the platform's build host — the build slots every account shares — and the builds holding them ran past their own declared deadlines, so the wait ended (ADR-280). It is the platform's capacity, not the account's plan: no limit of the account was exceeded. | Create the release again later; if it recurs, report it. |
verification_deadline_exceeds_plan |
— | The verification deadline the release derives from the test layers it declares (contracts/deploy/v1/release-deadline.json: setup, plus each declared layer's base and per-item cost) exceeds what the account's plan sells for one release (the declared meter deploy.release.verification_seconds). It is refused before anything is uploaded or built (the upload's admission), or at resolve when the source was a git repository or the plan narrowed after the upload; quota names the meter, the grain, reason exceeds_limit, the limit and the deadline derived (used), both in seconds. | Declare fewer integration scenarios or system spec files (or fold several into one), or move the account to a plan with a longer verification allowance; then create the release again. |
site_bundle_missing |
— | At activation, the release's published site bundle is not in the registry. | Create the release again; if it recurs, report it. |
instantiation_failed |
— | A function of the release does not instantiate on the functions host. | Fix the function so it builds and runs in its unit tests. |
part_not_built |
— | At activation, a function, container, data migration or the site has no built part. | Activate a release whose build completed. |
host_label_too_long |
— | The product host the environment composes exceeds one DNS label. | Shorten the product slug or the environment name. |
route_table_too_large |
— | The product's route table exceeds what the data plane carries. | Reduce the routes in config/routes.toml. |
secret_inject_undeclared |
— | A secret declares egress without declaring how it is injected. | Declare the injection in config/secrets.toml. |
secret_header_undeclared |
— | A secret is injected as a header without naming the header. | Name the header in config/secrets.toml. |
credential_name_taken |
— | A registered integration already holds the secret's name in that scope. | Rename the secret, or retire the integration holding the name. |
secret_unset |
— | Declared secrets have no value in the environment's scope. | Set them with aircloud secrets set. |
container_build_failed |
— | A container of the release failed to build. | Fix the container's Dockerfile or source. |
container_deploy_failed |
— | A container of the release failed to deploy. | Check the container starts and serves its declared port. |
migration_failed |
— | A data migration refused its run; the detail carries its error. | Fix the migration. |
migration_refused |
— | The functions host refused a data migration run without naming a code. | Read the detail; fix the migration. |
flex.schema_refused |
— | The data plane refused the schema without naming a code. | Read the detail; fix the schema under db/schema/. |
flex.seed_refused |
— | The data plane refused a fixture record without naming a code. | Read the detail; fix the record under test/fixtures/. |
release_not_eligible |
— | The release cannot be activated in that environment (not built, or not verified for production). | Activate a release that is eligible there. |
release_unfunded |
— | The product's account cannot fund what a release of this product spends: its verification environment, the schema changes converged twice (once in verification, once on promotion), its Apps and their databases, and the fixture records verification loads. The upload is refused before its source is stored or anything is built, so the release does not fail midway through verification. The refusal lists every shortfall: for a metered action, the units the release needs and the balance the account holds; for a count limit (entities, or one entity's records), the count the release reaches and the limit. | For each metered action, add credit or move the account to a plan that includes at least the units needed; for each count limit, move the account to a plan whose limit covers the count, or declare fewer entities or fixture records. Then create the release again. |
tenant_not_active |
— | The account is not active, so nothing can be activated and no git credential can be registered. | Bring the account back into good standing. |
environment_not_found |
— | The environment does not exist. | Name an existing environment. |
activation_in_flight |
— | Another activation of that environment is in progress. | Wait for it to finish, then try again. |
verification_failed |
— | The release pipeline did not verify the release (its tests failed, or the run errored); the verdict and the cause are carried. | Read the cause; fix the tests or the product. |
auto_promotion_failed |
— | The verified release could not be promoted to production automatically; the detail carries why. | Promote it with aircloud release promote once the cause is resolved. |
pipeline_deadline_exceeded |
— | The release pipeline ran past its deadline. | Shorten the test run, then create the release again. |
activation_refused |
— | The pipeline could not activate the release in its test environment; the cause names the refusal. | Resolve the named refusal. |
activation_failed |
— | The release's activation in the pipeline's test environment failed. | Read the cause; fix what the activation reports. |
activation_abandoned |
— | The release left the pipeline (it concluded failed, or was superseded) before its activation ran, so the activation was ended without touching any plane. | Create a new release. |
fixtures_invalid |
— | The test fixtures cannot be loaded in order (their references form a cycle). | Break the cycle in test/fixtures/. |
fixtures_refused |
— | The data plane refused a test fixture record; the cause names its code. | Fix the record under test/fixtures/. |
build.lockfile_missing |
— | package.json declares dependencies and there is no package-lock.json (only npm's lockfile is accepted). | Commit package-lock.json. |
build.lockfile_entry_untrusted |
— | A lockfile entry has no integrity, or its integrity carries no sha512 hash (DX59: sha1 cannot anchor trust), or it resolves outside the registry (git+, file:, a link, a tarball URL, another host); or the package carries npm-shrinkwrap.json; or the lockfile is older than v2. | Regenerate the lockfile against registry.npmjs.org with npm 7 or later. |
build.integrity_mismatch |
— | npm ci found a tarball whose hash differs from the lockfile's integrity. | Investigate: the lockfile is the trust anchor. |
build.cargo_lock_missing |
— | A Rust crate (a function, shared component or data migration) has no Cargo.lock; the build compiles a crate only from its committed lock (cargo --locked). | Run cargo generate-lockfile in the crate and commit Cargo.lock. |
build.cargo_lock_entry_untrusted |
— | A Cargo.lock entry is neither a crate of the product (a path package) nor a crates.io package with its sha256 checksum: a git source, another registry, or a registry entry without a checksum. The detail names the entry. | Depend on crates.io releases (or on a crate of the product by path) and regenerate Cargo.lock. |
build.cargo_lock_out_of_sync |
— | Cargo.toml and Cargo.lock disagree: cargo --locked would have to change the lock. | Run cargo update (or cargo generate-lockfile) in the crate and commit Cargo.lock. |
build.wit_supplied |
— | A Rust function or data migration crate carries a wit (a directory, file or link at <crate>/wit). The guest contract a Rust crate binds is the platform's: the build places the tree of the crate's world at <crate>/wit before cargo runs, so a tree of the product's own is refused rather than replaced (ADR-281); the path names it. | Delete the named wit and create the release again. For a local build, aircloud test prepare links it to the CLI's copy of the contract, and the source-ignore rule keeps that link out of a commit and of a release. |
build.lockfile_out_of_sync |
— | package.json and package-lock.json disagree (npm ci refuses to install). | Run npm install and commit the lockfile. |
build.engine_unsupported |
— | A package's engines.node excludes the builder's node. The platform npm config sets engine-strict (DX134), so npm refuses instead of silently skipping an optional native binding; the detail names the package, the required range and the builder node. | Admit the platform's node (versions.json .node) in the package's engines.node, or pin a dependency version whose engines admit it. |
build.install_script_unreviewed |
— | A dependency has an install script (preinstall, install, postinstall) that the package's package.json allowScripts neither approves nor denies. The builders' npm runs with strict-allow-scripts, so npm refuses before installing anything instead of running (npm 11) or skipping (npm 12) the script with a warning; the detail names each package and its script. | Review each named script in package.json allowScripts: true runs it, false skips it (npm install-scripts approve|deny writes the entry). |
build.component_missing |
— | A JavaScript function lists a shared component in its function.toml components and the product has no lib/<name>/ directory for it, so the builder cannot link it where the function's imports resolve; the path is the function's function.toml and the detail names the function and the component. | Generate the component (aircloud generate component <name>) or remove it from the function's components. |
build.component_link_conflict |
— | A function lists a shared component whose name an installed npm package already holds in the node_modules the component must be linked into (the function's, or the test layer's), so the import would be ambiguous; the path is the function's function.toml and the detail names the component and the node_modules entry. | Rename the component, or remove the dependency of the same name from the package.json that installs it. |
build.bundle_failed |
— | A JavaScript function's bundle step (esbuild) reported an error, such as an import that does not resolve; the message carries the build's reason and log_tail the end of what the build printed, where esbuild's [ERROR] lines name the file and the import. | Read log_tail; fix the import or the source it names, then create the release again. |
build.compile_failed |
— | A Rust function's crate did not compile (cargo reported error[E…] or could not compile); the message carries the build's reason and log_tail the end of what cargo printed, with the error and its location. | Read log_tail; fix the error cargo names (cargo build reproduces it locally), then create the release again. |
build.unit_tests_failed |
— | The product's unit layer failed: its test:unit (run once per release in the tests part, beside every JavaScript workload package), a Rust workload crate's cargo test (in the tests part; the path names the crate), or its site unit tests (test:site, in the site part); or the product has JavaScript unit tests but no test/package.json. The refusal carries layer unit. | Fix the test or the code. |
build.site_build_failed |
— | The site's build command failed; the detail carries its output. | Fix the build or app/site/site.toml. |
build.site_output_missing |
— | The site build produced no output directory, or an empty one. | Fix the build or the output directory in app/site/site.toml. |
build.site_output_invalid |
— | The site's output directory escapes app/site/ or contains a symbolic link. | Keep the output inside app/site/ and free of symbolic links. |
build.site_inputs_too_large |
— | The release's resource projection and locale bundle exceed what a site build carries. | Reduce the declared resources or translations the site receives. |
build.test_inputs_too_large |
— | The release's locale bundle exceeds what the tests part carries to the unit tests. | Reduce the product's translations. |
site_publish.entry_invalid |
— | The built site bundle holds an entry that is not a regular file or a directory (a symbolic link, a hard link, a device). | Make the site build write regular files only. |
site_publish.reserved_path |
— | A site file lives under a path the platform reserves (ADR-214: public/.well-known/, public/_air/). | Move the file out of the reserved path. |
slug_invalid |
— | The product slug is outside the grammar (lowercase words joined by single hyphens, at most 30 characters). | Choose a slug that follows the grammar. |
slug_taken |
— | Another product already holds that slug. | Choose another slug. |
tenant_owner_required |
— | The product would have no owner. Every product has exactly one owner, a Builder Account or a platform workload, and the internal create's owner is the calling token's workload; an operator token carries none. | Create the product from a Builder Account, or call the internal create with a workload token. |
product_not_found |
— | No product exists for that account, or it is not visible to the caller. | Create the product, or name the right account. |
release_not_found |
— | The release does not exist in that product. | Name a release from aircloud release list. |
test_run_not_found |
— | The release has no test run in that environment. | Name an environment from the release's test_runs (aircloud release show). |
test_artifacts_not_found |
— | The test run pushed no artifacts (it produced no file past its report, or they exceeded the collection bound). | Read the run's report and cause on the release; a run that writes artifacts carries them. |
test_artifact_not_found |
— | The test run's artifacts hold no regular file at that path. | Name a path the artifacts listing returns (aircloud release artifacts). |
test_artifact_path_invalid |
— | The artifact path is not a clean relative path (absolute, empty segments or dot segments). | Name the path exactly as the artifacts listing returns it. |
test_artifact_too_large |
— | The artifact exceeds the size the platform answers for one file. | Keep test artifacts small (screenshots and traces are far under the bound). |
host_not_found |
— | No product serves that host. | Check the host name. |
migration_not_found |
— | The release declares no data migration by that name. | Name a migration the release declares. |
scope_not_found |
— | The secret scope is neither an environment of the product nor ci. | Name an existing environment, or ci. |
secret_not_declared |
— | The secret is not declared in the product's latest release. | Declare it in config/secrets.toml and create a release first. |
secret_too_large |
— | The secret value exceeds the size limit. | Store a smaller value. |
secret_value_invalid |
— | The secret value is empty or not UTF-8 text. | Set a non-empty UTF-8 value. |
secret_value_encoding_invalid |
— | The secret value does not decode under the key encoding its declaration names (config/secrets.toml key_encoding: base64, hex, or standard-webhooks — a whsec_-prefixed base64 secret). The value is not stored. |
Set the value exactly as the sender issued it, or declare the key_encoding that matches it (a Stripe whsec_ secret is text). |
source_digest_mismatch |
— | The uploaded source does not match the digest the request declared. | Upload again with aircloud release create. |
rollback_contracting |
— | Rolling back would cross a contracting schema change, which the data cannot undo. | Create a new release that restores the previous behaviour instead. |
rollback_target_missing |
— | The environment has no previous release to roll back to. | Activate a specific release instead. |
locale_bundle_not_recorded |
— | The release records no compiled locale bundle. | Create a new release; its resolve records the bundle. |
tenant_not_in_good_standing |
— | The account is not in good standing, so it cannot release or activate. | Bring the account back into good standing. |
environment_reserved |
— | The ci and ci-* environments are owned by the platform. | Choose another environment name. |
environment_name_invalid |
— | The environment name is outside the environment grammar. | Choose a name that follows the grammar. |
name_conflict |
— | An active integration already holds that name. | Choose another name, or retire the integration holding it. |
not_pooled |
— | The tenant is not on the pooled data tier (unplaced, or already dedicated), so it cannot be promoted. | Check the tenant's placement before promoting. |
git_credential_invalid |
— | The git credential is not usable: its username or its token is empty. | Register it again with a non-empty username and the token piped on stdin. |
git_credential_not_found |
— | The product holds no git credential to delete. | Nothing to delete; register one with aircloud git-credentials set when a private repository needs it. |
entitlement
A refusal's body:
{
"error": {
"code": "<snake_case>",
"message": "<human>",
"correlation_id": "<uuid>",
"details": {}
}
}
| code | HTTP | meaning |
|---|---|---|
bad_request |
400 | The path or body is malformed: an unparseable uuid, an unknown body field, or an org_path outside the ADR-148 f6 grammar. Judged at the boundary, before the service is reached, so nothing is staged. |
forbidden |
403 | The caller is authenticated but not permitted: the token carries no required scope, the issuer plane is not accepted on this route, the token tenant_id does not match the path, or the target org_path is not a strict descendant of the token's own grain. A Builder grants strictly downward and can never grant itself a plan. |
already_exists |
409 | The catalog already holds the row being written — a plan version that exists, or a limit already declared for that coordinate. The mutation is refused rather than overwriting a row that assigned tenants may already hold. |
plan_not_found |
404 | The (plan_owner, plan_id, plan_version) triple names no catalog row. The caller supplied a plan_id that does not exist, a plan_version that was never authored, or — the case that is easiest to get wrong — omitted plan_owner for a plan the Builder itself authored, since a NULL plan_owner means the PLATFORM catalog rather than the caller's own. It is the read-side twin of already_exists: that code refuses a write because the row is there, this one refuses because the row a foreign key requires is not. Without it the plan_assignment foreign key surfaces as a bare Postgres 23503 through a 500 — a caller-caused error reported as a platform fault, which is the shape a Builder cannot act on and an operator cannot triage. |
tenant_not_found |
404 | The tenant_id names no tenant. It is the sibling of plan_not_found on the OTHER foreign-key family the grant relations carry: plan_assignment and limit_override each reference aircloud.tenants, and neither operation runs an existence pre-check. Reachable from CLIENT INPUT only on the INTERNAL plane, and that is the whole reason this code exists: the builder-plane routes hold the path tenant_id equal to the token's (gate_token_tenant_matches_path), so there the tenant is an authenticated identity and a foreign-key violation on it really would mean the SSoT lost a row — a platform fault, correctly a 500. The internal-plane token carries no tenant by design, so beginGrant runs no such match and the path tenant_id is caller-supplied and unverified. Routing that to internal/500 reports a caller-caused error as a platform fault, which is exactly what plan_not_found exists to prevent one FK family over. |
override_not_found |
404 | No live override with that id is visible at the caller's grain: it does not exist, it was already revoked, or it belongs to a grain outside the caller's subtree — RevokeOverride's lookup is scoped by the caller's grain, so those three are deliberately indistinguishable on the wire (telling them apart would leak the existence of an override the caller may not see). It is the revoke-side twin of override_exists, and the pair describes the renarrow_override sequence from both ends: that code refuses a create because a live override is there, this one refuses a revoke because none is. Routine rather than exceptional — override_exists's own message instructs the revoke-then-create sequence, so a retried or concurrent second revoke lands here, and without a declared code it surfaced as internal/500: a caller-caused error reported as a platform fault, which a Builder cannot act on and an operator cannot triage. |
override_exists |
409 | A live override already exists at this (meter, dimension) coordinate. limit_override_active_uniq admits exactly one, and revoked_at is the only mutable column, so re-narrowing is the renarrow_override sequence declared in grant-api-v1.json: revoke the live override, then create the new one. Without this code a Builder would receive a raw Postgres 23505 for a routine act. |
catalog_guard_violation |
409 | A plan-catalog guard refused the write. Two cases are reachable: plan_limits_guard — an assigned plan version is immutable, so re-pricing means minting a new version rather than editing the one tenants hold; and plan_assignment_platform_latest — a live grain on a platform plan sits on the plan's latest version, so an assignment naming a superseded platform version is refused (assign it naming no version). |
grant_overcommit |
409 | gate_grant_no_overcommit (item 13) refused: the sibling grants under the parent grain would total past what the parent itself holds. The message names the parent grain, its bound, the sibling total and the proposed grant. The grant is refused, never clamped — writing a smaller value than asked would record an authority nobody granted. |
grant_narrowing_conflict |
409 | gate_child_grants_within_parent_bound (item 20 / DEC-6) refused: the mutation would narrow a grain below a live grant beneath it. The message names each offending child with its own value and the new parent bound. Narrow the children first, then the parent. |
plan_authoring_widening |
409 | gate_builder_plan_narrows_own_grant (item 15 / AC6, ADR-166 facet 4b) refused: the plan version would DECLARE more for a platform meter than the Builder itself holds at its authoring grain. The message names each widening declaration with its declared value and the Builder's own bound, and reports the custom.-meter set that was exempt rather than dropping it silently. The gate judges the rows the transaction would commit, not the request, so declaring narrow then wide across two calls is refused on the second call. |
grant_refused |
409 | The CLASS code, returned when one refusal carries more than one gate family at once, or a family this mapper does not split out. It is deliberately not a fallback for unrecognised errors — those stay a 500 — so a refusal always means a gate actually refused. |
unavailable |
503 | The resolver could not read its store, so the gates could not be evaluated. Kept rigorously distinct from every 409 above by DEC-9: a database blip is not an answer, and reporting it as a refusal would tell a Builder it is not entitled to something it holds. |
internal |
500 | An error this mapper does not recognise. It fails LOUD as its own code rather than being absorbed into a neighbouring meaning — the defect writeQuotaDeny already paid for once, where a default arm silently turned every future code into a transient outage. |
unauthorized |
401 | The request carries no valid bearer token for the control-plane API: absent, malformed, expired or signed by an issuer this API does not accept. The control-plane API's generic transport code (DX140), declared once here. |
not_found |
404 | The addressed resource does not exist or is not visible to the caller (a tenant, an app, a database, a git credential, a capability). The control-plane API's generic transport code (DX140), declared once here; a surface with a more precise not-found code answers that one instead. |
conflict |
409 | The request conflicts with the resource's current state (it is already being deleted, a deploy is in flight, the tenant is not active, the name is taken). The control-plane API's generic transport code (DX140), declared once here. |
unsupported_media_type |
415 | The request body is not in a media type the route accepts (for example a release upload that is not the product source tarball media type). The control-plane API's generic transport code (DX140). |
request_timeout |
408 | The request body did not arrive within the origin's body read bound (contracts/api/v1/request-media-types.json body_read, or the route's own published bound such as the function invoke's pre_relay_deadline_ms); the connection is closed and nothing the body would have caused was done (#735, ADR-273 decision 7). The control-plane API's generic transport code, declared once here. |
unprocessable_entity |
422 | The body parses but a value is not acceptable to the route (for example an mcp_required_roles entry that is neither a registry scope nor a Builder role). The control-plane API's generic transport code (DX140). |
entitlement_unresolvable |
500 | The tenant's entitlement cannot be resolved (no plan limit resolves for the meter at the tenant's grain), so the quota verdict cannot be computed. It fails loud as its own code rather than as an allow or a quota denial. |
unresolvable_policy |
500 | The tenant has no current root plan assignment, so the wallet cannot resolve the policy that decides the request. Distinct from a quota denial: the account is misconfigured, not over its allowance. |
storage_quota_exceeded |
422 | Provisioning the database would take the tenant past its database storage allowance. The detail names the requested size and what the plan admits; the request is refused before anything is provisioned. |
unknown_meter |
400 | The wallet request names a meter that is not a gated meter of the metering registry vocabulary, so no pool can be addressed. Judged before the wallet is read; nothing is debited. |
undeclared_dimension |
400 | The wallet request carries a dimension key the meter does not declare, so the pool identity is not one the plan can hold a limit for. Refused rather than downgraded to the plan-level pool, which is a different pool. |
signup_challenge_invalid |
400 | The signup challenge is malformed or was not issued by this platform (its authenticating MAC does not verify, or its difficulty is below the declared one). Fetch a fresh challenge (#632 P20, ADR-238 decision 6). |
signup_challenge_expired |
400 | The signup challenge is older than contracts/auth/v1/account-lifecycle.json signup.pow_challenge_ttl_seconds. Fetch a fresh challenge and solve it again (#632 P20). |
signup_solution_invalid |
400 | The proof-of-work solution does not give the challenge's hash the leading zero bits it requires (#632 P20, ADR-238 decision 6). |
signup_challenge_replayed |
409 | The solved challenge already created an account (its nonce is that account's id): a challenge creates at most one account. Fetch a fresh challenge (#632 P20). |
account_slug_invalid |
400 | The account slug is outside contracts/auth/v1/account-lifecycle.json account_slug: its pattern, its length bounds, or a reserved word (#632 P20). |
account_slug_taken |
409 | Another account already holds this slug; account slugs are unique platform-wide (#632 P20). |
account_closing |
409 | The account is closing: its products, members and identity are being erased, and the account plane admits nothing but the read of the account's own status (GET /v1/account) and a repeated close (#632 P32). |
account_close_confirmation |
400 | Closing the account needs its owner to type the account's slug as the confirmation, and the request's confirmation does not name this account (#632 P32). |
account_owner_required |
403 | Only an owner member of the account deletes its products or closes it; the caller's token outlived its membership (#632 P32, ADR-239: the membership row is the authority). |
method_not_allowed |
405 | The route does not accept the request method (for example a GET on a callback that only accepts POST). The control-plane API's generic transport code (DX142), declared once here. |
member_role_requires_invite |
409 | The member self-registered, and the update adds a role outside the product's default_roles: a role above default_roles reaches a self-registered member only through a new invite, which re-proves possession of the address (ADR-254 decision 3, #250 operator decision D26). Remove the member and invite the address; an update within default_roles, or one removing roles, is accepted. |
org_node_archived |
409 | The node the request acts at, or one of its ancestors, is archived: archive suspends every authority acting at or below it — a session's membership homed there, a cron, route, event or direct invoke, and every write — until the node is unarchived (#384, ADR-295 decisions 4 and 7, ADR-293 decision 9). Read from the authority projection's node keys along the acting node chain; nothing was changed. |
org_node_not_empty |
409 | The node cannot be deleted because it is not archived: deletion is reachable only through the reversible archive, which suspends the subtree first (#384, ADR-295 decision 7: archive-before-delete is the fail-safe order). Archive the node, then delete it once the archive has settled. |
org_node_not_settled |
409 | The node is archived, but its archive is younger than the authority projection's bound (contracts/auth/v1/account-lifecycle.json authority_projection.propagation_bound_seconds), so a verifier may still admit a write there; deletion erases the subtree on every plane and waits until the archive is effective everywhere (#384, ADR-295 decision 7). Retry after the bound. |
grant_outside_home |
409 | Moving the membership's home is refused because one or more of its membership grants would fall outside the new home: a grant's node is always at or below its membership's home (#384, ADR-293 decision 3, operator decision D13). Revoke or re-grant those grants first; revocation stays an explicit, audited write and is never automatic. |
group_outside_home |
409 | Moving the membership's home is refused because the new home would leave it outside the node of a group it belongs to: a group admits only memberships homed at or below the group's node (#384, ADR-293 decisions 3 and 4). Remove the membership from the group first; group removal stays an explicit, audited write. |
profile_not_visible |
404 | The referenced profile is not visible at the node the request names: a profile is visible at its defining node and every node below it, never at a sibling or an ancestor (#384, ADR-293 decision 1). Reference a profile defined at the grant's node or above it. |
membership_required |
403 | The request did not select a membership the caller holds: the caller holds several memberships in the tenant and sent none, or the selection (the X-Air-Membership header, or a verified assertion's membership_id) names a membership that is unknown or belongs to another principal (#384, ADR-293 decision 9). Select one of the caller's own memberships; a forged id selects nothing. |
workspaces_undeclared |
404 | The product offers no workspace creation: its active production release declares no [authenticate.workspaces] in config/routes.toml, which is the one carrier of the declaration (#384, ADR-292 decisions 1 and 2). Nothing was created; a Builder enables the action by declaring it and releasing to production. |
address_blocked |
403 | The caller's normalized address holds a block in the tenant (the member block list, ADR-254 decision 6), so the platform places it in no new membership — registration and workspace creation alike (#384, ADR-292 decision 5). The block is lifted only by the members unblock operation; nothing was created. |
membership_home_nested |
409 | The new membership's home would nest with a home the caller already holds in the tenant — one an ancestor or descendant of the other — and a user's homes are pairwise disjoint (#384, ADR-293 decision 2, ADR-292 decision 2): a caller homed at or above the workspaces parent, the product's first admin for one, manages workspaces from above instead of creating one. |
org_node_quota_exceeded |
422 | Creating the node would take the tenant, or an ancestor grain of the new node, past its org-node allowance (the meter org.node.count, valued by the plan; ADR-082, #384 ADR-292 decision 3, operator decision D31). Checked at the single node create every creation path takes — the Builder CLI, the nodes.create capability operation and workspace creation; nothing was created. Archive keeps the count; delete returns it. |
org_node_name_invalid |
400 | The node's display name, once trimmed of surrounding blanks, is outside the org_node_name grammar (contracts/deploy/v1/skeleton-v1.json $defs/org_node_name, #785): empty, longer than its maxLength characters, or holding a control character. Refused at every path that names a node — the org-nodes create and rename, the nodes.create capability operation and workspace signup's display_name; nothing was written. |
grant_outside_group |
409 | The group grant's node is not at or below the group's own node: a group confers only at its node and below it, and each grant reaching a member is clipped to that member's home at evaluation (#384, ADR-293 decision 4). Grant at the group's node or below it; nothing was written. |
org_tree_in_use |
409 | The row is still referenced and is never removed with its dependants: a profile a grant references, a group with members or grants, a membership held in a group above the caller's reach (#384, run decision D36, DEC-9 — nothing in the org tree cascades). Revoke the references first, each an explicit audited write; nothing was removed. |
profile_role_undeclared |
409 | The profile would hold a role key the product's active production release does not declare in its [authenticate] roles (#384, ADR-293 decision 1): a profile bundles declared roles only. Declare the role in a release and activate it in production, or leave it out; a role a later release dropped stays on the profiles already holding it and gates nothing. Nothing was written. |
invitation_not_found |
404 | No pending invitation the caller may act on carries that id: it was accepted, revoked or never existed, or it was issued to another person — an invitation is answered only to the human it names, so another identity's invitation reads as absent (#384, operator decision D44). |
invitation_expired |
409 | The invitation is past its declared expiry (contracts/auth/v1/account-lifecycle.json) and can no longer be accepted (#384, operator decision D44). It confers nothing; ask the inviter to revoke it and invite again. |
invitation_email_unverified |
403 | The invitation is accepted only by its invitee signed in with a verified email equal to the invited address, which proves possession of the mailbox (#384, operator decision D44, ADR-254 pre-hijack). Verify the address on the signed-in account, then accept again; nothing was changed. |
feedback
| code | HTTP | meaning |
|---|---|---|
feedback.invalid |
400 | The report is not valid: an unknown kind, an empty or oversize message, an undeclared context key, a context value outside its key's grammar or over its bound, or a contact that is not one email address. The message names which. |
feedback.rate_limited |
429 | The account already filed the contract's per-account hourly number of reports (contracts/feedback/v1/feedback.json bounds.per_account_per_hour). Retry after the hour. |
feedback.public_saturated |
429 | The public form already admitted the platform-wide hourly number of reports (bounds.public_per_hour_ceiling). Retry after the hour, or report from the CLI, which is bounded per account instead. |
feedback.replayed |
409 | A report with this id already exists: the submission was already received. |
flex
A refusal's body:
{
"error": {
"code": "flex.<snake_case>",
"message": "<human>",
"correlation_id": "<uuid>",
"details": {}
}
}
| code | HTTP | meaning |
|---|---|---|
flex.unauthenticated |
401 | missing/invalid tenant_id claim, or an invalid functions-host acting-node assertion (x-air-principal, contracts/flex/v1/end-user-assertion.json: signature, issuer, audience, tenant, bound runtime sub, environment, correlation id, lifetime or node — or presented beside a bearer other than a service one; ADR-257 decision 3) — including, since ADR-293 decision 9 and ADR-295 decisions 3 and 4, an unknown kind or a claim the kind does not admit, an entry node outside the bearer's node, an acting node outside the entry node (widening), and a principal the authority projection holds no key for (#384 C11: a membership the selection does not name answers flex.membership_required, an archived or unknown acting node flex.org_node_archived) |
flex.forbidden |
403 | authenticated but not permitted — including (ADR-081 additive, #384 plan item B4): a write that CHANGES a field a field-policy rule makes non-writable at the record's node, or a query term on a field a base-layer rule hides for the whole request (ADR-294 decision 5), with details.rule naming the rule; and an elevated aggregate outside its declaration — an undeclared op or field on the declared entity's aggregate, or an identifying field (record id, owner field, natural key, unique field) in a filter, group_by or value role of the declaration (ADR-295 decision 8), with details.elevation naming the offending role and value. Other refusals may leave details as {} |
flex.not_found |
404 | entity/record not visible (RLS zero-row surfaces here — never a cross-tenant existence oracle) |
flex.name_conflict |
409 | slug collision (entity per tenant / field per entity); no auto-suffixing |
flex.immutable_field |
409 | attempt to change an immutable field type/config.dims/config.target_entity_id |
flex.validation |
400 | payload/query violates I3/I4/I5 (details carries per-field errors) |
flex.complexity_cap |
422 | query exceeds a complexity/grouping cap (depth/predicates/max-groups) |
flex.rate_limited |
429 | per-tenant rate/in-flight cap exceeded |
flex.quota_exhausted |
429 | QuotaGate deny |
flex.timeout |
504 | statement_timeout hit |
flex.internal |
500 | unexpected server error |
flex.write_fenced |
503 | tenant writes fenced by a placement operation (promotion/migration/DR — #182 I6); reads unaffected; retry after the fence window |
flex.environment_required |
400 | X-Air-Environment is absent on a surface that requires it: PUT /v1/schema, POST /v1/schema/plan, PUT /v1/entities/{name}/records/by-key/{key}, and every migration-mode request (ADR-217 D3 — no fallback to production there) |
flex.schema_omitted_live |
409 | PUT /v1/schema: a live base-layer entity or field, or an enum value the environment knows, is absent from the declaration; omission is never read as deletion, removal is an explicit tombstone (ADR-217 D3; for an enum value, a move into removed_values — ADR-258 decision 4) |
flex.schema_hard_delete |
409 | PUT /v1/schema: the declaration asks for a removal other than the tombstone (removed is not a boolean); convergence only ever soft-deletes (ADR-217 D3) |
flex.schema_required_unsatisfied |
409 | PUT /v1/schema: required would bind a field that live records of the target environment lack — a tightening, or a new required field on a populated entity; refused until a data migration fills them (ADR-217 D3, DX26) |
flex.schema_declared |
409 | a single-change schema-admin write (POST/PATCH/DELETE under /v1/entities) at the base layer (org_path /) of an environment whose base layer is declared; the repository declaration is its only writer, org overlays keep their endpoints (ADR-217 D4, DX35) |
flex.migration_contracting |
409 | migration mode (X-Air-Migration): a write to a field that is neither in X-Air-Migration-Expand nor currently null, any record delete, or any schema write — data migrations are expand-only (ADR-217 D7, DX34) |
flex.resolver_unavailable |
503 | the tenant's resolved entitlement could not be read — its published snapshot is absent, unreadable or stale (ADR-173 facet 4; #600, #626): a dependency outage, never a bound the caller exceeded and never a platform defect; retryable, and no Retry-After because no instant is known at which the snapshot will be readable — and, ADR-081 additive (#384, ADR-293 decision 8), the caller's authority: the watch of the authority projection could not prove it is current within the projection bound (contracts/auth/v1/account-lifecycle.json), or the projection could not be read; fail-closed, never read as no restriction |
flex.role_required |
403 | a record create, update or delete by an acting end user who holds none of the roles the entity's write_roles require for that operation at the record's node (ADR-279 decision 3; #384, roles are evaluated where the record sits); checked before any record is read, so it reveals nothing about a record. A call with no acting end user acts at the node its entry declares or derives (ADR-295 decision 1) and is never refused with it, though a deny binding that node still refuses it with flex.forbidden (ADR-293 decision 5); the keyed upsert stays flex.forbidden for an end user |
flex.membership_required |
403 | the request did not select a membership the caller holds: the caller holds several memberships in the tenant and sent none, or the selection (the X-Air-Membership header, or a verified assertion's membership_id) names a membership that is unknown or belongs to another principal (#384, ADR-293 decision 9). Select one of the caller's own memberships; a forged id selects nothing. The same condition answers membership_required on the control-plane API (contracts/entitlement/v1/errors.json) |
flex.org_node_archived |
409 | the node the request acts at, or one of its ancestors, is archived — or has no node key in the authority projection, so it cannot be proven active (an unknown node refuses here, never as no restriction): archive suspends every authority acting at or below it — a session's membership homed there, a cron, route, event or direct invoke — until the node is unarchived (#384, ADR-295 decisions 4 and 7, ADR-293 decision 9). Read from the authority projection's node keys along the acting node chain; nothing was changed. The same condition answers org_node_archived on the control-plane API (contracts/entitlement/v1/errors.json) |
flex.changeset_not_draft |
409 | a changeset operation (PATCH, DELETE, GET /diff, GET /impact or POST /publish under /v1/changesets/{id}) on a changeset that is published or discarded: only a draft is edited, previewed, discarded or published, and a published changeset is history (#384 D61, ADR-081 additive). Nothing was changed; author a new changeset |
functions
| code | HTTP | meaning |
|---|---|---|
functions.unauthenticated |
401 | missing/invalid/expired canonical token at the authenticated invoke surface |
functions.forbidden |
403 | token lacks tenant:functions, or tenant mismatch with the target function |
functions.not_found |
404 | function / version / environment / deployment does not resolve |
functions.validation |
400 | malformed invoke request / manifest / artifact reference / environment name |
functions.artifact_unsupported |
400 | supplied artifact is not a portable .wasm component (e.g. a .cwasm — rejected at intake) |
functions.slots_exhausted |
429 | pooling-allocator slot exhaustion — carries Retry-After (the D13 quota envelope); platform capacity, never billed |
functions.quota_exhausted |
429 | wallet denial at invocation admission — carries Retry-After (flex.quota_exhausted sibling) — and since ADR-236 Decision item 8 it carries Retry-After exactly when the refusal names a renewal: the seconds until the refusing wallet level's next funded lot starts; no Retry-After when nothing is scheduled to lift it |
functions.deadline_exceeded |
504 | total-invocation wall deadline hit (epoch bound in guest execution, or a remaining-budget upstream timeout in a host call) |
functions.memory_exceeded |
500 | per-instance memory limit hit mid-invocation (details name the limit) |
functions.sandbox_denied |
403 | side-effectful capability invoked in an environment whose link-set denies it |
functions.instantiation_failed |
500 | component failed to instantiate/link (details carry the class, never internals) |
functions.execution_error |
500 | the guest trapped/panicked mid-execution (post-instantiation) — distinct from instantiation failure |
functions.conflict |
409 | duplicate name on create (environment, function, component version) — the unique-constraint surface |
functions.unavailable |
503 | a required backing plane (the event stream) is unreachable for a read path — fail-loud, never an empty-success; admission-time wallet outage is functions.internal 500 |
functions.internal |
500 | unexpected host failure (includes admission-time wallet unavailability — fail-closed, no allow-all fallback) |
functions.guest_no_response |
500 | the guest's handle export returned WITHOUT a complete response — either the wasi:http response outparam was never set, or it was set with a BODY that never terminated. Either way there is no response and there never will be: under the sync bindings the instance is finished, so nothing can set the outparam or write another byte. Distinct from functions.execution_error (the guest trapped; this guest did not) and from functions.deadline_exceeded (a wall-clock overrun; this fault is detected inside the guest's own run and never waits). Billed: execution is past the ADR-082 chokepoint. |
functions.guest_server_error |
500 | a RECORD verdict on the D9 invocation event, never a host envelope: the guest RESPONDED, and its response status was a server error (>= 500, the 5xx class). The caller receives the guest's own response unchanged — status, headers and body — and the event carries the guest's actual status as response_status beside this code. A guest 4xx is NOT this code and records ok. Billed like every relayed response. |
functions.version_not_ready |
503 | the deployed version cannot run on THIS host yet: the platform .cwasm cache holds no entry for its component under this host's AOT cache tag (the wasmtime version, target arch and codegen config the host runs). Carries Retry-After. The platform's own unreadiness, never the component's fault: refused BEFORE the ADR-082 chokepoint, so unbilled (ADR-187), and retryable. Distinct from functions.instantiation_failed (the entry exists and the component cannot link — nothing heals that) and from functions.unavailable (a backing plane is unreachable for a read path). |
functions.locale_bundle_unresolvable |
503 | the aircloud:functions/i18n capability could not produce the locale bundle of the release the invocation resolved to (a pinned previous release included): the release records none, the control plane refused or failed the read, or the bytes did not hash to the recorded digest or are outside contracts/deploy/v1/locale-bundle-v1.json. A capability envelope the guest receives from locales() / bundle() — never a default bundle and never another release's (#645, ADR-223 D3/D10, plan DX95/DX96). details: {release_id} (null when the invocation resolved to no release). |
functions.resolver_unavailable |
503 | the tenant's resolved entitlement could not be read — the published snapshot the wallet judges against is absent, unreadable or stale (ADR-173 facet 4; #600, #626), at admission or behind quota.check: a dependency outage, never a bound the caller exceeded and never a host fault; retryable, unbilled (refused at the money decision, which decremented nothing), and no Retry-After because no instant is known at which the snapshot will be readable. Distinct from functions.internal (a host fault, including a wallet KV outage) and from functions.unavailable (a backing plane unreachable for a read path). |
functions.payload_too_large |
413 | POST /v1/tenants/{t}/functions/{name}/invocations: the encoded JSON payload exceeds payload_max_bytes of contracts/functions/v1/invoke-relay-v1.json — refused by the Builder API before the functions-host is called, so never billed and never recorded; the same bytes are refused again (not retryable). |
functions.invoke_outcome_unknown |
502 | POST /v1/tenants/{t}/functions/{name}/invocations: the invocation was handed to the functions-host and no verdict could be established — it may have run and may have been billed, or it may not have run. The envelope's correlation_id is the one the host adopted; GET /v1/tenants/{t}/invocations?correlation_id=<it> settles the outcome. Never retried by the relay, and not to be retried blindly: a retry can charge a second time. |
functions.invocation_filter_unknown |
400 | GET /v1/tenants/{t}/invocations and GET /v1/tenants/{t}/functions/{name}/invocations: the source filter is not a trigger-source domain (contracts/functions/v1/trigger-sources-v1.json, with the Builder's own app), or the environment filter is neither an environment of the product nor a platform pipeline environment (ci, ci-<n>). details carry filter (source | environment), value, and allowed: the domains, or the product's environments followed by ci and ci-<n>. The same read is refused again until the filter changes (not retryable). |
functions.org_path_undeclared |
409 | a principal-less entry — a public function-target [[route]], a [[triggers.events]] or [[triggers.cron]] entry — whose active release model declares no org_path (#384, ADR-295 decision 1): the functions-host never reads absence as '/', so the invocation is refused before the guest is invoked and before the wallet is charged (unbilled, not retryable). details carry entry (route | event | cron) and, when one was named, trigger_id. aircloud check refuses such a release with route.org_path_required or trigger.org_path_required, so only a release published before that grammar, or a model built wrong, reaches this refusal. |
functions.membership_required |
403 | the invocation's principal holds several memberships in the tenant and the request selected none, or the selection (the web-origin's signed membership_id on a session route, X-Air-Membership on a direct invoke) names a membership that is unknown or another principal's (#384, ADR-293 decision 9, contracts/auth/v1/authz-projection.json selection). Refused before admission, unbilled, not retryable. |
functions.org_node_archived |
409 | the node the invocation acts at, or one of its ancestors, is archived — or has no node key in the authority projection, so it cannot be proven active (#384, ADR-295 decision 4): archive suspends everything acting there (end users, public routes, events, crons, direct invokes), so the host refuses before the guest runs and refuses to sign an acting-node assertion for such a node. Unbilled; it lifts only when the node is unarchived (not retryable). details carry org_path. |