CloudPanorama Request evaluation

API reference

The contract, rendered: every operation with the credential, module and role it requires, its parameters, bodies and responses, and every schema.

Multi-cloud resource inventory, derived health status, and permanent status-transition history.

This document is the contract, not documentation written afterwards. It generates the Go server interface (internal/api/gen/) and the TypeScript client (web/src/api/schema.d.ts), both committed, and CI fails on drift. An endpoint that is not here does not exist — including the operational ones (/healthz, /readyz, /metrics), which are declared because the server answers them, even though no generated client should call them.

Four audiences, four authentication schemes

  • Public — /healthz, /readyz and /public/{tenantSlug}/status need no credentials. The public status endpoint has its own response type and exposes deliberately little; see PublicStatus.
  • Session — the web app, via an HttpOnly; Secure; SameSite=Lax cookie.
  • Collector bearer — collectors, which POST snapshots and never touch the database directly.
  • Agent bearer — provisioning agents running inside customers' own AWS accounts, on /provisioning/agent/* only. A session cookie is never accepted there and an agent token is never accepted anywhere else: the two audiences share no operation, which is why they are two schemes and not one with a wider scope.
  • Read token (apiToken) — a person's own bearer token, for a spreadsheet or a BI tool. It reads what that person's session reads, on the operations that list it beside the session, and nothing else; see apiToken below for how to issue and send one.

Errors

Every non-2xx response is an ErrorResponse. error.code is a stable machine string that the frontend and the collector both branch on, so it is API surface: renaming one is a breaking change. error.message is for a human and never echoes request payload or internal error text.

Tenancy

Every authenticated request is scoped to exactly one tenant, established by the session or the bearer token. No endpoint takes a tenant identifier as a parameter, except the public status page, which takes a slug precisely because it has no credential to derive one from.

The host is checked, never read for the tenant: a session or a read token presented at a host whose first label is another tenant's slug is 401 unauthorized, the answer an unknown credential gets. A host that names no tenant — the deployment's own (DEPLOYMENT_HOST), a self-hosted deployment's hostname, localhost, an address — has nothing to compare, and the credential decides as before. Machine credentials are not checked against the host.

Modules

An operation carrying x-module belongs to that module, and a tenant that has not bought it gets 403 module_not_entitled before the handler runs. The declaration is here rather than in the server because the server derives its middleware from this document: an endpoint group added without it would be served to every tenant regardless of what they pay for, and nothing would say so.

Operations without x-module are platform-wide — authentication, the account registry, entitlements themselves, and the probes. Entitlement is not authorization: what a tenant bought and what a user may do are separate questions, answered separately.

An operation may also carry x-entitlement-access, which says what it asks of the licence at its boundary (PLATFORM.md §7, 2026-09-06): view (the default, and the only value a session route may carry) is locked the moment the licence expires; start — an agent registering or claiming — likewise, so nothing new begins unlicensed; collect — the collection write path — keeps answering through the fourteen-day grace, for the modules the licence listed, then stops; settle — an agent's heartbeat, logs and status for an attempt it already holds — is never locked by expiry, because cloud state has already changed and the platform must learn the outcome, and is bounded by the fence rather than by the licence. Grace and settlement widen when, never what: a module the licence never listed is refused under every access.

An operation may also carry x-capability: a feature the licence names beside its modules (performance, which the Professional and Enterprise bundles carry). A tenant whose licence does not carry it gets 403 capability_not_entitled, after the module check and before the role check. The product reads the capability, never the tier's name, and the capability locks with the modules at expiry.

Authentication and policy

One credential per kind of caller, and a route accepts only what it declares; where it lists two, either one. Routes marked public take none. Each operation below says which credential it takes, which module the tenant must be entitled to, and which role a person must hold; the server derives the same from this document, so they cannot disagree. The credentials, and how to present each:

apiToken http bearer

A person's read token: Authorization: Bearer panorama_read_…. Issued by the person from their own session, POST /auth/api-tokens, after confirming their password; shown once, stored only as its SHA-256; named; expiring after 1 to 365 days (90 unless asked); revocable by the person, and by an administrator from /org/users/{userId}/api-tokens. Ten live tokens a person, two hundred an organisation.

It is the person, on every request: their role, their teams and what the organisation's licence holds are read when it is used, never kept in it, so a change to any of them applies to the next request, and a deactivated person's tokens are revoked. It reads only on the operations that list it as the alternative to sessionCookie, every one a GET; everywhere else it is 401, and it never writes. On those operations a bearer credential is the one that counts and a cookie sent beside it is not read.

Each token may make 120 requests a minute and an organisation's tokens 600 together; past either, 429 rate_limited with Retry-After. Its last use is recorded on it; requests are not logged one by one. An unknown, revoked or expired token is 401.

Send it to your own organisation's address (https://<your-organisation>.<domain>) or to the deployment's own host. At a host that names another organisation it is 401, as an unknown token is: the token still reads only its own organisation, and the host has to agree.

bearerAgent http bearer

Per-agent provisioning token, issued for exactly one (tenant_id, account_id) pair and stored only as sha256(token) in control.agent_tokens.

Both the tenant and the account come from the token (PROVISIONING_SPEC.md §3.6). A claim can never return another tenant's task and can never return another account's task, structurally rather than by a filter a handler could forget. Nothing in an agent request names a tenant or an account for the server to trust.

Fails closed (§3.7): no token, an unknown token, a revoked token, or a token whose account has been disabled or removed from the registry gets 401 or 403. There is no degraded mode.

bearerIngest http bearer

Per-tenant collector token. The token hash is the capability, so it also establishes which tenant the snapshots belong to. Authentication fails closed: with no token configured and no explicit opt-out, the server refuses to start rather than exposing an open ingest endpoint.

bearerMarker http bearer

A marker credential: the token a customer's own deployment system presents to post change markers, and nothing else. Issued by an administrator from the organisation settings, bound to the tenant and to the accounts or apps it may mark (empty lists mean any), revocable; panorama_marker_ prefixed, stored as its SHA-256 only. Never the read token, which writes nothing. Fails closed: an unknown or revoked token is 401.

bearerReview http bearer

The AI reviewer's per-review token, carried in the claim-check message the platform put on the operator's queue. It names its tenant inside the token and resolves, by its hash, to the one open review it may answer; it is spent by the answer and expires after a day. The reviewer holds no other credential of this platform. Fails closed: an unknown, spent or expired token is 401.

A session cookie is never accepted on /provisioning/agent/*, and this token is never accepted anywhere else.

sessionCookie cookie panorama_session

Set by POST /auth/login as HttpOnly; Secure; SameSite=Lax. Only the SHA-256 hash of the token is stored server-side.

The cookie is ambient, so every unsafe request on a route that takes it — and login/logout, which set and clear it — has to come from this deployment's own browser context: Sec-Fetch-Site: same-origin, or an Origin that is this server's host or one listed in CORS_ORIGINS; otherwise 403 origin_not_trusted, before the body is read. A body on such a route must be application/json; otherwise 415 unsupported_media_type. SameSite=Lax alone is not the boundary: it is about the site, and a sibling on the same registrable domain is same-site.

The cookie carries no Domain, so a browser sends it only to the host that set it; the server holds to that too: a session presented at a host that names another tenant is 401.

health (3)

health

Liveness and readiness, for orchestrators.

GET /healthz

publicoperation getHealthz

Liveness

The process is running and its HTTP stack works. Deliberately does NOT touch the database: if it did, one database blip would fail liveness on every pod at once and the orchestrator would restart the whole deployment while the database was already struggling.

Responses

StatusBodyMeaning
200HealthStatusThe process is alive.
429ErrorResponseRate limited.

GET /metrics

publicoperation getMetrics

Prometheus metrics

Included because it is genuinely served on this listener, and a contract that omits an endpoint the server answers is the drift this document exists to prevent.

Not part of the product API: no client generated from this spec should call it, and the response is Prometheus text, not JSON. It shares the main listener unless METRICS_LISTEN gives serve a port of its own, as the Kubernetes manifests do; then this route answers 404, because a scrape discloses queue depth, error rates and traffic volume.

Responses

StatusBodyMeaning
200stringMetrics in the Prometheus text exposition format.
404—METRICS_LISTEN has moved the metrics to a port of their own; the API listener does not serve them.
429ErrorResponseRate limited.

GET /readyz

publicoperation getReadyz

Readiness

This instance can serve traffic: the database answers, its schema is at the version this binary was built against, and the configured database roles are the ones expected.

Responses

StatusBodyMeaning
200HealthStatusReady.
429ErrorResponseRate limited.
503NotReadyStatusNot ready. reason names the cause and never contains a credential, a host or a role name — this endpoint is scraped and logged by other systems.
meta (2)

meta

This document, served by the server it describes.

GET /docs

publicoperation getDocs

API reference UI

A single static HTML page that renders /openapi.json. Read by humans only; no generated client should call it, and the response is HTML, not JSON.

Responses

StatusBodyMeaning
200stringThe documentation page.
429ErrorResponseRate limited.

GET /openapi.json

publicoperation getOpenapiSpec

This document

The specification embedded in the binary at build time, as JSON. It is the same artefact the Go server interface was generated from, so the documented API and the running API cannot disagree: they cannot be regenerated independently.

Served unauthenticated. The contract describes shapes, not data, and every consumer of it — the docs UI below, a collector author, a customer writing against the ingest API — needs it before it has a credential.

Responses

StatusBodyMeaning
200objectThe OpenAPI 3.0.3 document. Schema is object rather than a transcription of the OpenAPI meta-schema: describing this document's own structure inside itself buys nothing a reader does not already have, and generating types for it would be noise in every client.
429ErrorResponseRate limited.
public (2)

public

Unauthenticated. Deliberately minimal.

GET /public/tenant

public60 requests a minute per addressoperation getPublicTenant

The tenant this host shows

The front door: a visitor with no session lands on the public status page, and this says whose — the tenant the request host names by its first label (never the deployment's own host), or the one tenant of a single-tenant deployment. On the bare host of a deployment with several tenants there is none, and the answer is 404 — which says nothing about how many there are. The slug is what the status page's own URL carries anyway; the tenant's name is not in the answer.

Responses

StatusBodyMeaning
200PublicTenantThe tenant whose status page this host shows.
404ErrorResponsenot_found
429ErrorResponseRate limited.
500ErrorResponseinternal_error. The message is generic by design: internal error text never reaches a response body. The real error is logged with the request ID.

GET /public/{tenantSlug}/status

public60 requests a minute per addressoperation getPublicStatus

Public status page

Everything the non-technical audience sees. What it exposes is a decision, not an accident: never resource names, resource IDs, account IDs, regions, IP addresses, ARNs or additionalInfo. Timeline entries are aggregated counts, never lists of resources.

Parameters

NameInTypeDescription
tenantSlugrequiredpathstringThe only place a tenant is named in a request. Everywhere else the tenant comes from the credential; here there is none.

Responses

StatusBodyMeaning
200PublicStatusCurrent status summary and recent aggregated timeline.
404ErrorResponsenot_found
429ErrorResponseRate limited.
500ErrorResponseinternal_error. The message is generic by design: internal error text never reaches a response body. The real error is logged with the request ID.
auth (26)

auth

Login, logout, and the current user.

GET /admin/sso

credential sessionCookierole adminoperation getIdentityProvider

The tenant's identity provider

The registration, without its client secret, or null when the tenant has none. No provider is an answer, not a missing resource: in earlier releases this read was a 404, which a browser logs as a console error on every visit to a page whose honest state is "nothing registered yet". A registration's body is unchanged.

Responses

StatusBodyMeaning
200IdentityProviderThe registration, or null when none is registered.
401ErrorResponseunauthorized — no credential, or one that is not valid.
403ErrorResponseforbidden — authenticated, but the user's role does not permit this. Distinct from module_not_entitled, which is about the tenant's subscription rather than the user. On an unsafe request, also origin_not_trusted: the browser boundary refused the request's origin before anything else ran.
500ErrorResponseinternal_error. The message is generic by design: internal error text never reaches a response body. The real error is logged with the request ID.

PUT /admin/sso

credential sessionCookierole adminbody at most 16384 bytesoperation saveIdentityProvider

Register or change the tenant's identity provider

An OpenID Connect provider: its https issuer, the client's id and secret, the role a first sign-in gets, whether passwords stop working for everyone but break-glass administrators, and the email domains a first sign-in may come from. It asks for the password again: enforcing single sign-on can lock people out.

Request body application/json

FieldTypeDescription
clientIdrequiredstring
clientSecretstringRequired the first time; absent keeps the stored one.
defaultRolerequiredstring enum One of viewer, engineer, admin.
emailDomainsrequiredarray of string
enforcedrequiredboolean
groupsClaimstringThe claim the groups are read from; empty or absent keeps groups.
issuerrequiredstring

Responses

StatusBodyMeaning
200IdentityProviderThe registration.
400ErrorResponseinvalid_request — a malformed parameter or body. The batch ingest endpoint uses invalid_batch instead, because a collector branches on it differently: a bad batch is a bug in the collector, while a bad request is usually a bug in the caller's query string.
401ErrorResponseunauthorized — no credential, or one that is not valid.
403ErrorResponseforbidden — authenticated, but the user's role does not permit this. Distinct from module_not_entitled, which is about the tenant's subscription rather than the user. On an unsafe request, also origin_not_trusted: the browser boundary refused the request's origin before anything else ran.
409ErrorResponserequest_state_conflict — accounts are bound to another provider's issuer: POST /admin/sso/release first.
500ErrorResponseinternal_error. The message is generic by design: internal error text never reaches a response body. The real error is logged with the request ID.

DELETE /admin/sso

credential sessionCookierole adminoperation deleteIdentityProvider

Remove the tenant's identity provider

Passwords work again for everyone, and sign-ins in flight fail.

Responses

StatusBodyMeaning
204—Removed.
401ErrorResponseunauthorized — no credential, or one that is not valid.
403ErrorResponseforbidden — authenticated, but the user's role does not permit this. Distinct from module_not_entitled, which is about the tenant's subscription rather than the user. On an unsafe request, also origin_not_trusted: the browser boundary refused the request's origin before anything else ran.
500ErrorResponseinternal_error. The message is generic by design: internal error text never reaches a response body. The real error is logged with the request ID.

POST /admin/sso/release

credential sessionCookierole adminoperation releaseSignOnIdentities

Release every account's binding to a provider

Clears every account's provider identity (the owner's decision of 2026-09-14): the way to change the registration's issuer, which is refused while accounts are bound to another. Each account binds again, by its email, at its first sign-in through the provider then registered; break-glass passwords work throughout. Audited as auth.identity_provider.release, with how many accounts and their issuers.

Responses

StatusBodyMeaning
200SignOnIdentitiesReleasedHow many accounts were released.
401ErrorResponseunauthorized — no credential, or one that is not valid.
403ErrorResponseforbidden — authenticated, but the user's role does not permit this. Distinct from module_not_entitled, which is about the tenant's subscription rather than the user. On an unsafe request, also origin_not_trusted: the browser boundary refused the request's origin before anything else ran.
500ErrorResponseinternal_error. The message is generic by design: internal error text never reaches a response body. The real error is logged with the request ID.

GET /auth/api-tokens

credential sessionCookieoperation listOwnAPITokens

Your read tokens

The person's own read tokens, newest first, revoked and expired ones included as the record that they existed. Never the token: only its hash is stored. maxExpiresInDays is the longest life the organisation allows a new one.

Responses

StatusBodyMeaning
200OwnAPITokenListThe person's read tokens.
401ErrorResponseunauthorized — no credential, or one that is not valid.
500ErrorResponseinternal_error. The message is generic by design: internal error text never reaches a response body. The real error is logged with the request ID.

POST /auth/api-tokens

credential sessionCookierole vieweroperation issueAPIToken

Issue a read token

A read token for the person making the request, behind a password confirmation: a session must not turn into a credential that outlives it. The token is returned once, here, and never again; only sha256(token) is stored. Audited as auth.api_token.issue. 409 api_token_limit_reached past ten live tokens for the person or two hundred for the organisation. 400 invalid_request for a life longer than the organisation's longest (/org/api-token-settings).

Request body application/json

FieldTypeDescription
expiresInDaysintegerDays until the token stops working, at most the organisation's longest (/org/api-token-settings, 90 unless an administrator changed it). When absent, 90, or that longest when it is shorter.
namerequiredstringWhat the token is for; how a list tells two apart. Must not be blank.

Responses

StatusBodyMeaning
201APITokenIssuedThe token and its record. Store the token now; it cannot be read back.
400ErrorResponseinvalid_request — a malformed parameter or body. The batch ingest endpoint uses invalid_batch instead, because a collector branches on it differently: a bad batch is a bug in the collector, while a bad request is usually a bug in the caller's query string.
401ErrorResponseunauthorized — no credential, or one that is not valid.
403ErrorResponseforbidden — authenticated, but the user's role does not permit this. Distinct from module_not_entitled, which is about the tenant's subscription rather than the user. On an unsafe request, also origin_not_trusted: the browser boundary refused the request's origin before anything else ran.
409ErrorResponseapi_token_limit_reached — the person or the organisation holds as many live read tokens as it may.
413ErrorResponseThe request body exceeded the size the server will read. Declared because it is real: a body is otherwise read until the client stops sending, and one caller could exhaust the process. The cap is above any legitimate request — 500 items at the additionalInfo limit is roughly 160 MB — so reaching it means something is wrong with the sender.
415ErrorResponseunsupported_media_type — the body of a browser-facing route is not labelled application/json (a +json suffix and a charset parameter are fine). Refused before the body is read: a form post, or a "simple" cross-site request carrying JSON as text/plain to dodge the preflight, is not something this API reads. Bearer routes are not held to it.
500ErrorResponseinternal_error. The message is generic by design: internal error text never reaches a response body. The real error is logged with the request ID.

DELETE /auth/api-tokens/{tokenId}

credential sessionCookieoperation revokeOwnAPIToken

Revoke one of your read tokens

Idempotent. The token stops being honoured on the next request; the row stays as the record. A token that is not the person's is not found. Audited as auth.api_token.revoke.

Responses

StatusBodyMeaning
204—Revoked, or already was.
401ErrorResponseunauthorized — no credential, or one that is not valid.
403ErrorResponseorigin_not_trusted — the browser boundary. This is an unsafe request (anything but GET, HEAD, OPTIONS) on a route a browser calls with the person's own session — or on login/logout, which touch it — from a browser context this deployment has not named. Refused before the body is read and before the credential is looked at. What counts as the deployment's own: Sec-Fetch-Site: same-origin (or none, a user-initiated navigation); same-site or cross-site only when the Origin is listed in CORS_ORIGINS; without Fetch Metadata, an Origin (or a Referer's origin) that is this server's own host or listed. Origin: null is never trusted. A request carrying none of these is not a browser's and is not refused on this ground. Every session-authenticated mutation in this contract can answer it; the shared 403 responses below say so.
404ErrorResponsenot_found
500ErrorResponseinternal_error. The message is generic by design: internal error text never reaches a response body. The real error is logged with the request ID.

GET /auth/inbox

credential sessionCookieoperation listOwnInbox

Your inbox

The person's own notifications, newest first, with how many are unread. Entries are kept ninety days, read or not. A page ends where more says older ones follow; the next page is before the last entry's id.

Parameters

NameInTypeDescription
beforequeryinteger (int64)Only entries older than this id.
limitqueryintegerAt most this many entries; 100 when absent.

Responses

StatusBodyMeaning
200InboxPageA page of the person's inbox.
400ErrorResponseinvalid_request — a malformed parameter or body. The batch ingest endpoint uses invalid_batch instead, because a collector branches on it differently: a bad batch is a bug in the collector, while a bad request is usually a bug in the caller's query string.
401ErrorResponseunauthorized — no credential, or one that is not valid.
500ErrorResponseinternal_error. The message is generic by design: internal error text never reaches a response body. The real error is logged with the request ID.

POST /auth/inbox/read

credential sessionCookieoperation markInboxRead

Mark inbox entries read

Marks the named entries read, or every one with all. An id that is not the person's is passed over, never an error: it reveals nothing about whose it is.

Request body application/json

FieldTypeDescription
allbooleanEvery entry of the person's, whatever ids says.
idsarray of integer (int64)

Responses

StatusBodyMeaning
200InboxUnreadHow many of the person's entries stay unread.
400ErrorResponseinvalid_request — a malformed parameter or body. The batch ingest endpoint uses invalid_batch instead, because a collector branches on it differently: a bad batch is a bug in the collector, while a bad request is usually a bug in the caller's query string.
401ErrorResponseunauthorized — no credential, or one that is not valid.
403ErrorResponseorigin_not_trusted — the browser boundary. This is an unsafe request (anything but GET, HEAD, OPTIONS) on a route a browser calls with the person's own session — or on login/logout, which touch it — from a browser context this deployment has not named. Refused before the body is read and before the credential is looked at. What counts as the deployment's own: Sec-Fetch-Site: same-origin (or none, a user-initiated navigation); same-site or cross-site only when the Origin is listed in CORS_ORIGINS; without Fetch Metadata, an Origin (or a Referer's origin) that is this server's own host or listed. Origin: null is never trusted. A request carrying none of these is not a browser's and is not refused on this ground. Every session-authenticated mutation in this contract can answer it; the shared 403 responses below say so.
500ErrorResponseinternal_error. The message is generic by design: internal error text never reaches a response body. The real error is logged with the request ID.

POST /auth/login

publicbody at most 8192 bytesoperation login

Log in

Tenant-qualified. An email address does not identify a user — users is unique on (tenant_id, email), so the same address may exist in several tenants. The tenant is resolved from the request host, or from tenantSlug when the host does not identify one.

When the tenant enforces single sign-on, a right password for any account but a break-glass administrator is answered 403 with sso_required: the account signs in through the tenant's identity provider. Said only after the password verified, so it tells nothing to anyone without it.

Request body application/json

FieldTypeDescription
emailrequiredstringThe longest address RFC 5321 permits. Deliberately NOT format: email. Syntax beyond "not blank" is not validated — an address that an over-strict pattern rejects is a customer who cannot log in — and declaring a format the server does not enforce is a contract that lies to whoever generates a client from it.
passwordrequiredstring (password)Bounded because verifying it is an argon2 derivation over whatever was sent, and this endpoint is unauthenticated.
tenantSlugstringOptional. Needed only when the request host does not identify a tenant, because an email address alone does not identify a user. A DNS label, because it is a subdomain: acme is acme.cloudpanorama.example. Matched case-insensitively.

Responses

StatusBodyMeaning
200UserLogged in. Sets an HttpOnly; Secure; SameSite=Lax session cookie.
400ErrorResponseinvalid_request — a malformed parameter or body. The batch ingest endpoint uses invalid_batch instead, because a collector branches on it differently: a bad batch is a bug in the collector, while a bad request is usually a bug in the caller's query string.
401ErrorResponseWrong credentials, unknown tenant, or a disabled user — deliberately indistinguishable, so this endpoint cannot be used to enumerate tenants or accounts.
403ErrorResponseorigin_not_trusted — the browser boundary. This is an unsafe request (anything but GET, HEAD, OPTIONS) on a route a browser calls with the person's own session — or on login/logout, which touch it — from a browser context this deployment has not named. Refused before the body is read and before the credential is looked at. What counts as the deployment's own: Sec-Fetch-Site: same-origin (or none, a user-initiated navigation); same-site or cross-site only when the Origin is listed in CORS_ORIGINS; without Fetch Metadata, an Origin (or a Referer's origin) that is this server's own host or listed. Origin: null is never trusted. A request carrying none of these is not a browser's and is not refused on this ground. Every session-authenticated mutation in this contract can answer it; the shared 403 responses below say so.
413ErrorResponseThe request body exceeded the size the server will read. Declared because it is real: a body is otherwise read until the client stops sending, and one caller could exhaust the process. The cap is above any legitimate request — 500 items at the additionalInfo limit is roughly 160 MB — so reaching it means something is wrong with the sender.
415ErrorResponseunsupported_media_type — the body of a browser-facing route is not labelled application/json (a +json suffix and a charset parameter are fine). Refused before the body is read: a form post, or a "simple" cross-site request carrying JSON as text/plain to dodge the preflight, is not something this API reads. Bearer routes are not held to it.
429ErrorResponseRate limited.
500ErrorResponseinternal_error. The message is generic by design: internal error text never reaches a response body. The real error is logged with the request ID.

POST /auth/logout

publicoperation logout

Log out

Deletes the session server-side and clears the cookie. Idempotent, and accepted without a session: repeating it, or calling it after the session expired, is not an error.

A live session sent to a host that names another organisation is 401, as on every route that takes it: the session is not ended and the cookie is not cleared. Log out at your own organisation's address or the deployment's own host. A cookie that is no live session is 204 at any host.

Responses

StatusBodyMeaning
204—Logged out. Repeating this is not an error.
401ErrorResponseunauthorized — no credential, or one that is not valid.
403ErrorResponseorigin_not_trusted — the browser boundary. This is an unsafe request (anything but GET, HEAD, OPTIONS) on a route a browser calls with the person's own session — or on login/logout, which touch it — from a browser context this deployment has not named. Refused before the body is read and before the credential is looked at. What counts as the deployment's own: Sec-Fetch-Site: same-origin (or none, a user-initiated navigation); same-site or cross-site only when the Origin is listed in CORS_ORIGINS; without Fetch Metadata, an Origin (or a Referer's origin) that is this server's own host or listed. Origin: null is never trusted. A request carrying none of these is not a browser's and is not refused on this ground. Every session-authenticated mutation in this contract can answer it; the shared 403 responses below say so.
429ErrorResponseRate limited.
500ErrorResponseinternal_error. The message is generic by design: internal error text never reaches a response body. The real error is logged with the request ID.

GET /auth/me

credential sessionCookie300 requests a minute per addressoperation getCurrentUser

Current user

Who the session belongs to, which role they hold, and which tenant they are scoped to. The web app calls this on load to decide what to render — which clouds it offers among it (clouds); a 401 here is how it learns the session has expired.

Responses

StatusBodyMeaning
200UserThe authenticated user, their role and their tenant.
401ErrorResponseunauthorized — no credential, or one that is not valid.
429ErrorResponseRate limited.
500ErrorResponseinternal_error. The message is generic by design: internal error text never reaches a response body. The real error is logged with the request ID.

PATCH /auth/me

credential sessionCookiebody at most 8192 bytesoperation updateOwnProfile

Change your own display name and preferences

The person's own: a display name, shown where the portal names people, and the portal's preferences, which it applies for them — time zone, date and number formats, and theme. Only the fields present change; an empty display name clears it, and preferences are replaced whole — except what the server records: the first save's preferencesConfirmedAt, which nothing moves once it is there, and the first-run record, which a preferences save never changes, whether it carries preferences.firstRun or not. The first-run record changes only through firstRun at the top level of this body, and only in the fields sent, so dismissing the checklist cannot put back the display preferences a tab read long ago, and cannot undo a tour version recorded since. Nothing here changes what the person may do.

Request body application/json

FieldTypeDescription
displayNamestring
firstRunFirstRunChangeA change to the person's first-run record alone. Each field sent changes that field, by the field's own rule; a field left out keeps what is recorded. It never touches the display preferences or their confirmation, which only a preferences save changes: the checklist's Dismiss, and the tour's version, change the checklist and nothing a tab might hold stale.
preferencesPreferencesThe person's portal preferences, applied by the portal; an absent field is the browser's own.

Responses

StatusBodyMeaning
200UserThe person, as GET /auth/me answers.
400ErrorResponseinvalid_request — a field breaks its rule; the message names the field and the rule.
401ErrorResponseunauthorized — no credential, or one that is not valid.
403ErrorResponseorigin_not_trusted — the browser boundary. This is an unsafe request (anything but GET, HEAD, OPTIONS) on a route a browser calls with the person's own session — or on login/logout, which touch it — from a browser context this deployment has not named. Refused before the body is read and before the credential is looked at. What counts as the deployment's own: Sec-Fetch-Site: same-origin (or none, a user-initiated navigation); same-site or cross-site only when the Origin is listed in CORS_ORIGINS; without Fetch Metadata, an Origin (or a Referer's origin) that is this server's own host or listed. Origin: null is never trusted. A request carrying none of these is not a browser's and is not refused on this ground. Every session-authenticated mutation in this contract can answer it; the shared 403 responses below say so.
413ErrorResponseThe request body exceeded the size the server will read. Declared because it is real: a body is otherwise read until the client stops sending, and one caller could exhaust the process. The cap is above any legitimate request — 500 items at the additionalInfo limit is roughly 160 MB — so reaching it means something is wrong with the sender.
415ErrorResponseunsupported_media_type — the body of a browser-facing route is not labelled application/json (a +json suffix and a charset parameter are fine). Refused before the body is read: a form post, or a "simple" cross-site request carrying JSON as text/plain to dodge the preflight, is not something this API reads. Bearer routes are not held to it.
500ErrorResponseinternal_error. The message is generic by design: internal error text never reaches a response body. The real error is logged with the request ID.

GET /auth/me/notifications

credential sessionCookieoperation getOwnNotificationChoices

Your notification choices

What the person hears about in their inbox, in the portal only: status alerts, their teams' budgets and cost anomalies, their own provisioning requests and their teams' weekly digests. A choice they never made is its default — alerts and their own requests on, budgets, anomalies and the digest off.

Responses

StatusBodyMeaning
200NotificationChoicesThe person's choices.
401ErrorResponseunauthorized — no credential, or one that is not valid.
500ErrorResponseinternal_error. The message is generic by design: internal error text never reaches a response body. The real error is logged with the request ID.

PUT /auth/me/notifications

credential sessionCookieoperation setOwnNotificationChoices

Change your notification choices

Replaces the person's choices whole. They decide what reaches their own inbox and nothing else: the tenant's channels are the administrators'.

Request body application/json

FieldTypeDescription
alertsrequiredbooleanStatus alerts on the accounts the person may see — their teams', or every account for an administrator. An account no team owns reaches the administrators alone.
anomaliesrequiredbooleanCost anomalies on the person's teams and their teams' accounts; every one, for an administrator.
budgetsrequiredbooleanBudget thresholds of the person's teams; every team's, for an administrator.
digestrequiredbooleanThe weekly digest of each of the person's own teams: the team's spend, budgets, anomalies and alerts, with links back. Only their teams' — an administrator's too — and only while they are in the team. Sent only where the operator has set PORTAL_URL and the organisation has Spend.
myRequestsrequiredbooleanTheir own provisioning requests — approvals, successes and failures.

Responses

StatusBodyMeaning
200NotificationChoicesThe person's choices, as stored.
400ErrorResponseinvalid_request — a malformed parameter or body. The batch ingest endpoint uses invalid_batch instead, because a collector branches on it differently: a bad batch is a bug in the collector, while a bad request is usually a bug in the caller's query string.
401ErrorResponseunauthorized — no credential, or one that is not valid.
403ErrorResponseorigin_not_trusted — the browser boundary. This is an unsafe request (anything but GET, HEAD, OPTIONS) on a route a browser calls with the person's own session — or on login/logout, which touch it — from a browser context this deployment has not named. Refused before the body is read and before the credential is looked at. What counts as the deployment's own: Sec-Fetch-Site: same-origin (or none, a user-initiated navigation); same-site or cross-site only when the Origin is listed in CORS_ORIGINS; without Fetch Metadata, an Origin (or a Referer's origin) that is this server's own host or listed. Origin: null is never trusted. A request carrying none of these is not a browser's and is not refused on this ground. Every session-authenticated mutation in this contract can answer it; the shared 403 responses below say so.
500ErrorResponseinternal_error. The message is generic by design: internal error text never reaches a response body. The real error is logged with the request ID.

GET /auth/me/teams

credential sessionCookieoperation listOwnTeams

Your teams

The teams the person belongs to and their role in each, by name. Read-only here: an administrator changes membership.

Responses

StatusBodyMeaning
200OwnTeamListThe person's teams.
401ErrorResponseunauthorized — no credential, or one that is not valid.
500ErrorResponseinternal_error. The message is generic by design: internal error text never reaches a response body. The real error is logged with the request ID.

POST /auth/password

credential sessionCookiebody at most 8192 bytesoperation changePassword

Change the current user's password

Verifies the current password, stores the new one, ends every session the user holds — including the one making this request — and sets a fresh cookie for this browser, so the person is not logged out of the tab they typed into but every other tab is. A wrong current password counts against the login throttle for this account, so this endpoint cannot be used to guess a password an authenticated session does not know. Audited as auth.password.change.

A new password is at least 12 characters and at most 1024.

Request body application/json

FieldTypeDescription
currentPasswordrequiredstring
newPasswordrequiredstring

Responses

StatusBodyMeaning
204—Changed. A fresh session cookie is set.
400ErrorResponseinvalid_request — the current password is wrong, or the new one is too short or too long. One code for the wrong password and the weak replacement: the message says which.
401ErrorResponseunauthorized — no credential, or one that is not valid.
403ErrorResponseorigin_not_trusted — the browser boundary. This is an unsafe request (anything but GET, HEAD, OPTIONS) on a route a browser calls with the person's own session — or on login/logout, which touch it — from a browser context this deployment has not named. Refused before the body is read and before the credential is looked at. What counts as the deployment's own: Sec-Fetch-Site: same-origin (or none, a user-initiated navigation); same-site or cross-site only when the Origin is listed in CORS_ORIGINS; without Fetch Metadata, an Origin (or a Referer's origin) that is this server's own host or listed. Origin: null is never trusted. A request carrying none of these is not a browser's and is not refused on this ground. Every session-authenticated mutation in this contract can answer it; the shared 403 responses below say so.
413ErrorResponseThe request body exceeded the size the server will read. Declared because it is real: a body is otherwise read until the client stops sending, and one caller could exhaust the process. The cap is above any legitimate request — 500 items at the additionalInfo limit is roughly 160 MB — so reaching it means something is wrong with the sender.
415ErrorResponseunsupported_media_type — the body of a browser-facing route is not labelled application/json (a +json suffix and a charset parameter are fine). Refused before the body is read: a form post, or a "simple" cross-site request carrying JSON as text/plain to dodge the preflight, is not something this API reads. Bearer routes are not held to it.
429ErrorResponseRate limited.
500ErrorResponseinternal_error. The message is generic by design: internal error text never reaches a response body. The real error is logged with the request ID.

POST /auth/password/reset

publicbody at most 8192 bytesoperation redeemPasswordReset

Redeem a password reset

Sets the user's password from a reset an administrator issued. The token is burnt first — it works exactly once, before its expiry — then the password is written, every session the user holds is ended, and every other outstanding reset for them is withdrawn. An invalid, expired or already-used token is one answer (400 invalid_request), so a holder cannot tell which. Audited as auth.password.reset.

Request body application/json

FieldTypeDescription
newPasswordrequiredstring
tokenrequiredstring

Responses

StatusBodyMeaning
204—The password is set. Log in with it.
400ErrorResponseinvalid_request — a malformed parameter or body. The batch ingest endpoint uses invalid_batch instead, because a collector branches on it differently: a bad batch is a bug in the collector, while a bad request is usually a bug in the caller's query string.
403ErrorResponseorigin_not_trusted — the browser boundary. This is an unsafe request (anything but GET, HEAD, OPTIONS) on a route a browser calls with the person's own session — or on login/logout, which touch it — from a browser context this deployment has not named. Refused before the body is read and before the credential is looked at. What counts as the deployment's own: Sec-Fetch-Site: same-origin (or none, a user-initiated navigation); same-site or cross-site only when the Origin is listed in CORS_ORIGINS; without Fetch Metadata, an Origin (or a Referer's origin) that is this server's own host or listed. Origin: null is never trusted. A request carrying none of these is not a browser's and is not refused on this ground. Every session-authenticated mutation in this contract can answer it; the shared 403 responses below say so.
413ErrorResponseThe request body exceeded the size the server will read. Declared because it is real: a body is otherwise read until the client stops sending, and one caller could exhaust the process. The cap is above any legitimate request — 500 items at the additionalInfo limit is roughly 160 MB — so reaching it means something is wrong with the sender.
415ErrorResponseunsupported_media_type — the body of a browser-facing route is not labelled application/json (a +json suffix and a charset parameter are fine). Refused before the body is read: a form post, or a "simple" cross-site request carrying JSON as text/plain to dodge the preflight, is not something this API reads. Bearer routes are not held to it.
429ErrorResponseRate limited.
500ErrorResponseinternal_error. The message is generic by design: internal error text never reaches a response body. The real error is logged with the request ID.

GET /auth/sessions

credential sessionCookieoperation listOwnSessions

Your sessions

The person's own live sessions, the most recently used first: when each began and was last used, the address and browser it began from, and which one is making this request. Never a token: each has an id derived from its token's hash, which ends that session and looks nothing else up.

Responses

StatusBodyMeaning
200OwnSessionListThe person's live sessions.
401ErrorResponseunauthorized — no credential, or one that is not valid.
500ErrorResponseinternal_error. The message is generic by design: internal error text never reaches a response body. The real error is logged with the request ID.

DELETE /auth/sessions

credential sessionCookieoperation endOtherSessions

Sign out everywhere else

Ends every session of the person's but the one making this request.

Responses

StatusBodyMeaning
200SessionsEndedHow many sessions ended.
401ErrorResponseunauthorized — no credential, or one that is not valid.
403ErrorResponseorigin_not_trusted — the browser boundary. This is an unsafe request (anything but GET, HEAD, OPTIONS) on a route a browser calls with the person's own session — or on login/logout, which touch it — from a browser context this deployment has not named. Refused before the body is read and before the credential is looked at. What counts as the deployment's own: Sec-Fetch-Site: same-origin (or none, a user-initiated navigation); same-site or cross-site only when the Origin is listed in CORS_ORIGINS; without Fetch Metadata, an Origin (or a Referer's origin) that is this server's own host or listed. Origin: null is never trusted. A request carrying none of these is not a browser's and is not refused on this ground. Every session-authenticated mutation in this contract can answer it; the shared 403 responses below say so.
500ErrorResponseinternal_error. The message is generic by design: internal error text never reaches a response body. The real error is logged with the request ID.

DELETE /auth/sessions/{sessionId}

credential sessionCookieoperation endOwnSession

Sign out one of your sessions

Ends one of the person's other sessions. The session making the request is ended by logging out, and naming it here is refused; a session that is not the person's is not found.

Parameters

NameInTypeDescription
sessionIdrequiredpathstringA session's id, as GET /auth/sessions gives it.

Responses

StatusBodyMeaning
204—The session ended.
400ErrorResponseinvalid_request — the session named is the one making the request.
401ErrorResponseunauthorized — no credential, or one that is not valid.
403ErrorResponseorigin_not_trusted — the browser boundary. This is an unsafe request (anything but GET, HEAD, OPTIONS) on a route a browser calls with the person's own session — or on login/logout, which touch it — from a browser context this deployment has not named. Refused before the body is read and before the credential is looked at. What counts as the deployment's own: Sec-Fetch-Site: same-origin (or none, a user-initiated navigation); same-site or cross-site only when the Origin is listed in CORS_ORIGINS; without Fetch Metadata, an Origin (or a Referer's origin) that is this server's own host or listed. Origin: null is never trusted. A request carrying none of these is not a browser's and is not refused on this ground. Every session-authenticated mutation in this contract can answer it; the shared 403 responses below say so.
404ErrorResponsenot_found
500ErrorResponseinternal_error. The message is generic by design: internal error text never reaches a response body. The real error is logged with the request ID.

GET /auth/sso/callback

publicoperation finishSingleSignOn

The identity provider's redirect back

Takes the attempt the state names, once, and only for the browser that started it (its attempt cookie); exchanges the code with its PKCE verifier; verifies the ID token and its nonce; finds the account by its email — vouched for by the provider or by the tenant's domains, and bound to the provider's subject on its first sign-in — or creates it with the provider's default role; and issues a session on the same path a password does, redirecting to the return path. Any failure redirects to the login page with sso=failed, and the log says why.

A step-up's return (POST /auth/step-up/sso) records the step-up on the session cookie it brings back — but not on a session sent to a host that names another organisation: that return fails as one from another browser does.

Parameters

NameInTypeDescription
statequerystring
codequerystring
errorquerystringThe provider's error, when the person cancelled or was refused.

Responses

StatusBodyMeaning
302—To the return path with the session cookie, or to the login page.

GET /auth/sso/options

publicoperation getSingleSignOnOptions

Whether this tenant signs in through an identity provider

For the login page, before anyone signs in: whether the tenant the request is for — by its host, tenantSlug, or the only tenant — has an identity provider, and whether it is enforced. Never the issuer or the client. A tenant that cannot be resolved has neither.

Parameters

NameInTypeDescription
tenantSlugquerystring

Responses

StatusBodyMeaning
200SingleSignOnOptionsThe tenant's single sign-on options.
500ErrorResponseinternal_error. The message is generic by design: internal error text never reaches a response body. The real error is logged with the request ID.

GET /auth/sso/start

publicoperation startSingleSignOn

Begin a sign-in through the tenant's identity provider

A browser navigation, not a call: it redirects to the tenant's provider with a single-use state, a nonce and a PKCE challenge, kept for ten minutes. When the tenant has no provider, or the provider cannot be reached, it redirects back to the login page with sso=unavailable.

Parameters

NameInTypeDescription
tenantSlugquerystring
returnToquerystringA path on this site to return to; anything else is the portal's root.

Responses

StatusBodyMeaning
302—To the identity provider, or back to the login page.

POST /auth/step-up

credential sessionCookiebody at most 8192 bytesoperation stepUp

Confirm the password before a destructive action

Verifies the current user's password again and records the check on this session. An operation marked x-step-up: true (removing a user from a team or a team from an account, archiving or renaming a team, archiving a template, revoking a user's sessions or an agent's token, disabling an account, a channel or a budget, deleting a channel or a budget, changing a user, issuing a password reset) answers 403 step_up_required unless the session calling it did this in the last 10 minutes. Only this session is confirmed: another device or browser profile of the same user is asked again. Tabs of one browser profile share its session cookie, and so share the check. A wrong password counts against the login throttle for the account, as a failed login does.

Request body application/json

FieldTypeDescription
passwordrequiredstringThe current user's password.

Responses

StatusBodyMeaning
204—Confirmed. For 10 minutes this session may take destructive actions.
400ErrorResponseinvalid_request — the password is wrong, or longer than a password can be.
401ErrorResponseunauthorized — no credential, or one that is not valid.
403ErrorResponseorigin_not_trusted — the browser boundary. This is an unsafe request (anything but GET, HEAD, OPTIONS) on a route a browser calls with the person's own session — or on login/logout, which touch it — from a browser context this deployment has not named. Refused before the body is read and before the credential is looked at. What counts as the deployment's own: Sec-Fetch-Site: same-origin (or none, a user-initiated navigation); same-site or cross-site only when the Origin is listed in CORS_ORIGINS; without Fetch Metadata, an Origin (or a Referer's origin) that is this server's own host or listed. Origin: null is never trusted. A request carrying none of these is not a browser's and is not refused on this ground. Every session-authenticated mutation in this contract can answer it; the shared 403 responses below say so.
413ErrorResponseThe request body exceeded the size the server will read. Declared because it is real: a body is otherwise read until the client stops sending, and one caller could exhaust the process. The cap is above any legitimate request — 500 items at the additionalInfo limit is roughly 160 MB — so reaching it means something is wrong with the sender.
415ErrorResponseunsupported_media_type — the body of a browser-facing route is not labelled application/json (a +json suffix and a charset parameter are fine). Refused before the body is read: a form post, or a "simple" cross-site request carrying JSON as text/plain to dodge the preflight, is not something this API reads. Bearer routes are not held to it.
429ErrorResponseRate limited.
500ErrorResponseinternal_error. The message is generic by design: internal error text never reaches a response body. The real error is logged with the request ID.

POST /auth/step-up/sso

credential sessionCookiebody at most 8192 bytesoperation stepUpAtProvider

Confirm at the identity provider before a destructive action

The step-up of an account that signs in through the tenant's identity provider, which may have no password anyone knows: answers the provider's authorization URL for a sign-in that asks it to authenticate the person afresh (prompt=login, max_age=0), and sets the browser's attempt cookie. The browser goes there; the provider's redirect back to /auth/sso/callback confirms this session, as POST /auth/step-up does, when the person the provider names is this session's account (and, where the provider says when, it authenticated them in the last five minutes), then returns to returnTo.

Request body application/json

FieldTypeDescription
returnTostringA path on this site to come back to after the provider; anything else is the portal's root.

Responses

StatusBodyMeaning
200StepUpAtProviderWhere the browser goes to confirm.
401ErrorResponseunauthorized — no credential, or one that is not valid.
403ErrorResponseorigin_not_trusted — the browser boundary. This is an unsafe request (anything but GET, HEAD, OPTIONS) on a route a browser calls with the person's own session — or on login/logout, which touch it — from a browser context this deployment has not named. Refused before the body is read and before the credential is looked at. What counts as the deployment's own: Sec-Fetch-Site: same-origin (or none, a user-initiated navigation); same-site or cross-site only when the Origin is listed in CORS_ORIGINS; without Fetch Metadata, an Origin (or a Referer's origin) that is this server's own host or listed. Origin: null is never trusted. A request carrying none of these is not a browser's and is not refused on this ground. Every session-authenticated mutation in this contract can answer it; the shared 403 responses below say so.
404ErrorResponsenot_found — your organisation has no identity provider.
409ErrorResponserequest_state_conflict — this account has never signed in through the tenant's provider; it confirms with its password.
413ErrorResponseThe request body exceeded the size the server will read. Declared because it is real: a body is otherwise read until the client stops sending, and one caller could exhaust the process. The cap is above any legitimate request — 500 items at the additionalInfo limit is roughly 160 MB — so reaching it means something is wrong with the sender.
415ErrorResponseunsupported_media_type — the body of a browser-facing route is not labelled application/json (a +json suffix and a charset parameter are fine). Refused before the body is read: a form post, or a "simple" cross-site request carrying JSON as text/plain to dodge the preflight, is not something this API reads. Bearer routes are not held to it.
500ErrorResponseinternal_error. The message is generic by design: internal error text never reaches a response body. The real error is logged with the request ID.
dashboard (2)

dashboard

The start page, in one read, and the first-run checklist it carries.

GET /dashboard

credential sessionCookieoperation getDashboard

The start page, in one request

Everything the Dashboard shows (MILESTONE_1 §4.2), read once.

Not a new source of truth. Every number here comes from the repository that serves the page it links to, under the viewer's own scope, so the Dashboard cannot disagree with what a person finds when they follow it.

Nothing blocks on the slowest section. Each section is read with its own deadline; one that fails or overruns answers {"unavailable": true} and the rest of the page is unaffected. A start page that is blank because one widget is slow is worse than no start page at all.

A section the viewer may not have is absent, not locked. Entitlement, capability and role are decided per section: a tenant without Spend has no spend key, rather than a teaser. The shell's locked-module affordance is where a plan is sold.

Any signed-in role may call this; what comes back differs.

Responses

StatusBodyMeaning
200DashboardThe Dashboard.
401ErrorResponseunauthorized — no credential, or one that is not valid.
500ErrorResponseinternal_error. The message is generic by design: internal error text never reaches a response body. The real error is logged with the request ID.

GET /first-run

credential sessionCookieoperation getFirstRun

The setup checklist, for the person asking

The first-run checklist (MILESTONE_1 §7.1): the steps that apply to the viewer's platform role and to the modules the tenant's licence serves, in the checklist's order.

Every step is done by recorded state — an enabled account, a budget, a request of the viewer's own — and never because a page was opened or a box was ticked. So the checklist cannot be talked into being complete, and it cannot fall behind what the tenant has done: a budget a colleague set ticks the step for everyone it is for.

The Dashboard carries the same steps while one of them is not done and the viewer has not dismissed the checklist (firstRun). This read answers either way, so the checklist can be shown again.

Any signed-in role may call this; what comes back differs.

Responses

StatusBodyMeaning
200FirstRunThe viewer's checklist.
401ErrorResponseunauthorized — no credential, or one that is not valid.
500ErrorResponseinternal_error. The message is generic by design: internal error text never reaches a response body. The real error is logged with the request ID.
resources (17)

resources

Inventory and its render metadata.

GET /estate/change-alerts

credential sessionCookiemodule estateoperation listChangeAlerts

The last day's change alerts

The change alerts of the last 24 hours, newest first, at most 100: each with its subscription's condition and team, the resource, and the diff entries it fired on, and the declared app when the subscription names one. A throttled one is the alert that said the subscription reached its hourly limit. An administrator reads every one; a member those their teams' subscriptions raised and those on their teams' accounts — the Alerts page's own reach.

Responses

StatusBodyMeaning
200ChangeAlertListThe alerts.
401ErrorResponseunauthorized — no credential, or one that is not valid.
403ErrorResponseforbidden — authenticated, but the user's role does not permit this. Distinct from module_not_entitled, which is about the tenant's subscription rather than the user. On an unsafe request, also origin_not_trusted: the browser boundary refused the request's origin before anything else ran.
500ErrorResponseinternal_error. The message is generic by design: internal error text never reaches a response body. The real error is logged with the request ID.

GET /estate/change-subscriptions

credential sessionCookiemodule estateoperation listChangeSubscriptions

The tenant's live change subscriptions

Every live subscription, newest first. Any member reads them; writing is a lead's for their team or an administrator's.

Responses

StatusBodyMeaning
200ChangeSubscriptionListThe subscriptions.
401ErrorResponseunauthorized — no credential, or one that is not valid.
403ErrorResponseforbidden — authenticated, but the user's role does not permit this. Distinct from module_not_entitled, which is about the tenant's subscription rather than the user. On an unsafe request, also origin_not_trusted: the browser boundary refused the request's origin before anything else ran.
500ErrorResponseinternal_error. The message is generic by design: internal error text never reaches a response body. The real error is logged with the request ID.

POST /estate/change-subscriptions

credential sessionCookiemodule estaterole engineerbody at most 8192 bytesoperation createChangeSubscription

Subscribe to a configuration change

One predicate over the canonical configuration diff: a path, an operator and, for equals, a literal. A details path must be a field of every named resource type's descriptor. An administrator writes for any team or the whole tenant; a lead for a team they lead, over that team's accounts. A team's subscription covers the team's accounts and the resources whose attribution tag names the team, as the ownership resolver's tag tier reads it; accountIds narrows either. A match is delivered as estate.config_change to the chosen channelIds — Slack or webhook channels; a lead's its team's own or the tenant's default ones — or, with none chosen or none left enabled, to the team's channels (the tenant's when none), always — Triage does not route it — and past hourlyLimit in an hour the channel is told once and the rest of the hour is not sent. Audited as estate.change_subscription.create.

Request body application/json

FieldTypeDescription
accountIdsarray of string
appIdstring (uuid)One live declared app (GET /apps/declared): the subscription then covers the app's members alone — its tag matches and included resources, less the excluded — and its team is the app's (absent teamId takes it; another is a 400). A lead subscribes to their own team's apps.
channelIdsarray of string (uuid)Where its alerts go: Slack or webhook channels of the tenant — a lead's, its team's own or the tenant's default ones. Absent or empty is the team's routing.
hourlyLimitinteger
predicaterequiredChangeSubscriptionPredicate
resourceTypesarray of ChangeSubscriptionTypeAbsent or empty is every type; a details path needs at least one.
teamIdstring (uuid)Absent is the whole tenant, an administrator's alone.

Responses

StatusBodyMeaning
201ChangeSubscriptionThe subscription.
400ErrorResponseinvalid_request — a malformed parameter or body. The batch ingest endpoint uses invalid_batch instead, because a collector branches on it differently: a bad batch is a bug in the collector, while a bad request is usually a bug in the caller's query string.
401ErrorResponseunauthorized — no credential, or one that is not valid.
403ErrorResponseforbidden — authenticated, but the user's role does not permit this. Distinct from module_not_entitled, which is about the tenant's subscription rather than the user. On an unsafe request, also origin_not_trusted: the browser boundary refused the request's origin before anything else ran.
404ErrorResponsenot_found
500ErrorResponseinternal_error. The message is generic by design: internal error text never reaches a response body. The real error is logged with the request ID.

POST /estate/change-subscriptions/preview

credential sessionCookiemodule estaterole engineerbody at most 8192 bytesoperation previewChangeSubscription

Test a draft subscription against the last day

Replays the last 24 hours of stored configuration diffs through the draft, admitted by exactly the rules a create is, and writes nothing. Says how many changes matched, how many the hourly limit would have held back, and lists the newest 50. A day of more than 5,000 changes is read from its newest 5,000 and says so (partial).

Request body application/json

FieldTypeDescription
accountIdsarray of string
appIdstring (uuid)One live declared app (GET /apps/declared): the subscription then covers the app's members alone — its tag matches and included resources, less the excluded — and its team is the app's (absent teamId takes it; another is a 400). A lead subscribes to their own team's apps.
channelIdsarray of string (uuid)Where its alerts go: Slack or webhook channels of the tenant — a lead's, its team's own or the tenant's default ones. Absent or empty is the team's routing.
hourlyLimitinteger
predicaterequiredChangeSubscriptionPredicate
resourceTypesarray of ChangeSubscriptionTypeAbsent or empty is every type; a details path needs at least one.
teamIdstring (uuid)Absent is the whole tenant, an administrator's alone.

Responses

StatusBodyMeaning
200ChangeSubscriptionPreviewWhat the draft would have alerted on.
400ErrorResponseinvalid_request — a malformed parameter or body. The batch ingest endpoint uses invalid_batch instead, because a collector branches on it differently: a bad batch is a bug in the collector, while a bad request is usually a bug in the caller's query string.
401ErrorResponseunauthorized — no credential, or one that is not valid.
403ErrorResponseforbidden — authenticated, but the user's role does not permit this. Distinct from module_not_entitled, which is about the tenant's subscription rather than the user. On an unsafe request, also origin_not_trusted: the browser boundary refused the request's origin before anything else ran.
404ErrorResponsenot_found
500ErrorResponseinternal_error. The message is generic by design: internal error text never reaches a response body. The real error is logged with the request ID.

DELETE /estate/change-subscriptions/{subscriptionId}

credential sessionCookiemodule estaterole engineeroperation deleteChangeSubscription

End a change subscription

The row stays, marked ended; audited. A lead ends their team's; an administrator any. An ended subscription alerts nothing more: an ingest that read it as live before the ending writes no alert, and an alert not yet sent is not sent. The alerts it raised stay on the Alerts page for their day.

Parameters

NameInTypeDescription
subscriptionIdrequiredpathstring (uuid)

Responses

StatusBodyMeaning
204—Ended.
401ErrorResponseunauthorized — no credential, or one that is not valid.
403ErrorResponseforbidden — authenticated, but the user's role does not permit this. Distinct from module_not_entitled, which is about the tenant's subscription rather than the user. On an unsafe request, also origin_not_trusted: the browser boundary refused the request's origin before anything else ran.
404ErrorResponsenot_found
500ErrorResponseinternal_error. The message is generic by design: internal error text never reaches a response body. The real error is logged with the request ID.

GET /estate/impact

credential sessionCookiemodule estateoperation getImpact

The impact of one change — before and after, and the affected neighbours

The investigation of one change: the resource it happened to and its neighbours, each with what changed in a window around the anchor — status transitions, performance deviations after the anchor against before it (the licence's performance capability permitting; the panel says when it does not), and settled cost per day (the Spend module permitting; likewise). A neighbour with a transition or a deviation inside the window is affected, labelled as changed in the window; the method sentence says a change correlated in time is not a proven cause. Every panel says what it lacks, and each limit is stated. The neighbours are read at the anchor's time; when that time is past and the kinds are not retained, the topology as it is now is read instead and topology.currentOnly says so, never silently. The investigation is this URL; nothing is stored.

Parameters

NameInTypeDescription
accountIdrequiredquerystring
regionrequiredquerystring
systemrequiredquerystring
servicerequiredquerystring
resourceTyperequiredquerystring
resourceIdrequiredquerystring
atquerystring (date-time)The anchor's time; the window is either side of it. Absent, the resource's latest status transition is the anchor — what an alert is — and a resource with none is a 400.
windowquerystring enumEither side of the anchor, for status and performance.
costDaysqueryintegerSettled days either side of the anchor's day, for cost.
depthqueryinteger
kindqueryarray of NeighbourEdgeKindThe neighbour kinds to walk; every kind when absent.

Responses

StatusBodyMeaning
200ImpactThe investigation.
400ErrorResponseinvalid_request — a malformed parameter or body. The batch ingest endpoint uses invalid_batch instead, because a collector branches on it differently: a bad batch is a bug in the collector, while a bad request is usually a bug in the caller's query string.
401ErrorResponseunauthorized — no credential, or one that is not valid.
403ErrorResponseforbidden — authenticated, but the user's role does not permit this. Distinct from module_not_entitled, which is about the tenant's subscription rather than the user. On an unsafe request, also origin_not_trusted: the browser boundary refused the request's origin before anything else ran.
404ErrorResponsenot_found
500ErrorResponseinternal_error. The message is generic by design: internal error text never reaches a response body. The real error is logged with the request ID.

GET /estate/investigate

credential sessionCookiemodule estateoperation getInvestigation

The investigator — ranked candidate explanations for a symptom, each citing its evidence

The investigator: for a symptom on a resource at a time — a status transition, an anomaly, a deviation — the ranked list of candidate explanations. Each candidate is a change the feed knows about on the resource itself, on a neighbour or on a declared app the resource belongs to, inside the window before the symptom (and a minute of clock skew after it), ranked by how close in time, how near in the topology, what kind of change and whether a person is recorded behind it; the method sentence says a candidate is a change correlated in time, not a proven cause. Every candidate cites the rows it rests on — the feed row with its evidence, the edge with its confidence — and says what would raise it. Missing data is a row in missing, never a silence: attribution, stored deviations, cost not licensed or not settled, no configuration change in the window, relationships unavailable at that time, neighbours not read. The list is deterministic and model-free; the narrative behind a configured endpoint is a later round and may only select among these candidates by id. The viewer's team scope bounds the feed and the relationships. Nothing is stored.

Parameters

NameInTypeDescription
accountIdrequiredquerystring
regionrequiredquerystring
systemrequiredquerystring
servicerequiredquerystring
resourceTyperequiredquerystring
resourceIdrequiredquerystring
atquerystring (date-time)The symptom's time. Absent, the resource's latest status transition is the symptom — what an alert is — and a resource with none is a 400.
windowquerystring enumHow far before the symptom the candidates are looked for.
depthqueryinteger

Responses

StatusBodyMeaning
200InvestigationThe investigation.
400ErrorResponseinvalid_request — a malformed parameter or body. The batch ingest endpoint uses invalid_batch instead, because a collector branches on it differently: a bad batch is a bug in the collector, while a bad request is usually a bug in the caller's query string.
401ErrorResponseunauthorized — no credential, or one that is not valid.
403ErrorResponseforbidden — authenticated, but the user's role does not permit this. Distinct from module_not_entitled, which is about the tenant's subscription rather than the user. On an unsafe request, also origin_not_trusted: the browser boundary refused the request's origin before anything else ran.
404ErrorResponsenot_found
500ErrorResponseinternal_error. The message is generic by design: internal error text never reaches a response body. The real error is logged with the request ID.

GET /resource-types

credential sessionCookie or apiTokenmodule estateoperation listResourceTypes

Render metadata for every known resource type

How a field is displayed is decided by the backend, next to the collector that produces it — not by the frontend. That makes the render schema part of this contract; otherwise it becomes a second, untyped interface and drifts.

The renderer implements exactly the format values in FieldFormat and renders anything unknown as json, so a backend that adds a field cannot break the UI.

Scoped to the caller's tenant: the types described are the ones the tenant's own accounts can hold, so an estate with no Google project is not told about Google's types.

Cacheable, and the ETag says exactly when: it changes on a deploy that changes the metadata and when this tenant's clouds change — the first account in a cloud it had none in. A client that caches this for a session must drop that cache when it registers or removes an account, or it will keep the answer from before.

Parameters

NameInTypeDescription
If-None-MatchheaderstringA previously returned ETag.

Responses

StatusBodyMeaning
200ResourceTypeListDescriptors for every registered resource type.
304—The client's If-None-Match still matches. No body.
401ErrorResponseunauthorized — no credential, or one that is not valid.
403ErrorResponseforbidden — authenticated, but the user's role does not permit this. Distinct from module_not_entitled, which is about the tenant's subscription rather than the user. On an unsafe request, also origin_not_trusted: the browser boundary refused the request's origin before anything else ran.
429ErrorResponseRate limited.
500ErrorResponseinternal_error. The message is generic by design: internal error text never reaches a response body. The real error is logged with the request ID.

GET /resources

credential sessionCookie or apiTokenmodule estateoperation listResources

List resources

Tombstoned resources are excluded by the shared filter builder, not by each caller. A resource whose bucket has not been collected inside its freshness window — the module's cadence plus the scheduler's jitter and the configured headroom — is included and marked stale, with collectedAt saying when it was last confirmed: not looking lately is not the same as gone. stale=true or stale=false narrows to one or the other.

Parameters

NameInTypeDescription
accountIdqueryarray of stringRepeatable. Any of the given accounts.
servicequeryarray of stringRepeatable.
resourceTypequeryarray of stringRepeatable.
categoryqueryarray of ResourceCategoryRepeatable. Narrows to the resource types in those categories (MILESTONE_1 §3) — one taxonomy, so this filter, the catalog's grouping and the Dashboard's blocks mean the same thing by compute. A category this deployment collects no types for matches nothing, which is the truthful answer rather than "everything".
regionqueryarray of stringRepeatable.
systemqueryarray of string enumThe cloud the resource's account is of — aws, gcp or azure — repeatable. A list that mixes clouds narrows to one; the value is the resource's cloud, the same word the account registry carries: the key's system, or the cloud a type collected under another word runs in — aws takes the EKS nodes and pods keyed k8s.
statusqueryarray of TransitionStatusRepeatable. On /timeline this filters on the destination status, where deleted is meaningful; on resource endpoints it filters live health, where it is not.
stalequerybooleantrue narrows to resources whose bucket has no successful collection inside its freshness window, false to those confirmed lately. Absent, both are returned and each carries stale.
ownershipquerystring enumNarrows to resources the ownership resolver calls owned, unowned or in conflict. Absent, all are returned. It narrows what is shown, never what may be seen: the team scope stays the authority.
ownerTeamIdquerystring (uuid)Narrows to resources the resolver gives to this team.
tagqueryarray of stringRepeatable, key:value, and ANDed rather than ORed — "production things owned by nobody" is the question people ask, and it is two tags. Split on the last colon. AWS allows : in both keys and values, so no separator is unambiguous and the rule has to be stated: this one keeps namespaced keys working (aws:cloudformation:stack-name:my-stack is that key with the value my-stack, and panorama:request_id:9f2c is the provisioning join key) at the cost of a value that itself contains a colon, which cannot be expressed here. Anything without a colon, or with an empty key, is a 400; the value may be empty, because a tag with no value is a tag AWS will happily return. Keys and values match exactly, case included. Inventory stores tags as the provider returned them (see Resource.tags), so folding case here would make the filter disagree with what the response shows.
pagequeryinteger1-based. Normalised server-side rather than rejected: < 1 becomes 1, and a page past the end is clamped to totalPages. Deliberately no minimum: a schema constraint would make the generated server reject page=0 before the handler could normalise it, which contradicts the normalisation this very description promises.
pageSizequeryintegerNormalised server-side, not rejected: < 1 becomes 100, > 500 becomes 500. No minimum or maximum, for the same reason as page — a constraint here would turn a clamp into a 400.

Responses

StatusBodyMeaning
200ResourcePageA page of resources.
400ErrorResponseinvalid_request — a malformed parameter or body. The batch ingest endpoint uses invalid_batch instead, because a collector branches on it differently: a bad batch is a bug in the collector, while a bad request is usually a bug in the caller's query string.
401ErrorResponseunauthorized — no credential, or one that is not valid.
403ErrorResponseforbidden — authenticated, but the user's role does not permit this. Distinct from module_not_entitled, which is about the tenant's subscription rather than the user. On an unsafe request, also origin_not_trusted: the browser boundary refused the request's origin before anything else ran.
429ErrorResponseRate limited.
500ErrorResponseinternal_error. The message is generic by design: internal error text never reaches a response body. The real error is logged with the request ID.

GET /resources/acknowledgements

credential sessionCookiemodule estateoperation listAcknowledgements

The acknowledged alerts

The alerts a member has acknowledged and nobody has reversed, whose resource has not changed status since: the Alerts page's Acknowledged view, newest first, narrowed by the page's filters. A member sees only their teams' accounts'; an administrator every account's (the owner, 2026-09-14).

Parameters

NameInTypeDescription
accountIdqueryarray of stringRepeatable. Any of the given accounts.
servicequeryarray of stringRepeatable.
statusqueryarray of TransitionStatusRepeatable. On /timeline this filters on the destination status, where deleted is meaningful; on resource endpoints it filters live health, where it is not.

Responses

StatusBodyMeaning
200AcknowledgementListThe active acknowledgements.
400ErrorResponseinvalid_request — a malformed parameter or body. The batch ingest endpoint uses invalid_batch instead, because a collector branches on it differently: a bad batch is a bug in the collector, while a bad request is usually a bug in the caller's query string.
401ErrorResponseunauthorized — no credential, or one that is not valid.
403ErrorResponseforbidden — authenticated, but the user's role does not permit this. Distinct from module_not_entitled, which is about the tenant's subscription rather than the user. On an unsafe request, also origin_not_trusted: the browser boundary refused the request's origin before anything else ran.
500ErrorResponseinternal_error. The message is generic by design: internal error text never reaches a response body. The real error is logged with the request ID.

POST /resources/acknowledgements

credential sessionCookiemodule estatebody at most 8192 bytesoperation acknowledgeAlert

Acknowledge an alert

Acknowledges a failed or degraded resource's newest transition, with a required note. The alert leaves the Alerts page until the resource's status changes again, and that transition's notification, if it has not been sent, is held. Any member may acknowledge an alert on one of their teams' accounts, an administrator any. Recorded in the audit log.

Request body application/json

FieldTypeDescription
accountIdrequiredstring
noterequiredstring
regionrequiredstring
resourceIdrequiredstring
resourceTyperequiredstring
servicerequiredstring
systemrequiredstring

Responses

StatusBodyMeaning
201AcknowledgementThe acknowledgement.
400ErrorResponseinvalid_request — a malformed parameter or body. The batch ingest endpoint uses invalid_batch instead, because a collector branches on it differently: a bad batch is a bug in the collector, while a bad request is usually a bug in the caller's query string.
401ErrorResponseunauthorized — no credential, or one that is not valid.
403ErrorResponseforbidden — authenticated, but the user's role does not permit this. Distinct from module_not_entitled, which is about the tenant's subscription rather than the user. On an unsafe request, also origin_not_trusted: the browser boundary refused the request's origin before anything else ran.
404ErrorResponsenot_found
409ErrorResponseThe resource is not failed or degraded, or its alert is acknowledged already.
500ErrorResponseinternal_error. The message is generic by design: internal error text never reaches a response body. The real error is logged with the request ID.

POST /resources/acknowledgements/reverse

credential sessionCookiemodule estatebody at most 4096 bytesoperation reverseAcknowledgement

Reverse an acknowledgement

Reverses the active acknowledgement of a resource's alert: it returns to the Alerts page. The acknowledgement is kept, with who reversed it and when, and the audit log records it. The same team rule as acknowledging.

Request body application/json

FieldTypeDescription
accountIdrequiredstring
regionrequiredstring
resourceIdrequiredstring
resourceTyperequiredstring
servicerequiredstring
systemrequiredstring

Responses

StatusBodyMeaning
204—Reversed.
400ErrorResponseinvalid_request — a malformed parameter or body. The batch ingest endpoint uses invalid_batch instead, because a collector branches on it differently: a bad batch is a bug in the collector, while a bad request is usually a bug in the caller's query string.
401ErrorResponseunauthorized — no credential, or one that is not valid.
403ErrorResponseforbidden — authenticated, but the user's role does not permit this. Distinct from module_not_entitled, which is about the tenant's subscription rather than the user. On an unsafe request, also origin_not_trusted: the browser boundary refused the request's origin before anything else ran.
409ErrorResponseThe alert is not acknowledged.
500ErrorResponseinternal_error. The message is generic by design: internal error text never reaches a response body. The real error is logged with the request ID.

GET /resources/detail

credential sessionCookie or apiTokenmodule estateoperation getResourceDetail

One resource, in full

Identified by its natural key rather than a surrogate id, because that is what the collector knows and what the UI has in hand.

Parameters

NameInTypeDescription
accountIdrequiredquerystring
regionrequiredquerystring
systemrequiredquerystring
servicerequiredquerystring
resourceTyperequiredquerystring
resourceIdrequiredquerystring

Responses

StatusBodyMeaning
200ResourceDetailThe resource, with its recent snapshots and transitions.
400ErrorResponseinvalid_request — a malformed parameter or body. The batch ingest endpoint uses invalid_batch instead, because a collector branches on it differently: a bad batch is a bug in the collector, while a bad request is usually a bug in the caller's query string.
401ErrorResponseunauthorized — no credential, or one that is not valid.
403ErrorResponseforbidden — authenticated, but the user's role does not permit this. Distinct from module_not_entitled, which is about the tenant's subscription rather than the user. On an unsafe request, also origin_not_trusted: the browser boundary refused the request's origin before anything else ran.
404ErrorResponsenot_found
429ErrorResponseRate limited.
500ErrorResponseinternal_error. The message is generic by design: internal error text never reaches a response body. The real error is logged with the request ID.

GET /resources/neighbours

credential sessionCookiemodule estateoperation getResourceNeighbours

A resource's relationships

The resource's neighbours at depth one or two, each edge with its kind, its category (structural or operational), its confidence (a collector observed it, a person declared it, or it was inferred) and the evidence it rests on — read at request time from the inventory, provisioning, the declared apps and the ownership resolver. Unknown edges are absent; lookedFor lists the kinds tried. Depth two is capped: truncated with per-kind counts when a cap is hit. A member sees a neighbour, and passes through an intermediate node, only when their teams reach its account; inadmissible nodes are counted in elided, never rendered.

at asks for the graph as of a time. A past time is not reconstructed in this round: every kind looked for is reported in unavailable with its reason (not retained, or current-only source for app membership, ownership and provenance), and no edge of today is shown for it.

Parameters

NameInTypeDescription
accountIdrequiredquerystring
regionrequiredquerystring
systemrequiredquerystring
servicerequiredquerystring
resourceTyperequiredquerystring
resourceIdrequiredquerystring
depthqueryintegerOne or two; anything else reads as one.
kindqueryarray of NeighbourEdgeKindRepeatable. Only these kinds; omitted, every kind.
atquerystring (date-time)The graph as of this time; omitted, now.

Responses

StatusBodyMeaning
200ResourceNeighboursThe graph around the resource.
400ErrorResponseinvalid_request — a malformed parameter or body. The batch ingest endpoint uses invalid_batch instead, because a collector branches on it differently: a bad batch is a bug in the collector, while a bad request is usually a bug in the caller's query string.
401ErrorResponseunauthorized — no credential, or one that is not valid.
403ErrorResponseforbidden — authenticated, but the user's role does not permit this. Distinct from module_not_entitled, which is about the tenant's subscription rather than the user. On an unsafe request, also origin_not_trusted: the browser boundary refused the request's origin before anything else ran.
404ErrorResponsenot_found
500ErrorResponseinternal_error. The message is generic by design: internal error text never reaches a response body. The real error is logged with the request ID.

GET /resources/search

credential sessionCookie or apiTokenmodule estateoperation searchResources

Search resources

The same filters as /resources, plus free text and sorting, and always with statusCounts — computed over the full filtered set, not the current page, so the UI can say "3 failed" while showing page 2 of 13 — and typeCounts, the types that set holds, which the Type filter offers.

Parameters

NameInTypeDescription
accountIdqueryarray of stringRepeatable. Any of the given accounts.
servicequeryarray of stringRepeatable.
resourceTypequeryarray of stringRepeatable.
categoryqueryarray of ResourceCategoryRepeatable. Narrows to the resource types in those categories (MILESTONE_1 §3) — one taxonomy, so this filter, the catalog's grouping and the Dashboard's blocks mean the same thing by compute. A category this deployment collects no types for matches nothing, which is the truthful answer rather than "everything".
regionqueryarray of stringRepeatable.
systemqueryarray of string enumThe cloud the resource's account is of — aws, gcp or azure — repeatable. A list that mixes clouds narrows to one; the value is the resource's cloud, the same word the account registry carries: the key's system, or the cloud a type collected under another word runs in — aws takes the EKS nodes and pods keyed k8s.
statusqueryarray of TransitionStatusRepeatable. On /timeline this filters on the destination status, where deleted is meaningful; on resource endpoints it filters live health, where it is not.
stalequerybooleantrue narrows to resources whose bucket has no successful collection inside its freshness window, false to those confirmed lately. Absent, both are returned and each carries stale.
ownershipquerystring enumNarrows to resources the ownership resolver calls owned, unowned or in conflict. Absent, all are returned. It narrows what is shown, never what may be seen: the team scope stays the authority.
ownerTeamIdquerystring (uuid)Narrows to resources the resolver gives to this team.
acknowledgedquerybooleanfalse narrows to resources whose newest transition nobody has acknowledged — the Alerts page's default — and true to those a member has, and nobody has reversed. Absent, both are returned. Set, the result is the Alerts page's, and a member's is narrowed to their teams' accounts (the owner, 2026-09-14); the Resources list, which does not set it, stays the tenant's.
tagqueryarray of stringRepeatable, key:value, and ANDed rather than ORed — "production things owned by nobody" is the question people ask, and it is two tags. Split on the last colon. AWS allows : in both keys and values, so no separator is unambiguous and the rule has to be stated: this one keeps namespaced keys working (aws:cloudformation:stack-name:my-stack is that key with the value my-stack, and panorama:request_id:9f2c is the provisioning join key) at the cost of a value that itself contains a colon, which cannot be expressed here. Anything without a colon, or with an empty key, is a 400; the value may be empty, because a tag with no value is a tag AWS will happily return. Keys and values match exactly, case included. Inventory stores tags as the provider returned them (see Resource.tags), so folding case here would make the filter disagree with what the response shows.
queryquerystringFree-text match against resource name, resource ID or account ID, case-insensitively.
sortByquerystring enumA closed set, not a column name. Anything interpolated into SQL goes through an allowlist.
sortDirectionquerystring enum
pagequeryinteger1-based. Normalised server-side rather than rejected: < 1 becomes 1, and a page past the end is clamped to totalPages. Deliberately no minimum: a schema constraint would make the generated server reject page=0 before the handler could normalise it, which contradicts the normalisation this very description promises.
pageSizequeryintegerNormalised server-side, not rejected: < 1 becomes 100, > 500 becomes 500. No minimum or maximum, for the same reason as page — a constraint here would turn a clamp into a 400.
cursorquerystringThe previous page's nextCursor. The page is then the rows after it in the order, read from an index rather than counted past — the same cost on the thousandth page as on the second. Only the default order, by resource name, issues and accepts one; page still says which page number this is — the reader's place in the walk, which a cursor read does not clamp to the count, since the rows after the cursor are what it reads. A cursor that does not decode, or that was issued for the other direction, is a 400.

Responses

StatusBodyMeaning
200ResourceSearchPageA page of matching resources, with counts over the whole match.
400ErrorResponseinvalid_request — a malformed parameter or body. The batch ingest endpoint uses invalid_batch instead, because a collector branches on it differently: a bad batch is a bug in the collector, while a bad request is usually a bug in the caller's query string.
401ErrorResponseunauthorized — no credential, or one that is not valid.
403ErrorResponseforbidden — authenticated, but the user's role does not permit this. Distinct from module_not_entitled, which is about the tenant's subscription rather than the user. On an unsafe request, also origin_not_trusted: the browser boundary refused the request's origin before anything else ran.
429ErrorResponseRate limited.
500ErrorResponseinternal_error. The message is generic by design: internal error text never reaches a response body. The real error is logged with the request ID.

GET /resources/snapshots

credential sessionCookie or apiTokenmodule estateoperation listResourceSnapshots

A resource's observations, newest first

Every retained observation of one resource, paged by a cursor, so the detail page can offer any two to diff. Identified by the natural key, as /resources/detail is. A tombstoned or stale resource is not found here either.

Parameters

NameInTypeDescription
accountIdrequiredquerystring
regionrequiredquerystring
systemrequiredquerystring
servicerequiredquerystring
resourceTyperequiredquerystring
resourceIdrequiredquerystring
cursorquerystringThe previous page's nextCursor.
pageSizequeryinteger

Responses

StatusBodyMeaning
200SnapshotPageOne page of observations, newest first.
400ErrorResponseinvalid_request — a malformed parameter or body. The batch ingest endpoint uses invalid_batch instead, because a collector branches on it differently: a bad batch is a bug in the collector, while a bad request is usually a bug in the caller's query string.
401ErrorResponseunauthorized — no credential, or one that is not valid.
403ErrorResponseforbidden — authenticated, but the user's role does not permit this. Distinct from module_not_entitled, which is about the tenant's subscription rather than the user. On an unsafe request, also origin_not_trusted: the browser boundary refused the request's origin before anything else ran.
404ErrorResponsenot_found
429ErrorResponseRate limited.
500ErrorResponseinternal_error. The message is generic by design: internal error text never reaches a response body. The real error is logged with the request ID.

GET /resources/snapshots/diff

credential sessionCookiemodule estateoperation getSnapshotDiff

What changed between two observations

The canonical diff of any two retained observations of one resource, named by the ids /resources/snapshots returns. The two may be given in either order; the response is always older to newer and says when the selection was reversed. An id that is not one of this resource's retained observations is not found.

Parameters

NameInTypeDescription
accountIdrequiredquerystring
regionrequiredquerystring
systemrequiredquerystring
servicerequiredquerystring
resourceTyperequiredquerystring
resourceIdrequiredquerystring
fromqueryinteger (int64)
toqueryinteger (int64)
snapshotIdqueryinteger (int64)The stored diff for that observation, as the ingest wrote it — that observation against the one before it, not recomputed. It is what a row of the change feed cites, and what the resource page's history expands into, so the page shows the same entries the feed counted rather than a fresh comparison that might differ after a descriptor changed. Give either snapshotId or both from and to. Neither, or both, is a 400: the two answer different questions, and guessing which was meant is how a reader ends up looking at a diff they did not ask for. A snapshotId whose stored diff has aged out of retention is a 404, as an unknown observation is.

Responses

StatusBodyMeaning
200SnapshotDiffThe diff, older to newer.
400ErrorResponseinvalid_request — a malformed parameter or body. The batch ingest endpoint uses invalid_batch instead, because a collector branches on it differently: a bad batch is a bug in the collector, while a bad request is usually a bug in the caller's query string.
401ErrorResponseunauthorized — no credential, or one that is not valid.
403ErrorResponseforbidden — authenticated, but the user's role does not permit this. Distinct from module_not_entitled, which is about the tenant's subscription rather than the user. On an unsafe request, also origin_not_trusted: the browser boundary refused the request's origin before anything else ran.
404ErrorResponsenot_found
500ErrorResponseinternal_error. The message is generic by design: internal error text never reaches a response body. The real error is logged with the request ID.
timeline (1)

timeline

Status-transition history.

GET /timeline

credential sessionCookie or apiTokenmodule estateoperation getTimeline

Status transitions

Status is derived once at ingest and stored, so every change is materialised. This reads that history; it never recomputes status.

Parameters

NameInTypeDescription
fromquerystring (date-time)Inclusive lower bound on occurredAt.
toquerystring (date-time)Exclusive upper bound on occurredAt. The window defaults to the last 7 days and may not exceed 90 days; a wider request is a 400.
accountIdqueryarray of stringRepeatable. Any of the given accounts.
servicequeryarray of stringRepeatable.
statusqueryarray of TransitionStatusRepeatable. On /timeline this filters on the destination status, where deleted is meaningful; on resource endpoints it filters live health, where it is not.
cursorquerystringOpaque keyset cursor from the previous page's nextCursor. Omit for the first page. Keyset rather than page numbers, and not merely for speed: this table only grows and is written to continuously, so an offset silently skips or repeats rows whenever a transition is inserted mid-pagination. The cursor encodes (occurredAt, id), which is stable under concurrent inserts. OFFSET 100000 also gets slower forever.
pageSizequeryintegerNormalised server-side, not rejected: < 1 becomes 100, > 500 becomes 500. No minimum or maximum, for the same reason as page — a constraint here would turn a clamp into a 400.

Responses

StatusBodyMeaning
200TimelinePageA page of transitions, newest first.
400ErrorResponseinvalid_request — a malformed parameter or body. The batch ingest endpoint uses invalid_batch instead, because a collector branches on it differently: a bad batch is a bug in the collector, while a bad request is usually a bug in the caller's query string.
401ErrorResponseunauthorized — no credential, or one that is not valid.
403ErrorResponseforbidden — authenticated, but the user's role does not permit this. Distinct from module_not_entitled, which is about the tenant's subscription rather than the user. On an unsafe request, also origin_not_trusted: the browser boundary refused the request's origin before anything else ran.
429ErrorResponseRate limited.
500ErrorResponseinternal_error. The message is generic by design: internal error text never reaches a response body. The real error is logged with the request ID.
changes (2)

changes

The change feed: one timeline of everything the product knows changed, and the markers customers' systems post to it.

GET /changes

credential sessionCookie or apiTokenmodule estateoperation getChanges

The change feed

One feed of everything the product knows changed — configuration diffs, status transitions, acknowledgements, provisioning outcomes, cost anomalies, budget thresholds and the markers customers' systems post — read at request time from each source's own table, never from a second store. Ordered by the event's own time, never the ingest's: a late report lands in its place.

A member sees the events on the accounts their teams reach and on their teams' own scopes; an administrator everything. Each source contributes at most one page of rows to a page; truncated names the kinds that had more, and a narrower window or fewer kinds shows the rest. Pages are keyed by (at, kind, id), so a cursor yields the same page on every read, late arrivals included.

Parameters

NameInTypeDescription
fromquerystring (date-time)Inclusive lower bound on at.
toquerystring (date-time)Exclusive upper bound on at. The window defaults to the last 7 days and may not exceed 90 days; a wider request is a 400. Ninety days are ninety calendar days in any time zone: up to 90 × 24 hours and the 2 hours a clock change can add inside them, so the midnights that open and close ninety local days are a window whichever way the clocks moved between them.
kindqueryarray of ChangeKindRepeatable. Only these kinds; omitted, every kind.
accountIdqueryarray of stringRepeatable. Any of the given accounts.
resourceKeyquerystringOne resource, with exactly one accountId. Sources without a resource drop out.
resourceTypequerystringEvents on resources of this type; sources without a resource drop out.
servicequeryarray of stringRepeatable. Events on resources of these services — the resource key's third field; sources without a resource drop out.
statusqueryarray of TransitionStatusRepeatable. Status changes to these statuses — the destination status, where deleted is meaningful. Only a status change has one, so every other kind drops out. The status view is a query that can yield status changes alone: kind=status_transition, or any status. Its rows carry what the retired Timeline page showed, in detail: the raw states (fromState, toState), how long the previous status held (heldForSeconds, absent on a first observation) and the latest acknowledgement, reversed or not, as a TransitionAcknowledgement (acknowledgement, shown to the viewers /resources/detail shows it to). One resource's history carries the same.
appIdquerystring (uuid)A declared app's resources (by its rules) and the markers naming it; sources without a resource drop out.
teamIdquerystring (uuid)One team's accounts and scopes — narrowed within the viewer's own reach, never widened.
cursorquerystringOpaque cursor from the previous page's nextCursor; omit for the first page.
pageSizequeryintegerNormalised server-side, not rejected: < 1 becomes 100, > 500 becomes 500. No minimum or maximum, for the same reason as page — a constraint here would turn a clamp into a 400.

Responses

StatusBodyMeaning
200ChangeFeedPageA page of the feed, newest first.
400ErrorResponseinvalid_request — a malformed parameter or body. The batch ingest endpoint uses invalid_batch instead, because a collector branches on it differently: a bad batch is a bug in the collector, while a bad request is usually a bug in the caller's query string.
401ErrorResponseunauthorized — no credential, or one that is not valid.
403ErrorResponseforbidden — authenticated, but the user's role does not permit this. Distinct from module_not_entitled, which is about the tenant's subscription rather than the user. On an unsafe request, also origin_not_trusted: the browser boundary refused the request's origin before anything else ran.
429ErrorResponseRate limited.
500ErrorResponseinternal_error. The message is generic by design: internal error text never reaches a response body. The real error is logged with the request ID.

POST /changes/markers

credential bearerMarkermodule estate600 requests a minute per addressoperation postChangeMarker

Post an external marker

A deploy, a release, a runbook step — what a customer's own system says happened, at the time it says. The product reads nothing from that system; it pushes. The marker carries a time, a label, a URL back, a scope (an account, a declared app, resource keys, or none for a tenant-wide marker) and an idempotency key.

Replay safety. The idempotency key is per credential: the same key with the same marker returns 200 with the stored marker and writes nothing; the same key with a different marker is 409.

Reach. A credential bound to accounts or apps may mark only those; a marker outside them, or one naming no account under a credential bound to accounts, is 403.

Request body application/json

FieldTypeDescription
accountIdstringThe account the marker is about; omitted, the marker is the tenant's.
appIdstring (uuid)The declared app the marker is about.
atrequiredstring (date-time)When it happened, by the poster's clock.
idempotencyKeyrequiredstring
labelrequiredstring
resourceKeysarray of stringThe resources it touched, as keys under accountId.
urlstringA link back to the poster's own record.

Responses

StatusBodyMeaning
200ChangeMarkerThe same marker again, as stored the first time.
201ChangeMarkerThe marker, stored.
400ErrorResponseinvalid_request — a malformed parameter or body. The batch ingest endpoint uses invalid_batch instead, because a collector branches on it differently: a bad batch is a bug in the collector, while a bad request is usually a bug in the caller's query string.
401ErrorResponseunauthorized — no credential, or one that is not valid.
403ErrorResponseforbidden — authenticated, but the user's role does not permit this. Distinct from module_not_entitled, which is about the tenant's subscription rather than the user. On an unsafe request, also origin_not_trusted: the browser boundary refused the request's origin before anything else ran.
409ErrorResponseconflict — this idempotency key names a different marker.
413ErrorResponseThe request body exceeded the size the server will read. Declared because it is real: a body is otherwise read until the client stops sending, and one caller could exhaust the process. The cap is above any legitimate request — 500 items at the additionalInfo limit is roughly 160 MB — so reaching it means something is wrong with the sender.
415ErrorResponseunsupported_media_type — the body of a browser-facing route is not labelled application/json (a +json suffix and a charset parameter are fine). Refused before the body is read: a form post, or a "simple" cross-site request carrying JSON as text/plain to dodge the preflight, is not something this API reads. Bearer routes are not held to it.
500ErrorResponseinternal_error. The message is generic by design: internal error text never reaches a response body. The real error is logged with the request ID.
findings (6)

findings

Findings: what needs attention, grouped by the team that owns it, with exceptions that expire.

GET /findings

credential sessionCookiemodule estateoperation listFindings

What needs attention, grouped by owner

Findings are read at request time from each source's own table — open cost anomalies, live resources whose status is unknown, Trusted Advisor's recommendations, spend without a resource, drift both ways and stale buckets (FindingSource) — never from a second store. Grouped by the owning team, teams by name, the unowned group last; within a group ordered by one priority component (sort, default exposure), most urgent first, unknown last. Costs in different currencies are grouped by currency and never compared by amount.

A member sees the findings on the accounts their teams reach and about their teams; an administrator everything, the unowned group included. Findings under a live exception are listed apart, under excepted; one whose material fingerprint changed since it was excepted is back in its group with changedSinceExcepted.

Parameters

NameInTypeDescription
sortquerystring enum

Responses

StatusBodyMeaning
200FindingListThe findings.
400ErrorResponseinvalid_request — a malformed parameter or body. The batch ingest endpoint uses invalid_batch instead, because a collector branches on it differently: a bad batch is a bug in the collector, while a bad request is usually a bug in the caller's query string.
401ErrorResponseunauthorized — no credential, or one that is not valid.
403ErrorResponseforbidden — authenticated, but the user's role does not permit this. Distinct from module_not_entitled, which is about the tenant's subscription rather than the user. On an unsafe request, also origin_not_trusted: the browser boundary refused the request's origin before anything else ran.
500ErrorResponseinternal_error. The message is generic by design: internal error text never reaches a response body. The real error is logged with the request ID.

POST /findings/exceptions

credential sessionCookiemodule estatebody at most 4096 bytesoperation createFindingException

Except a finding until an expiry

Sets a current finding aside, with a reason, until expiresAt — at most the tenant's maxExceptionDays away (GET /findings/settings). An administrator excepts any finding; anyone else only one they see whose owning team they lead (403 otherwise). The exception binds to the finding's occurrence and material fingerprint: a recurrence, or a material change, shows the finding again. Audited as findings.except. 409 exception_exists is a finding already excepted at its fingerprint.

Request body application/json

FieldTypeDescription
expiresAtrequiredstring (date-time)
reasonrequiredstring
scoperequiredstring
sourcerequiredFindingSourceWhere a finding comes from, each read from its own table:
  • cost_anomaly — an open spend anomaly; scope its account or team.
  • unknown_status — a live resource whose status is unknown; scope the resource.
  • trusted_advisor — a recommendation Trusted Advisor flags (warning or error); sourceId is <account>/<check>/<resource>, scope the resource where the check names one the inventory maps, else the account; the figure is AWS's estimate and label the check's name.
  • cost_without_resource — spend in the last 14 days on a resource the inventory looks for and no longer has, or never had; scope the key.
  • drift_missing — a provisioned create the inventory sees fewer of than were stamped; sourceId the provisioned resource, scope its team, label its name.
  • drift_unprovisioned — a live resource of a kind provisioning produces that nobody provisioned; scope the resource.
  • stale_bucket — a bucket nothing has collected inside its window; sourceId <account>/<region>/<system>/<service>/<type>, scope the account, label the bucket.
  • tag_policy — a live resource with spend in the last 14 days whose tags the tenant's tag policy is not satisfied by, as the tag compliance read's violations list it; scope the resource, faults what to fix, the figure its spend. A change to the resource's tags is a new occurrence, which no earlier exception covers.
  • action_unverified — a provisioning action whose recorded verdict is unverified: its window passed and what a complete collection saw of its target was never the state the action reaches; sourceId the request, scope the team that asked (the request's own scope), label the request's name, accountId the target's account. Open until a complete collection registered after the action sees the target in that state, or a newer succeeded action of the same kind on the same target speaks for it; the recorded verdict itself never changes.
  • action_not_yet_observable — the same for a verdict of not_yet_observable: the window passed with no complete collection seeing the target at all.
One of cost_anomaly, unknown_status, trusted_advisor, cost_without_resource, drift_missing, drift_unprovisioned, stale_bucket, tag_policy, action_unverified, action_not_yet_observable.
sourceIdrequiredstring

Responses

StatusBodyMeaning
201FindingExceptionThe exception.
400ErrorResponseinvalid_request — a malformed parameter or body. The batch ingest endpoint uses invalid_batch instead, because a collector branches on it differently: a bad batch is a bug in the collector, while a bad request is usually a bug in the caller's query string.
401ErrorResponseunauthorized — no credential, or one that is not valid.
403ErrorResponseforbidden — authenticated, but the user's role does not permit this. Distinct from module_not_entitled, which is about the tenant's subscription rather than the user. On an unsafe request, also origin_not_trusted: the browser boundary refused the request's origin before anything else ran.
404ErrorResponsenot_found
409ErrorResponseexception_exists — the finding already has a live exception.
413ErrorResponseThe request body exceeded the size the server will read. Declared because it is real: a body is otherwise read until the client stops sending, and one caller could exhaust the process. The cap is above any legitimate request — 500 items at the additionalInfo limit is roughly 160 MB — so reaching it means something is wrong with the sender.
415ErrorResponseunsupported_media_type — the body of a browser-facing route is not labelled application/json (a +json suffix and a charset parameter are fine). Refused before the body is read: a form post, or a "simple" cross-site request carrying JSON as text/plain to dodge the preflight, is not something this API reads. Bearer routes are not held to it.
500ErrorResponseinternal_error. The message is generic by design: internal error text never reaches a response body. The real error is logged with the request ID.

DELETE /findings/exceptions/{exceptionId}

credential sessionCookiemodule estateoperation revokeFindingException

Revoke an exception before its expiry

Its finding shows again; the exception stays as history. Idempotent: revoking a revoked or expired exception changes nothing. An administrator revokes any; anyone else only one on a current finding they could except now. Audited as findings.exception.revoke.

Responses

StatusBodyMeaning
204—Revoked, or already was.
401ErrorResponseunauthorized — no credential, or one that is not valid.
403ErrorResponseforbidden — authenticated, but the user's role does not permit this. Distinct from module_not_entitled, which is about the tenant's subscription rather than the user. On an unsafe request, also origin_not_trusted: the browser boundary refused the request's origin before anything else ran.
404ErrorResponsenot_found
500ErrorResponseinternal_error. The message is generic by design: internal error text never reaches a response body. The real error is logged with the request ID.

GET /findings/resolved

credential sessionCookiemodule estateoperation listResolvedFindings

Findings an action resolved, with its evidence

The findings an action resolved: each one a request asked for from the finding (its origin) whose recorded verdict is verified — a later complete collection saw the target in the state the action reaches. An unverified or not_yet_observable verdict resolves nothing (it is a finding of its own), an action still awaiting its verdict resolves nothing yet, and an action asked for from no finding resolves none. A finding whose source cleared with no such action is not listed: it is gone, as before.

Every field is read from the request and its verdict, which are written once and never changed — never from the source — so a resolution reads the same after the source stops reporting the finding and after the proving snapshot and run age out. stillReported says whether the source still reports the same occurrence (Trusted Advisor refreshes on its own schedule); while it does, the finding is also open in GET /findings, which is the source's.

An administrator sees every resolution. Anyone else sees one whose finding is in their teams' accounts and whose request is their team's, so the request it names is one they may open. Newest verdict first; one per request.

Parameters

NameInTypeDescription
pagequeryinteger1-based. Normalised server-side rather than rejected: < 1 becomes 1, and a page past the end is clamped to totalPages. Deliberately no minimum: a schema constraint would make the generated server reject page=0 before the handler could normalise it, which contradicts the normalisation this very description promises.
pageSizequeryintegerNormalised server-side, not rejected: < 1 becomes 100, > 500 becomes 500. No minimum or maximum, for the same reason as page — a constraint here would turn a clamp into a 400.

Responses

StatusBodyMeaning
200ResolvedFindingPageA page of resolved findings.
401ErrorResponseunauthorized — no credential, or one that is not valid.
403ErrorResponseforbidden — authenticated, but the user's role does not permit this. Distinct from module_not_entitled, which is about the tenant's subscription rather than the user. On an unsafe request, also origin_not_trusted: the browser boundary refused the request's origin before anything else ran.
500ErrorResponseinternal_error. The message is generic by design: internal error text never reaches a response body. The real error is logged with the request ID.

GET /findings/settings

credential sessionCookiemodule estateoperation getFindingSettings

The tenant's findings settings

The maximum number of days an exception may run: 90 until an administrator changes it (isDefault).

Responses

StatusBodyMeaning
200FindingSettingsThe settings.
401ErrorResponseunauthorized — no credential, or one that is not valid.
403ErrorResponseforbidden — authenticated, but the user's role does not permit this. Distinct from module_not_entitled, which is about the tenant's subscription rather than the user. On an unsafe request, also origin_not_trusted: the browser boundary refused the request's origin before anything else ran.
500ErrorResponseinternal_error. The message is generic by design: internal error text never reaches a response body. The real error is logged with the request ID.

PUT /findings/settings

credential sessionCookiemodule estaterole adminbody at most 1024 bytesoperation setFindingSettings

Set the maximum exception expiry

1 to 365 days. Existing exceptions keep the expiry they were given. Audited as findings.settings.update with the settings before and after.

Request body application/json

FieldTypeDescription
maxExceptionDaysrequiredinteger

Responses

StatusBodyMeaning
200FindingSettingsThe settings as they now stand.
400ErrorResponseinvalid_request — a malformed parameter or body. The batch ingest endpoint uses invalid_batch instead, because a collector branches on it differently: a bad batch is a bug in the collector, while a bad request is usually a bug in the caller's query string.
401ErrorResponseunauthorized — no credential, or one that is not valid.
403ErrorResponseforbidden — authenticated, but the user's role does not permit this. Distinct from module_not_entitled, which is about the tenant's subscription rather than the user. On an unsafe request, also origin_not_trusted: the browser boundary refused the request's origin before anything else ran.
413ErrorResponseThe request body exceeded the size the server will read. Declared because it is real: a body is otherwise read until the client stops sending, and one caller could exhaust the process. The cap is above any legitimate request — 500 items at the additionalInfo limit is roughly 160 MB — so reaching it means something is wrong with the sender.
415ErrorResponseunsupported_media_type — the body of a browser-facing route is not labelled application/json (a +json suffix and a charset parameter are fine). Refused before the body is read: a form post, or a "simple" cross-site request carrying JSON as text/plain to dodge the preflight, is not something this API reads. Bearer routes are not held to it.
500ErrorResponseinternal_error. The message is generic by design: internal error text never reaches a response body. The real error is logged with the request ID.
performance (6)

performance

Estate's Performance section: the CloudWatch latency, errors and saturation of the resources the inventory lists, kept 14 days. Needs the licence's performance capability.

GET /performance/hottest

credential sessionCookie or apiTokenmodule estateoperation getPerformanceHottest

The hottest resources, ranked only against the same signal

The resources working hardest now (MILESTONE_1 §12.5), from the points the Performance job stored and never from a live read.

Raw values from different signals share no scale — a queue depth of 500, a CPU at 95 % and forty database connections cannot be put in one order — so a resource is ranked only against the others measured by the same signal. Each type declares its defining signal per kind; the answer is a list of groups, one per category and defining signal, and there is no order across groups. Within a group the order is the signal's highest value over the window for saturation (the maximum kept beside an average included), the 95th percentile of its stored values for latency, and the rate for errors — a count per hour where they are counted, their mean where they already are a rate. A group of errors lists only resources that had any; a group with no leader is left out.

A leader's value is always its raw measurement in the group's unit; pressure, value over capacity, is present only where the signal declares a capacity (capacity), and never invented.

One indexed query per group, and limit at most 50.

Parameters

NameInTypeDescription
windowqueryPerformanceHottestWindowThe last hour, or the last 24 hours (the default), ending at the current step.
categoryqueryPerformanceSectionOne section's groups; all of them when absent.
kindqueryPerformanceKindOne kind's groups; all three when absent.
limitqueryintegerLeaders per group.

Responses

StatusBodyMeaning
200PerformanceHottestThe groups and their leaders.
400ErrorResponseinvalid_request — a malformed parameter or body. The batch ingest endpoint uses invalid_batch instead, because a collector branches on it differently: a bad batch is a bug in the collector, while a bad request is usually a bug in the caller's query string.
401ErrorResponseunauthorized — no credential, or one that is not valid.
403ErrorResponsemodule_not_entitled — the tenant has not bought Estate; or capability_not_entitled — its licence does not carry performance (x-capability).
429ErrorResponseRate limited.
500ErrorResponseinternal_error. The message is generic by design: internal error text never reaches a response body. The real error is logged with the request ID.

GET /performance/pod

credential sessionCookiemodule estate30 requests a minute per addressoperation getPerformancePod

A pod's own series, read from CloudWatch now

A Kubernetes pod's own series and its containers', read from CloudWatch when someone opens it rather than by the hourly job (the owner's decision of 2026-09-14): Container Insights with enhanced observability names each pod and container, and reading every pod every hour would be the customer's largest CloudWatch bill. The answer is kept for the window's step, so a page reloaded within it reads nothing more; a pod has no history here before the first look. Up to eight containers are read; containersCut says how many more there are.

Each read asks CloudWatch for up to 31 billed metrics, so the route has ceilings: reads of one pod and window at once share one CloudWatch read; at most four distinct reads run at once in a process, and past that the answer is 429 rate_limited with a Retry-After, not a wait; and each address may ask 30 times a minute (x-rate-limit). A pod's restarts are its workload's: CloudWatch publishes them only per workload, and the series says so.

Parameters

NameInTypeDescription
accountIdrequiredquerystring
regionrequiredquerystring
resourceIdrequiredquerystringThe pod's inventory id, cluster:namespace:name.
windowrequiredqueryPerformanceLiveWindow1h at a five-minute step, 24h at fifteen minutes, 7d at an hour, ending at the current step. Not 30d: a pod is read from CloudWatch when it is opened, and has no hourly rollup.

Responses

StatusBodyMeaning
200PerformancePodWindowThe pod and its own series over the window.
400ErrorResponseinvalid_request — a malformed parameter or body. The batch ingest endpoint uses invalid_batch instead, because a collector branches on it differently: a bad batch is a bug in the collector, while a bad request is usually a bug in the caller's query string.
401ErrorResponseunauthorized — no credential, or one that is not valid.
403ErrorResponsemodule_not_entitled — the tenant has not bought Estate; or capability_not_entitled — its licence does not carry performance (x-capability).
404ErrorResponsenot_found
429ErrorResponseRate limited.
500ErrorResponseinternal_error. The message is generic by design: internal error text never reaches a response body. The real error is logged with the request ID.
502ErrorResponseupstream_error — CloudWatch did not answer for the pod: the account's reader role refused the read, or the service failed.

GET /performance/resource

credential sessionCookie or apiTokenmodule estateoperation getPerformanceResource

One resource's latency, errors and saturation over a window

The CloudWatch series the Performance job stored for one resource, named by its natural key as /resources/detail names it. A step longer than the stored five minutes combines the points by the signal's own statistic: an average is averaged, a sum summed, a maximum's maximum taken. A kind the type publishes no metric for is listed in absentKinds rather than borrowed from another metric; a type the catalogue does not measure has no series and all three kinds absent. The 30d window is read from the hourly rollup the job keeps beside the points and from nothing else: the points are kept 14 days, the rollup 90 by default.

Parameters

NameInTypeDescription
accountIdrequiredquerystring
regionrequiredquerystring
systemrequiredquerystring
servicerequiredquerystring
resourceTyperequiredquerystring
resourceIdrequiredquerystring
windowrequiredqueryPerformanceWindow1h at a five-minute step, 24h at fifteen minutes, 7d at an hour, 30d at six hours from the hourly rollup, ending at the current step.
comparequerystring enumprevious: each series also carries the window before this one, at the same step.

Responses

StatusBodyMeaning
200PerformanceResourceWindowThe resource and its series over the window.
400ErrorResponseinvalid_request — a malformed parameter or body. The batch ingest endpoint uses invalid_batch instead, because a collector branches on it differently: a bad batch is a bug in the collector, while a bad request is usually a bug in the caller's query string.
401ErrorResponseunauthorized — no credential, or one that is not valid.
403ErrorResponsemodule_not_entitled — the tenant has not bought Estate; or capability_not_entitled — its licence does not carry performance (x-capability).
404ErrorResponsenot_found
429ErrorResponseRate limited.
500ErrorResponseinternal_error. The message is generic by design: internal error text never reaches a response body. The real error is logged with the request ID.

GET /performance/sections/{section}

credential sessionCookiemodule estateoperation getPerformanceSection

A section's resources and their series over a window

Every live resource, across the tenant's accounts, whose type the catalogue places in the section, a page at a time in key order, each with its series as /performance/resource gives them. A section whose types the catalogue has not seeded yet is an empty page, not an error.

Parameters

NameInTypeDescription
sectionrequiredpathPerformanceSection
windowrequiredqueryPerformanceWindow1h at a five-minute step, 24h at fifteen minutes, 7d at an hour, 30d at six hours from the hourly rollup, ending at the current step.
limitqueryintegerResources per page.
comparequerystring enumprevious: each series also carries the window before this one, at the same step.
cursorquerystringThe previous page's nextCursor: the page is the resources after it in key order, over the window the first page described. The cursor carries that window's end, so every page of one listing has the same from and to however long the reader took to ask. A cursor that does not decode, belongs to another window, or names a window this API did not give out is a 400.

Responses

StatusBodyMeaning
200PerformanceSectionPageA page of the section's resources and their series.
400ErrorResponseinvalid_request — a malformed parameter or body. The batch ingest endpoint uses invalid_batch instead, because a collector branches on it differently: a bad batch is a bug in the collector, while a bad request is usually a bug in the caller's query string.
401ErrorResponseunauthorized — no credential, or one that is not valid.
403ErrorResponsemodule_not_entitled — the tenant has not bought Estate; or capability_not_entitled — its licence does not carry performance (x-capability).
500ErrorResponseinternal_error. The message is generic by design: internal error text never reaches a response body. The real error is logged with the request ID.

GET /performance/sparklines

credential sessionCookiemodule estateoperation getPerformanceSparklines

A page of rows' sparklines, in one read

The last 24 hours of each named resource's defining signal, an hour a point, from the stored points (MILESTONE_1 §12.7): what a table's sparkline column draws, for the page on screen in one request — never one a row. A row whose type declares no defining signal of the kind has no item; one that has, and nothing stored, has an item with no point. An hour's point is the signal's own combination of its five-minute values, as a chart's step is.

Parameters

NameInTypeDescription
keyrequiredqueryarray of stringA resource, as accountId/region/system/service/resourceType/resourceId — its detail page's key. Repeatable, at most 100: a page of rows.
kindqueryPerformanceKindThe kind to draw; when absent, each type's first of saturation, latency and errors that it declares a defining signal for.

Responses

StatusBodyMeaning
200PerformanceSparklinesThe rows' sparklines.
400ErrorResponseinvalid_request — a malformed parameter or body. The batch ingest endpoint uses invalid_batch instead, because a collector branches on it differently: a bad batch is a bug in the collector, while a bad request is usually a bug in the caller's query string.
401ErrorResponseunauthorized — no credential, or one that is not valid.
403ErrorResponsemodule_not_entitled — the tenant has not bought Estate; or capability_not_entitled — its licence does not carry performance (x-capability).
500ErrorResponseinternal_error. The message is generic by design: internal error text never reaches a response body. The real error is logged with the request ID.

GET /performance/topology

credential sessionCookiemodule estateoperation getPerformanceTopology

How a resource's parts hang together, for a drawing

An EKS cluster → its managed node groups, then its Karpenter node pools, then the nodes in neither → its nodes → their pods, and the pods on no node the inventory holds apart; a load balancer → its target groups → their targets. Each part carries its status and its load: the newest stored point of its first saturation series since loadSince, from the inventory and what the Performance job stored — no CloudWatch read. Up to 500 nodes and 5,000 pods a cluster and 500 targets a balancer; a part's cut is how many of its children were left out. A resource has a topology when hasTopology says so.

Parameters

NameInTypeDescription
accountIdrequiredquerystring
regionrequiredquerystring
systemrequiredquerystring
servicerequiredquerystring
resourceTyperequiredquerystring
resourceIdrequiredquerystring

Responses

StatusBodyMeaning
200PerformanceTopologyThe resource and its parts.
400ErrorResponseinvalid_request — a malformed parameter or body. The batch ingest endpoint uses invalid_batch instead, because a collector branches on it differently: a bad batch is a bug in the collector, while a bad request is usually a bug in the caller's query string.
401ErrorResponseunauthorized — no credential, or one that is not valid.
403ErrorResponsemodule_not_entitled — the tenant has not bought Estate; or capability_not_entitled — its licence does not carry performance (x-capability).
404ErrorResponseNo live resource by this key, or its type has no topology.
500ErrorResponseinternal_error. The message is generic by design: internal error text never reaches a response body. The real error is logged with the request ID.
accounts (2)

accounts

Connected cloud accounts.

GET /accounts

credential sessionCookie or apiTokenoperation listAccounts

Connected accounts

Every cloud account registered for this tenant, enabled or not, so the UI can show a disabled account and offer to enable it.

Readable by any role — knowing which accounts exist is not privileged, while changing them is, which is why the mutating operations live under /admin. externalId is never included; see Account.

Responses

StatusBodyMeaning
200AccountListEvery account registered for this tenant.
401ErrorResponseunauthorized — no credential, or one that is not valid.
403ErrorResponseforbidden — authenticated, but the user's role does not permit this. Distinct from module_not_entitled, which is about the tenant's subscription rather than the user. On an unsafe request, also origin_not_trusted: the browser boundary refused the request's origin before anything else ran.
429ErrorResponseRate limited.
500ErrorResponseinternal_error. The message is generic by design: internal error text never reaches a response body. The real error is logged with the request ID.

GET /accounts/overview

credential sessionCookie or apiTokenmodule estateoperation getAccountsOverview

Every account, with how it is being read and what it holds

The Estate module's accounts page (the owner, 2026-09-18: "Accounts page in estate should be an overview. Accounts page in admin is management. Two different pages.").

One entry per registered account: the registry row as /accounts gives it, the state of its collection — the worst cell, the stalest cell, and each module's last success — and what the account holds, by status and by region.

Readable by any role, like /accounts itself: knowing which accounts exist and whether they are being read is not privileged. Everything that changes an account stays under /admin.

A disabled account is listed with its counts and no collection state: nothing is scheduled for it, which is not the same as a collector failing.

Responses

StatusBodyMeaning
200AccountOverviewListEvery account registered for this tenant, in registry order.
401ErrorResponseunauthorized — no credential, or one that is not valid.
403ErrorResponseforbidden — authenticated, but the user's role does not permit this. Distinct from module_not_entitled, which is about the tenant's subscription rather than the user. On an unsafe request, also origin_not_trusted: the browser boundary refused the request's origin before anything else ran.
429ErrorResponseRate limited.
500ErrorResponseinternal_error. The message is generic by design: internal error text never reaches a response body. The real error is logged with the request ID.
admin (16)

admin

Operator endpoints. Require the admin role.

POST /admin/accounts

credential sessionCookierole adminoperation registerAccount

Register an AWS account

Registers an AWS account disabled and answers with the onboarding material: the external id the customer's trust policy must demand, the role ARN to create, and the exact command to run in their account.

Disabled, always. Collecting from an account whose role has not been created yet would fail at the first attempt and tell the operator nothing useful; /verify says exactly what is missing, and enabling is a separate, deliberate act — the same rule discovery follows.

The external id is the tenant's, not the account's: every account this tenant registers demands the same one, so the customer deploys one template everywhere. A tenant that has none yet gets one here.

Request body application/json

FieldTypeDescription
accountIdrequiredstringThe twelve-digit AWS account id.
environmentrequiredstring enum One of production, staging, sdlc, shared, unknown.
isManagementbooleanWhether this is the organisation's management account — the one discovery enumerates through. At most one per tenant.
namerequiredstring
regionsarray of stringThe regions to collect in. Empty means every region the account has.
roleNamestringThe name of the reader role in the customer's account. Defaults to the template's own panorama-reader; a customer whose naming standard forbids that says so here, and the onboarding material names what they chose.

Responses

StatusBodyMeaning
201RegisteredAccountThe account, disabled, and what to do next in the customer's account.
400ErrorResponseinvalid_request — a malformed parameter or body. The batch ingest endpoint uses invalid_batch instead, because a collector branches on it differently: a bad batch is a bug in the collector, while a bad request is usually a bug in the caller's query string.
401ErrorResponseunauthorized — no credential, or one that is not valid.
403ErrorResponseforbidden — authenticated, but the user's role does not permit this. Distinct from module_not_entitled, which is about the tenant's subscription rather than the user. On an unsafe request, also origin_not_trusted: the browser boundary refused the request's origin before anything else ran.
409ErrorResponseinvalid_request — that account id is already registered, or this registration claims the management account and another already is.
413ErrorResponseThe request body exceeded the size the server will read. Declared because it is real: a body is otherwise read until the client stops sending, and one caller could exhaust the process. The cap is above any legitimate request — 500 items at the additionalInfo limit is roughly 160 MB — so reaching it means something is wrong with the sender.
415ErrorResponseunsupported_media_type — the body of a browser-facing route is not labelled application/json (a +json suffix and a charset parameter are fine). Refused before the body is read: a form post, or a "simple" cross-site request carrying JSON as text/plain to dodge the preflight, is not something this API reads. Bearer routes are not held to it.
500ErrorResponseinternal_error. The message is generic by design: internal error text never reaches a response body. The real error is logged with the request ID.

POST /admin/accounts/discover

credential sessionCookierole adminoperation discoverAccounts

Enumerate accounts through AWS Organizations

Registers every account it finds with enabled = false. Collecting from an account nobody has approved would be both surprising and expensive, so enabling is always a separate, deliberate act.

Assumes the reader role of the account marked isManagement and calls organizations:ListAccounts through it. With no account so marked this is a 400 that says so — there is nothing to assume.

Responses

StatusBodyMeaning
200DiscoveryResultWhat discovery found, and what changed as a result.
400ErrorResponseinvalid_request — a malformed parameter or body. The batch ingest endpoint uses invalid_batch instead, because a collector branches on it differently: a bad batch is a bug in the collector, while a bad request is usually a bug in the caller's query string.
401ErrorResponseunauthorized — no credential, or one that is not valid.
403ErrorResponseforbidden — authenticated, but the user's role does not permit this. Distinct from module_not_entitled, which is about the tenant's subscription rather than the user. On an unsafe request, also origin_not_trusted: the browser boundary refused the request's origin before anything else ran.
500ErrorResponseinternal_error. The message is generic by design: internal error text never reaches a response body. The real error is logged with the request ID.
502ErrorResponseAWS Organizations could not be reached or refused the call.

POST /admin/accounts/projects

credential sessionCookierole adminoperation registerProject

Register a Google Cloud project

The Google Cloud half of registration. A project is registered disabled and answers with its onboarding material: the workload identity provider the deployment signs in through, the reader service account to grant impersonation on, and the role to bind.

A separate path rather than a field on the AWS one, because the two share no credential model: an AWS account is a role ARN and an external id, a project is workload identity federation and a service account, and a caller that sent the wrong half should be told so by the contract rather than by a runtime error.

Request body application/json

FieldTypeDescription
environmentrequiredstring enum One of production, staging, sdlc, shared, unknown.
namerequiredstring
projectIdrequiredstringThe project id, not its number and not its display name.
serviceAccountstringThe reader service account the deployment impersonates. Defaults to the convention the reader-role documentation uses.

Responses

StatusBodyMeaning
201RegisteredAccountThe project, disabled, and what to do next in Google Cloud.
400ErrorResponseinvalid_request — a malformed parameter or body. The batch ingest endpoint uses invalid_batch instead, because a collector branches on it differently: a bad batch is a bug in the collector, while a bad request is usually a bug in the caller's query string.
401ErrorResponseunauthorized — no credential, or one that is not valid.
403ErrorResponseforbidden — authenticated, but the user's role does not permit this. Distinct from module_not_entitled, which is about the tenant's subscription rather than the user. On an unsafe request, also origin_not_trusted: the browser boundary refused the request's origin before anything else ran.
409ErrorResponseinvalid_request — that project id is already registered.
413ErrorResponseThe request body exceeded the size the server will read. Declared because it is real: a body is otherwise read until the client stops sending, and one caller could exhaust the process. The cap is above any legitimate request — 500 items at the additionalInfo limit is roughly 160 MB — so reaching it means something is wrong with the sender.
415ErrorResponseunsupported_media_type — the body of a browser-facing route is not labelled application/json (a +json suffix and a charset parameter are fine). Refused before the body is read: a form post, or a "simple" cross-site request carrying JSON as text/plain to dodge the preflight, is not something this API reads. Bearer routes are not held to it.
500ErrorResponseinternal_error. The message is generic by design: internal error text never reaches a response body. The real error is logged with the request ID.

POST /admin/accounts/subscriptions

credential sessionCookierole adminoperation registerSubscription

Register an Azure subscription

The Azure half of registration (MILESTONE_1 §14.2). A subscription is registered disabled, by its id, with the directory (tenant) and the app registration this deployment signs in as, and answers with its onboarding material: the issuer and subject the app's federated credential must trust — or, for a deployment off AWS, the certificate it must hold — and the custom role to assign the app at the subscription. No client secret is taken or stored, ever.

Its own path, as a project's is: a subscription shares no credential model with an AWS account or a Google Cloud project.

Request body application/json

FieldTypeDescription
clientIdrequiredstringThe application (client) id of the app registration this deployment signs in as — the one deploy/azure/reader.bicep outputs.
environmentrequiredstring enum One of production, staging, sdlc, shared, unknown.
namerequiredstring
subscriptionIdrequiredstringThe subscription's id, a GUID; held lower-case.
tenantIdrequiredstringThe directory (tenant) the subscription trusts, by its id.

Responses

StatusBodyMeaning
201RegisteredAccountThe subscription, disabled, and what to do next in Azure.
400ErrorResponseinvalid_request — a malformed parameter or body. The batch ingest endpoint uses invalid_batch instead, because a collector branches on it differently: a bad batch is a bug in the collector, while a bad request is usually a bug in the caller's query string.
401ErrorResponseunauthorized — no credential, or one that is not valid.
403ErrorResponseforbidden — authenticated, but the user's role does not permit this. Distinct from module_not_entitled, which is about the tenant's subscription rather than the user. On an unsafe request, also origin_not_trusted: the browser boundary refused the request's origin before anything else ran.
409ErrorResponseinvalid_request — that subscription is already registered.
413ErrorResponseThe request body exceeded the size the server will read. Declared because it is real: a body is otherwise read until the client stops sending, and one caller could exhaust the process. The cap is above any legitimate request — 500 items at the additionalInfo limit is roughly 160 MB — so reaching it means something is wrong with the sender.
415ErrorResponseunsupported_media_type — the body of a browser-facing route is not labelled application/json (a +json suffix and a charset parameter are fine). Refused before the body is read: a form post, or a "simple" cross-site request carrying JSON as text/plain to dodge the preflight, is not something this API reads. Bearer routes are not held to it.
500ErrorResponseinternal_error. The message is generic by design: internal error text never reaches a response body. The real error is logged with the request ID.

GET /admin/accounts/{accountId}

credential sessionCookierole adminoperation getAccount

One account

The admin view of a single account, for the edit form. Identical in shape to an entry from /accounts — externalId is withheld here too, because an admin session is not a reason to hand out half of the confused-deputy defence.

Responses

StatusBodyMeaning
200AccountThe account, without externalId.
401ErrorResponseunauthorized — no credential, or one that is not valid.
403ErrorResponseforbidden — authenticated, but the user's role does not permit this. Distinct from module_not_entitled, which is about the tenant's subscription rather than the user. On an unsafe request, also origin_not_trusted: the browser boundary refused the request's origin before anything else ran.
404ErrorResponsenot_found
500ErrorResponseinternal_error. The message is generic by design: internal error text never reaches a response body. The real error is logged with the request ID.

PUT /admin/accounts/{accountId}

credential sessionCookierole adminoperation updateAccount

Enable, disable, or re-scope an account

A partial update: only the fields present are changed.

Enabling is the operation that costs money and makes API calls into a customer's account, so it is deliberately separate from discovery, and it is where the subscription's account cap is enforced.

Request body application/json

FieldTypeDescription
cloudtrailAttributionbooleanTurn CloudTrail attribution on or off for this account. On asks the platform to call cloudtrail:LookupEvents in the account, which the reader role grants; a stack older than that answers each lookup permission_denied. Turning it on for a Google Cloud project or an Azure subscription is a 400.
enabledboolean
environmentstring enum One of production, staging, sdlc, shared, unknown.
isManagementbooleanMark (or unmark) this account as the organisation's management account. Marking a second one is a 409: there is at most one.
namestring
regionsarray of string
roleArnstringCorrect the reader role's ARN when it differs from the convention discovery assumed (role/panorama-reader). The external id cannot be set here: it is per tenant, generated once by panorama admin add-management-account, and inherited by every account discovery registers.

Responses

StatusBodyMeaning
200AccountThe updated account.
400ErrorResponseinvalid_request — a malformed parameter or body. The batch ingest endpoint uses invalid_batch instead, because a collector branches on it differently: a bad batch is a bug in the collector, while a bad request is usually a bug in the caller's query string.
401ErrorResponseunauthorized — no credential, or one that is not valid.
403ErrorResponseforbidden — authenticated, but the user's role does not permit this. Distinct from module_not_entitled, which is about the tenant's subscription rather than the user. On an unsafe request, also origin_not_trusted: the browser boundary refused the request's origin before anything else ran.
404ErrorResponsenot_found
409ErrorResponseaccount_limit_reached — enabling this account would exceed the tenant's subscription cap. The message names the cap and the current count, because "upgrade" is only actionable if you know by how much. invalid_request with a 409 — marking this account as the management account when another already is; unmark that one first.
413ErrorResponseThe request body exceeded the size the server will read. Declared because it is real: a body is otherwise read until the client stops sending, and one caller could exhaust the process. The cap is above any legitimate request — 500 items at the additionalInfo limit is roughly 160 MB — so reaching it means something is wrong with the sender.
415ErrorResponseunsupported_media_type — the body of a browser-facing route is not labelled application/json (a +json suffix and a charset parameter are fine). Refused before the body is read: a form post, or a "simple" cross-site request carrying JSON as text/plain to dodge the preflight, is not something this API reads. Bearer routes are not held to it.
500ErrorResponseinternal_error. The message is generic by design: internal error text never reaches a response body. The real error is logged with the request ID.

DELETE /admin/accounts/{accountId}

credential sessionCookierole adminoperation removeAccount

Remove an account from the registry

Takes the account out of the registry. Its inventory, its history and its spend stay where they are: a resource that was collected was collected, and deleting the evidence because the account was unregistered would rewrite the past. What stops is collection, and the account's agent tokens are removed with it.

Two kinds of account are kept, and the refusal says which and what to do instead (409): one that provisioning requests name, and one a team holds. Nothing is removed on the way to a refusal: it is decided under a lock on the account, in the transaction that would delete it.

Step-up applies, as it does to the Admin area's other destructive actions. Registering the same account id again is a fresh registration with a fresh trust setup to verify.

Responses

StatusBodyMeaning
204—The account is no longer registered.
401ErrorResponseunauthorized — no credential, or one that is not valid.
403ErrorResponseforbidden — authenticated, but the user's role does not permit this. Distinct from module_not_entitled, which is about the tenant's subscription rather than the user. On an unsafe request, also origin_not_trusted: the browser boundary refused the request's origin before anything else ran.
404ErrorResponsenot_found
409ErrorResponseThe account stays in the registry, exactly as it was. account_has_provisioning_history — provisioning requests name this account. They are the record of what was done in it, and what they made must stay destroyable, so it is never removed. Disable it instead (PUT /admin/accounts/{accountId} with enabled: false): nothing is collected from a disabled account, and its agent is refused new work (403 account_not_managed). This is the answer when a team holds the account too, since taking it off the team would not let it go. account_assigned — a team holds this account, and the message names the team. Unassign it there first (DELETE /org/teams/{teamId}/accounts/{accountId}), then remove it — or disable it instead.
500ErrorResponseinternal_error. The message is generic by design: internal error text never reaches a response body. The real error is logged with the request ID.

POST /admin/accounts/{accountId}/collect-now

credential sessionCookierole adminoperation collectAccountNow

Queue one collection of this account now

Queues one job for every inventory module of the account's cloud — per region for a regional module, once under global for a per-account one — exactly as panorama admin collect-now does. A worker collects them; a module with a job already waiting keeps that one.

For an account an administrator wants read before its next slot: after a registration is verified and enabled, or after fixing a permission the verification named. A disabled account is a 409: nothing is scheduled for it, and queueing work nobody will do is not an answer.

Audited as estate.account.collect, by the administrator, with the jobs and the modules queued, in the transaction that queues them: a collection whose row cannot be written queues nothing and is a 500. A refusal is not audited.

Responses

StatusBodyMeaning
202CollectionQueuedThe jobs are queued.
401ErrorResponseunauthorized — no credential, or one that is not valid.
403ErrorResponseforbidden — authenticated, but the user's role does not permit this. Distinct from module_not_entitled, which is about the tenant's subscription rather than the user. On an unsafe request, also origin_not_trusted: the browser boundary refused the request's origin before anything else ran.
404ErrorResponsenot_found
409ErrorResponseinvalid_request — the account is disabled, or it names no regions for its regional modules, so there is nothing to queue.
500ErrorResponseinternal_error. The message is generic by design: internal error text never reaches a response body. The real error is logged with the request ID.

GET /admin/accounts/{accountId}/onboarding

credential sessionCookierole adminoperation getAccountOnboarding

What to do in the customer's account, including the external id

The guided onboarding an administrator needs to finish a registration, or to repeat one later: the external id, the role ARN, the template and the exact command (AWS), or the provider, the reader service account and the role to bind (Google Cloud).

This is the one read that returns the external id, and it is behind step-up. /accounts and GET /admin/accounts/{accountId} both withhold it deliberately — an admin session is not a reason to hand out half of the confused-deputy defence — but an administrator completing the trust setup has to see it once, and asking them to read it out of the database would be worse. Step-up is what makes "once, on purpose" different from "on every page load".

Every answered read is audited as estate.account.onboarding_read, by the administrator, naming the account and its cloud and never the external id; the row is written in the read's own transaction, and a read whose row cannot be written is a 500 with nothing in it.

Responses

StatusBodyMeaning
200AccountOnboardingThe onboarding material for this account's cloud.
401ErrorResponseunauthorized — no credential, or one that is not valid.
403ErrorResponseforbidden — authenticated, but the user's role does not permit this. Distinct from module_not_entitled, which is about the tenant's subscription rather than the user. On an unsafe request, also origin_not_trusted: the browser boundary refused the request's origin before anything else ran.
404ErrorResponsenot_found
500ErrorResponseinternal_error. The message is generic by design: internal error text never reaches a response body. The real error is logged with the request ID.

POST /admin/accounts/{accountId}/verify

credential sessionCookierole adminoperation verifyAccount

Assume the role and report exactly what is missing

Attempts the cross-account role assumption and the calls a collection needs, then reports precisely which permissions are absent. "Access denied" without saying what to grant turns onboarding into guesswork.

When the account has a Cost & Usage Report configured and the tenant holds the cost module, the report role is checked the same way: its assumption, s3:ListBucket on the report prefix and s3:GetObject on the newest manifest, each named with the exact ARN to grant on — or, with no role entered yet, the check that says which template to deploy.

A Google Cloud project is verified as its reader, and — for a tenant that holds Estate — its regions are discovered once (MILESTONE_1 §13.4): the regions it uses are added to the account, so its first collection is already regional, and regions lists them. A check verification did not run is listed in skipped with the reason, rather than passed in silence.

Parameters

NameInTypeDescription
accountIdrequiredpathstringThe account's id: a 12-digit AWS account id, a Google Cloud project id — six to thirty lower-case letters, digits and hyphens, a letter first — or an Azure subscription id, a lower-case GUID, so no two clouds' ids can be mistaken for each other.

Responses

StatusBodyMeaning
200AccountVerificationThe verification ran. ok reports the outcome — a failed check is not an HTTP error, it is the answer to the question asked.
401ErrorResponseunauthorized — no credential, or one that is not valid.
403ErrorResponseforbidden — authenticated, but the user's role does not permit this. Distinct from module_not_entitled, which is about the tenant's subscription rather than the user. On an unsafe request, also origin_not_trusted: the browser boundary refused the request's origin before anything else ran.
404ErrorResponsenot_found
500ErrorResponseinternal_error. The message is generic by design: internal error text never reaches a response body. The real error is logged with the request ID.

GET /admin/audit

credential sessionCookierole adminoperation listAuditLog

The tenant's audit log

Every change the tenant's audit log records, newest first: who, what, when, and the before and after as recorded — a secret never is. Administrators only, read under the tenant's row-level security.

Parameters

NameInTypeDescription
actorquerystring (uuid)Only this account's actions.
actionquerystringOnly actions that begin with this, as auth. or auth.identity_provider.update.
subjectTypequerystring
subjectIdquerystring
fromquerystring (date-time)Entries at or after this instant.
toquerystring (date-time)Entries before this instant.
limitqueryinteger
cursorquerystringThe previous page's nextCursor; the page is the entries older than its last.

Responses

StatusBodyMeaning
200AuditLogPageA page of the log.
400ErrorResponseinvalid_request — a malformed parameter or body. The batch ingest endpoint uses invalid_batch instead, because a collector branches on it differently: a bad batch is a bug in the collector, while a bad request is usually a bug in the caller's query string.
401ErrorResponseunauthorized — no credential, or one that is not valid.
403ErrorResponseforbidden — authenticated, but the user's role does not permit this. Distinct from module_not_entitled, which is about the tenant's subscription rather than the user. On an unsafe request, also origin_not_trusted: the browser boundary refused the request's origin before anything else ran.
500ErrorResponseinternal_error. The message is generic by design: internal error text never reaches a response body. The real error is logged with the request ID.

GET /admin/cloud-identity

credential sessionCookierole adminoperation getCloudIdentity

How this deployment signs in to each cloud

The deployment's home cloud and how it reaches each of the others (the owner, 2026-10-10). The platform holds one identity, in its home — PLATFORM_HOME_CLOUD, AWS when unset — and reaches every other cloud by federating from it, never with a second credential. Operator configuration, read-only here: Admin → Accounts shows it. Administrators only, because it describes the deployment rather than anything a member reads; it names no account, role or certificate.

Responses

StatusBodyMeaning
200CloudIdentityThe home and one entry per cloud, in the product's order.
401ErrorResponseunauthorized — no credential, or one that is not valid.
403ErrorResponseforbidden — authenticated, but the user's role does not permit this. Distinct from module_not_entitled, which is about the tenant's subscription rather than the user. On an unsafe request, also origin_not_trusted: the browser boundary refused the request's origin before anything else ran.
500ErrorResponseinternal_error. The message is generic by design: internal error text never reaches a response body. The real error is logged with the request ID.

GET /admin/collector-runs

credential sessionCookiemodule estaterole adminoperation listCollectorRuns

Recent runs for one account

Parameters

NameInTypeDescription
accountIdrequiredquerystring
limitqueryinteger

Responses

StatusBodyMeaning
200CollectorRunListThe most recent runs, newest first.
400ErrorResponseinvalid_request — a malformed parameter or body. The batch ingest endpoint uses invalid_batch instead, because a collector branches on it differently: a bad batch is a bug in the collector, while a bad request is usually a bug in the caller's query string.
401ErrorResponseunauthorized — no credential, or one that is not valid.
403ErrorResponseforbidden — authenticated, but the user's role does not permit this. Distinct from module_not_entitled, which is about the tenant's subscription rather than the user. On an unsafe request, also origin_not_trusted: the browser boundary refused the request's origin before anything else ran.
500ErrorResponseinternal_error. The message is generic by design: internal error text never reaches a response body. The real error is logged with the request ID.

GET /admin/collector-runs/matrix

credential sessionCookiemodule estaterole adminoperation getCollectorRunMatrix

Account × module freshness grid

Answers "is the data I am looking at even fresh?". Without it a silently broken collector is invisible, which is the failure mode that makes an inventory worse than none.

Each column says its cloud and its category (MILESTONE_1 §6), so the grid can be drawn one cloud at a time with its columns grouped by what they read.

Responses

StatusBodyMeaning
200CollectorMatrixOne entry per account, region and module, plus a per-account roll-up in which the worst cell wins, and the columns those entries name.
401ErrorResponseunauthorized — no credential, or one that is not valid.
403ErrorResponseforbidden — authenticated, but the user's role does not permit this. Distinct from module_not_entitled, which is about the tenant's subscription rather than the user. On an unsafe request, also origin_not_trusted: the browser boundary refused the request's origin before anything else ran.
500ErrorResponseinternal_error. The message is generic by design: internal error text never reaches a response body. The real error is logged with the request ID.

GET /admin/overview

credential sessionCookierole adminoperation getAdminOverview

The Admin area's overview

One read for the Admin area's landing page: the licence as GET /entitlements answers it, the users, the accounts and their collection health as the collector matrix rolls it up, single sign-on's state, and the latest audit entries.

Responses

StatusBodyMeaning
200AdminOverviewThe overview.
401ErrorResponseunauthorized — no credential, or one that is not valid.
403ErrorResponseforbidden — authenticated, but the user's role does not permit this. Distinct from module_not_entitled, which is about the tenant's subscription rather than the user. On an unsafe request, also origin_not_trusted: the browser boundary refused the request's origin before anything else ran.
500ErrorResponseinternal_error. The message is generic by design: internal error text never reaches a response body. The real error is logged with the request ID.

GET /manual

credential sessionCookierole adminoperation getOperatorManual

The operator's manual

One HTML page for the deployment's administrators: every action only the owner can take, with the commands, what to check and what the session does instead. Rendered from platform/docs/operator-manual.md at build time and embedded in the binary; read by humans only, the response is HTML, not JSON.

Responses

StatusBodyMeaning
200stringThe manual.
401ErrorResponseunauthorized — no credential, or one that is not valid.
403ErrorResponseforbidden — authenticated, but the user's role does not permit this. Distinct from module_not_entitled, which is about the tenant's subscription rather than the user. On an unsafe request, also origin_not_trusted: the browser boundary refused the request's origin before anything else ran.
entitlements (1)

entitlements

What the tenant's subscription unlocks.

GET /entitlements

credential sessionCookieoperation getEntitlements

What this tenant's subscription unlocks

Entitlement is not authorization. Roles say what a user may do; entitlements say what a tenant has bought. They are separate mechanisms with separate error codes, because "upgrade your plan" and "ask your admin" are different problems.

A tenant with no entitlement row has nothing unlocked until a licence is installed. The one exception is a deployment run with ENTITLEMENTS_UNRESTRICTED, the development posture, where a missing row means every module and no cap; state says which.

Responses

StatusBodyMeaning
200EntitlementsThe tenant's modules and account cap.
401ErrorResponseunauthorized — no credential, or one that is not valid.
500ErrorResponseinternal_error. The message is generic by design: internal error text never reaches a response body. The real error is logged with the request ID.
ingest (4)

ingest

Collector-facing. Bearer token, machine only.

POST /collection-runs

credential bearerIngestmodule estateoperation registerCollectionRun

Register a run and the buckets it may reconcile

A run may only reconcile buckets it registered, and only after a complete enumeration. Registering claims the next generation for each bucket, which fences any older run out.

Replay-safe: the runId is the idempotency key. An identical re-registration returns the original result and does not bump generations again — doing so would supersede the run's own registration and it could never write. A different bucket set for the same runId is run_registration_conflict.

Parameters

NameInTypeDescription
X-Collection-Leaseheaderstring (uuid)The claim this request is doing the work of, as returned by the dequeue that took the job. Required with a control-plane worker credential, and forbidden with a per-tenant ingest token. The two credentials answer "which tenant" differently: a per-tenant token names one, so a lease beside it would be a second and contradictory answer; the shared fleet's names none, so without a lease there is nothing this server can act on. Sending one where it does not belong is 400; omitting it where it does is 401. It names a lease and not a tenant, and that is deliberate — no endpoint here takes a tenant identifier. What the server does with it is read a row the caller cannot write. The lease also binds the run: a run records the claim that registered it, and only that claim may write to it afterwards. A worker holding one live lease cannot finalize another run of the same tenant.

Request body application/json

FieldTypeDescription
accountIdrequiredstring
bucketsrequiredarray of CollectionRunBucketMust not be empty. A module that declares no buckets can neither store nor finalize, which is a wiring bug worth failing loudly. Every bucket's region must equal the run's region, and the set must contain no duplicates. Both are 400 invalid_request, and both are rejected rather than tidied up because each is a claim on a generation: a duplicate would bump one bucket's fence twice and supersede the run's own registration, and a bucket in another region would supersede whichever run legitimately owns that region — after which finalize there would tombstone live resources. global is a region like any other here: a global bucket belongs to a run whose own region is global.
modulerequiredstring
regionrequiredstring
runIdrequiredstring (uuid)Generated by the collector before its first attempt, and reused on every retry. This is the idempotency key for registration.

Responses

StatusBodyMeaning
200CollectionRunRegisteredRegistered, or re-registered identically. The response is the same either way, deliberately: a collector cannot tell whether its first attempt was lost, and must not need to.
400ErrorResponseinvalid_request — a malformed parameter or body. The batch ingest endpoint uses invalid_batch instead, because a collector branches on it differently: a bad batch is a bug in the collector, while a bad request is usually a bug in the caller's query string.
401ErrorResponseunauthorized — no credential, or one that is not valid.
403ErrorResponsemodule_not_entitled — this tenant's subscription does not include the module the run collects for, and the fourteen-day grace after expiry has passed (x-entitlement-access: collect).
409ErrorResponserun_registration_conflict — this runId is already registered with a different account, region, module or bucket set.
413ErrorResponseThe request body exceeded the size the server will read. Declared because it is real: a body is otherwise read until the client stops sending, and one caller could exhaust the process. The cap is above any legitimate request — 500 items at the additionalInfo limit is roughly 160 MB — so reaching it means something is wrong with the sender.
500ErrorResponseinternal_error. The message is generic by design: internal error text never reaches a response body. The real error is logged with the request ID.

POST /collection-runs/{runId}/fail

credential bearerIngestmodule estateoperation failCollectionRun

Mark a run failed and close it

Must be callable after the module's own deadline has fired, so it runs on a separate bounded lifecycle context.

This closes the run against a late batch and a late finalize, and records what happened. It does not release a fence — registration always claims the next generation, so a later run supersedes this one whether or not it was marked failed. A run left running stays open to reconciling on an enumeration that never completed, and reports as in-progress, until cleanup reaps it.

Idempotent: failing a terminal run is a no-op that succeeds.

Parameters

NameInTypeDescription
X-Collection-Leaseheaderstring (uuid)The claim this request is doing the work of, as returned by the dequeue that took the job. Required with a control-plane worker credential, and forbidden with a per-tenant ingest token. The two credentials answer "which tenant" differently: a per-tenant token names one, so a lease beside it would be a second and contradictory answer; the shared fleet's names none, so without a lease there is nothing this server can act on. Sending one where it does not belong is 400; omitting it where it does is 401. It names a lease and not a tenant, and that is deliberate — no endpoint here takes a tenant identifier. What the server does with it is read a row the caller cannot write. The lease also binds the run: a run records the claim that registered it, and only that claim may write to it afterwards. A worker holding one live lease cannot finalize another run of the same tenant.
runIdrequiredpathstring (uuid)

Request body application/json

FieldTypeDescription
reasonrequiredstringWhy the run failed, for the admin matrix. Recorded verbatim, so it must not contain credentials or raw provider payloads. Clients truncate to this length before sending. The server truncates rather than rejecting — uniquely among the body constraints here. This is the call that closes a run against a late batch and a late finalize, and refusing it because an error string ran long would leave the run open for the whole stale-run window over something cosmetic.
resourcesAttemptedrequiredintegerResources the enumeration observed before it failed, or 0 if it never got that far. SET on the run, never added to. The bound is the column's: resources_attempted is a PostgreSQL int, and a larger value would fail inside the transaction as a 500 for what is a malformed request. Out of range is 400 invalid_request.

Responses

StatusBodyMeaning
204—Recorded.
400ErrorResponseinvalid_request — a malformed parameter or body. The batch ingest endpoint uses invalid_batch instead, because a collector branches on it differently: a bad batch is a bug in the collector, while a bad request is usually a bug in the caller's query string.
401ErrorResponseunauthorized — no credential, or one that is not valid.
403ErrorResponsemodule_not_entitled — this tenant's subscription does not include the module the run collects for, and the fourteen-day grace after expiry has passed (x-entitlement-access: collect).
404ErrorResponserun_not_found
413ErrorResponseThe request body exceeded the size the server will read. Declared because it is real: a body is otherwise read until the client stops sending, and one caller could exhaust the process. The cap is above any legitimate request — 500 items at the additionalInfo limit is roughly 160 MB — so reaching it means something is wrong with the sender.
500ErrorResponseinternal_error. The message is generic by design: internal error text never reaches a response body. The real error is logged with the request ID.

POST /collection-runs/{runId}/finalize

credential bearerIngestmodule estateoperation finalizeCollectionRun

Reconcile: tombstone what this run did not observe

Only ever called after a complete enumeration. A run that returned an incomplete result stores its partials and never reaches here — it does not know what is missing, and deleting on that basis removes live resources.

Replay-safe: finalizing an already-finalized run returns the stored tombstone count. Finalizing twice must never tombstone twice.

Parameters

NameInTypeDescription
X-Collection-Leaseheaderstring (uuid)The claim this request is doing the work of, as returned by the dequeue that took the job. Required with a control-plane worker credential, and forbidden with a per-tenant ingest token. The two credentials answer "which tenant" differently: a per-tenant token names one, so a lease beside it would be a second and contradictory answer; the shared fleet's names none, so without a lease there is nothing this server can act on. Sending one where it does not belong is 400; omitting it where it does is 401. It names a lease and not a tenant, and that is deliberate — no endpoint here takes a tenant identifier. What the server does with it is read a row the caller cannot write. The lease also binds the run: a run records the claim that registered it, and only that claim may write to it afterwards. A worker holding one live lease cannot finalize another run of the same tenant.
runIdrequiredpathstring (uuid)

Request body application/json

FieldTypeDescription
resourcesAttemptedrequiredintegerResources the enumeration observed. SET on the run, never added to, so a replayed finalize leaves it unchanged. The bound is the column's — a PostgreSQL int. Out of range is 400 invalid_request.

Responses

StatusBodyMeaning
200FinalizeResponseReconciled, or already finalized and reporting the stored result.
400ErrorResponseinvalid_request — a malformed parameter or body. The batch ingest endpoint uses invalid_batch instead, because a collector branches on it differently: a bad batch is a bug in the collector, while a bad request is usually a bug in the caller's query string.
401ErrorResponseunauthorized — no credential, or one that is not valid.
403ErrorResponsemodule_not_entitled — this tenant's subscription does not include the module the run collects for, and the fourteen-day grace after expiry has passed (x-entitlement-access: collect).
404ErrorResponserun_not_found
409ErrorResponserun_not_running (the run failed) or run_superseded (a newer run has claimed one of these buckets, so this one may no longer decide what is missing).
413ErrorResponseThe request body exceeded the size the server will read. Declared because it is real: a body is otherwise read until the client stops sending, and one caller could exhaust the process. The cap is above any legitimate request — 500 items at the additionalInfo limit is roughly 160 MB — so reaching it means something is wrong with the sender.
500ErrorResponseinternal_error. The message is generic by design: internal error text never reaches a response body. The real error is logged with the request ID.

POST /ingest/snapshots/batch

credential bearerIngestmodule estatebody at most 2.01326592e+08 bytesoperation ingestSnapshotBatch

Store a batch of observed resources

Status is derived here, once, and stored as a column — a transition that was never materialised cannot be detected later.

Every write is replayable. The Idempotency-Key header is required and must be generated before the retry loop, not inside it: a lost response is indistinguishable from a lost request, and a key minted per attempt makes a retry look like new data.

Parameters

NameInTypeDescription
X-Collection-Leaseheaderstring (uuid)The claim this request is doing the work of, as returned by the dequeue that took the job. Required with a control-plane worker credential, and forbidden with a per-tenant ingest token. The two credentials answer "which tenant" differently: a per-tenant token names one, so a lease beside it would be a second and contradictory answer; the shared fleet's names none, so without a lease there is nothing this server can act on. Sending one where it does not belong is 400; omitting it where it does is 401. It names a lease and not a tenant, and that is deliberate — no endpoint here takes a tenant identifier. What the server does with it is read a row the caller cannot write. The lease also binds the run: a run records the claim that registered it, and only that claim may write to it afterwards. A worker holding one live lease cannot finalize another run of the same tenant.
Idempotency-KeyrequiredheaderstringStable for the lifetime of one chunk, across every retry of it. Repeating a key with the same body returns the stored response and writes nothing; repeating it with a different body is idempotency_key_reuse. maxLength is enforced by the server, not only declared: a longer key would otherwise reach the database and fail as an index-size error inside the transaction, reporting a malformed request as a 500 — the class a collector retries. Over-length is 400 invalid_request.

Request body application/json

FieldTypeDescription
itemsrequiredarray of SnapshotInputNo duplicate resource keys within a batch: a duplicate makes the upsert order-dependent, which makes transitions non-deterministic.
runIdrequiredstring (uuid)The run these observations belong to. Every item must fall inside a bucket that run registered, or the batch is rejected.

Responses

StatusBodyMeaning
200SnapshotBatchResponseStored, or replayed from the stored result. Identical either way.
400ErrorResponseinvalid_batch, idempotency_key_required, or invalid_request for an Idempotency-Key longer than maxLength. The message names the offending index and field and never echoes the value — an error body must not become a data-exfiltration channel.
401ErrorResponseunauthorized — no credential, or one that is not valid.
403ErrorResponsemodule_not_entitled — this tenant's subscription does not include the module producing these snapshots.
404ErrorResponserun_not_found — no run with this ID in this tenant. Matches POST /collection-runs/{runId}/finalize and /fail, and tasks 23 and 25, which require 404 here.
409ErrorResponseidempotency_key_reuse, run_not_running, run_superseded, run_account_mismatch or run_bucket_mismatch — the last two meaning the batch is for a bucket this run never registered. run_not_found is 404, not here: an unknown run is a missing resource, and the collector's handling differs. A 409 says the run lost a claim it once held; a 404 says it never had one. Never retry a 4xx. A 409 means the run lost its claim; retrying identically fails identically and builds a hot loop.
413ErrorResponseThe request body exceeded the size the server will read. Declared because it is real: a body is otherwise read until the client stops sending, and one caller could exhaust the process. The cap is above any legitimate request — 500 items at the additionalInfo limit is roughly 160 MB — so reaching it means something is wrong with the sender.
500ErrorResponseinternal_error. The message is generic by design: internal error text never reaches a response body. The real error is logged with the request ID.
org (37)

org

The tenant's teams, members, accounts and tag values (PLATFORM.md §4). Platform-level: every module scopes by it, so none of them owns it. Reads for any session; writes require the admin role.

GET /org/api-token-settings

credential sessionCookierole adminoperation getAPITokenSettings

The longest life of a new read token

The organisation's longest read-token life, in days: 90 until an administrator sets it.

Responses

StatusBodyMeaning
200APITokenSettingsThe setting.
401ErrorResponseunauthorized — no credential, or one that is not valid.
403ErrorResponseforbidden — authenticated, but the user's role does not permit this. Distinct from module_not_entitled, which is about the tenant's subscription rather than the user. On an unsafe request, also origin_not_trusted: the browser boundary refused the request's origin before anything else ran.
500ErrorResponseinternal_error. The message is generic by design: internal error text never reaches a response body. The real error is logged with the request ID.

PUT /org/api-token-settings

credential sessionCookierole adminbody at most 1024 bytesoperation setAPITokenSettings

Set the longest life of a new read token

From 1 to 365 days. It bounds the tokens issued from then on, and the life one gets when its person asks for none (90, or this when shorter); a token already issued keeps its expiry. Audited as auth.api_token.settings.update with the setting before and after.

Request body application/json

FieldTypeDescription
maxExpiresInDaysrequiredintegerThe longest life, in days, of a read token issued from now on.

Responses

StatusBodyMeaning
200APITokenSettingsThe setting as it now stands.
400ErrorResponseinvalid_request — a malformed parameter or body. The batch ingest endpoint uses invalid_batch instead, because a collector branches on it differently: a bad batch is a bug in the collector, while a bad request is usually a bug in the caller's query string.
401ErrorResponseunauthorized — no credential, or one that is not valid.
403ErrorResponseforbidden — authenticated, but the user's role does not permit this. Distinct from module_not_entitled, which is about the tenant's subscription rather than the user. On an unsafe request, also origin_not_trusted: the browser boundary refused the request's origin before anything else ran.
413ErrorResponseThe request body exceeded the size the server will read. Declared because it is real: a body is otherwise read until the client stops sending, and one caller could exhaust the process. The cap is above any legitimate request — 500 items at the additionalInfo limit is roughly 160 MB — so reaching it means something is wrong with the sender.
415ErrorResponseunsupported_media_type — the body of a browser-facing route is not labelled application/json (a +json suffix and a charset parameter are fine). Refused before the body is read: a form post, or a "simple" cross-site request carrying JSON as text/plain to dodge the preflight, is not something this API reads. Bearer routes are not held to it.
500ErrorResponseinternal_error. The message is generic by design: internal error text never reaches a response body. The real error is logged with the request ID.

GET /org/marker-credentials

credential sessionCookierole adminoperation listMarkerCredentials

Marker credentials

The credentials customers' deployment systems post change markers with, revoked ones included as the record that they existed. Never the token: only its hash is stored.

Responses

StatusBodyMeaning
200MarkerCredentialListThis tenant's marker credentials, newest first.
401ErrorResponseunauthorized — no credential, or one that is not valid.
403ErrorResponseforbidden — authenticated, but the user's role does not permit this. Distinct from module_not_entitled, which is about the tenant's subscription rather than the user. On an unsafe request, also origin_not_trusted: the browser boundary refused the request's origin before anything else ran.
500ErrorResponseinternal_error. The message is generic by design: internal error text never reaches a response body. The real error is logged with the request ID.

POST /org/marker-credentials

credential sessionCookierole adminoperation issueMarkerCredential

Issue a marker credential

Admin only. The credential is bound at issue to the accounts and apps it may mark; empty lists mean any. The token value is returned once, here, and never again: only sha256(token) is stored.

Request body application/json

FieldTypeDescription
accountIdsarray of string
appIdsarray of string (uuid)
namerequiredstringThe credential's name, as the Teams & access page lists it. Whitespace at either end is removed before it is stored, and what remains must not be empty: a name that is empty or only whitespace is 400 invalid_request, "name: must not be blank".

Responses

StatusBodyMeaning
201MarkerCredentialIssuedThe credential and its token. Store the token now; it cannot be read back.
400ErrorResponseinvalid_request — a malformed parameter or body. The batch ingest endpoint uses invalid_batch instead, because a collector branches on it differently: a bad batch is a bug in the collector, while a bad request is usually a bug in the caller's query string.
401ErrorResponseunauthorized — no credential, or one that is not valid.
403ErrorResponseforbidden — authenticated, but the user's role does not permit this. Distinct from module_not_entitled, which is about the tenant's subscription rather than the user. On an unsafe request, also origin_not_trusted: the browser boundary refused the request's origin before anything else ran.
413ErrorResponseThe request body exceeded the size the server will read. Declared because it is real: a body is otherwise read until the client stops sending, and one caller could exhaust the process. The cap is above any legitimate request — 500 items at the additionalInfo limit is roughly 160 MB — so reaching it means something is wrong with the sender.
415ErrorResponseunsupported_media_type — the body of a browser-facing route is not labelled application/json (a +json suffix and a charset parameter are fine). Refused before the body is read: a form post, or a "simple" cross-site request carrying JSON as text/plain to dodge the preflight, is not something this API reads. Bearer routes are not held to it.
500ErrorResponseinternal_error. The message is generic by design: internal error text never reaches a response body. The real error is logged with the request ID.

DELETE /org/marker-credentials/{credentialId}

credential sessionCookierole adminoperation revokeMarkerCredential

Revoke a marker credential

Idempotent. The row stays as the record; the token stops being honoured at once.

Responses

StatusBodyMeaning
204—Revoked, or already was.
401ErrorResponseunauthorized — no credential, or one that is not valid.
403ErrorResponseforbidden — authenticated, but the user's role does not permit this. Distinct from module_not_entitled, which is about the tenant's subscription rather than the user. On an unsafe request, also origin_not_trusted: the browser boundary refused the request's origin before anything else ran.
404ErrorResponsenot_found
500ErrorResponseinternal_error. The message is generic by design: internal error text never reaches a response body. The real error is logged with the request ID.

GET /org/notification-channels

credential sessionCookieoperation listNotificationChannels

The tenant's notification destinations, redacted

Every channel: its kind, name, team scope, whether it is enabled, the last test delivery, and a redacted view of the target — the host, a hint of the path's end and a fingerprint. The target itself is never read back: a Slack incoming-webhook URL is a bearer secret, and a read that returned it would be a copy in every browser that opened the page. Any session may list; only an administrator writes.

Responses

StatusBodyMeaning
200NotificationChannelListThe channels, by name.
401ErrorResponseunauthorized — no credential, or one that is not valid.
500ErrorResponseinternal_error. The message is generic by design: internal error text never reaches a response body. The real error is logged with the request ID.

POST /org/notification-channels

credential sessionCookierole adminbody at most 8192 bytesoperation createNotificationChannel

Add a notification destination

Creates a channel from a target this deployment will post to. The target is checked before it is stored: https only, no credentials in the URL, a host that resolves, and every address it resolves to public — loopback, link-local (the cloud metadata endpoint), RFC 1918, unique-local, carrier-grade NAT and the reserved ranges are refused, as is a host that resolves to one public and one private address (a rebinding setup). The same rule is applied again at send time, on the address actually dialled, so a name that changes its answer later gains nothing. Redirects are never followed.

A channel with a team routes that team's events; without one it is the tenant default. The same target for the same kind and team is 409 channel_exists. Audited as notify.channel.create with the redacted view, never the target.

Request body application/json

FieldTypeDescription
enabledboolean
kindrequiredNotificationChannelKindslack posts {"text": …} to an incoming webhook; webhook posts the message as JSON with its kind, for a receiver that routes on it; teams posts an Adaptive Card, within Teams' size limit, to a Microsoft Teams Workflows HTTP trigger URL — under the same destination policy as a webhook; pagerduty sends Events API v2 events — trigger on a failed or degraded resource or a budget threshold, acknowledge on an acknowledgement, resolve on recovery — one incident per tenant, kind and resource; its target is the integration's 32-character routing key, and the endpoint is PagerDuty's; email sends each message as text and HTML to its target, a comma-separated list of at most twenty plain addresses, through the operator's SMTP relay — never a relay the tenant names; refused when the deployment has none; ai-reviewer hands every item to the operator's AI reviewer queue as a small claim-check message — its target is the deployment's queue, set by the platform, never given by the tenant. One of slack, webhook, ai-reviewer, teams, pagerduty, email.
namerequiredstring
targetrequiredstringAn https URL this deployment will post to. Never read back.
teamIdstring (uuid)Scope the channel to a team; omit for the tenant default.

Responses

StatusBodyMeaning
201NotificationChannelThe channel, redacted.
400ErrorResponseinvalid_request — a malformed parameter or body. The batch ingest endpoint uses invalid_batch instead, because a collector branches on it differently: a bad batch is a bug in the collector, while a bad request is usually a bug in the caller's query string.
401ErrorResponseunauthorized — no credential, or one that is not valid.
403ErrorResponseforbidden — authenticated, but the user's role does not permit this. Distinct from module_not_entitled, which is about the tenant's subscription rather than the user. On an unsafe request, also origin_not_trusted: the browser boundary refused the request's origin before anything else ran.
404ErrorResponsenot_found
409ErrorResponsechannel_exists — the same target is already a channel of that kind for that team (or the tenant default).
413ErrorResponseThe request body exceeded the size the server will read. Declared because it is real: a body is otherwise read until the client stops sending, and one caller could exhaust the process. The cap is above any legitimate request — 500 items at the additionalInfo limit is roughly 160 MB — so reaching it means something is wrong with the sender.
415ErrorResponseunsupported_media_type — the body of a browser-facing route is not labelled application/json (a +json suffix and a charset parameter are fine). Refused before the body is read: a form post, or a "simple" cross-site request carrying JSON as text/plain to dodge the preflight, is not something this API reads. Bearer routes are not held to it.
500ErrorResponseinternal_error. The message is generic by design: internal error text never reaches a response body. The real error is logged with the request ID.

GET /org/notification-channels/{channelId}

credential sessionCookieoperation getNotificationChannel

One channel, redacted

Responses

StatusBodyMeaning
200NotificationChannelThe channel.
401ErrorResponseunauthorized — no credential, or one that is not valid.
404ErrorResponsenot_found
500ErrorResponseinternal_error. The message is generic by design: internal error text never reaches a response body. The real error is logged with the request ID.

PATCH /org/notification-channels/{channelId}

credential sessionCookierole adminbody at most 8192 bytesoperation updateNotificationChannel

Rename, rescope, enable or disable a channel, or rotate its target

A partial update. A present target is the rotation: it is checked the way a new one is, stored in place of the old, and the last test delivery is cleared with it — a green diagnostic must not outlive the destination it described. enabled: false takes the channel out of routing without losing it. Audited as notify.channel.update, notify.channel.rotate, notify.channel.enable or notify.channel.disable, with the redacted view.

It asks for the password (x-step-up): enabled: false stops the channel's deliveries, as deleting it would. Any other change asks too; it is the same operation.

Request body application/json

FieldTypeDescription
enabledboolean
namestring
targetstringA new target — the rotation. Checked like a new one; the last test is cleared.
teamIdstring (uuid)Move the channel to a team.
tenantDefaultbooleantrue takes the channel out of its team and makes it a tenant default.

Responses

StatusBodyMeaning
200NotificationChannelThe channel as it now stands.
400ErrorResponseinvalid_request — a malformed parameter or body. The batch ingest endpoint uses invalid_batch instead, because a collector branches on it differently: a bad batch is a bug in the collector, while a bad request is usually a bug in the caller's query string.
401ErrorResponseunauthorized — no credential, or one that is not valid.
403ErrorResponseforbidden — authenticated, but the user's role does not permit this. Distinct from module_not_entitled, which is about the tenant's subscription rather than the user. On an unsafe request, also origin_not_trusted: the browser boundary refused the request's origin before anything else ran.
404ErrorResponsenot_found
409ErrorResponsechannel_exists — the same target is already a channel of that kind for that team (or the tenant default).
413ErrorResponseThe request body exceeded the size the server will read. Declared because it is real: a body is otherwise read until the client stops sending, and one caller could exhaust the process. The cap is above any legitimate request — 500 items at the additionalInfo limit is roughly 160 MB — so reaching it means something is wrong with the sender.
415ErrorResponseunsupported_media_type — the body of a browser-facing route is not labelled application/json (a +json suffix and a charset parameter are fine). Refused before the body is read: a form post, or a "simple" cross-site request carrying JSON as text/plain to dodge the preflight, is not something this API reads. Bearer routes are not held to it.
500ErrorResponseinternal_error. The message is generic by design: internal error text never reaches a response body. The real error is logged with the request ID.

DELETE /org/notification-channels/{channelId}

credential sessionCookierole adminoperation deleteNotificationChannel

Remove a channel

Gone, not disabled: the row and its target are deleted. Audited as notify.channel.delete. Prefer enabled: false for a destination that may come back.

Responses

StatusBodyMeaning
204—Removed.
401ErrorResponseunauthorized — no credential, or one that is not valid.
403ErrorResponseforbidden — authenticated, but the user's role does not permit this. Distinct from module_not_entitled, which is about the tenant's subscription rather than the user. On an unsafe request, also origin_not_trusted: the browser boundary refused the request's origin before anything else ran.
404ErrorResponsenot_found
500ErrorResponseinternal_error. The message is generic by design: internal error text never reaches a response body. The real error is logged with the request ID.

POST /org/notification-channels/{channelId}/test

credential sessionCookierole admin30 requests a minute per addressoperation testNotificationChannel

Send a test message and report what happened

Delivers one fixed message through the same sender the outbox uses — the same client, the same destination policy, the same retries — and reports the outcome: delivered, refused (the destination answered with an error, or with a redirect this platform does not follow), unreachable (no answer: DNS, TLS, a timeout) or blocked (the destination policy refused it before a byte was sent). The HTTP status and an error class come with it; the destination's own words, and its URL, never do. The outcome is kept on the channel as its last test. Rate-limited per address: an administrator's diagnostic, not an outbound scanner.

Responses

StatusBodyMeaning
200NotificationChannelTestWhat the test delivery did.
401ErrorResponseunauthorized — no credential, or one that is not valid.
403ErrorResponseforbidden — authenticated, but the user's role does not permit this. Distinct from module_not_entitled, which is about the tenant's subscription rather than the user. On an unsafe request, also origin_not_trusted: the browser boundary refused the request's origin before anything else ran.
404ErrorResponsenot_found
429ErrorResponseRate limited.
500ErrorResponseinternal_error. The message is generic by design: internal error text never reaches a response body. The real error is logged with the request ID.

GET /org/ownership/overrides

credential sessionCookieoperation listOwnershipOverrides

The tenant's ownership overrides

Live first, then revoked, newest first within each; a team narrows the list.

Parameters

NameInTypeDescription
teamIdquerystring (uuid)
includeRevokedqueryboolean

Responses

StatusBodyMeaning
200OwnershipOverrideListThe overrides.
400ErrorResponseinvalid_request — a malformed parameter or body. The batch ingest endpoint uses invalid_batch instead, because a collector branches on it differently: a bad batch is a bug in the collector, while a bad request is usually a bug in the caller's query string.
401ErrorResponseunauthorized — no credential, or one that is not valid.
500ErrorResponseinternal_error. The message is generic by design: internal error text never reaches a response body. The real error is logged with the request ID.

POST /org/ownership/overrides

credential sessionCookierole engineerbody at most 8192 bytesoperation createOwnershipOverride

Set an explicit owner

An administrator anywhere; a lead only within their teams' accounts and tag values, and only in favour of a team they lead. One live override per match; the team must be open. Audited as org.ownership.override.

Request body application/json

FieldTypeDescription
accountIdstring
expiresAtstring (date-time)
kindrequiredstring enum One of resource, tag, account.
reasonrequiredstring
resourceKeystring
tagKeystring
tagValuestring
teamIdrequiredstring (uuid)

Responses

StatusBodyMeaning
200OwnershipOverrideThe override.
400ErrorResponseinvalid_request — a malformed parameter or body. The batch ingest endpoint uses invalid_batch instead, because a collector branches on it differently: a bad batch is a bug in the collector, while a bad request is usually a bug in the caller's query string.
401ErrorResponseunauthorized — no credential, or one that is not valid.
403ErrorResponseforbidden — authenticated, but the user's role does not permit this. Distinct from module_not_entitled, which is about the tenant's subscription rather than the user. On an unsafe request, also origin_not_trusted: the browser boundary refused the request's origin before anything else ran.
404ErrorResponsenot_found
409ErrorResponseThe match already has a live override, or the team is archived.
500ErrorResponseinternal_error. The message is generic by design: internal error text never reaches a response body. The real error is logged with the request ID.

DELETE /org/ownership/overrides/{overrideId}

credential sessionCookierole engineeroperation revokeOwnershipOverride

Revoke an ownership override

The row stays, marked revoked; audited as org.ownership.override_revoked. A lead revokes within the reach they may set.

Parameters

NameInTypeDescription
overrideIdrequiredpathstring (uuid)

Responses

StatusBodyMeaning
200OwnershipOverrideThe override, revoked.
401ErrorResponseunauthorized — no credential, or one that is not valid.
403ErrorResponseforbidden — authenticated, but the user's role does not permit this. Distinct from module_not_entitled, which is about the tenant's subscription rather than the user. On an unsafe request, also origin_not_trusted: the browser boundary refused the request's origin before anything else ran.
404ErrorResponsenot_found
500ErrorResponseinternal_error. The message is generic by design: internal error text never reaches a response body. The real error is logged with the request ID.

GET /org/teams

credential sessionCookieoperation listTeams

The teams, live by default

Live teams by name, with how many members and accounts each has. Archived teams are hidden unless includeArchived is set; they are never offered by a picker.

Parameters

NameInTypeDescription
includeArchivedqueryboolean

Responses

StatusBodyMeaning
200TeamListThe teams.
401ErrorResponseunauthorized — no credential, or one that is not valid.
500ErrorResponseinternal_error. The message is generic by design: internal error text never reaches a response body. The real error is logged with the request ID.

POST /org/teams

credential sessionCookierole adminoperation createTeam

Create a team

Name and slug are each unique per tenant; a collision on either is 409 team_exists. The slug is derived from the name when omitted.

Request body application/json

FieldTypeDescription
namerequiredstring
slugstringDerived from the name when omitted.

Responses

StatusBodyMeaning
201TeamCreated.
400ErrorResponseinvalid_request — a malformed parameter or body. The batch ingest endpoint uses invalid_batch instead, because a collector branches on it differently: a bad batch is a bug in the collector, while a bad request is usually a bug in the caller's query string.
401ErrorResponseunauthorized — no credential, or one that is not valid.
403ErrorResponseforbidden — authenticated, but the user's role does not permit this. Distinct from module_not_entitled, which is about the tenant's subscription rather than the user. On an unsafe request, also origin_not_trusted: the browser boundary refused the request's origin before anything else ran.
409ErrorResponseteam_exists — the name or the slug is already a team's.
413ErrorResponseThe request body exceeded the size the server will read. Declared because it is real: a body is otherwise read until the client stops sending, and one caller could exhaust the process. The cap is above any legitimate request — 500 items at the additionalInfo limit is roughly 160 MB — so reaching it means something is wrong with the sender.
415ErrorResponseunsupported_media_type — the body of a browser-facing route is not labelled application/json (a +json suffix and a charset parameter are fine). Refused before the body is read: a form post, or a "simple" cross-site request carrying JSON as text/plain to dodge the preflight, is not something this API reads. Bearer routes are not held to it.
500ErrorResponseinternal_error. The message is generic by design: internal error text never reaches a response body. The real error is logged with the request ID.

GET /org/teams/{teamId}

credential sessionCookieoperation getTeam

One team, with its members, accounts and tag values

Everything the org settings page shows for one team. Each account carries attributedSpend: what the rollups currently attribute to this team through the account (attribution team_account), by currency — the number that moves to the untagged bucket if the account is unassigned, shown before the confirm.

A team the caller's tenant does not hold is 404, whether it does not exist or belongs to someone else; the two are not distinguished.

Responses

StatusBodyMeaning
200TeamDetailThe team.
401ErrorResponseunauthorized — no credential, or one that is not valid.
404ErrorResponsenot_found
500ErrorResponseinternal_error. The message is generic by design: internal error text never reaches a response body. The real error is logged with the request ID.

PATCH /org/teams/{teamId}

credential sessionCookierole adminoperation updateTeam

Rename or archive a team

A partial update. archived: true closes the team: its memberships, accounts and tag values stay, its history stays readable, and it leaves every picker. archived: false reopens it. Renaming into an existing name is 409 team_exists.

It asks for the password (x-step-up): archiving takes the team out of every picker, as a removal would. A rename is the same operation and asks too, as a change to a user does.

Request body application/json

FieldTypeDescription
archivedboolean
namestring

Responses

StatusBodyMeaning
200TeamThe team as it now stands.
400ErrorResponseinvalid_request — a malformed parameter or body. The batch ingest endpoint uses invalid_batch instead, because a collector branches on it differently: a bad batch is a bug in the collector, while a bad request is usually a bug in the caller's query string.
401ErrorResponseunauthorized — no credential, or one that is not valid.
403ErrorResponseforbidden — authenticated, but the user's role does not permit this. Distinct from module_not_entitled, which is about the tenant's subscription rather than the user. On an unsafe request, also origin_not_trusted: the browser boundary refused the request's origin before anything else ran.
404ErrorResponsenot_found
409ErrorResponseteam_exists — the name or the slug is already a team's.
413ErrorResponseThe request body exceeded the size the server will read. Declared because it is real: a body is otherwise read until the client stops sending, and one caller could exhaust the process. The cap is above any legitimate request — 500 items at the additionalInfo limit is roughly 160 MB — so reaching it means something is wrong with the sender.
415ErrorResponseunsupported_media_type — the body of a browser-facing route is not labelled application/json (a +json suffix and a charset parameter are fine). Refused before the body is read: a form post, or a "simple" cross-site request carrying JSON as text/plain to dodge the preflight, is not something this API reads. Bearer routes are not held to it.
500ErrorResponseinternal_error. The message is generic by design: internal error text never reaches a response body. The real error is logged with the request ID.

PUT /org/teams/{teamId}/accounts/{accountId}

credential sessionCookierole adminoperation assignTeamAccount

Assign an AWS account to the team

An account belongs to at most one team (0101's unique index — the reason cost's account-level fallback is deterministic). An account another team owns is 409 account_assigned, and the message names the team; move it by unassigning there first, so the consequence is seen where it lands. Assigning re-attributes the account's untagged history to this team on the next rebuild, which the response reports as enqueued.

Responses

StatusBodyMeaning
200TeamAccountChangeThe assignment, and what it changes.
401ErrorResponseunauthorized — no credential, or one that is not valid.
403ErrorResponseforbidden — authenticated, but the user's role does not permit this. Distinct from module_not_entitled, which is about the tenant's subscription rather than the user. On an unsafe request, also origin_not_trusted: the browser boundary refused the request's origin before anything else ran.
404ErrorResponsenot_found
409ErrorResponseaccount_assigned — another team owns the account, named in the message. team_archived — the team is closed.
500ErrorResponseinternal_error. The message is generic by design: internal error text never reaches a response body. The real error is logged with the request ID.

DELETE /org/teams/{teamId}/accounts/{accountId}

credential sessionCookierole adminoperation unassignTeamAccount

Unassign an AWS account from the team

The operation with a consequence: the account-level fallback stops applying, and every dollar the rollups attribute to this team through the account moves to the untagged bucket on the next rebuild. 200, not 204, because the response says what that is — spendMoving, by currency, over usageFrom..usageTo.

Responses

StatusBodyMeaning
200TeamAccountChangeWhat moved.
401ErrorResponseunauthorized — no credential, or one that is not valid.
403ErrorResponseforbidden — authenticated, but the user's role does not permit this. Distinct from module_not_entitled, which is about the tenant's subscription rather than the user. On an unsafe request, also origin_not_trusted: the browser boundary refused the request's origin before anything else ran.
404ErrorResponsenot_found
500ErrorResponseinternal_error. The message is generic by design: internal error text never reaches a response body. The real error is logged with the request ID.

PUT /org/teams/{teamId}/directory-group

credential sessionCookierole adminoperation bindTeamDirectoryGroup

Bind the team to a group of the identity provider

The team's membership then follows the provider's groups claim at each sign-in: a person carrying the group joins it, one no longer carrying it leaves it. A binding decides membership and nothing else — the platform role stays whatever it is. One live team per group; an archived team takes no binding.

Request body application/json

FieldTypeDescription
grouprequiredstringThe group exactly as the provider names it in the groups claim — an id or a name, whichever the provider emits.

Responses

StatusBodyMeaning
200TeamThe team, bound.
400ErrorResponseinvalid_request — a malformed parameter or body. The batch ingest endpoint uses invalid_batch instead, because a collector branches on it differently: a bad batch is a bug in the collector, while a bad request is usually a bug in the caller's query string.
401ErrorResponseunauthorized — no credential, or one that is not valid.
403ErrorResponseforbidden — authenticated, but the user's role does not permit this. Distinct from module_not_entitled, which is about the tenant's subscription rather than the user. On an unsafe request, also origin_not_trusted: the browser boundary refused the request's origin before anything else ran.
404ErrorResponsenot_found
409ErrorResponseThat group is bound to another live team of the tenant.
500ErrorResponseinternal_error. The message is generic by design: internal error text never reaches a response body. The real error is logged with the request ID.

DELETE /org/teams/{teamId}/directory-group

credential sessionCookierole adminoperation unbindTeamDirectoryGroup

Unbind the team from its group

The memberships stay as they are and the team is hand-kept again. Idempotent.

Responses

StatusBodyMeaning
200TeamThe team, hand-kept.
401ErrorResponseunauthorized — no credential, or one that is not valid.
403ErrorResponseforbidden — authenticated, but the user's role does not permit this. Distinct from module_not_entitled, which is about the tenant's subscription rather than the user. On an unsafe request, also origin_not_trusted: the browser boundary refused the request's origin before anything else ran.
404ErrorResponsenot_found
500ErrorResponseinternal_error. The message is generic by design: internal error text never reaches a response body. The real error is logged with the request ID.

PUT /org/teams/{teamId}/members/{userId}

credential sessionCookierole adminoperation setTeamMember

Add a user to the team, or set their team role

Idempotent: a user already on the team has their role set. The team role (member / lead) is scope within the tenant and confers no platform privilege — that is the user's platform role, and the two axes are never blended (PLATFORM.md §4). An archived team takes no new members: 409 team_archived.

Request body application/json

FieldTypeDescription
teamRolerequiredTeamRoleA user's role within one team. Deliberately disjoint from the platform roles so the two axes cannot be compared by accident. One of member, lead.

Responses

StatusBodyMeaning
200TeamMemberThe membership.
400ErrorResponseinvalid_request — a malformed parameter or body. The batch ingest endpoint uses invalid_batch instead, because a collector branches on it differently: a bad batch is a bug in the collector, while a bad request is usually a bug in the caller's query string.
401ErrorResponseunauthorized — no credential, or one that is not valid.
403ErrorResponseforbidden — authenticated, but the user's role does not permit this. Distinct from module_not_entitled, which is about the tenant's subscription rather than the user. On an unsafe request, also origin_not_trusted: the browser boundary refused the request's origin before anything else ran.
404ErrorResponsenot_found
409ErrorResponseteam_archived — the team is closed and takes no new assignment.
413ErrorResponseThe request body exceeded the size the server will read. Declared because it is real: a body is otherwise read until the client stops sending, and one caller could exhaust the process. The cap is above any legitimate request — 500 items at the additionalInfo limit is roughly 160 MB — so reaching it means something is wrong with the sender.
415ErrorResponseunsupported_media_type — the body of a browser-facing route is not labelled application/json (a +json suffix and a charset parameter are fine). Refused before the body is read: a form post, or a "simple" cross-site request carrying JSON as text/plain to dodge the preflight, is not something this API reads. Bearer routes are not held to it.
500ErrorResponseinternal_error. The message is generic by design: internal error text never reaches a response body. The real error is logged with the request ID.

DELETE /org/teams/{teamId}/members/{userId}

credential sessionCookierole adminoperation removeTeamMember

Remove a user from the team

Responses

StatusBodyMeaning
204—Removed.
401ErrorResponseunauthorized — no credential, or one that is not valid.
403ErrorResponseforbidden — authenticated, but the user's role does not permit this. Distinct from module_not_entitled, which is about the tenant's subscription rather than the user. On an unsafe request, also origin_not_trusted: the browser boundary refused the request's origin before anything else ran.
404ErrorResponsenot_found
500ErrorResponseinternal_error. The message is generic by design: internal error text never reaches a response body. The real error is logged with the request ID.

POST /org/teams/{teamId}/tag-values

credential sessionCookierole adminoperation addTeamTagValue

Map a tag value onto the team

Matched case-insensitively, and a value maps to at most one team: one another team holds is 409 tag_value_mapped, naming it. Adding a value re-attributes everything tagged with it on the next rebuild.

Request body application/json

FieldTypeDescription
tagValuerequiredstring

Responses

StatusBodyMeaning
200TeamTagValueChangeThe mapping, and whether a rebuild was enqueued.
400ErrorResponseinvalid_request — a malformed parameter or body. The batch ingest endpoint uses invalid_batch instead, because a collector branches on it differently: a bad batch is a bug in the collector, while a bad request is usually a bug in the caller's query string.
401ErrorResponseunauthorized — no credential, or one that is not valid.
403ErrorResponseforbidden — authenticated, but the user's role does not permit this. Distinct from module_not_entitled, which is about the tenant's subscription rather than the user. On an unsafe request, also origin_not_trusted: the browser boundary refused the request's origin before anything else ran.
404ErrorResponsenot_found
409ErrorResponsetag_value_mapped — another team holds the value, named in the message. team_archived — the team is closed.
413ErrorResponseThe request body exceeded the size the server will read. Declared because it is real: a body is otherwise read until the client stops sending, and one caller could exhaust the process. The cap is above any legitimate request — 500 items at the additionalInfo limit is roughly 160 MB — so reaching it means something is wrong with the sender.
415ErrorResponseunsupported_media_type — the body of a browser-facing route is not labelled application/json (a +json suffix and a charset parameter are fine). Refused before the body is read: a form post, or a "simple" cross-site request carrying JSON as text/plain to dodge the preflight, is not something this API reads. Bearer routes are not held to it.
500ErrorResponseinternal_error. The message is generic by design: internal error text never reaches a response body. The real error is logged with the request ID.

DELETE /org/teams/{teamId}/tag-values/{tagValue}

credential sessionCookierole adminoperation removeTeamTagValue

Unmap a tag value from the team

Everything tagged with the value moves to the untagged bucket — or to whichever lower-priority rule then matches — on the next rebuild.

Responses

StatusBodyMeaning
200TeamTagValueChangeThe mapping that was removed, and whether a rebuild was enqueued.
401ErrorResponseunauthorized — no credential, or one that is not valid.
403ErrorResponseforbidden — authenticated, but the user's role does not permit this. Distinct from module_not_entitled, which is about the tenant's subscription rather than the user. On an unsafe request, also origin_not_trusted: the browser boundary refused the request's origin before anything else ran.
404ErrorResponsenot_found
500ErrorResponseinternal_error. The message is generic by design: internal error text never reaches a response body. The real error is logged with the request ID.

GET /org/triage/agreement

credential sessionCookierole adminoperation getTriageAgreement

How often Triage's suggestions matched what people did

Per kind with a record of what people did (status transitions and cost anomalies; budgets and provisioning events have none): the shadow decisions at least a day old that the model answered and no floor decided (judged); those whose suggestion matched what people did (agreed) — none and nobody acknowledged the event or had the reviewer find something, or notify/reviewer and someone did; and those that said none where people acted (missed, the costly error). Counts, never a rate (TRIAGE_SPEC §9, §15).

Responses

StatusBodyMeaning
200TriageAgreementListThe counts, one per kind.
401ErrorResponseunauthorized — no credential, or one that is not valid.
403ErrorResponseforbidden — authenticated, but the user's role does not permit this. Distinct from module_not_entitled, which is about the tenant's subscription rather than the user. On an unsafe request, also origin_not_trusted: the browser boundary refused the request's origin before anything else ran.
500ErrorResponseinternal_error. The message is generic by design: internal error text never reaches a response body. The real error is logged with the request ID.

GET /org/triage/decisions

credential sessionCookieoperation listTriageDecisions

Triage's decisions, newest first

The record of TRIAGE_SPEC §9: what was asked, what answered, what §8's order suggests and what was done. Filtered by kind, or to one resource (resourceKey, which needs accountId).

Under the audience rule: an administrator reads every decision; anyone else reads the decisions whose account one of their teams owns, and none without an account.

Parameters

NameInTypeDescription
kindqueryTriageKind
accountIdquerystring
resourceKeyquerystringThe resource's key within accountId, which it needs.
cursorquerystringThe nextCursor of the previous page.
pageSizequeryintegerNormalised server-side, not rejected: < 1 becomes 100, > 500 becomes 500. No minimum or maximum, for the same reason as page — a constraint here would turn a clamp into a 400.

Responses

StatusBodyMeaning
200TriageDecisionPageA page of decisions.
400ErrorResponseinvalid_request — a malformed parameter or body. The batch ingest endpoint uses invalid_batch instead, because a collector branches on it differently: a bad batch is a bug in the collector, while a bad request is usually a bug in the caller's query string.
401ErrorResponseunauthorized — no credential, or one that is not valid.
500ErrorResponseinternal_error. The message is generic by design: internal error text never reaches a response body. The real error is logged with the request ID.

GET /org/triage/settings

credential sessionCookierole adminoperation listTriageSettings

Triage's settings, one per event kind

The four kinds Triage decides (TRIAGE_SPEC §8): each kind's mode and thresholds, the stored row where an administrator set one and the default where none did — shadow once the deployment configures a provider, off without one. providerConfigured says which default applies.

Responses

StatusBodyMeaning
200TriageSettingListThe settings, one per kind.
401ErrorResponseunauthorized — no credential, or one that is not valid.
403ErrorResponseforbidden — authenticated, but the user's role does not permit this. Distinct from module_not_entitled, which is about the tenant's subscription rather than the user. On an unsafe request, also origin_not_trusted: the browser boundary refused the request's origin before anything else ran.
500ErrorResponseinternal_error. The message is generic by design: internal error text never reaches a response body. The real error is logged with the request ID.

PUT /org/triage/settings/{kind}

credential sessionCookierole adminbody at most 1024 bytesoperation setTriageSetting

Set one kind's mode and thresholds

Replaces the kind's mode. An absent threshold keeps its current value (the default 0.90 for none, 0.70 for reviewer). Audited as triage.settings.update with the settings before and after.

enforce routes the kind's events by Triage's outcome. It is refused with 400 invalid_request on a deployment with no decision model, and until the kind's shadow record holds at least 100 decisions the model answered on the current question version; it pins the model that answered them and the question version (enforcedModel, enforcedQuestion), and a different one drops the kind back to shadow (audited as triage.settings.model_changed, administrators told in their inbox). Any other mode clears the pin.

Request body application/json

FieldTypeDescription
moderequiredTriageMode One of off, shadow, enforce.
noneThresholdnumber (double)
reviewerThresholdnumber (double)

Responses

StatusBodyMeaning
200TriageSettingThe kind's settings as they now stand.
400ErrorResponseinvalid_request — a malformed parameter or body. The batch ingest endpoint uses invalid_batch instead, because a collector branches on it differently: a bad batch is a bug in the collector, while a bad request is usually a bug in the caller's query string.
401ErrorResponseunauthorized — no credential, or one that is not valid.
403ErrorResponseforbidden — authenticated, but the user's role does not permit this. Distinct from module_not_entitled, which is about the tenant's subscription rather than the user. On an unsafe request, also origin_not_trusted: the browser boundary refused the request's origin before anything else ran.
413ErrorResponseThe request body exceeded the size the server will read. Declared because it is real: a body is otherwise read until the client stops sending, and one caller could exhaust the process. The cap is above any legitimate request — 500 items at the additionalInfo limit is roughly 160 MB — so reaching it means something is wrong with the sender.
415ErrorResponseunsupported_media_type — the body of a browser-facing route is not labelled application/json (a +json suffix and a charset parameter are fine). Refused before the body is read: a form post, or a "simple" cross-site request carrying JSON as text/plain to dodge the preflight, is not something this API reads. Bearer routes are not held to it.
500ErrorResponseinternal_error. The message is generic by design: internal error text never reaches a response body. The real error is logged with the request ID.

GET /org/users

credential sessionCookierole adminoperation listOrgUsers

The tenant's users, for the membership picker and the directory

Every user in the tenant with their platform role and the teams they are on. Admin only: a member list is a directory, and a directory is not a viewer's to read.

Deactivated users are left out unless includeDisabled is set: the picker must not offer them, and the directory that manages them must show them.

Parameters

NameInTypeDescription
includeDisabledquerybooleanAlso list users whose access has been deactivated.

Responses

StatusBodyMeaning
200OrgUserListThe users, by email.
401ErrorResponseunauthorized — no credential, or one that is not valid.
403ErrorResponseforbidden — authenticated, but the user's role does not permit this. Distinct from module_not_entitled, which is about the tenant's subscription rather than the user. On an unsafe request, also origin_not_trusted: the browser boundary refused the request's origin before anything else ran.
500ErrorResponseinternal_error. The message is generic by design: internal error text never reaches a response body. The real error is logged with the request ID.

POST /org/users

credential sessionCookierole adminbody at most 8192 bytesoperation createOrgUser

Add a user to the tenant

Creates a user with a platform role and a generated temporary password. The password is in the response once and is never stored in plaintext or logged: hand it to the person, who changes it with POST /auth/password. There is no mail transport in the platform, so an invitation would be a token the administrator has to carry across anyway; a password with the same lifetime is the smaller thing.

The email must be new in the tenant (409 user_exists). The change is audited as org.user.create.

Request body application/json

FieldTypeDescription
emailrequiredstring
rolerequiredstring enum One of viewer, engineer, admin.

Responses

StatusBodyMeaning
201OrgUserCreatedThe user, and their temporary password, shown once.
400ErrorResponseinvalid_request — a malformed parameter or body. The batch ingest endpoint uses invalid_batch instead, because a collector branches on it differently: a bad batch is a bug in the collector, while a bad request is usually a bug in the caller's query string.
401ErrorResponseunauthorized — no credential, or one that is not valid.
403ErrorResponseforbidden — authenticated, but the user's role does not permit this. Distinct from module_not_entitled, which is about the tenant's subscription rather than the user. On an unsafe request, also origin_not_trusted: the browser boundary refused the request's origin before anything else ran.
409ErrorResponseuser_exists — a user with that email is already in the tenant.
413ErrorResponseThe request body exceeded the size the server will read. Declared because it is real: a body is otherwise read until the client stops sending, and one caller could exhaust the process. The cap is above any legitimate request — 500 items at the additionalInfo limit is roughly 160 MB — so reaching it means something is wrong with the sender.
415ErrorResponseunsupported_media_type — the body of a browser-facing route is not labelled application/json (a +json suffix and a charset parameter are fine). Refused before the body is read: a form post, or a "simple" cross-site request carrying JSON as text/plain to dodge the preflight, is not something this API reads. Bearer routes are not held to it.
500ErrorResponseinternal_error. The message is generic by design: internal error text never reaches a response body. The real error is logged with the request ID.

PATCH /org/users/{userId}

credential sessionCookierole adminbody at most 8192 bytesoperation updateOrgUser

Change a user's role, or deactivate and reactivate them

A partial update. role moves the platform role; disabled: true deactivates the account — every session they hold ends there and then, and a login is refused with the same answer as a wrong password — and disabled: false reactivates it. A role change ends the user's sessions too: the next login carries the new role, and no open tab keeps the old one.

The last enabled administrator cannot be demoted or deactivated (409 last_admin); two administrators changing each other at once are serialised on the row, so one of them is refused. A user in another tenant is 404, never a silent success. Audited as org.user.set_role, org.user.disable or org.user.enable.

Request body application/json

FieldTypeDescription
breakGlassbooleanAn administrator who keeps a password when the tenant enforces single sign-on. Only an administrator can be one.
disabledbooleantrue deactivates the account; false reactivates it.
rolestring enum One of viewer, engineer, admin.

Responses

StatusBodyMeaning
200OrgUserThe user as they now stand.
400ErrorResponseinvalid_request — a malformed parameter or body. The batch ingest endpoint uses invalid_batch instead, because a collector branches on it differently: a bad batch is a bug in the collector, while a bad request is usually a bug in the caller's query string.
401ErrorResponseunauthorized — no credential, or one that is not valid.
403ErrorResponseforbidden — authenticated, but the user's role does not permit this. Distinct from module_not_entitled, which is about the tenant's subscription rather than the user. On an unsafe request, also origin_not_trusted: the browser boundary refused the request's origin before anything else ran.
404ErrorResponsenot_found
409ErrorResponselast_admin — the change would leave the tenant with no enabled administrator, and nothing inside the product could repair that.
413ErrorResponseThe request body exceeded the size the server will read. Declared because it is real: a body is otherwise read until the client stops sending, and one caller could exhaust the process. The cap is above any legitimate request — 500 items at the additionalInfo limit is roughly 160 MB — so reaching it means something is wrong with the sender.
415ErrorResponseunsupported_media_type — the body of a browser-facing route is not labelled application/json (a +json suffix and a charset parameter are fine). Refused before the body is read: a form post, or a "simple" cross-site request carrying JSON as text/plain to dodge the preflight, is not something this API reads. Bearer routes are not held to it.
500ErrorResponseinternal_error. The message is generic by design: internal error text never reaches a response body. The real error is logged with the request ID.

GET /org/users/{userId}/api-tokens

credential sessionCookierole adminoperation listOrgUserAPITokens

A person's read tokens

Every read token the person holds, newest first, revoked and expired ones included — the Admin area's view of what reads on their behalf. Never the token. A person of another organisation is not found.

Responses

StatusBodyMeaning
200APITokenListThe person's read tokens.
401ErrorResponseunauthorized — no credential, or one that is not valid.
403ErrorResponseforbidden — authenticated, but the user's role does not permit this. Distinct from module_not_entitled, which is about the tenant's subscription rather than the user. On an unsafe request, also origin_not_trusted: the browser boundary refused the request's origin before anything else ran.
404ErrorResponsenot_found
500ErrorResponseinternal_error. The message is generic by design: internal error text never reaches a response body. The real error is logged with the request ID.

DELETE /org/users/{userId}/api-tokens/{tokenId}

credential sessionCookierole adminoperation revokeOrgUserAPIToken

Revoke a person's read token

Idempotent, as the person's own revocation is, and audited as auth.api_token.revoke with the administrator as the actor.

Responses

StatusBodyMeaning
204—Revoked, or already was.
401ErrorResponseunauthorized — no credential, or one that is not valid.
403ErrorResponseforbidden — authenticated, but the user's role does not permit this. Distinct from module_not_entitled, which is about the tenant's subscription rather than the user. On an unsafe request, also origin_not_trusted: the browser boundary refused the request's origin before anything else ran.
404ErrorResponsenot_found
500ErrorResponseinternal_error. The message is generic by design: internal error text never reaches a response body. The real error is logged with the request ID.

POST /org/users/{userId}/password-resets

credential sessionCookierole adminoperation issuePasswordReset

Issue a single-use password reset for a user

Returns a reset token once. It is valid for 24 hours and for exactly one redemption at POST /auth/password/reset; only its SHA-256 hash is stored, so a lost token is replaced by issuing another. Issuing a new reset does not revoke the user's sessions — redeeming it does. Audited as org.user.reset_password, with the expiry and never the token.

Responses

StatusBodyMeaning
201PasswordResetIssuedThe reset token, shown once, and when it expires.
401ErrorResponseunauthorized — no credential, or one that is not valid.
403ErrorResponseforbidden — authenticated, but the user's role does not permit this. Distinct from module_not_entitled, which is about the tenant's subscription rather than the user. On an unsafe request, also origin_not_trusted: the browser boundary refused the request's origin before anything else ran.
404ErrorResponsenot_found
500ErrorResponseinternal_error. The message is generic by design: internal error text never reaches a response body. The real error is logged with the request ID.

DELETE /org/users/{userId}/sessions

credential sessionCookierole adminoperation revokeOrgUserSessions

End every session a user holds

Every open session of the user ends now; the account, its role and its password are untouched, and the next login works. For a lost laptop, or a session an administrator wants gone without deactivating the person. Audited as org.user.revoke_sessions.

Responses

StatusBodyMeaning
204—The sessions are gone.
401ErrorResponseunauthorized — no credential, or one that is not valid.
403ErrorResponseforbidden — authenticated, but the user's role does not permit this. Distinct from module_not_entitled, which is about the tenant's subscription rather than the user. On an unsafe request, also origin_not_trusted: the browser boundary refused the request's origin before anything else ran.
404ErrorResponsenot_found
500ErrorResponseinternal_error. The message is generic by design: internal error text never reaches a response body. The real error is logged with the request ID.
apps (6)

apps

Applications as tags define them, and as teams declare them (platform/docs/declared-apps.md): a group per value of each attribution tag key, joining the inventory's health, Spend's month-to-date amount and Provisioning's count. Platform-level and read-only; the modules it reads from keep their own scoping.

GET /apps

credential sessionCookie or apiTokenmodule estateoperation listApps

Applications, as the attribution tags define them

One row per application, where an application is one value of one of the tenant's attribution tag keys — the keys Spend already attributes by (PUT /cost/settings), so nothing new is configured. Each row carries the inventory's health counts for the tag value, and — only when the tenant holds the module — Spend's month-to-date amortized amount for it (per currency) and how many of its live resources Provisioning created.

Scoping follows each module's own rule: health is tenant-wide, as the inventory is; spend and the provisioned count cover the accounts a team manages — every team's for an admin, the viewer's own teams' for a member — as Spend and Provisioning do. An account no team manages is outside both, for everyone. A group is present when at least one live resource carries the value. A tenant with no attribution rules has no groups and tagKeys says so.

Responses

StatusBodyMeaning
200AppGroupsThe groups, the keys they came from, and the period spend covers.
401ErrorResponseunauthorized — no credential, or one that is not valid.
403ErrorResponseforbidden — authenticated, but the user's role does not permit this. Distinct from module_not_entitled, which is about the tenant's subscription rather than the user. On an unsafe request, also origin_not_trusted: the browser boundary refused the request's origin before anything else ran.
429ErrorResponseRate limited.
500ErrorResponseinternal_error. The message is generic by design: internal error text never reaches a response body. The real error is logged with the request ID.

GET /apps/declared

credential sessionCookie or apiTokenmodule estateoperation listDeclaredApps

The apps teams have declared

Every live declared app (platform/docs/declared-apps.md): a name a team owns, defined by tag matches plus resources added and excluded, with the same columns as the tag-defined groups — the inventory's health for its live members, month-to-date spend at one grain (see DeclaredAppMoney), and how many of its members Provisioning created — each column only with its module. Membership is evaluated against the live inventory when read; a resource counts once however many matches it hits.

Scoping follows each module's rule, as GET /apps does: health is tenant-wide; spend and the provisioned count cover the accounts the viewer's teams manage. canCreate says whether the viewer may declare an app, ownableTeams which open teams they may give one to, and each app's canEdit whether the viewer may change it. Each answers on the platform role floor and the team rule together, so the portal never guesses — neither about the action nor about the teams it may offer.

Responses

StatusBodyMeaning
200DeclaredAppListThe declared apps, by name, and the period spend covers.
401ErrorResponseunauthorized — no credential, or one that is not valid.
403ErrorResponseforbidden — authenticated, but the user's role does not permit this. Distinct from module_not_entitled, which is about the tenant's subscription rather than the user. On an unsafe request, also origin_not_trusted: the browser boundary refused the request's origin before anything else ran.
429ErrorResponseRate limited.
500ErrorResponseinternal_error. The message is generic by design: internal error text never reaches a response body. The real error is logged with the request ID.

POST /apps/declared

credential sessionCookiemodule estaterole engineerbody at most 65536 bytesoperation createDeclaredApp

Declare an app

Creates an app from its definition. The caller must be a lead of the owning team or an admin (403 forbidden otherwise); the team must exist and be open; the name must be free among live apps (409 app_exists). A definition needs at least one tag match or one added resource; explicit resources are accountId plus a resource key (region/system/service/type/id). Audited as apps.declared.create with the definition.

Request body application/json

FieldTypeDescription
descriptionstring
excludearray of ResourceRefResources kept out although a match hits them.
includearray of ResourceRefResources added by hand.
matchesrequiredarray of TagMatch
namerequiredstring
teamIdrequiredstring (uuid)

Responses

StatusBodyMeaning
201DeclaredAppThe app, with the list's columns.
400ErrorResponseinvalid_request — a malformed parameter or body. The batch ingest endpoint uses invalid_batch instead, because a collector branches on it differently: a bad batch is a bug in the collector, while a bad request is usually a bug in the caller's query string.
401ErrorResponseunauthorized — no credential, or one that is not valid.
403ErrorResponseforbidden — authenticated, but the user's role does not permit this. Distinct from module_not_entitled, which is about the tenant's subscription rather than the user. On an unsafe request, also origin_not_trusted: the browser boundary refused the request's origin before anything else ran.
404ErrorResponsenot_found
409ErrorResponseapp_exists: a live app already has this name; team_archived: the owning team is closed.
413ErrorResponseThe request body exceeded the size the server will read. Declared because it is real: a body is otherwise read until the client stops sending, and one caller could exhaust the process. The cap is above any legitimate request — 500 items at the additionalInfo limit is roughly 160 MB — so reaching it means something is wrong with the sender.
415ErrorResponseunsupported_media_type — the body of a browser-facing route is not labelled application/json (a +json suffix and a charset parameter are fine). Refused before the body is read: a form post, or a "simple" cross-site request carrying JSON as text/plain to dodge the preflight, is not something this API reads. Bearer routes are not held to it.
500ErrorResponseinternal_error. The message is generic by design: internal error text never reaches a response body. The real error is logged with the request ID.

GET /apps/declared/{appId}

credential sessionCookiemodule estateoperation getDeclaredApp

One declared app in full

The app with the list's columns, its live members (each saying whether a tag match or an explicit addition put it there), and — with Spend — a six-month trend, each month resolved to its own grain by the same rule. An archived app is readable and says so.

Responses

StatusBodyMeaning
200DeclaredAppDetailThe app, its members and its trend.
401ErrorResponseunauthorized — no credential, or one that is not valid.
403ErrorResponseforbidden — authenticated, but the user's role does not permit this. Distinct from module_not_entitled, which is about the tenant's subscription rather than the user. On an unsafe request, also origin_not_trusted: the browser boundary refused the request's origin before anything else ran.
404ErrorResponsenot_found
500ErrorResponseinternal_error. The message is generic by design: internal error text never reaches a response body. The real error is logged with the request ID.

PUT /apps/declared/{appId}

credential sessionCookiemodule estaterole engineerbody at most 65536 bytesoperation replaceDeclaredApp

Replace an app's definition

The whole definition, replaced. The caller must be allowed on the app's current team and on the new one, if it moves. An archived app is 409 app_archived. Audited as apps.declared.update with the definition before and after.

Request body application/json

FieldTypeDescription
descriptionstring
excludearray of ResourceRefResources kept out although a match hits them.
includearray of ResourceRefResources added by hand.
matchesrequiredarray of TagMatch
namerequiredstring
teamIdrequiredstring (uuid)

Responses

StatusBodyMeaning
200DeclaredAppThe app as it now is.
400ErrorResponseinvalid_request — a malformed parameter or body. The batch ingest endpoint uses invalid_batch instead, because a collector branches on it differently: a bad batch is a bug in the collector, while a bad request is usually a bug in the caller's query string.
401ErrorResponseunauthorized — no credential, or one that is not valid.
403ErrorResponseforbidden — authenticated, but the user's role does not permit this. Distinct from module_not_entitled, which is about the tenant's subscription rather than the user. On an unsafe request, also origin_not_trusted: the browser boundary refused the request's origin before anything else ran.
404ErrorResponsenot_found
409ErrorResponseapp_exists, app_archived or team_archived.
413ErrorResponseThe request body exceeded the size the server will read. Declared because it is real: a body is otherwise read until the client stops sending, and one caller could exhaust the process. The cap is above any legitimate request — 500 items at the additionalInfo limit is roughly 160 MB — so reaching it means something is wrong with the sender.
415ErrorResponseunsupported_media_type — the body of a browser-facing route is not labelled application/json (a +json suffix and a charset parameter are fine). Refused before the body is read: a form post, or a "simple" cross-site request carrying JSON as text/plain to dodge the preflight, is not something this API reads. Bearer routes are not held to it.
500ErrorResponseinternal_error. The message is generic by design: internal error text never reaches a response body. The real error is logged with the request ID.

POST /apps/declared/{appId}/archive

credential sessionCookiemodule estaterole engineeroperation archiveDeclaredApp

Archive an app

Hides the app from the list and makes it read-only; its definition and history stay readable. Never a delete. Audited as apps.declared.archive.

Responses

StatusBodyMeaning
200DeclaredAppThe app, archived.
401ErrorResponseunauthorized — no credential, or one that is not valid.
403ErrorResponseforbidden — authenticated, but the user's role does not permit this. Distinct from module_not_entitled, which is about the tenant's subscription rather than the user. On an unsafe request, also origin_not_trusted: the browser boundary refused the request's origin before anything else ran.
404ErrorResponsenot_found
409ErrorResponseapp_archived: already archived.
500ErrorResponseinternal_error. The message is generic by design: internal error text never reaches a response body. The real error is logged with the request ID.
provisioning-catalog (13)

provisioning-catalog

Terraform template catalog and its immutable pinned versions. Behind RequireModule("provisioning").

GET /provisioning/library

credential sessionCookiemodule provisioningoperation listProvisioningLibrary

The built-in library

The versioned templates the platform ships (MILESTONE_1 §8.1), as the library stands at the platform's latest release, retired ones included and marked — each with the tenant's enabled state, the accounts whose agent can run it, and the agent version it needs.

The library ships inside the agent image and the platform never sends its code: a library version pins a hash, and an agent runs the bytes its own image holds under that hash. So a version is offered only to an agent whose library release holds it. release is the first library release that does, agentVersion the first agent version carrying that release, and runnableAccounts the enabled accounts in the template's cloud whose agent last reported such a release (an AWS template does not run in a Google Cloud project, whatever its agent holds, and a request for one there is refused). An account of that cloud that is not listed has no agent, or one that must be upgraded to agentVersion.

updateAvailable is an administrator's notice and absent for anyone else: the library now carries a version other than the one the tenant enabled. Publishing it is the ordinary publish, with fromLibrary: true and the sha256 whose change was read as librarySha256; nothing changes under a tenant on its own.

Parameters

NameInTypeDescription
pagequeryinteger1-based. Normalised server-side rather than rejected: < 1 becomes 1, and a page past the end is clamped to totalPages. Deliberately no minimum: a schema constraint would make the generated server reject page=0 before the handler could normalise it, which contradicts the normalisation this very description promises.
pageSizequeryintegerNormalised server-side, not rejected: < 1 becomes 100, > 500 becomes 500. No minimum or maximum, for the same reason as page — a constraint here would turn a clamp into a 400.

Responses

StatusBodyMeaning
200LibraryTemplatePageA page of the library, by slug.
400ErrorResponseinvalid_request — a malformed parameter or body. The batch ingest endpoint uses invalid_batch instead, because a collector branches on it differently: a bad batch is a bug in the collector, while a bad request is usually a bug in the caller's query string.
401ErrorResponseunauthorized — no credential, or one that is not valid.
403ErrorResponsemodule_not_entitled — RequireModule("provisioning") refused this before the handler ran, because the tenant's subscription does not include the provisioning module — or forbidden, when the module is entitled and the user's role or team scope does not permit the operation. Two codes because they are two mechanisms and two remedies: "upgrade your plan" and "ask your admin" are different problems, and the portal renders a locked-module upsell for the first and an access message for the second. On an unsafe request, also origin_not_trusted: the browser boundary refused the request's origin before anything else ran.
500ErrorResponseinternal_error. The message is generic by design: internal error text never reaches a response body. The real error is logged with the request ID.

GET /provisioning/library/{slug}

credential sessionCookiemodule provisioningoperation getProvisioningLibraryTemplate

One library template, with its source, schema and README

The template as the library now carries it: meta.json's fields, main.tf, the variables schema and the README — what a person reads before enabling it, from the platform's own copy of the library. For an administrator whose enabled version is not the library's, diff is the change from theirs, one unified diff per file that changed.

Responses

StatusBodyMeaning
200LibraryTemplateDetailThe template.
401ErrorResponseunauthorized — no credential, or one that is not valid.
403ErrorResponsemodule_not_entitled — RequireModule("provisioning") refused this before the handler ran, because the tenant's subscription does not include the provisioning module — or forbidden, when the module is entitled and the user's role or team scope does not permit the operation. Two codes because they are two mechanisms and two remedies: "upgrade your plan" and "ask your admin" are different problems, and the portal renders a locked-module upsell for the first and an access message for the second. On an unsafe request, also origin_not_trusted: the browser boundary refused the request's origin before anything else ran.
404ErrorResponsenot_found
500ErrorResponseinternal_error. The message is generic by design: internal error text never reaches a response body. The real error is logged with the request ID.

POST /provisioning/library/{slug}/clone

credential sessionCookiemodule provisioningrole adminoperation cloneProvisioningLibraryTemplate

Clone a library template into a template of the tenant's own

Admin only. Creates an ordinary template of source type inline whose version 1 is the library's main.tf and variables schema, under the slug the body names. It is the tenant's from then on: edited in the template editor, and run only by an agent whose operator allows inline sources, or once the admin publishes it from git (MILESTONE_1 §8.1). A retired library template may be cloned; that is how a tenant keeps maintaining one. An action template is not: it acts on an existing resource and makes nothing, and a clone is a template requests create from.

Request body application/json

FieldTypeDescription
namestringIts name, when not the library's title.
slugrequiredstringThe new template's slug. A slug that names one of the Library's runners is refused (400): a git or inline template runs as the runner its slug names, so a Library template's slug, a slug beginning generated-, negative-control, and a slug on one of their Google service accounts are reserved.

Responses

StatusBodyMeaning
201TemplateThe new inline template. Its latestVersion is 1.
400ErrorResponseinvalid_request — a malformed parameter or body. The batch ingest endpoint uses invalid_batch instead, because a collector branches on it differently: a bad batch is a bug in the collector, while a bad request is usually a bug in the caller's query string.
401ErrorResponseunauthorized — no credential, or one that is not valid.
403ErrorResponsemodule_not_entitled — RequireModule("provisioning") refused this before the handler ran, because the tenant's subscription does not include the provisioning module — or forbidden, when the module is entitled and the user's role or team scope does not permit the operation. Two codes because they are two mechanisms and two remedies: "upgrade your plan" and "ask your admin" are different problems, and the portal renders a locked-module upsell for the first and an access message for the second. On an unsafe request, also origin_not_trusted: the browser boundary refused the request's origin before anything else ran.
404ErrorResponsenot_found
409ErrorResponserequest_state_conflict — the slug is taken, or the library template is an action template.
413ErrorResponseThe request body exceeded the size the server will read. Declared because it is real: a body is otherwise read until the client stops sending, and one caller could exhaust the process. The cap is above any legitimate request — 500 items at the additionalInfo limit is roughly 160 MB — so reaching it means something is wrong with the sender.
415ErrorResponseunsupported_media_type — the body of a browser-facing route is not labelled application/json (a +json suffix and a charset parameter are fine). Refused before the body is read: a form post, or a "simple" cross-site request carrying JSON as text/plain to dodge the preflight, is not something this API reads. Bearer routes are not held to it.
500ErrorResponseinternal_error. The message is generic by design: internal error text never reaches a response body. The real error is logged with the request ID.

POST /provisioning/library/{slug}/enable

credential sessionCookiemodule provisioningrole adminoperation enableProvisioningLibraryTemplate

Enable a library template

Admin only. Creates a template of source type library and publishes its version 1, pinned to the library's current hash for the slug and to the first release that carried it: resolved at once, and with no source stored — the agent runs its own copy of those bytes (MILESTONE_1 §8.1). Its name, description and category are the library's title, summary and category; rename it with the ordinary update.

The template's slug is the library's unless slug names another. One template per library slug: a second enable is refused, an archived template's included — restoring that one is the way back. A template the library retired is not enabled anew; it may be cloned.

Request body application/json

FieldTypeDescription
slugstringThe template's slug, when not the library's. Another slug is held to the rule a created template's is: one that names one of the Library's runners — another Library template's slug, a slug beginning generated-, negative-control, or a slug on one of their Google service accounts — is refused (400).

Responses

StatusBodyMeaning
201TemplateThe enabled template. Its latestVersion is 1.
400ErrorResponseinvalid_request — a malformed parameter or body. The batch ingest endpoint uses invalid_batch instead, because a collector branches on it differently: a bad batch is a bug in the collector, while a bad request is usually a bug in the caller's query string.
401ErrorResponseunauthorized — no credential, or one that is not valid.
403ErrorResponsemodule_not_entitled — RequireModule("provisioning") refused this before the handler ran, because the tenant's subscription does not include the provisioning module — or forbidden, when the module is entitled and the user's role or team scope does not permit the operation. Two codes because they are two mechanisms and two remedies: "upgrade your plan" and "ask your admin" are different problems, and the portal renders a locked-module upsell for the first and an access message for the second. On an unsafe request, also origin_not_trusted: the browser boundary refused the request's origin before anything else ran.
404ErrorResponsenot_found
409ErrorResponserequest_state_conflict — the tenant has a template enabled from this library slug already, the template slug is taken, or the library retired the template.
413ErrorResponseThe request body exceeded the size the server will read. Declared because it is real: a body is otherwise read until the client stops sending, and one caller could exhaust the process. The cap is above any legitimate request — 500 items at the additionalInfo limit is roughly 160 MB — so reaching it means something is wrong with the sender.
415ErrorResponseunsupported_media_type — the body of a browser-facing route is not labelled application/json (a +json suffix and a charset parameter are fine). Refused before the body is read: a form post, or a "simple" cross-site request carrying JSON as text/plain to dodge the preflight, is not something this API reads. Bearer routes are not held to it.
500ErrorResponseinternal_error. The message is generic by design: internal error text never reaches a response body. The real error is logged with the request ID.

GET /provisioning/templates

credential sessionCookiemodule provisioningoperation listProvisioningTemplates

List catalog templates

The catalog an engineer picks from. Archived templates are excluded unless asked for: an archived template may still have live resources provisioned from it, so it is hidden, never deleted.

The requester's catalog (MILESTONE_1 §8.4) reads this with requestable=true: by category, then by resource type, then the templates themselves — and "Create another like this" asks it for one type's templates. The filters combine; each narrows.

Parameters

NameInTypeDescription
categoryqueryarray of stringRepeatable. Any of the given categories.
resourceTypequeryarray of stringRepeatable. Templates whose resourceTypes name any of the given types, <cloud>.<service>.<type> as the inventory names them. A template that declares none — one written before templates said what they make — matches no type.
cloudqueryTemplateCloudTemplates for this cloud. A template that names none matches no cloud.
qquerystringCase-insensitive text within a template's name, description, slug or one of its resource types — the catalog's search box.
requestablequerybooleanOnly templates a request may be made from now: not archived, with a requestableVersion. Default false.
includeArchivedquerybooleanInclude templates with archivedAt set. Default false.
pagequeryinteger1-based. Normalised server-side rather than rejected: < 1 becomes 1, and a page past the end is clamped to totalPages. Deliberately no minimum: a schema constraint would make the generated server reject page=0 before the handler could normalise it, which contradicts the normalisation this very description promises.
pageSizequeryintegerNormalised server-side, not rejected: < 1 becomes 100, > 500 becomes 500. No minimum or maximum, for the same reason as page — a constraint here would turn a clamp into a 400.

Responses

StatusBodyMeaning
200TemplatePageA page of templates.
400ErrorResponseinvalid_request — a malformed parameter or body. The batch ingest endpoint uses invalid_batch instead, because a collector branches on it differently: a bad batch is a bug in the collector, while a bad request is usually a bug in the caller's query string.
401ErrorResponseunauthorized — no credential, or one that is not valid.
403ErrorResponsemodule_not_entitled — RequireModule("provisioning") refused this before the handler ran, because the tenant's subscription does not include the provisioning module — or forbidden, when the module is entitled and the user's role or team scope does not permit the operation. Two codes because they are two mechanisms and two remedies: "upgrade your plan" and "ask your admin" are different problems, and the portal renders a locked-module upsell for the first and an access message for the second. On an unsafe request, also origin_not_trusted: the browser boundary refused the request's origin before anything else ran.
500ErrorResponseinternal_error. The message is generic by design: internal error text never reaches a response body. The real error is logged with the request ID.

POST /provisioning/templates

credential sessionCookiemodule provisioningrole adminbody at most 4.194304e+06 bytesoperation createProvisioningTemplate

Register a template

Admin only — the catalog is curated, not crowd-sourced (D6). Creating a template creates no version and therefore nothing requestable: a template with no resolved version cannot be requested at all.

A library template is not created here: it is enabled from the library (POST /provisioning/library/{slug}/enable), which pins what it runs.

cloud and resourceTypes say what the template makes: the requester's catalog lists it under each type, and a request for it is refused for an account in another cloud. Both are optional — a template that declares no type lists under its category alone — but a type names its cloud, so types need cloud, and each must be of it.

A git template's URL is checked before it is stored: scheme allowlist, no credentials embedded in the URL, and the host must resolve outside private and reserved ranges. That last one is SSRF defence and it is not optional (PROVISIONING_SPEC.md §8.2).

Request body application/json

FieldTypeDescription
categoryrequiredTemplateCategoryWhat a template creates, in the one taxonomy (MILESTONE_1 §3). The same vocabulary as ResourceCategory without other: that bucket is where a value written before the set existed lists, not something a new template may declare. A template stored with a category outside this set — written before the taxonomy, or by an older client — is read back as other; nothing rewrites the row, and category=other on the catalog finds exactly those. One of compute, networking, databases, messaging, serverless, storage, security_and_identity.
cloudTemplateCloudThe cloud a template makes its resources in, as the account registry names it (Account.system): a library template's (MILESTONE_1 §8.1) or one of the tenant's own. azure is a library template's, one Import generated for an Azure resource, and — since then slice 6 — one of the tenant's own and a copy published to the catalog. A request for a template of the tenant's own in an Azure account waits for an agent whose Azure runners are scoped to resource groups (capability azure-resource-groups). One of aws, gcp, azure.
descriptionstring
gitRefstringDefault ref for new versions. Only for git.
gitUrlstringRequired for git, rejected for inline.
namerequiredstring
resourceTypesarray of stringWhat the template makes, <cloud>.<service>.<type>, each of cloud, each once. Types need cloud.
slugrequiredstringA slug that names one of the Library's runners is refused (400): a git or inline template runs as the runner its slug names, so a Library template's slug, a slug beginning generated-, negative-control, and a slug on one of their Google service accounts are reserved.
sourceTyperequiredstring enumgit or inline. A library template is enabled from the library, never created here. One of git, inline.

Responses

StatusBodyMeaning
201TemplateThe registered template.
400ErrorResponseinvalid_request — a malformed parameter or body. The batch ingest endpoint uses invalid_batch instead, because a collector branches on it differently: a bad batch is a bug in the collector, while a bad request is usually a bug in the caller's query string.
401ErrorResponseunauthorized — no credential, or one that is not valid.
403ErrorResponsemodule_not_entitled — RequireModule("provisioning") refused this before the handler ran, because the tenant's subscription does not include the provisioning module — or forbidden, when the module is entitled and the user's role or team scope does not permit the operation. Two codes because they are two mechanisms and two remedies: "upgrade your plan" and "ask your admin" are different problems, and the portal renders a locked-module upsell for the first and an access message for the second. On an unsafe request, also origin_not_trusted: the browser boundary refused the request's origin before anything else ran.
409ErrorResponseinvalid_request is used for a malformed slug; this is the narrower case of a well-formed slug that is already taken. slug is unique per tenant and immutable, because it is what a request pins to and what a URL names.
413ErrorResponseThe request body exceeded the size the server will read. Declared because it is real: a body is otherwise read until the client stops sending, and one caller could exhaust the process. The cap is above any legitimate request — 500 items at the additionalInfo limit is roughly 160 MB — so reaching it means something is wrong with the sender.
415ErrorResponseunsupported_media_type — the body of a browser-facing route is not labelled application/json (a +json suffix and a charset parameter are fine). Refused before the body is read: a form post, or a "simple" cross-site request carrying JSON as text/plain to dodge the preflight, is not something this API reads. Bearer routes are not held to it.
500ErrorResponseinternal_error. The message is generic by design: internal error text never reaches a response body. The real error is logged with the request ID.

GET /provisioning/templates/{templateId}

credential sessionCookiemodule provisioningoperation getProvisioningTemplate

One template

Responses

StatusBodyMeaning
200TemplateThe template.
401ErrorResponseunauthorized — no credential, or one that is not valid.
403ErrorResponsemodule_not_entitled — RequireModule("provisioning") refused this before the handler ran, because the tenant's subscription does not include the provisioning module — or forbidden, when the module is entitled and the user's role or team scope does not permit the operation. Two codes because they are two mechanisms and two remedies: "upgrade your plan" and "ask your admin" are different problems, and the portal renders a locked-module upsell for the first and an access message for the second. On an unsafe request, also origin_not_trusted: the browser boundary refused the request's origin before anything else ran.
404ErrorResponsenot_found
500ErrorResponseinternal_error. The message is generic by design: internal error text never reaches a response body. The real error is logged with the request ID.

PATCH /provisioning/templates/{templateId}

credential sessionCookiemodule provisioningrole adminbody at most 4.194304e+06 bytesoperation updateProvisioningTemplate

Rename, recategorise, or archive a template

Admin only. A partial update: only the fields present are changed.

slug and sourceType are absent from the request body deliberately. Both are pinned by every request and every provisioned resource that came from this template, and changing either would rewrite history.

cloud and resourceTypes are held to the same rule as at create, on the template as it stands after the change. A library template's are the library's, taken from each version it publishes, and are refused here.

It asks for the password (x-step-up): archiving takes the template out of the catalogue. Any other change asks too; it is the same operation.

Request body application/json

FieldTypeDescription
archivedbooleanTrue archives, false restores. Archiving hides the template and closes it to new versions; it never touches what was provisioned from it.
categoryTemplateCategoryWhat a template creates, in the one taxonomy (MILESTONE_1 §3). The same vocabulary as ResourceCategory without other: that bucket is where a value written before the set existed lists, not something a new template may declare. A template stored with a category outside this set — written before the taxonomy, or by an older client — is read back as other; nothing rewrites the row, and category=other on the catalog finds exactly those. One of compute, networking, databases, messaging, serverless, storage, security_and_identity.
cloudTemplateCloudMoves the template: its next version is published for this cloud, and the requester's catalog lists it here once one is. Every version already published keeps the cloud it was published for, and a request for it still runs only there. Refused for a library template.
descriptionstring
gitRefstring
namestring
resourceTypesarray of stringReplaces the template's types; empty clears them. Held with cloud, as the template stands after the change, to the rule at create. Refused for a library template.

Responses

StatusBodyMeaning
200TemplateThe updated template.
400ErrorResponseinvalid_request — a malformed parameter or body. The batch ingest endpoint uses invalid_batch instead, because a collector branches on it differently: a bad batch is a bug in the collector, while a bad request is usually a bug in the caller's query string.
401ErrorResponseunauthorized — no credential, or one that is not valid.
403ErrorResponsemodule_not_entitled — RequireModule("provisioning") refused this before the handler ran, because the tenant's subscription does not include the provisioning module — or forbidden, when the module is entitled and the user's role or team scope does not permit the operation. Two codes because they are two mechanisms and two remedies: "upgrade your plan" and "ask your admin" are different problems, and the portal renders a locked-module upsell for the first and an access message for the second. On an unsafe request, also origin_not_trusted: the browser boundary refused the request's origin before anything else ran.
404ErrorResponsenot_found
413ErrorResponseThe request body exceeded the size the server will read. Declared because it is real: a body is otherwise read until the client stops sending, and one caller could exhaust the process. The cap is above any legitimate request — 500 items at the additionalInfo limit is roughly 160 MB — so reaching it means something is wrong with the sender.
415ErrorResponseunsupported_media_type — the body of a browser-facing route is not labelled application/json (a +json suffix and a charset parameter are fine). Refused before the body is read: a form post, or a "simple" cross-site request carrying JSON as text/plain to dodge the preflight, is not something this API reads. Bearer routes are not held to it.
500ErrorResponseinternal_error. The message is generic by design: internal error text never reaches a response body. The real error is logged with the request ID.

GET /provisioning/templates/{templateId}/versions

credential sessionCookiemodule provisioningoperation listProvisioningTemplateVersions

Published versions of a template

Newest first. A version with resolveState other than resolved is listed — the catalog UI shows a failed resolution with the agent's own error, which is the only way an admin can act on it — but it cannot be requested.

Parameters

NameInTypeDescription
pagequeryinteger1-based. Normalised server-side rather than rejected: < 1 becomes 1, and a page past the end is clamped to totalPages. Deliberately no minimum: a schema constraint would make the generated server reject page=0 before the handler could normalise it, which contradicts the normalisation this very description promises.
pageSizequeryintegerNormalised server-side, not rejected: < 1 becomes 100, > 500 becomes 500. No minimum or maximum, for the same reason as page — a constraint here would turn a clamp into a 400.

Responses

StatusBodyMeaning
200TemplateVersionPageA page of versions.
400ErrorResponseinvalid_request — a malformed parameter or body. The batch ingest endpoint uses invalid_batch instead, because a collector branches on it differently: a bad batch is a bug in the collector, while a bad request is usually a bug in the caller's query string.
401ErrorResponseunauthorized — no credential, or one that is not valid.
403ErrorResponsemodule_not_entitled — RequireModule("provisioning") refused this before the handler ran, because the tenant's subscription does not include the provisioning module — or forbidden, when the module is entitled and the user's role or team scope does not permit the operation. Two codes because they are two mechanisms and two remedies: "upgrade your plan" and "ask your admin" are different problems, and the portal renders a locked-module upsell for the first and an access message for the second. On an unsafe request, also origin_not_trusted: the browser boundary refused the request's origin before anything else ran.
404ErrorResponsenot_found
500ErrorResponseinternal_error. The message is generic by design: internal error text never reaches a response body. The real error is logged with the request ID.

POST /provisioning/templates/{templateId}/versions

credential sessionCookiemodule provisioningrole adminbody at most 4.194304e+06 bytesoperation publishProvisioningTemplateVersion

Publish the next version

Admin only, and the moment a version's source identity is fixed.

For an inline source the publish is synchronous and complete: the exact bytes are stored, their sha256 is computed, and resolveState is resolved on the way out.

For a git source it is asynchronous and needs a live agent (D7): the platform holds no git credential and never reaches the customer's network. The publishing transaction therefore also enqueues a provisioning request with action: resolve (PROVISIONING_SPEC.md §6.6), which an agent claims through the ordinary claim and reports on the ordinary terminal status — there is no resolve endpoint, and no second claim/fence/replay mechanism to keep in step with the first. Until an agent reports, the version is pending and unrequestable: a 201 here is not a promise that the version will ever become usable.

The request targets the executor the platform chooses: the first enabled account with a live agent, else the first with any registered agent, recorded as resolveAccountId. With no such account the version is published pending with no request and no resolveAccountId — waiting — and the next agent registration enqueues it; the catalog says so.

For a library template the body is {fromLibrary: true, librarySha256} and nothing else (MILESTONE_1 §8.1): the next version is pinned to the hash the library now carries for the template's library slug, with the library's variables schema, resolved at once and with no source stored. It is how an administrator takes a newer library version, deliberately; versions already published, and every request pinned to one, keep the hash they pinned.

librarySha256 is the hash whose change the administrator read — the library detail's sha256 — and the publish pins that hash or nothing. When the library carries another version of the slug by the time the publish lands (the platform was upgraded between the read and the publish, or a rollout's instances carry different releases), the answer is 409 library_changed and no version is created: read the detail again, show its change, and publish the sha256 it names.

Request body application/json

FieldTypeDescription
fromLibrarybooleanFor library templates, and only true: publish the version the library now carries for the template's library slug (MILESTONE_1 §8.1). Requires librarySha256.
gitRefstringFor git templates. Defaults to the template's gitRef. This is what an agent resolves; it is never what runs.
importImportDeclarationFor inline templates: what this version declares it can take over (MILESTONE_1 §9.4), checked at publish against the source and the schema — every address a resource the source declares, every pre-filled variable one the schema declares with a value it accepts, and no identifier or pre-fill reading outside the resource key, resourceName and additionalInfo (400 invalid_request, naming the rule). Refused for git, whose source is not known at publish, and for library, whose declaration is the library's.
inlineSourcestringFor inline templates: the exact bytes, capped at 256 KiB, UTF-8, no NUL. Their sha256 is the version's source identity, so an inline version is resolved the instant it is published — "approved code equals executed code" holds without a clone.
librarySha256stringWith fromLibrary, and required with it: the hash of the library version whose change the administrator read — the library detail's sha256, 64 lower-case hexadecimal digits. The publish pins this hash or nothing: when the library no longer carries it for the slug, the answer is 409 library_changed and no version is created. Refused on a git or inline publish.
outputsarray of TemplateOutputFor git and inline templates: the outputs this version declares (MILESTONE_1 §11.4). Each name at most once, each of the variables schema's five types. Refused for library, whose outputs are the library's.
variablesSchemaVariablesSchemaStored on the version and immutable with it. The portal generates the request form from this; the agent uses it to write generated.auto.tfvars as proper HCL through an encoder, never by string concatenation, because hand-rolled escaping is how tfvars injection happens.

Responses

StatusBodyMeaning
201TemplateVersionThe new version. resolved for inline, pending for git.
400ErrorResponseinvalid_request — a malformed parameter or body. The batch ingest endpoint uses invalid_batch instead, because a collector branches on it differently: a bad batch is a bug in the collector, while a bad request is usually a bug in the caller's query string.
401ErrorResponseunauthorized — no credential, or one that is not valid.
403ErrorResponsemodule_not_entitled — RequireModule("provisioning") refused this before the handler ran, because the tenant's subscription does not include the provisioning module — or forbidden, when the module is entitled and the user's role or team scope does not permit the operation. Two codes because they are two mechanisms and two remedies: "upgrade your plan" and "ask your admin" are different problems, and the portal renders a locked-module upsell for the first and an access message for the second. On an unsafe request, also origin_not_trusted: the browser boundary refused the request's origin before anything else ran.
404ErrorResponsenot_found
409ErrorResponserequest_state_conflict — the template is archived, so it takes no new versions; or, with fromLibrary, its latest version already pins the hash the library carries. library_changed — with fromLibrary, the library carries another version of the template's slug than librarySha256 names: it changed since the change was read. Nothing was published. Read the library detail again, and publish only once its change is shown.
413ErrorResponseThe request body exceeded the size the server will read. Declared because it is real: a body is otherwise read until the client stops sending, and one caller could exhaust the process. The cap is above any legitimate request — 500 items at the additionalInfo limit is roughly 160 MB — so reaching it means something is wrong with the sender.
415ErrorResponseunsupported_media_type — the body of a browser-facing route is not labelled application/json (a +json suffix and a charset parameter are fine). Refused before the body is read: a form post, or a "simple" cross-site request carrying JSON as text/plain to dodge the preflight, is not something this API reads. Bearer routes are not held to it.
500ErrorResponseinternal_error. The message is generic by design: internal error text never reaches a response body. The real error is logged with the request ID.

GET /provisioning/templates/{templateId}/versions/{version}

credential sessionCookiemodule provisioningoperation getProvisioningTemplateVersion

One version, with its variables schema

The variables schema served here is what the portal generates the request form from. It is never reimplemented client-side: a client-side check is a convenience, and the Go validator at submit is the authority (PROVISIONING_SPEC.md §8.1).

Responses

StatusBodyMeaning
200TemplateVersionThe version.
401ErrorResponseunauthorized — no credential, or one that is not valid.
403ErrorResponsemodule_not_entitled — RequireModule("provisioning") refused this before the handler ran, because the tenant's subscription does not include the provisioning module — or forbidden, when the module is entitled and the user's role or team scope does not permit the operation. Two codes because they are two mechanisms and two remedies: "upgrade your plan" and "ask your admin" are different problems, and the portal renders a locked-module upsell for the first and an access message for the second. On an unsafe request, also origin_not_trusted: the browser boundary refused the request's origin before anything else ran.
404ErrorResponsenot_found
500ErrorResponseinternal_error. The message is generic by design: internal error text never reaches a response body. The real error is logged with the request ID.

POST /provisioning/templates/{templateId}/versions/{version}/copy-to-catalog

credential sessionCookiemodule provisioningrole adminoperation copyGeneratedTemplateToCatalog

Publish a generated configuration to the catalog

A configuration an agent generated for an import (the owner, 2026-10-12: a copy). A new catalog template is made, inline, of the generated template's cloud and resource types and named as the body names it; its version 1 holds the configuration's bytes, pinned by the same sha256, with no variables. Its import declaration is the generated version's addresses with each literal identifier turned back into the companion table's identifier template — {resourceId} rather than the one bucket's name — so the template can take over any resource of its type. The literal values in the HCL stay as they are; turning them into variables is a new version, published as any other. The import's own template is not changed. Audited (provisioning.template.copied_from_generated).

Refused: a version that is not a generated configuration — a catalog template's, or a generated template's skeleton — (409 request_state_conflict); a slug already taken (409); a malformed slug or an unknown category (400).

Request body application/json

FieldTypeDescription
categoryrequiredTemplateCategoryWhat a template creates, in the one taxonomy (MILESTONE_1 §3). The same vocabulary as ResourceCategory without other: that bucket is where a value written before the set existed lists, not something a new template may declare. A template stored with a category outside this set — written before the taxonomy, or by an older client — is read back as other; nothing rewrites the row, and category=other on the catalog finds exactly those. One of compute, networking, databases, messaging, serverless, storage, security_and_identity.
descriptionstring
namerequiredstring
slugrequiredstringThe new template's slug. A slug that names one of the Library's runners is refused (400): a git or inline template runs as the runner its slug names, so a Library template's slug, a slug beginning generated-, negative-control, and a slug on one of their Google service accounts are reserved.

Responses

StatusBodyMeaning
201TemplateThe new catalog template.
400ErrorResponseinvalid_request — a malformed parameter or body. The batch ingest endpoint uses invalid_batch instead, because a collector branches on it differently: a bad batch is a bug in the collector, while a bad request is usually a bug in the caller's query string.
401ErrorResponseunauthorized — no credential, or one that is not valid.
403ErrorResponsemodule_not_entitled — RequireModule("provisioning") refused this before the handler ran, because the tenant's subscription does not include the provisioning module — or forbidden, when the module is entitled and the user's role or team scope does not permit the operation. Two codes because they are two mechanisms and two remedies: "upgrade your plan" and "ask your admin" are different problems, and the portal renders a locked-module upsell for the first and an access message for the second. On an unsafe request, also origin_not_trusted: the browser boundary refused the request's origin before anything else ran.
404ErrorResponsenot_found
409ErrorResponserequest_state_conflict — the version is not a generated configuration, or the slug is taken.
500ErrorResponseinternal_error. The message is generic by design: internal error text never reaches a response body. The real error is logged with the request ID.

POST /provisioning/templates/{templateId}/versions/{version}/resolve

credential sessionCookiemodule provisioningrole adminoperation requeueProvisioningVersionResolve

Re-offer a git resolution to an agent

Admin only. A failed resolution is retryable: the usual causes are a ref that did not exist yet, a repository the agent could not reach, and an agent that was not running at all. A pending one may be requeued too: one waiting with no executor, or one whose request is still queued for an account whose agent has gone — that request is cancelled, and the executor is chosen again by the same rule as at publish. The two request rows are the record of the move.

This enqueues a new request with action: resolve (§6.6). It does not reopen the old one: attempts, fencing and replay behave here exactly as they do for a create, because it is the same mechanism. A resolution an agent is running is left to finish.

A resolved version's source identity is write-once — re-resolving it would silently change what an existing approval approved — and is refused.

Responses

StatusBodyMeaning
202TemplateVersionRequeued. resolveState is back to pending; resolveAccountId names the account whose agent will claim it, or is absent when none is eligible yet.
401ErrorResponseunauthorized — no credential, or one that is not valid.
403ErrorResponsemodule_not_entitled — RequireModule("provisioning") refused this before the handler ran, because the tenant's subscription does not include the provisioning module — or forbidden, when the module is entitled and the user's role or team scope does not permit the operation. Two codes because they are two mechanisms and two remedies: "upgrade your plan" and "ask your admin" are different problems, and the portal renders a locked-module upsell for the first and an access message for the second. On an unsafe request, also origin_not_trusted: the browser boundary refused the request's origin before anything else ran.
404ErrorResponsenot_found
409ErrorResponseresolution_conflict — the version is resolved (its source identity is written once and never updated), or an agent is running its resolution now.
500ErrorResponseinternal_error. The message is generic by design: internal error text never reaches a response body. The real error is logged with the request ID.
provisioning-requests (13)

provisioning-requests

Provision and destroy requests, and the single approval decision each one gets. Behind RequireModule("provisioning").

POST /provisioning/actions

credential sessionCookiemodule provisioningrole engineeroperation createProvisioningAction

Ask for an action on an existing resource

An action on one resource the inventory sees — stop so far — asked for, approved, claimed and audited as a create is, and held to the one resource it names. Under the licence's remediation capability, as every action route is.

Checked before the request exists, in the order a person meets it: the kind; the target's key, whole; the account enabled and owned by a team the caller belongs to (administrators included, as for a create); the pinned version resolved, its template not archived, and a version of an action template of this kind — a library template whose meta.json names the kind; the resource seen and not tombstoned, of the type the kind acts on, in the state it needs (a stop needs the instance running), and held by no provisioning state — a resource an apply's state holds, by its owner record or its stamp, is refused with the owner named, because an action is not run beside the state that holds its resource.

An origin names the finding the action answers, by its identity. It is checked after the account and before the request exists: the licence carries Estate, whose read findings are; a finding of that identity exists as the findings read projects it now and the caller sees it on their own teams' accounts; it is about the target (its scope is the target's key); and its source is one the kind answers. The request records the finding as read then — its identity, its occurrence and fingerprint, its label and figure — and its reads return it as origin. Without one, nothing changes.

The request is born pending_approval with the server's variables and a derived name, and approved as any is: never by the requester, by the owning team or an administrator, once. The approval pins the target with the rest, and the claim checks the target again — seen, ready, held by nobody — failing the request (configuration) when it no longer stands. The claim leases the resource, so two actions on it never run at once, and offers the task only to an agent that advertises the kind's capability and plan-inspection. Its plan may create the kind's one resource and nothing else. It leaves no provisioned resource and is never partialApply; a failed one is asked for again, and approved again — Retry refuses it.

Request body application/json

FieldTypeDescription
accountIdrequiredstring
actionrequiredResourceActionKindAn action on an existing resource — the closed set, one so far. stop stops a running EC2 instance (aws.ec2.instance): its template sets aws_ec2_instance_state to stopped, in a state of the request's own whose one resource only forgets the instance when it is deleted, so it never becomes a second owner of it. One of stop.
originResourceActionOriginThe finding an action answers, by its identity as GET /findings shows it — (source, sourceId, scope), all three. Only the identity is taken from the asker: the server finds the finding as the findings read projects it now, on the asker's own scope (their teams' accounts), and records what it read — never anything the asker says it showed. Refused unless such a finding exists and the asker sees it, its scope is the target's key — the finding is about the resource the action acts on — and its source is one the kind answers (stop: trusted_advisor).
regionrequiredstring
resourceIdrequiredstring
resourceTyperequiredstring
servicerequiredstring
systemrequiredstring
templateIdrequiredstring (uuid)A template enabled from the library whose version carries out this kind of action. A version that makes things, or carries out another kind, is refused.
templateVersionrequiredintegerExplicit, never "latest", as for a create.

Responses

StatusBodyMeaning
201ProvisionRequestCreated in pending_approval; the approvers are told.
400ErrorResponseinvalid_request, naming the field: a kind the platform does not know, a target not named whole, a resource not of the type the kind acts on (resourceType), a version that does not carry out this kind (templateId); an origin not named whole, about another resource than the target, or of a source the kind does not answer.
401ErrorResponseunauthorized — no credential, or one that is not valid.
403ErrorResponsecapability_not_entitled — the licence does not carry remediation; module_not_entitled — the plan does not carry Provisioning, or an origin was named and it does not carry Estate; or forbidden — a viewer, or an account no team of the caller's holds.
404ErrorResponsenot_found: the template version, or the resource — the inventory does not see one of exactly that key; or the origin — no finding of that identity exists now that the caller sees.
409ErrorResponseresource_already_managed — a provisioning state holds the resource, the owner named; request_state_conflict — the resource is not in the state the action needs (a stop of an instance that is not running), or the template is archived; version_unresolved; account_not_managed.
413ErrorResponseThe request body exceeded the size the server will read. Declared because it is real: a body is otherwise read until the client stops sending, and one caller could exhaust the process. The cap is above any legitimate request — 500 items at the additionalInfo limit is roughly 160 MB — so reaching it means something is wrong with the sender.
415ErrorResponseunsupported_media_type — the body of a browser-facing route is not labelled application/json (a +json suffix and a charset parameter are fine). Refused before the body is read: a form post, or a "simple" cross-site request carrying JSON as text/plain to dodge the preflight, is not something this API reads. Bearer routes are not held to it.
500ErrorResponseinternal_error. The message is generic by design: internal error text never reaches a response body. The real error is logged with the request ID.

GET /provisioning/actions/offers

credential sessionCookiemodule provisioningrole engineeroperation getProvisioningActionOffers

The actions that may be asked for on one resource now

What a resource's page offers: each action on the resource that POST /provisioning/actions would accept from the caller now, with the template and the version it would pin. Under the same capability and role as that route.

The submit's own checks, in its order, short of making the request: an account enabled and held by a team the caller belongs to (administrators included); a version of an action template of the kind — the newest resolved version of each template that is not archived; the resource seen and not tombstoned, of the type the kind acts on, in the state it needs, and held by no provisioning state; and the variables the server fills from it valid by the version's schema (an id the template's pattern refuses is not offered). Where one fails nothing is offered, and that is not an error: the answer is an empty list. As at the submit, the check of the state that holds the resource may record the owner its stamp names (§4.6).

An offer is the answer of the moment it is read: the submit judges everything again. Not a paged list: it is bounded by the closed set of kinds and the action templates enabled for them — library templates only, one per library slug (an inline or git action version is not offered, and an action template is not cloned).

Parameters

NameInTypeDescription
accountIdrequiredquerystring
regionrequiredquerystring
systemrequiredquerystring
servicerequiredquerystring
resourceTyperequiredquerystring
resourceIdrequiredquerystring
originSourcequeryFindingSourceWith originSourceId and originScope, all three or none: the finding the action would answer. Only actions a submit with that origin would accept are offered — none where the finding is not one the caller sees about this resource, of a source the kind answers, or the plan does not carry Estate.
originSourceIdquerystring
originScopequerystring

Responses

StatusBodyMeaning
200ResourceActionOfferListThe actions offered, by kind and then template name; empty when none is.
400ErrorResponseinvalid_request — the key is not named whole, or the origin is named in part.
401ErrorResponseunauthorized — no credential, or one that is not valid.
403ErrorResponsecapability_not_entitled — the licence does not carry remediation; module_not_entitled; or forbidden — a viewer.
500ErrorResponseinternal_error. The message is generic by design: internal error text never reaches a response body. The real error is logged with the request ID.

GET /provisioning/requests

credential sessionCookiemodule provisioningoperation listProvisioningRequests

List provision and destroy requests

Newest first. Scoped by team through team_accounts by the shared filter builder, never by a predicate hand-copied into this query.

Parameters

NameInTypeDescription
statequeryarray of ProvisionRequestStateRepeatable. Any of the given request states.
accountIdqueryarray of stringRepeatable. Any of the given accounts.
templateIdquerystring (uuid)
awaitingMyDecisionquerybooleanOnly requests in pending_approval that this user may decide: someone else's, and their own exactly when they may approve it themselves because nobody else could (by the rule the decision applies) — and none at all for a viewer, who decides nothing.
fromquerystring (date-time)Only requests made at or after this instant. The Dashboard's "failed in 7 days" links here with the same instant its count used, so the list shows the rows the number counted.
expiresBeforequerystring (date-time)Only requests whose expiresAt — the pending_approval deadline — is before this instant. With awaitingMyDecision, the approvals that expire first: the Dashboard's "expiring within a day" links to the approvals queue with the same instant its count used, so the page lists the rows the number counted. Named after the field it bounds.
pagequeryinteger1-based. Normalised server-side rather than rejected: < 1 becomes 1, and a page past the end is clamped to totalPages. Deliberately no minimum: a schema constraint would make the generated server reject page=0 before the handler could normalise it, which contradicts the normalisation this very description promises.
pageSizequeryintegerNormalised server-side, not rejected: < 1 becomes 100, > 500 becomes 500. No minimum or maximum, for the same reason as page — a constraint here would turn a clamp into a 400.

Responses

StatusBodyMeaning
200ProvisionRequestPageA page of requests.
400ErrorResponseinvalid_request — a malformed parameter, or a state that is not a request state. Several states are the parameter repeated (state=queued&state=running); state=queued,running is one value naming no state, and is refused rather than matching nothing.
401ErrorResponseunauthorized — no credential, or one that is not valid.
403ErrorResponsemodule_not_entitled — RequireModule("provisioning") refused this before the handler ran, because the tenant's subscription does not include the provisioning module — or forbidden, when the module is entitled and the user's role or team scope does not permit the operation. Two codes because they are two mechanisms and two remedies: "upgrade your plan" and "ask your admin" are different problems, and the portal renders a locked-module upsell for the first and an access message for the second. On an unsafe request, also origin_not_trusted: the browser boundary refused the request's origin before anything else ran.
500ErrorResponseinternal_error. The message is generic by design: internal error text never reaches a response body. The real error is logged with the request ID.

POST /provisioning/requests

credential sessionCookiemodule provisioningrole engineeroperation createProvisioningRequest

Request a create or a destroy

The request pins (templateId, templateVersion) at submit, and that pin never moves. Everything downstream — the approval, the claim, the apply — refers to the one source identity the version already carries, which is what makes "the approval approved this code" true rather than hopeful.

Variables are validated server-side against the version's schema before the request exists. The response to invalid input is a per-variable error, not a single "invalid request": a generated form that cannot say which field is wrong is a worse form than no form.

A create's name takes the request's name. When the version declares a string variable called name and variables leaves it out, the platform fills it from the request's name before validating: as it is when the variable accepts it, else shortened to the longest prefix, with no trailing hyphen, that the variable's rules accept (runs of hyphens folded when that keeps more of it). A value the requester gives always wins. The filled value is stored with the rest, so the approver reads it, the approval pins it and the agent runs it. A request name no prefix of which fits is refused, 400 invalid_request on the name variable, before the request exists.

A destroy runs with the variables its create was given. It carries none of its own: the platform copies the create's stored variables into it when it is submitted — sensitive values included, reused on the server and never sent back by whoever asks — so the destroy plans the configuration the create applied. Planned with the schema's defaults instead, a template that reads an empty subnet as "the default VPC" fails to destroy, before deleting anything, in an account that has none. A destroy that names any variable is refused, 400 invalid_request naming variables, before the request exists.

A template that names its cloud makes resources in that cloud only: a create whose target is an account in another — an AWS template aimed at a Google Cloud project — is refused, 400 invalid_request naming targetAccountId, before the request exists. A destroy is never refused for it: it removes what a create made, in the account that create targeted.

There is no in-place update in v1 (D5). Changing a resource is a destroy followed by a create, and each goes through approval.

Request body application/json

FieldTypeDescription
actionrequiredProvisionActioncreate, destroy or adopt. resolve is refused here with invalid_request: a resolution request is created by the transaction that publishes a git-sourced version (§6.6), never by a person, and one submitted by hand would pin a version to a SHA nobody asked for. So is an action on an existing resource — stop — which is asked for at POST /provisioning/actions, under the licence's remediation capability. An adopt (MILESTONE_1 §9.3) names a recorded import (importId), and the template version and account it was recorded under; its variables are validated as a create's (the pre-fill, GET /provisioning/imports/prefill, suggests them). Refused: an import that is not recorded (409 request_state_conflict); a version that declares no import of the resource's type, or whose identifiers the resource's inventory record cannot finish (400 invalid_request); a resource the inventory no longer sees (404); and a resource another state holds — a stamp of an apply, finished or in flight, or an owner row — or that an untagged apply in its account may hold (409 resource_already_managed, naming it). The server finishes every identifier at submit; the approval pins them. An adopt of a generated import names the import's own template and version, as the import reads them (its skeleton). The server finishes the identifiers of the resource and of its known companions from the companion table, the ones the inventory cannot confirm marked optional; the request is created queued for its generate step, needing an agent that advertises generate (agentVersionNeeded names the version), and nobody is asked to approve anything yet. The step plans the generated configuration and applies nothing; when it reports, the configuration is stored as a new version of the private template, the request pins it and moves to pending_approval, and the approval pins that version's sha like any version's. A type the companion table does not know, or an inventory record that cannot finish the skeleton or an identifier, is refused (400 invalid_request, naming it). When no agent of the account generates (none unrevoked advertised generate, adopt and plan-inspection under the version it runs), the platform renders the configuration itself from the inventory record — the resource and the companions the record confirms — and stores it as the private template's next version, marked not yet planned. The request pins it, queued, and cannot be approved until an agent that generates has re-planned it imports-only (generated.phase is unplanned). A type the platform has no generator for, or a record that lacks what the configuration needs, is refused (400 invalid_request) with the reason, naming the attribute; nothing is stored.
importIdstring (uuid)Required for adopt, rejected otherwise — the recorded import it makes fully managed.
namerequiredstringThe requester's name for the thing. For a create it also fills the version's string variable called name, when it declares one and variables leaves it out, made to fit that variable's rules (the operation's description).
resourceIdstring (uuid)Required for destroy, rejected for create.
targetAccountIdrequiredstringA 12-digit AWS account id, or a Google Cloud project id. Must exist in this tenant's accounts registry, be enabled, and be owned by a team the requester belongs to.
teamIdstring (uuid)The owning team, which decides who may approve. Optional when the requester belongs to exactly one team that owns the account; required when they belong to several, because guessing would decide the approver.
templateIdrequiredstring (uuid)
templateVersionrequiredintegerExplicit, never "latest". A request that pinned a moving target would be approved against one version and applied from another.
variablesobjectcreate only. Validated against the pinned version's schema before the request exists — both the value types, which this schema constrains, and the names, which it cannot: a key the version does not declare is rejected by the validator. Values marked sensitive are stored unmasked (the agent needs them) and masked everywhere they are read back. A destroy takes none. It runs with the variables its create was given, copied from the create when the destroy is submitted, sensitive values included, so the destroy plans exactly the configuration the create applied. A destroy that names any variable — even the create's own value — is refused, 400 invalid_request naming variables; an empty object names none and is accepted.

Responses

StatusBodyMeaning
201ProvisionRequestCreated in pending_approval, with expiresAt already set. The approvers for the target account's owning team are notified through the platform outbox.
400ProvisionRequestValidationErrorinvalid_request. details names every variable that failed and why, so the form can mark the fields rather than the page. A destroy sent variables is refused here too, its message naming variables and the rule: "variables: a destroy runs with the variables its create was given and takes none of its own".
401ErrorResponseunauthorized — no credential, or one that is not valid.
403ErrorResponsemodule_not_entitled — RequireModule("provisioning") refused this before the handler ran, because the tenant's subscription does not include the provisioning module — or forbidden, when the module is entitled and the user's role or team scope does not permit the operation. Two codes because they are two mechanisms and two remedies: "upgrade your plan" and "ask your admin" are different problems, and the portal renders a locked-module upsell for the first and an access message for the second. On an unsafe request, also origin_not_trusted: the browser boundary refused the request's origin before anything else ran.
404ErrorResponsenot_found
409ErrorResponseversion_unresolved — the pinned version has no source identity yet, or its resolution failed, so there is nothing an agent could be told to execute — or account_not_managed, when the target account is not in this tenant's registry, is disabled, or is not owned by a team this user belongs to. request_state_conflict covers a destroy its resource cannot take now: already destroyed, or another request on it in flight, its create's retry included.
413ErrorResponseThe request body exceeded the size the server will read. Declared because it is real: a body is otherwise read until the client stops sending, and one caller could exhaust the process. The cap is above any legitimate request — 500 items at the additionalInfo limit is roughly 160 MB — so reaching it means something is wrong with the sender.
415ErrorResponseunsupported_media_type — the body of a browser-facing route is not labelled application/json (a +json suffix and a charset parameter are fine). Refused before the body is read: a form post, or a "simple" cross-site request carrying JSON as text/plain to dodge the preflight, is not something this API reads. Bearer routes are not held to it.
500ErrorResponseinternal_error. The message is generic by design: internal error text never reaches a response body. The real error is logged with the request ID.

GET /provisioning/requests/{requestId}

credential sessionCookiemodule provisioningoperation getProvisioningRequest

One request, with everything an approver needs

The approver's view, which D1 had to earn: approval fires before any agent claims the request, so there is no Terraform plan to show. What replaces it is PROVISIONING_SPEC.md §11 — the pinned version and its source identity, every variable with sensitive values masked, the target account and its environment, the requester and their team, and this template's recent outcomes in this account.

Parameters

NameInTypeDescription
sourcequerybooleanfalse leaves out version.inlineSource, up to 256 KiB, and nothing else. This is for a reader polling a request whose bytes it already holds: the portal polls a live request every three seconds and reads the bytes once. Everything else is read in the same snapshot either way, version.inlineSha256 and bindingSha256 included. The binding pins the source's identity, not its bytes. A reader that shows bytes must therefore show the bytes whose sha256 is the version.inlineSha256 of the answer it takes bindingSha256 from. When a poll's inlineSha256 differs from the bytes it holds (the request was regenerated), it reads them again, without this parameter, before it offers an approval. Default true: the first read, and any reader that does not ask, gets the bytes.

Responses

StatusBodyMeaning
200ProvisionRequestDetailThe request.
401ErrorResponseunauthorized — no credential, or one that is not valid.
403ErrorResponsemodule_not_entitled — RequireModule("provisioning") refused this before the handler ran, because the tenant's subscription does not include the provisioning module — or forbidden, when the module is entitled and the user's role or team scope does not permit the operation. Two codes because they are two mechanisms and two remedies: "upgrade your plan" and "ask your admin" are different problems, and the portal renders a locked-module upsell for the first and an access message for the second. On an unsafe request, also origin_not_trusted: the browser boundary refused the request's origin before anything else ran.
404ErrorResponsenot_found
500ErrorResponseinternal_error. The message is generic by design: internal error text never reaches a response body. The real error is logged with the request ID.

POST /provisioning/requests/{requestId}/cancel

credential sessionCookiemodule provisioningrole engineeroperation cancelProvisioningRequest

Cancel a queued or running request

From queued this is immediate and terminal: nothing has run, so the request goes straight to cancelled.

From running it is a request to stop, not a stop. The request moves to cancelling and the agent learns about it on its next heartbeat — cancellation rides the heartbeat response rather than getting a polled endpoint of its own, because the agent is already calling it on a known interval and one fewer endpoint is one fewer thing a third-party implementer can get wrong. The request reaches cancelled only when the agent reports it there.

Infrastructure already created before the stop is never auto-destroyed (D10). If the apply had begun, expect partialApply = true and a state backend location to act on.

Responses

StatusBodyMeaning
200ProvisionRequestcancelled if nothing had been claimed, cancelling if an agent is running it.
401ErrorResponseunauthorized — no credential, or one that is not valid.
403ErrorResponsemodule_not_entitled — RequireModule("provisioning") refused this before the handler ran, because the tenant's subscription does not include the provisioning module — or forbidden, when the module is entitled and the user's role or team scope does not permit the operation. Two codes because they are two mechanisms and two remedies: "upgrade your plan" and "ask your admin" are different problems, and the portal renders a locked-module upsell for the first and an access message for the second. On an unsafe request, also origin_not_trusted: the browser boundary refused the request's origin before anything else ran.
404ErrorResponsenot_found
409ErrorResponserequest_state_conflict — the request is terminal, or still awaiting approval (withdraw it instead).
500ErrorResponseinternal_error. The message is generic by design: internal error text never reaches a response body. The real error is logged with the request ID.

POST /provisioning/requests/{requestId}/decision

credential sessionCookiemodule provisioningrole engineeroperation decideProvisioningRequest

Approve or reject

A request is decided exactly once, ever: approvals is keyed (tenant_id, request_id). Approving writes the decision and sets the request to queued in one transaction — approved is deliberately not a state, because it could only ever be observed as a limbo where a request is blessed but not runnable.

Who may decide: role engineer or admin and membership of a team that owns the target account through team_accounts. A tenant admin may always decide.

Approving your own request (the owner, 2026-10-12): refused while anybody else could decide it — another enabled admin, or another enabled engineer of the request's team. When nobody else could, the requester may approve it (never reject it: withdraw is theirs), after confirming their identity within the last 10 minutes (POST /auth/step-up), and the approval is recorded as a self-approval (approval.selfApproved). Who else could decide is counted when the decision is made, under the request's lock, so a second approver refuses it again from then on. There is no setting.

Request body application/json

FieldTypeDescription
bindingSha256stringThe request detail's bindingSha256 as the approver read it: what they are approving. Checked on every approval that names it — a request whose binding moved since is 409 binding_changed and nothing is decided — and required to approve an adopt of a generated import, whose configuration a regenerate replaces while it waits. Ignored on a rejection.
commentstringRecorded permanently and shown to the requester. Optional on approve; a rejection without one is technically allowed and practically unkind.
decisionrequiredstring enum One of approved, rejected.

Responses

StatusBodyMeaning
200ProvisionRequestDecided. The request is queued on approve and rejected on reject, and the decision, its actor, its comment and its timestamp are permanent — they outlive log retention.
400ErrorResponseinvalid_request — a malformed parameter or body. The batch ingest endpoint uses invalid_batch instead, because a collector branches on it differently: a bad batch is a bug in the collector, while a bad request is usually a bug in the caller's query string.
401ErrorResponseunauthorized — no credential, or one that is not valid.
403ErrorResponseself_approval_refused — the actor is the requester, and somebody else could decide the request, or the decision is a rejection. Refused for everyone including admins, by the server rather than by a UI that hides the button. step_up_required — the actor is the requester and may approve their own request, but their session has not confirmed its identity in the last 10 minutes; confirm with POST /auth/step-up and send the decision again. Otherwise forbidden (not an approver for this account's team) or module_not_entitled.
404ErrorResponsenot_found
409ErrorResponserequest_state_conflict — the request is not in pending_approval. It has already been decided, withdrawn, or expired; the message names the state it is actually in. A decision at or after expiresAt expires the request then and there, in the same transaction, and is refused with this code: the deadline does not wait for the nightly sweep. binding_changed — an approval whose bindingSha256 is not the request's binding now, or an approval of a generated adopt that names none: read the request again. Checked after who may decide and the deadline; no decision is written. An approval of a request that pins a configuration the platform rendered and no agent has planned yet is refused with this code too.
413ErrorResponseThe request body exceeded the size the server will read. Declared because it is real: a body is otherwise read until the client stops sending, and one caller could exhaust the process. The cap is above any legitimate request — 500 items at the additionalInfo limit is roughly 160 MB — so reaching it means something is wrong with the sender.
415ErrorResponseunsupported_media_type — the body of a browser-facing route is not labelled application/json (a +json suffix and a charset parameter are fine). Refused before the body is read: a form post, or a "simple" cross-site request carrying JSON as text/plain to dodge the preflight, is not something this API reads. Bearer routes are not held to it.
500ErrorResponseinternal_error. The message is generic by design: internal error text never reaches a response body. The real error is logged with the request ID.

POST /provisioning/requests/{requestId}/regenerate

credential sessionCookiemodule provisioningrole engineeroperation regenerateProvisioningRequest

Generate an adopt's configuration again

An adopt of a generated import waiting for its approval. The configuration it is waiting to have approved is set aside: the request pins its private template's skeleton again, with the identifiers the inventory gives now, and is queued for an agent to generate anew. The next configuration is a new version with its own sha, and the approval that follows pins that one — a configuration is never approved on the strength of another.

Also an adopt that failed on the re-plan of a configuration the platform rendered (slice 4): the plan was not imports-only, and the failure message carries its diff. The request is queued again at its skeleton, its attempt moved on and its import adopting again, for an agent to generate the configuration itself.

The requester, or an engineer or administrator of the team that owns the account.

Responses

StatusBodyMeaning
200ProvisionRequestQueued for its generate step.
400ErrorResponseinvalid_request — a malformed parameter or body. The batch ingest endpoint uses invalid_batch instead, because a collector branches on it differently: a bad batch is a bug in the collector, while a bad request is usually a bug in the caller's query string.
401ErrorResponseunauthorized — no credential, or one that is not valid.
403ErrorResponsemodule_not_entitled — RequireModule("provisioning") refused this before the handler ran, because the tenant's subscription does not include the provisioning module — or forbidden, when the module is entitled and the user's role or team scope does not permit the operation. Two codes because they are two mechanisms and two remedies: "upgrade your plan" and "ask your admin" are different problems, and the portal renders a locked-module upsell for the first and an access message for the second. On an unsafe request, also origin_not_trusted: the browser boundary refused the request's origin before anything else ran.
404ErrorResponsenot_found
409ErrorResponserequest_state_conflict — not in pending_approval (nor failed on the re-plan of the platform's configuration), not an adopt of a generated import, or its import is no longer this request's.
500ErrorResponseinternal_error. The message is generic by design: internal error text never reaches a response body. The real error is logged with the request ID.

POST /provisioning/requests/{requestId}/retry

credential sessionCookiemodule provisioningrole engineeroperation retryProvisioningRequest

Put a failed request back on the queue

Retry is an explicit operator action and never automatic (D10). It returns the request to queued and bumps attempt, which is the fence: every agent write carrying the old attempt is refused task_superseded from that moment, so the previous task cannot resurrect and write over the new one.

The pinned version does not move. A retry re-runs the code that was approved, not the code that is current.

Responses

StatusBodyMeaning
200ProvisionRequestQueued again, with attempt incremented.
401ErrorResponseunauthorized — no credential, or one that is not valid.
403ErrorResponsemodule_not_entitled — RequireModule("provisioning") refused this before the handler ran, because the tenant's subscription does not include the provisioning module — or forbidden, when the module is entitled and the user's role or team scope does not permit the operation. Two codes because they are two mechanisms and two remedies: "upgrade your plan" and "ask your admin" are different problems, and the portal renders a locked-module upsell for the first and an access message for the second. On an unsafe request, also origin_not_trusted: the browser boundary refused the request's origin before anything else ran.
404ErrorResponsenot_found
409ErrorResponserequest_state_conflict — only a failed request may be retried, and not a create whose resource has a destroy requested, running or done, nor a step of a Blueprint run, which is retried by resuming its run — or version_unresolved, if the pinned version's resolution has since been invalidated.
500ErrorResponseinternal_error. The message is generic by design: internal error text never reaches a response body. The real error is logged with the request ID.

PUT /provisioning/requests/{requestId}/sealed-values

credential sessionCookiemodule provisioningrole engineeroperation putProvisioningRequestSealedValues

Supply a request's secret values, sealed

The secret channel. Every needed variable, sealed in the browser to every agent sealing listed, for the attempt it named. Stored sealed, bound to the request, the attempt, the agent and the variable, for at most 24 hours; deleted when the task that uses them ends, or the request does. Never logged, never audited, never returned: the audit log records who sealed which variables for which attempt and agents.

Only an adopt of a generated import whose configuration needs values, while it is pending_approval or queued. The requester, or an engineer or administrator of the team that owns the account.

Request body application/json

FieldTypeDescription
attemptrequiredinteger
valuesrequiredarray of ProvisionSealedValue

Responses

StatusBodyMeaning
204—Sealed for the attempt; the import may be claimed.
400ErrorResponseinvalid_request — a malformed parameter or body. The batch ingest endpoint uses invalid_batch instead, because a collector branches on it differently: a bad batch is a bug in the collector, while a bad request is usually a bug in the caller's query string.
401ErrorResponseunauthorized — no credential, or one that is not valid.
403ErrorResponsemodule_not_entitled — RequireModule("provisioning") refused this before the handler ran, because the tenant's subscription does not include the provisioning module — or forbidden, when the module is entitled and the user's role or team scope does not permit the operation. Two codes because they are two mechanisms and two remedies: "upgrade your plan" and "ask your admin" are different problems, and the portal renders a locked-module upsell for the first and an access message for the second. On an unsafe request, also origin_not_trusted: the browser boundary refused the request's origin before anything else ran.
404ErrorResponsenot_found
409ErrorResponserequest_state_conflict — not an adopt of a generated import that needs values, or not pending_approval or queued, or the attempt moved on, or the agents and their keys are not the ones that can claim it now: read sealing again and seal again.
500ErrorResponseinternal_error. The message is generic by design: internal error text never reaches a response body. The real error is logged with the request ID.

GET /provisioning/requests/{requestId}/sealing

credential sessionCookiemodule provisioningrole engineeroperation getProvisioningRequestSealing

What a request's secret values are sealed to

An adopt of a generated import whose configuration needs secret values (agent-protocol.md §3.10). The request's current attempt, the variables it needs, and the public key of every agent of its account that can claim the import. The browser seals each value to each of them; the platform never sees one in clear.

The requester, or an engineer or administrator of the team that owns the account — whoever may send the values.

Responses

StatusBodyMeaning
200ProvisionRequestSealingThe keys to seal to.
401ErrorResponseunauthorized — no credential, or one that is not valid.
403ErrorResponsemodule_not_entitled — RequireModule("provisioning") refused this before the handler ran, because the tenant's subscription does not include the provisioning module — or forbidden, when the module is entitled and the user's role or team scope does not permit the operation. Two codes because they are two mechanisms and two remedies: "upgrade your plan" and "ask your admin" are different problems, and the portal renders a locked-module upsell for the first and an access message for the second. On an unsafe request, also origin_not_trusted: the browser boundary refused the request's origin before anything else ran.
404ErrorResponsenot_found
409ErrorResponserequest_state_conflict — not an adopt of a generated import that needs values, or not pending_approval or queued; or no agent of the account can open them, or more than 16 can.
500ErrorResponseinternal_error. The message is generic by design: internal error text never reaches a response body. The real error is logged with the request ID.

GET /provisioning/requests/{requestId}/trail

credential sessionCookiemodule provisioningoperation getProvisioningRequestTrail

A request's trail, from its asking to its verification

Every hop of one request on one read — the request as it was asked (its requester, time, template version and target; a Blueprint step names its run), its approval (who, when, and the step-up the decision recorded), the attempt that ended it (its permanent summary and its scrubbed log), and, for a succeeded action, its verification (the verdict and the observation that proves it). Each hop names the records it rests on by id.

sources lists those records in the trail's order, each with how long it is kept and whether it can still be read now. A request, its approval, the decision's audit record, a task's summary, a recorded verdict and a deletion transition are permanent. A task's log, a collection run and an inventory snapshot are removed by retention; each says when (expiresAt, from the deployment's own retention settings) while it is still there. An unchanged observation lives only on the resource's row, until the bucket's next collection. A source no longer inspectable has expired; the verdict that named it stands, and the trail says what can no longer be inspected.

Visible to whoever may read the request: the requester, the owning team and tenant admins. A request outside the reader's scope is a 404, as on the request read.

Responses

StatusBodyMeaning
200ProvisionRequestTrailThe trail.
401ErrorResponseunauthorized — no credential, or one that is not valid.
403ErrorResponsemodule_not_entitled — RequireModule("provisioning") refused this before the handler ran, because the tenant's subscription does not include the provisioning module — or forbidden, when the module is entitled and the user's role or team scope does not permit the operation. Two codes because they are two mechanisms and two remedies: "upgrade your plan" and "ask your admin" are different problems, and the portal renders a locked-module upsell for the first and an access message for the second. On an unsafe request, also origin_not_trusted: the browser boundary refused the request's origin before anything else ran.
404ErrorResponsenot_found
500ErrorResponseinternal_error. The message is generic by design: internal error text never reaches a response body. The real error is logged with the request ID.

POST /provisioning/requests/{requestId}/withdraw

credential sessionCookiemodule provisioningrole engineeroperation withdrawProvisioningRequest

Withdraw a request awaiting approval

The requester's own escape hatch, and only theirs. Terminal: a withdrawn request is not resubmitted, it is replaced by a new one.

Only from pending_approval. Once approved the request is queued and an agent may already have claimed it, so stopping it is a cancel, which is a different operation with a different shape.

Responses

StatusBodyMeaning
200ProvisionRequestWithdrawn.
401ErrorResponseunauthorized — no credential, or one that is not valid.
403ErrorResponsemodule_not_entitled — RequireModule("provisioning") refused this before the handler ran, because the tenant's subscription does not include the provisioning module — or forbidden, when the module is entitled and the user's role or team scope does not permit the operation. Two codes because they are two mechanisms and two remedies: "upgrade your plan" and "ask your admin" are different problems, and the portal renders a locked-module upsell for the first and an access message for the second. On an unsafe request, also origin_not_trusted: the browser boundary refused the request's origin before anything else ran.
404ErrorResponsenot_found
409ErrorResponserequest_state_conflict — not in pending_approval.
500ErrorResponseinternal_error. The message is generic by design: internal error text never reaches a response body. The real error is logged with the request ID.
provisioning-tasks (3)

provisioning-tasks

What an agent actually did: task state, and the streamed logs. Behind RequireModule("provisioning").

GET /provisioning/requests/{requestId}/tasks

credential sessionCookiemodule provisioningoperation listProvisioningTasks

Every attempt at one request

One task per attempt, newest first. A request that was reaped twice and then succeeded has three tasks, and the two abandoned ones are part of the audit record rather than noise to hide.

Paged like every other list on the platform. maxAttempts bounds this at three today, but the bound is a tenant setting and a list operation whose size depends on a setting is a list that needs a page.

Parameters

NameInTypeDescription
pagequeryinteger1-based. Normalised server-side rather than rejected: < 1 becomes 1, and a page past the end is clamped to totalPages. Deliberately no minimum: a schema constraint would make the generated server reject page=0 before the handler could normalise it, which contradicts the normalisation this very description promises.
pageSizequeryintegerNormalised server-side, not rejected: < 1 becomes 100, > 500 becomes 500. No minimum or maximum, for the same reason as page — a constraint here would turn a clamp into a 400.

Responses

StatusBodyMeaning
200ProvisionTaskPageA page of tasks, newest first.
400ErrorResponseinvalid_request — a malformed parameter or body. The batch ingest endpoint uses invalid_batch instead, because a collector branches on it differently: a bad batch is a bug in the collector, while a bad request is usually a bug in the caller's query string.
401ErrorResponseunauthorized — no credential, or one that is not valid.
403ErrorResponsemodule_not_entitled — RequireModule("provisioning") refused this before the handler ran, because the tenant's subscription does not include the provisioning module — or forbidden, when the module is entitled and the user's role or team scope does not permit the operation. Two codes because they are two mechanisms and two remedies: "upgrade your plan" and "ask your admin" are different problems, and the portal renders a locked-module upsell for the first and an access message for the second. On an unsafe request, also origin_not_trusted: the browser boundary refused the request's origin before anything else ran.
404ErrorResponsenot_found
500ErrorResponseinternal_error. The message is generic by design: internal error text never reaches a response body. The real error is logged with the request ID.

GET /provisioning/tasks/{taskId}

credential sessionCookiemodule provisioningoperation getProvisioningTask

One task and its permanent summary

The summary outlives the logs. Full logs are kept 30 days; what the task did, whether it worked, how long each stage took, and which agent and OpenTofu version ran it are kept forever. An audit that expires is not an audit.

Responses

StatusBodyMeaning
200ProvisionTaskDetailThe task, with its summary once it is terminal.
401ErrorResponseunauthorized — no credential, or one that is not valid.
403ErrorResponsemodule_not_entitled — RequireModule("provisioning") refused this before the handler ran, because the tenant's subscription does not include the provisioning module — or forbidden, when the module is entitled and the user's role or team scope does not permit the operation. Two codes because they are two mechanisms and two remedies: "upgrade your plan" and "ask your admin" are different problems, and the portal renders a locked-module upsell for the first and an access message for the second. On an unsafe request, also origin_not_trusted: the browser boundary refused the request's origin before anything else ran.
404ErrorResponsenot_found
500ErrorResponseinternal_error. The message is generic by design: internal error text never reaches a response body. The real error is logged with the request ID.

GET /provisioning/tasks/{taskId}/logs

credential sessionCookiemodule provisioningoperation getProvisioningTaskLogs

Streamed task logs

Cursor-paginated on the agent-assigned seq, oldest first, so the portal can tail a running apply by re-requesting from nextCursor. Not page numbers: lines arrive continuously at the head, and an offset into a growing table silently skips or repeats rows.

Lines are scrubbed by the agent, before they are sent (PROVISIONING_SPEC.md §7.5), including exact-value redaction of every variable the schema marks sensitive. The platform stores what it is given; it does not get a second chance.

Visible to the requester, the owning team and tenant admins. Retention is 30 days, after which this returns an empty page for a task whose summary is still there — which is the documented behaviour and not a bug.

A tailer polls with nextCursor, which is always returned and always positions after the last line the caller has seen. It is a watermark, not a "there is more" flag: an empty page at the tail comes back with the same watermark, and the next poll returns only lines appended since. An earlier draft of this contract omitted nextCursor at the tail and told clients to repeat their input cursor, which for the first page — where there is no input cursor — replays the page just consumed, forever.

A tailer stops on completion, and on nothing else. The agent reports the terminal status before it drains its last log batches, so the task is terminal while the lines that explain a failure are still arriving; completion stays pending until the line at the agent's watermark has been paged out, and turns incomplete or unverified after a bounded grace so no reader waits forever for a line that will never come.

Parameters

NameInTypeDescription
cursorquerystringOpaque keyset cursor from the previous page's nextCursor. Omit for the first page, which starts at the lowest retained seq.
pageSizequeryintegerNormalised server-side, not rejected: < 1 becomes 100, > 500 becomes 500. No minimum or maximum, for the same reason as page — a constraint here would turn a clamp into a 400.

Responses

StatusBodyMeaning
200ProvisionTaskLogPageA page of log lines, oldest first.
400ErrorResponseinvalid_request — a malformed parameter or body. The batch ingest endpoint uses invalid_batch instead, because a collector branches on it differently: a bad batch is a bug in the collector, while a bad request is usually a bug in the caller's query string.
401ErrorResponseunauthorized — no credential, or one that is not valid.
403ErrorResponsemodule_not_entitled — RequireModule("provisioning") refused this before the handler ran, because the tenant's subscription does not include the provisioning module — or forbidden, when the module is entitled and the user's role or team scope does not permit the operation. Two codes because they are two mechanisms and two remedies: "upgrade your plan" and "ask your admin" are different problems, and the portal renders a locked-module upsell for the first and an access message for the second. On an unsafe request, also origin_not_trusted: the browser boundary refused the request's origin before anything else ran.
404ErrorResponsenot_found
500ErrorResponseinternal_error. The message is generic by design: internal error text never reaches a response body. The real error is logged with the request ID.
provisioning-drift (3)

provisioning-drift

Provisioned resources, and the two drift directions computed against the Estate inventory. Behind RequireModule("provisioning").

GET /provisioning/drift/missing

credential sessionCookie or apiTokenmodule provisioningoperation listProvisioningDriftMissing

Drift: provisioned but missing

Resources this tenant provisioned that the Estate inventory no longer sees. Computed live, by joining provisioned_resources against inventory rows carrying panorama:request_id — no new tables, no materialised view, no staleness window (D12).

expectedCount is the number of tagged resources the agent found in state immediately after a successful apply. Without it, "provisioned-but-missing" has nothing to be missing from.

A resource is compared only once the inventory has looked for it (PROVISIONING_SPEC §10, 2026-09-29): a kind counts when a collection of it that started after the create last succeeded (a retry's success included) has finished, in every region the account is collected in. A create the collectors have not yet run since is not listed — it has not been missed, only not yet looked for.

Rows with provenanceTagged = false are excluded rather than reported as fully missing: the stamp never verifiably landed, so their absence from inventory says nothing. They carry a visible "provenance unverified" badge on the resource instead — a stamp that silently failed to land is worse than no stamp, so it is never silently missing.

Parameters

NameInTypeDescription
accountIdqueryarray of stringRepeatable. Any of the given accounts.
pagequeryinteger1-based. Normalised server-side rather than rejected: < 1 becomes 1, and a page past the end is clamped to totalPages. Deliberately no minimum: a schema constraint would make the generated server reject page=0 before the handler could normalise it, which contradicts the normalisation this very description promises.
pageSizequeryintegerNormalised server-side, not rejected: < 1 becomes 100, > 500 becomes 500. No minimum or maximum, for the same reason as page — a constraint here would turn a clamp into a 400.

Responses

StatusBodyMeaning
200DriftMissingPageA page of provisioned-but-missing findings.
400ErrorResponseinvalid_request — a malformed parameter or body. The batch ingest endpoint uses invalid_batch instead, because a collector branches on it differently: a bad batch is a bug in the collector, while a bad request is usually a bug in the caller's query string.
401ErrorResponseunauthorized — no credential, or one that is not valid.
403ErrorResponsemodule_not_entitled — RequireModule("provisioning") refused this before the handler ran, because the tenant's subscription does not include the provisioning module — or forbidden, when the module is entitled and the user's role or team scope does not permit the operation. Two codes because they are two mechanisms and two remedies: "upgrade your plan" and "ask your admin" are different problems, and the portal renders a locked-module upsell for the first and an access message for the second. On an unsafe request, also origin_not_trusted: the browser boundary refused the request's origin before anything else ran.
429ErrorResponseRate limited.
500ErrorResponseinternal_error. The message is generic by design: internal error text never reaches a response body. The real error is logged with the request ID.

GET /provisioning/drift/unprovisioned

credential sessionCookie or apiTokenmodule provisioningoperation listProvisioningDriftUnprovisioned

Drift: existing but unprovisioned

Live inventory in accounts under management that carries no panorama:request_id tag — infrastructure somebody made by hand.

Scoped to the (service, resourceType) pairs this tenant's provisioning has actually produced before, which makes the filter self-calibrating: it learns what provisioning creates by watching it, rather than from a hand-maintained type list that would go stale. An unfiltered version of this query is noise, and noise is how a drift page gets ignored.

A resource with a live import (MILESTONE_1 §9.2) is somebody's now, and leaves this list; imported=true lists exactly those, each with the import that took it, so a recorded import is never simply gone.

Parameters

NameInTypeDescription
accountIdqueryarray of stringRepeatable. Any of the given accounts.
servicequeryarray of stringRepeatable.
importedquerybooleantrue: only the resources a live import has recorded, each with importId. false, the default: only the ones nobody has.
pagequeryinteger1-based. Normalised server-side rather than rejected: < 1 becomes 1, and a page past the end is clamped to totalPages. Deliberately no minimum: a schema constraint would make the generated server reject page=0 before the handler could normalise it, which contradicts the normalisation this very description promises.
pageSizequeryintegerNormalised server-side, not rejected: < 1 becomes 100, > 500 becomes 500. No minimum or maximum, for the same reason as page — a constraint here would turn a clamp into a 400.

Responses

StatusBodyMeaning
200DriftUnprovisionedPageA page of existing-but-unprovisioned findings.
400ErrorResponseinvalid_request — a malformed parameter or body. The batch ingest endpoint uses invalid_batch instead, because a collector branches on it differently: a bad batch is a bug in the collector, while a bad request is usually a bug in the caller's query string.
401ErrorResponseunauthorized — no credential, or one that is not valid.
403ErrorResponsemodule_not_entitled — RequireModule("provisioning") refused this before the handler ran, because the tenant's subscription does not include the provisioning module — or forbidden, when the module is entitled and the user's role or team scope does not permit the operation. Two codes because they are two mechanisms and two remedies: "upgrade your plan" and "ask your admin" are different problems, and the portal renders a locked-module upsell for the first and an access message for the second. On an unsafe request, also origin_not_trusted: the browser boundary refused the request's origin before anything else ran.
429ErrorResponseRate limited.
500ErrorResponseinternal_error. The message is generic by design: internal error text never reaches a response body. The real error is logged with the request ID.

GET /provisioning/resources

credential sessionCookie or apiTokenmodule provisioningoperation listProvisionedResources

What provisioning has created

The provenance record. Every row here answers "who asked for this, from which template version, approved by whom" for the resources carrying its panorama:request_id tag.

And, since then (MILESTONE_1 §9.2), the resources recorded by Import: origin says whether a row was provisioned here or imported, and management whether the platform manages it — a state it applied holds it — or only recorded it. A recorded import applied nothing, so it has no create request, no stamp, no expected count and no status, and it names the resource it records in import. It is never a destroy's target and never a drift finding. A filter on status or createRequestId is about applies, and leaves recorded imports out.

Parameters

NameInTypeDescription
accountIdqueryarray of stringRepeatable. Any of the given accounts.
templateIdquerystring (uuid)
originqueryProvisionedResourceOriginOnly rows of this origin: provisioned here, or imported.
createRequestIdquerystring (uuid)The create request whose resource this is: the one row that create left, or none. The request page finds what to destroy with it, where a page of the account's resources could miss the row (review round 2).
statusqueryarray of ProvisionedResourceStatusRepeatable.
pagequeryinteger1-based. Normalised server-side rather than rejected: < 1 becomes 1, and a page past the end is clamped to totalPages. Deliberately no minimum: a schema constraint would make the generated server reject page=0 before the handler could normalise it, which contradicts the normalisation this very description promises.
pageSizequeryintegerNormalised server-side, not rejected: < 1 becomes 100, > 500 becomes 500. No minimum or maximum, for the same reason as page — a constraint here would turn a clamp into a 400.

Responses

StatusBodyMeaning
200ProvisionedResourcePageA page of provisioned resources.
400ErrorResponseinvalid_request — a malformed parameter or body. The batch ingest endpoint uses invalid_batch instead, because a collector branches on it differently: a bad batch is a bug in the collector, while a bad request is usually a bug in the caller's query string.
401ErrorResponseunauthorized — no credential, or one that is not valid.
403ErrorResponsemodule_not_entitled — RequireModule("provisioning") refused this before the handler ran, because the tenant's subscription does not include the provisioning module — or forbidden, when the module is entitled and the user's role or team scope does not permit the operation. Two codes because they are two mechanisms and two remedies: "upgrade your plan" and "ask your admin" are different problems, and the portal renders a locked-module upsell for the first and an access message for the second. On an unsafe request, also origin_not_trusted: the browser boundary refused the request's origin before anything else ran.
429ErrorResponseRate limited.
500ErrorResponseinternal_error. The message is generic by design: internal error text never reaches a response body. The real error is logged with the request ID.
provisioning-imports (6)

provisioning-imports

Import (MILESTONE_1 §9): a resource made outside the platform, recorded as the organisation's under a template version and owned by a team. The first step, recorded, writes nothing to the customer's cloud, its tags or any state. Behind RequireModule("provisioning").

GET /provisioning/imports

credential sessionCookiemodule provisioningoperation listResourceImports

Recorded imports

Newest first. A reader sees an import when either of its two teams is in their scope: the team the resource's account belongs to, or the team the import names as the owner. An administrator sees every one. The six key parameters, given together, find the imports of one resource.

Parameters

NameInTypeDescription
accountIdqueryarray of stringRepeatable. Any of the given accounts.
regionquerystring
systemquerystring
servicequerystring
resourceTypequerystring
resourceIdquerystring
statequeryarray of ResourceImportStateRepeatable.
pagequeryinteger1-based. Normalised server-side rather than rejected: < 1 becomes 1, and a page past the end is clamped to totalPages. Deliberately no minimum: a schema constraint would make the generated server reject page=0 before the handler could normalise it, which contradicts the normalisation this very description promises.
pageSizequeryintegerNormalised server-side, not rejected: < 1 becomes 100, > 500 becomes 500. No minimum or maximum, for the same reason as page — a constraint here would turn a clamp into a 400.

Responses

StatusBodyMeaning
200ResourceImportPageA page of imports.
400ErrorResponseinvalid_request — a malformed parameter or body. The batch ingest endpoint uses invalid_batch instead, because a collector branches on it differently: a bad batch is a bug in the collector, while a bad request is usually a bug in the caller's query string.
401ErrorResponseunauthorized — no credential, or one that is not valid.
403ErrorResponsemodule_not_entitled — RequireModule("provisioning") refused this before the handler ran, because the tenant's subscription does not include the provisioning module — or forbidden, when the module is entitled and the user's role or team scope does not permit the operation. Two codes because they are two mechanisms and two remedies: "upgrade your plan" and "ask your admin" are different problems, and the portal renders a locked-module upsell for the first and an access message for the second. On an unsafe request, also origin_not_trusted: the browser boundary refused the request's origin before anything else ran.
500ErrorResponseinternal_error. The message is generic by design: internal error text never reaches a response body. The real error is logged with the request ID.

POST /provisioning/imports

credential sessionCookiemodule provisioningrole engineeroperation createResourceImport

Record an import

Records that a resource the inventory sees is the organisation's, under a template version whose template's resourceTypes contains the resource's type, owned by a team. Nothing is written to the customer's cloud, its tags or any state; drift is not computed for it; it cannot be destroyed from the platform. Making it fully managed is the second step.

What it changes: the resource's page shows its provenance and the provisioned_by relationship; the Provisioned list shows it with origin imported and management recorded; the ownership resolver takes the team as the resource's explicit owner, second only to a resource override; the change feed and the resource's history show it (provisioning_import); the audit log records it (provisioning.import.recorded); and it leaves Drift's unprovisioned list.

templateVersion may be left out: the template's requestable version is pinned — the newest a request could pin now.

Refused: a viewer (403 forbidden, by role); an account outside the caller's team scope — the caller must be a member of the team the account belongs to, administrators included (403 forbidden); an account no team holds (409 account_not_managed); a resource the inventory does not see (404 not_found); a template whose types do not include the resource's, a version of another cloud, an archived template or an archived team (400 invalid_request, naming the field); a version that cannot be requested (409 version_unresolved, 409 version_needs_schema); a resource with a live import (409 already_imported); and a resource a state already holds — the stamp of an apply, finished or in flight, or an adoption — with the owner named (409 resource_already_managed).

Request body application/json

FieldTypeDescription
accountIdrequiredstring
regionrequiredstring
resourceIdrequiredstring
resourceTyperequiredstring
servicerequiredstring
systemrequiredstring
teamIdrequiredstring (uuid)
templateIdstring (uuid)Required unless templateMode is generated, and refused with it.
templateModeResourceImportTemplateModeHow an import is recorded: catalog, under a catalog template's version; generated, under a private template generated for it. Absent on a create means catalog. One of catalog, generated.
templateVersioninteger

Responses

StatusBodyMeaning
201ResourceImportRecorded.
400ErrorResponseinvalid_request — a malformed parameter or body. The batch ingest endpoint uses invalid_batch instead, because a collector branches on it differently: a bad batch is a bug in the collector, while a bad request is usually a bug in the caller's query string.
401ErrorResponseunauthorized — no credential, or one that is not valid.
403ErrorResponsemodule_not_entitled — RequireModule("provisioning") refused this before the handler ran, because the tenant's subscription does not include the provisioning module — or forbidden, when the module is entitled and the user's role or team scope does not permit the operation. Two codes because they are two mechanisms and two remedies: "upgrade your plan" and "ask your admin" are different problems, and the portal renders a locked-module upsell for the first and an access message for the second. On an unsafe request, also origin_not_trusted: the browser boundary refused the request's origin before anything else ran.
404ErrorResponsenot_found
409ErrorResponsealready_imported, resource_already_managed, account_not_managed, version_unresolved or version_needs_schema.
500ErrorResponseinternal_error. The message is generic by design: internal error text never reaches a response body. The real error is logged with the request ID.

GET /provisioning/imports/generation

credential sessionCookiemodule provisioningrole engineeroperation getResourceImportGeneration

Preview generating a template for a resource

What generating a template for one resource would import, before anything is recorded: the addresses and identifiers the server would hand the generate step — the resource and its known companions, the ones the inventory cannot confirm optional — and the gaps the companion table states for its type, each derived on the server from the record's key and additionalInfo and nothing else. Beside them, the agent version that generates (agentVersion) and whether an enabled agent of the account advertises it now (agentReady). An agent advertises it only when it runs inline sources (the owner, 2026-10-11); until one does, an adopt of a generated import waits, queued.

The caller must be a member of the team the resource's account belongs to, as for recording (403 forbidden). Refused: a resource type the companion table does not know, or a record that cannot finish the provider configuration or an identifier (400 invalid_request, naming which); a resource the inventory does not see (404).

Parameters

NameInTypeDescription
accountIdrequiredquerystring
regionrequiredquerystring
systemrequiredquerystring
servicerequiredquerystring
resourceTyperequiredquerystring
resourceIdrequiredquerystring

Responses

StatusBodyMeaning
200ImportGenerationThe preview.
400ErrorResponseinvalid_request — a malformed parameter or body. The batch ingest endpoint uses invalid_batch instead, because a collector branches on it differently: a bad batch is a bug in the collector, while a bad request is usually a bug in the caller's query string.
401ErrorResponseunauthorized — no credential, or one that is not valid.
403ErrorResponsemodule_not_entitled — RequireModule("provisioning") refused this before the handler ran, because the tenant's subscription does not include the provisioning module — or forbidden, when the module is entitled and the user's role or team scope does not permit the operation. Two codes because they are two mechanisms and two remedies: "upgrade your plan" and "ask your admin" are different problems, and the portal renders a locked-module upsell for the first and an access message for the second. On an unsafe request, also origin_not_trusted: the browser boundary refused the request's origin before anything else ran.
404ErrorResponsenot_found
409ErrorResponseaccount_not_managed — no team holds the resource's account.
500ErrorResponseinternal_error. The message is generic by design: internal error text never reaches a response body. The real error is logged with the request ID.

GET /provisioning/imports/prefill

credential sessionCookiemodule provisioningrole engineeroperation getResourceImportPrefill

Pre-fill an adopt from the inventory

The variables one resource's inventory record fills in under one template version, and the identifier each address the version declares would be imported by (MILESTONE_1 §9.4) — derived on the server from the record's key, resourceName and additionalInfo and nothing else. The portal renders the variables in the ordinary generated form and knows nothing of any template.

The caller must be a member of the team the resource's account belongs to, as for recording (403 forbidden). Refused: a version that declares no import of the resource's type, or a record that cannot finish an identifier (400 invalid_request, naming which); a resource the inventory does not see, or no such version (404).

Parameters

NameInTypeDescription
accountIdrequiredquerystring
regionrequiredquerystring
systemrequiredquerystring
servicerequiredquerystring
resourceTyperequiredquerystring
resourceIdrequiredquerystring
templateIdrequiredquerystring (uuid)
versionrequiredqueryinteger

Responses

StatusBodyMeaning
200ImportPrefillThe pre-fill.
400ErrorResponseinvalid_request — a malformed parameter or body. The batch ingest endpoint uses invalid_batch instead, because a collector branches on it differently: a bad batch is a bug in the collector, while a bad request is usually a bug in the caller's query string.
401ErrorResponseunauthorized — no credential, or one that is not valid.
403ErrorResponsemodule_not_entitled — RequireModule("provisioning") refused this before the handler ran, because the tenant's subscription does not include the provisioning module — or forbidden, when the module is entitled and the user's role or team scope does not permit the operation. Two codes because they are two mechanisms and two remedies: "upgrade your plan" and "ask your admin" are different problems, and the portal renders a locked-module upsell for the first and an access message for the second. On an unsafe request, also origin_not_trusted: the browser boundary refused the request's origin before anything else ran.
404ErrorResponsenot_found
409ErrorResponseaccount_not_managed — no team holds the resource's account.
500ErrorResponseinternal_error. The message is generic by design: internal error text never reaches a response body. The real error is logged with the request ID.

POST /provisioning/imports/{importId}/release

credential sessionCookiemodule provisioningrole engineeroperation releaseResourceImport

Release a recorded import

Undoes a recorded import: the resource is nobody's again as far as Import is concerned, and it may be imported again. Audited (provisioning.import.released), in the change feed, and the ownership resolver forgets the team. The row stays, released, as the record.

Refused in any state but recorded (409 import_state_conflict): an import on its way to being fully managed, or managed, leaves by its own request, never by this. The caller must be a member of the team the resource's account belongs to, as for recording (403 forbidden); an import the caller cannot see is 404.

Parameters

NameInTypeDescription
importIdrequiredpathstring (uuid)

Responses

StatusBodyMeaning
200ResourceImportReleased.
400ErrorResponseinvalid_request — a malformed parameter or body. The batch ingest endpoint uses invalid_batch instead, because a collector branches on it differently: a bad batch is a bug in the collector, while a bad request is usually a bug in the caller's query string.
401ErrorResponseunauthorized — no credential, or one that is not valid.
403ErrorResponsemodule_not_entitled — RequireModule("provisioning") refused this before the handler ran, because the tenant's subscription does not include the provisioning module — or forbidden, when the module is entitled and the user's role or team scope does not permit the operation. Two codes because they are two mechanisms and two remedies: "upgrade your plan" and "ask your admin" are different problems, and the portal renders a locked-module upsell for the first and an access message for the second. On an unsafe request, also origin_not_trusted: the browser boundary refused the request's origin before anything else ran.
404ErrorResponsenot_found
409ErrorResponseimport_state_conflict — the import is no longer only recorded.
500ErrorResponseinternal_error. The message is generic by design: internal error text never reaches a response body. The real error is logged with the request ID.

POST /provisioning/imports/{importId}/release-from-state

credential sessionCookiemodule provisioningrole engineeroperation releaseResourceImportFromState

Ask to release an imported resource from state

The way back from imported (MILESTONE_1 §9.3.4): an import whose state holds the resource, and whose retag failed or was cancelled, is released from that state by a request of its own — an adopt with releaseFromState, copying the adopt it undoes, which is approved as any request is. Its agent removes the declared addresses from the state the import wrote (state rm, inspected against forget rules) and changes nothing in the cloud; its success returns the import to recorded and deletes the owner record. Destruction is never the way out.

Refused while the import is not imported, or a request still acts on it (409 request_state_conflict). The caller must be a member of the team the resource's account belongs to (403 forbidden); an import the caller cannot see is 404.

Parameters

NameInTypeDescription
importIdrequiredpathstring (uuid)

Responses

StatusBodyMeaning
201ProvisionRequestCreated in pending_approval.
400ErrorResponseinvalid_request — a malformed parameter or body. The batch ingest endpoint uses invalid_batch instead, because a collector branches on it differently: a bad batch is a bug in the collector, while a bad request is usually a bug in the caller's query string.
401ErrorResponseunauthorized — no credential, or one that is not valid.
403ErrorResponsemodule_not_entitled — RequireModule("provisioning") refused this before the handler ran, because the tenant's subscription does not include the provisioning module — or forbidden, when the module is entitled and the user's role or team scope does not permit the operation. Two codes because they are two mechanisms and two remedies: "upgrade your plan" and "ask your admin" are different problems, and the portal renders a locked-module upsell for the first and an access message for the second. On an unsafe request, also origin_not_trusted: the browser boundary refused the request's origin before anything else ran.
404ErrorResponsenot_found
409ErrorResponserequest_state_conflict — the import is not imported, or a request still acts on it; account_not_managed — no team holds the resource's account.
500ErrorResponseinternal_error. The message is generic by design: internal error text never reaches a response body. The real error is logged with the request ID.
provisioning-blueprints (18)

provisioning-blueprints

Blueprints (MILESTONE_1 §11): graphs of template steps, published and pinned like templates, run across accounts under one approval that binds what is knowable when it is given. Each step is carried out by an ordinary request, created already queued. Behind RequireModule("provisioning").

GET /provisioning/blueprint-runs

credential sessionCookiemodule provisioningoperation listProvisioningBlueprintRuns

List Blueprint runs

Newest first. An administrator reads every run of the organisation; anyone else the runs of their teams.

Parameters

NameInTypeDescription
statequeryarray of BlueprintRunStateRepeatable.
awaitingMyDecisionquerybooleanOnly runs in pending_approval this user may decide — their own exactly when they may approve it themselves because nobody else could — and none for a viewer.
pagequeryinteger1-based. Normalised server-side rather than rejected: < 1 becomes 1, and a page past the end is clamped to totalPages. Deliberately no minimum: a schema constraint would make the generated server reject page=0 before the handler could normalise it, which contradicts the normalisation this very description promises.
pageSizequeryintegerNormalised server-side, not rejected: < 1 becomes 100, > 500 becomes 500. No minimum or maximum, for the same reason as page — a constraint here would turn a clamp into a 400.

Responses

StatusBodyMeaning
200BlueprintRunPageA page of runs.
400ErrorResponseinvalid_request — a malformed parameter or body. The batch ingest endpoint uses invalid_batch instead, because a collector branches on it differently: a bad batch is a bug in the collector, while a bad request is usually a bug in the caller's query string.
401ErrorResponseunauthorized — no credential, or one that is not valid.
403ErrorResponsemodule_not_entitled — RequireModule("provisioning") refused this before the handler ran, because the tenant's subscription does not include the provisioning module — or forbidden, when the module is entitled and the user's role or team scope does not permit the operation. Two codes because they are two mechanisms and two remedies: "upgrade your plan" and "ask your admin" are different problems, and the portal renders a locked-module upsell for the first and an access message for the second. On an unsafe request, also origin_not_trusted: the browser boundary refused the request's origin before anything else ran.
500ErrorResponseinternal_error. The message is generic by design: internal error text never reaches a response body. The real error is logged with the request ID.

POST /provisioning/blueprint-runs

credential sessionCookiemodule provisioningrole engineeroperation createProvisioningBlueprintRun

Ask for a run of a Blueprint version

A run pins one version, supplies the run's variables — validated against the version's variables schema before the run exists, with a per-variable error — and maps every slot to accounts: one for a slot that takes one, one or more for a slot that takes several. Two slots may map to one account.

The run has one team: the team that owns every slot account through team_accounts, which the requester must belong to. Every slot account must be in the registry, enabled, of the slot's cloud, and served by an agent that called within the protocol's heartbeat timeout, registered what the slot's steps need of it (outputs, for a step whose outputs a later step wires) and holds the library release a library step pins. Installing that agent is the one thing a Blueprint cannot do.

Created in pending_approval with its approval deadline set, and the approvers told (provisioning.blueprint_approval_requested; reminded once, half-way to the deadline, and expired at it). Nothing runs before its one approval.

Request body application/json

FieldTypeDescription
blueprintIdrequiredstring (uuid)
namerequiredstringThe run's name; each step's request is named after it.
slotAccountsrequiredobjectEvery slot of the version to its accounts.
teamIdstring (uuid)Optional; when given, it must be the team that owns every slot account.
variablesobject
versionrequiredinteger

Responses

StatusBodyMeaning
201BlueprintRunCreated in pending_approval.
400ProvisionRequestValidationErrorinvalid_request — the name, the slot mapping's shape, or a run variable (details names every variable that failed and why).
401ErrorResponseunauthorized — no credential, or one that is not valid.
403ErrorResponseforbidden — the requester does not belong to the team that owns the accounts.
404ErrorResponsenot_found
409ErrorResponseaccount_not_managed — a slot account is not in the registry, is disabled, is of another cloud than its slot, or is owned by no team or by another team than the run's; slot_account_unqualified — a slot account has no live agent that can run its steps, and the message says why; tag_policy_unsatisfiable; or request_state_conflict — the Blueprint or a step's template is archived.
413ErrorResponseThe request body exceeded the size the server will read. Declared because it is real: a body is otherwise read until the client stops sending, and one caller could exhaust the process. The cap is above any legitimate request — 500 items at the additionalInfo limit is roughly 160 MB — so reaching it means something is wrong with the sender.
415ErrorResponseunsupported_media_type — the body of a browser-facing route is not labelled application/json (a +json suffix and a charset parameter are fine). Refused before the body is read: a form post, or a "simple" cross-site request carrying JSON as text/plain to dodge the preflight, is not something this API reads. Bearer routes are not held to it.
500ErrorResponseinternal_error. The message is generic by design: internal error text never reaches a response body. The real error is logged with the request ID.

GET /provisioning/blueprint-runs/{runId}

credential sessionCookiemodule provisioningoperation getProvisioningBlueprintRun

One run, with what its approval binds

The approver's view. A run's one approval binds what is knowable when it is given (MILESTONE_1 §11.3): per step instance the account, the template version identity, every literal and run variable (a sensitive one masked), and for each input wired from an upstream output the expression — producer step, declared output, consumer variable — and the consumer variable's constraints. A value an upstream apply has not produced yet appears nowhere, because none exists.

A destroy run's binding names its create run, and per step instance the resource its create left (with the digest of the variables that create ran with, which the destroy runs with too) or none — a step that never applied, skipped — and its needs reversed: the steps whose destroys go first. It has no input.

While the run waits for its decision, binding is what an approval would store now and bindingSha256 its digest, which the approval names; once approved, both are what the approval stored.

Responses

StatusBodyMeaning
200BlueprintRunThe run.
401ErrorResponseunauthorized — no credential, or one that is not valid.
403ErrorResponsemodule_not_entitled — RequireModule("provisioning") refused this before the handler ran, because the tenant's subscription does not include the provisioning module — or forbidden, when the module is entitled and the user's role or team scope does not permit the operation. Two codes because they are two mechanisms and two remedies: "upgrade your plan" and "ask your admin" are different problems, and the portal renders a locked-module upsell for the first and an access message for the second. On an unsafe request, also origin_not_trusted: the browser boundary refused the request's origin before anything else ran.
404ErrorResponsenot_found
500ErrorResponseinternal_error. The message is generic by design: internal error text never reaches a response body. The real error is logged with the request ID.

POST /provisioning/blueprint-runs/{runId}/cancel

credential sessionCookiemodule provisioningrole engineeroperation cancelProvisioningBlueprintRun

Cancel a running or failed run

Stops the run's graph (MILESTONE_1 §11.2): the run moves to cancelling, nothing new is queued, and every step whose request exists is stopped as a request's cancel stops it — a queued request is cancelled with its attempt bumped, so no claim can take it, and a running one is cancelling, which its agent learns at its next heartbeat. The run is cancelled once none of its steps is still at work: at once when none was running, otherwise when the last one's agent reports. A step that never started stays waiting.

From running or failed — a failed run's steps still queued for a Resume are cancelled with it — of a create or a destroy run. An engineer of the run's team or an administrator cancels it. A cancelled run is final; what it made is taken down by its destroy run. Audited (provisioning.blueprint_run.cancelled); nobody is notified, since a person did it.

Responses

StatusBodyMeaning
200BlueprintRunCancelled, or cancelling while a step stops.
401ErrorResponseunauthorized — no credential, or one that is not valid.
403ErrorResponseforbidden — not of the run's team, and not an administrator.
404ErrorResponsenot_found
409ErrorResponserequest_state_conflict — the run is not running or failed, the message naming its state.
500ErrorResponseinternal_error. The message is generic by design: internal error text never reaches a response body. The real error is logged with the request ID.

POST /provisioning/blueprint-runs/{runId}/decision

credential sessionCookiemodule provisioningrole engineeroperation decideProvisioningBlueprintRun

Approve or reject a run

One approval for every step. A run is decided exactly once. The approver may not be the requester while anybody else could decide the run, and must belong to the run's team unless an administrator (D1, D8). When nobody else could — no other enabled administrator and no other enabled engineer of the run's team — the requester may approve their own run after confirming their identity within the last 10 minutes, and the run is recorded as self-approved (selfApproved). Approving names the bindingSha256 the approver read: a binding that moved since is refused and nothing is decided.

Approving stores the binding and its digest on the run, moves it to running and, in the same transaction, creates a request for every step whose needs are met — already queued, since the run's decision authorised it, each with an approval in the approver's name. Each is an ordinary request from then on: claimed by its own account's agent, fenced, logged, provenance-stamped. The claim of each also refuses it when the run is no longer running or anything the approval bound has changed, naming what moved. Audited (provisioning.blueprint_run.decided).

The advance. When a step's request succeeds, its outputs — the attempt that succeeded, alone — are resolved into the inputs of the steps it unblocks, every value held to its consumer variable's full constraints exactly as a requester's value is, and those steps are queued, in the transaction of the report and again from a scheduler sweep. The values are recorded on the step (resolvedInputs) and audited (provisioning.blueprint_run.step_resolved). A value missing or refused stops the graph (output_missing, output_rejected), naming the expression and the rule; a failed step stops it too. The run is succeeded when every step is.

Request body application/json

FieldTypeDescription
bindingSha256stringRequired to approve: the digest of the binding the approver read. A binding that moved since is refused (409 binding_changed).
commentstring
decisionrequiredstring enum One of approved, rejected.

Responses

StatusBodyMeaning
200BlueprintRunDecided.
400ErrorResponseinvalid_request — a malformed parameter or body. The batch ingest endpoint uses invalid_batch instead, because a collector branches on it differently: a bad batch is a bug in the collector, while a bad request is usually a bug in the caller's query string.
401ErrorResponseunauthorized — no credential, or one that is not valid.
403ErrorResponseself_approval_refused — the actor is the requester, whatever their role, and somebody else could decide the run, or the decision is a rejection; step_up_required — the requester may approve their own run, but their session has not confirmed its identity in the last 10 minutes; or forbidden, not an approver for the run's team.
404ErrorResponsenot_found
409ErrorResponserequest_state_conflict — the run is not in pending_approval, the message naming its state; a decision at or after expiresAt expires it then and is refused the same way. binding_changed — the binding named is not the run's binding now; read it again.
413ErrorResponseThe request body exceeded the size the server will read. Declared because it is real: a body is otherwise read until the client stops sending, and one caller could exhaust the process. The cap is above any legitimate request — 500 items at the additionalInfo limit is roughly 160 MB — so reaching it means something is wrong with the sender.
415ErrorResponseunsupported_media_type — the body of a browser-facing route is not labelled application/json (a +json suffix and a charset parameter are fine). Refused before the body is read: a form post, or a "simple" cross-site request carrying JSON as text/plain to dodge the preflight, is not something this API reads. Bearer routes are not held to it.
500ErrorResponseinternal_error. The message is generic by design: internal error text never reaches a response body. The real error is logged with the request ID.

POST /provisioning/blueprint-runs/{runId}/destroy

credential sessionCookiemodule provisioningrole engineeroperation destroyProvisioningBlueprintRun

Ask for the destroy run of a create run

A destroy is a run of kind destroy (MILESTONE_1 §11.3): the create run's graph reversed — a step's destroy waits until every step that needed it is destroyed — one child destroy per step against the provisioned resource its create left, with the variables that create ran with, and a step that never applied skipped. It has its own approval, decided as any run's, and it asks for the password (x-step-up), as destructive actions do.

It takes the create run's Blueprint version, team, slot mapping, name and variables; the body is empty. Asked only by a member of the run's team, of a create run that is succeeded, failed or cancelled with no step still started, while something it made remains, and one at a time: not while another destroy run of it is pending_approval, running, cancelling or failed, nor once one has succeeded. The slot accounts where something it made still stands are held again to the run's rules: enabled, of the slot's cloud, owned by the run's team, and served by a live agent that holds the library release a step pins. An account where nothing stands gets no destroy and is not judged.

Once a destroy run is asked — while it waits for its decision, and ever after it is approved — the create run is not resumed. Created in pending_approval, the approvers told; audited (provisioning.blueprint_run.destroy_requested).

Responses

StatusBodyMeaning
201BlueprintRunThe destroy run, in pending_approval.
401ErrorResponseunauthorized — no credential, or one that is not valid.
403ErrorResponsestep_up_required — the session has not re-verified its password recently; forbidden — the requester does not belong to the run's team, or the role does not permit it.
404ErrorResponsenot_found
409ErrorResponserequest_state_conflict, the message naming why: the run is not a settled create run, a step of it is still started, nothing it made remains, or another destroy run of it is open or has succeeded. account_not_managed or slot_account_unqualified — a slot account no longer meets the run's rules, the message naming it.
500ErrorResponseinternal_error. The message is generic by design: internal error text never reaches a response body. The real error is logged with the request ID.

POST /provisioning/blueprint-runs/{runId}/resume

credential sessionCookiemodule provisioningrole engineeroperation resumeProvisioningBlueprintRun

Resume a failed run

Failure stops the graph: nothing new is queued, a step already running finishes, and a step queued and not yet claimed waits. Resume continues it (MILESTONE_1 §11.3). Every failed step's request is queued again exactly as a retry queues a request — attempt bumped, which refuses every write of the attempt that failed — the run returns to running, and every step whose needs are met is queued. An engineer of the run's team or an administrator resumes it. Audited (provisioning.blueprint_run.resumed).

Responses

StatusBodyMeaning
200BlueprintRunRunning again.
401ErrorResponseunauthorized — no credential, or one that is not valid.
403ErrorResponseforbidden — not of the run's team, and not an administrator.
404ErrorResponsenot_found
409ErrorResponserequest_state_conflict, the message naming why: the run is not failed; it stopped at a wired value (output_missing, output_rejected), which its producer's outputs, written once, would give again; a failed step's request was cancelled; a destroy of what a failed step made is requested or done; or a destroy run of it is waiting for its decision or was approved.
500ErrorResponseinternal_error. The message is generic by design: internal error text never reaches a response body. The real error is logged with the request ID.

POST /provisioning/blueprint-runs/{runId}/withdraw

credential sessionCookiemodule provisioningrole engineeroperation withdrawProvisioningBlueprintRun

Withdraw a run awaiting approval

The requester's own, and only from pending_approval.

Responses

StatusBodyMeaning
200BlueprintRunWithdrawn.
401ErrorResponseunauthorized — no credential, or one that is not valid.
403ErrorResponsemodule_not_entitled — RequireModule("provisioning") refused this before the handler ran, because the tenant's subscription does not include the provisioning module — or forbidden, when the module is entitled and the user's role or team scope does not permit the operation. Two codes because they are two mechanisms and two remedies: "upgrade your plan" and "ask your admin" are different problems, and the portal renders a locked-module upsell for the first and an access message for the second. On an unsafe request, also origin_not_trusted: the browser boundary refused the request's origin before anything else ran.
404ErrorResponsenot_found
409ErrorResponserequest_state_conflict — not in pending_approval.
500ErrorResponseinternal_error. The message is generic by design: internal error text never reaches a response body. The real error is logged with the request ID.

GET /provisioning/blueprints

credential sessionCookiemodule provisioningoperation listProvisioningBlueprints

List Blueprints

The catalog's Blueprints by name, each with its latest version. Archived ones only when asked for: a run of one may still stand.

Parameters

NameInTypeDescription
includeArchivedqueryboolean
pagequeryinteger1-based. Normalised server-side rather than rejected: < 1 becomes 1, and a page past the end is clamped to totalPages. Deliberately no minimum: a schema constraint would make the generated server reject page=0 before the handler could normalise it, which contradicts the normalisation this very description promises.
pageSizequeryintegerNormalised server-side, not rejected: < 1 becomes 100, > 500 becomes 500. No minimum or maximum, for the same reason as page — a constraint here would turn a clamp into a 400.

Responses

StatusBodyMeaning
200BlueprintPageA page of Blueprints.
400ErrorResponseinvalid_request — a malformed parameter or body. The batch ingest endpoint uses invalid_batch instead, because a collector branches on it differently: a bad batch is a bug in the collector, while a bad request is usually a bug in the caller's query string.
401ErrorResponseunauthorized — no credential, or one that is not valid.
403ErrorResponsemodule_not_entitled — RequireModule("provisioning") refused this before the handler ran, because the tenant's subscription does not include the provisioning module — or forbidden, when the module is entitled and the user's role or team scope does not permit the operation. Two codes because they are two mechanisms and two remedies: "upgrade your plan" and "ask your admin" are different problems, and the portal renders a locked-module upsell for the first and an access message for the second. On an unsafe request, also origin_not_trusted: the browser boundary refused the request's origin before anything else ran.
500ErrorResponseinternal_error. The message is generic by design: internal error text never reaches a response body. The real error is logged with the request ID.

POST /provisioning/blueprints

credential sessionCookiemodule provisioningrole adminoperation createProvisioningBlueprint

Add a Blueprint to the catalog

Administrators only, as templates are. The Blueprint has no version until one is published. Audited (provisioning.blueprint.created).

Request body application/json

FieldTypeDescription
descriptionstring
namerequiredstring
slugrequiredstring

Responses

StatusBodyMeaning
201BlueprintCreated, with no version yet.
400ErrorResponseinvalid_request — a malformed parameter or body. The batch ingest endpoint uses invalid_batch instead, because a collector branches on it differently: a bad batch is a bug in the collector, while a bad request is usually a bug in the caller's query string.
401ErrorResponseunauthorized — no credential, or one that is not valid.
403ErrorResponsemodule_not_entitled — RequireModule("provisioning") refused this before the handler ran, because the tenant's subscription does not include the provisioning module — or forbidden, when the module is entitled and the user's role or team scope does not permit the operation. Two codes because they are two mechanisms and two remedies: "upgrade your plan" and "ask your admin" are different problems, and the portal renders a locked-module upsell for the first and an access message for the second. On an unsafe request, also origin_not_trusted: the browser boundary refused the request's origin before anything else ran.
409ErrorResponserequest_state_conflict — a Blueprint with this slug exists.
413ErrorResponseThe request body exceeded the size the server will read. Declared because it is real: a body is otherwise read until the client stops sending, and one caller could exhaust the process. The cap is above any legitimate request — 500 items at the additionalInfo limit is roughly 160 MB — so reaching it means something is wrong with the sender.
415ErrorResponseunsupported_media_type — the body of a browser-facing route is not labelled application/json (a +json suffix and a charset parameter are fine). Refused before the body is read: a form post, or a "simple" cross-site request carrying JSON as text/plain to dodge the preflight, is not something this API reads. Bearer routes are not held to it.
500ErrorResponseinternal_error. The message is generic by design: internal error text never reaches a response body. The real error is logged with the request ID.

GET /provisioning/blueprints/{blueprintId}

credential sessionCookiemodule provisioningoperation getProvisioningBlueprint

One Blueprint

Responses

StatusBodyMeaning
200BlueprintThe Blueprint.
401ErrorResponseunauthorized — no credential, or one that is not valid.
403ErrorResponsemodule_not_entitled — RequireModule("provisioning") refused this before the handler ran, because the tenant's subscription does not include the provisioning module — or forbidden, when the module is entitled and the user's role or team scope does not permit the operation. Two codes because they are two mechanisms and two remedies: "upgrade your plan" and "ask your admin" are different problems, and the portal renders a locked-module upsell for the first and an access message for the second. On an unsafe request, also origin_not_trusted: the browser boundary refused the request's origin before anything else ran.
404ErrorResponsenot_found
500ErrorResponseinternal_error. The message is generic by design: internal error text never reaches a response body. The real error is logged with the request ID.

GET /provisioning/blueprints/{blueprintId}/versions

credential sessionCookiemodule provisioningoperation listProvisioningBlueprintVersions

A Blueprint's published versions

Newest first.

Parameters

NameInTypeDescription
pagequeryinteger1-based. Normalised server-side rather than rejected: < 1 becomes 1, and a page past the end is clamped to totalPages. Deliberately no minimum: a schema constraint would make the generated server reject page=0 before the handler could normalise it, which contradicts the normalisation this very description promises.
pageSizequeryintegerNormalised server-side, not rejected: < 1 becomes 100, > 500 becomes 500. No minimum or maximum, for the same reason as page — a constraint here would turn a clamp into a 400.

Responses

StatusBodyMeaning
200BlueprintVersionPageA page of versions.
400ErrorResponseinvalid_request — a malformed parameter or body. The batch ingest endpoint uses invalid_batch instead, because a collector branches on it differently: a bad batch is a bug in the collector, while a bad request is usually a bug in the caller's query string.
401ErrorResponseunauthorized — no credential, or one that is not valid.
403ErrorResponsemodule_not_entitled — RequireModule("provisioning") refused this before the handler ran, because the tenant's subscription does not include the provisioning module — or forbidden, when the module is entitled and the user's role or team scope does not permit the operation. Two codes because they are two mechanisms and two remedies: "upgrade your plan" and "ask your admin" are different problems, and the portal renders a locked-module upsell for the first and an access message for the second. On an unsafe request, also origin_not_trusted: the browser boundary refused the request's origin before anything else ran.
404ErrorResponsenot_found
500ErrorResponseinternal_error. The message is generic by design: internal error text never reaches a response body. The real error is logged with the request ID.

POST /provisioning/blueprints/{blueprintId}/versions

credential sessionCookiemodule provisioningrole adminbody at most 262144 bytesoperation publishProvisioningBlueprintVersion

Publish the next version

Administrators only. A version is a graph — slots, the run's variables schema, and steps, each a template version applied in a slot with its inputs wired from a run variable, a literal or an upstream step's output — and it is immutable once published.

Refused with 400 invalid_request, naming the step, the input and the rule (MILESTONE_1 §11.1): a graph whose needs form a cycle; a step whose template version does not exist, has not resolved, has no variables schema, belongs to an archived template, or is not the identity the body names; a slot whose cloud is not its templates'; an input naming a variable the template version does not declare, of another type, or — for a literal — refused by any of the variable's constraints; a literal wired into a sensitive variable, a run variable that is not sensitive wired into one, or a run variable that may be left out wired into a required one; an output reference to an output its producer does not declare, declares sensitive, or declares of another type — or, from a step that runs once per account of its slot, anything but a string output into a list(string) variable; an output of a step not named in needs; and a required variable no input supplies.

The stored version pins each step's template version by its source identity — the git commit, the inline sha256 or the library hash — which the platform writes; graphSha256 is the digest of the whole. Audited (provisioning.blueprint.published).

Request body application/json

FieldTypeDescription
nonOverlappingCidrsarray of stringRun variables, each a string or list(string) and none sensitive, whose values are address ranges that must not overlap (§11.6): a run is refused when a value is not a range written as its range, such as 10.0.0.0/16, or when two of them overlap.
slotsrequiredarray of BlueprintSlot
stepsrequiredarray of BlueprintStep
variablesVariablesSchemaStored on the version and immutable with it. The portal generates the request form from this; the agent uses it to write generated.auto.tfvars as proper HCL through an encoder, never by string concatenation, because hand-rolled escaping is how tfvars injection happens.

Responses

StatusBodyMeaning
201BlueprintVersionPublished.
400ErrorResponseinvalid_request — a malformed parameter or body. The batch ingest endpoint uses invalid_batch instead, because a collector branches on it differently: a bad batch is a bug in the collector, while a bad request is usually a bug in the caller's query string.
401ErrorResponseunauthorized — no credential, or one that is not valid.
403ErrorResponsemodule_not_entitled — RequireModule("provisioning") refused this before the handler ran, because the tenant's subscription does not include the provisioning module — or forbidden, when the module is entitled and the user's role or team scope does not permit the operation. Two codes because they are two mechanisms and two remedies: "upgrade your plan" and "ask your admin" are different problems, and the portal renders a locked-module upsell for the first and an access message for the second. On an unsafe request, also origin_not_trusted: the browser boundary refused the request's origin before anything else ran.
404ErrorResponsenot_found
409ErrorResponserequest_state_conflict — the Blueprint is archived.
413ErrorResponseThe request body exceeded the size the server will read. Declared because it is real: a body is otherwise read until the client stops sending, and one caller could exhaust the process. The cap is above any legitimate request — 500 items at the additionalInfo limit is roughly 160 MB — so reaching it means something is wrong with the sender.
415ErrorResponseunsupported_media_type — the body of a browser-facing route is not labelled application/json (a +json suffix and a charset parameter are fine). Refused before the body is read: a form post, or a "simple" cross-site request carrying JSON as text/plain to dodge the preflight, is not something this API reads. Bearer routes are not held to it.
500ErrorResponseinternal_error. The message is generic by design: internal error text never reaches a response body. The real error is logged with the request ID.

GET /provisioning/blueprints/{blueprintId}/versions/{version}

credential sessionCookiemodule provisioningoperation getProvisioningBlueprintVersion

One version, with its graph

Responses

StatusBodyMeaning
200BlueprintVersionThe version.
401ErrorResponseunauthorized — no credential, or one that is not valid.
403ErrorResponsemodule_not_entitled — RequireModule("provisioning") refused this before the handler ran, because the tenant's subscription does not include the provisioning module — or forbidden, when the module is entitled and the user's role or team scope does not permit the operation. Two codes because they are two mechanisms and two remedies: "upgrade your plan" and "ask your admin" are different problems, and the portal renders a locked-module upsell for the first and an access message for the second. On an unsafe request, also origin_not_trusted: the browser boundary refused the request's origin before anything else ran.
404ErrorResponsenot_found
500ErrorResponseinternal_error. The message is generic by design: internal error text never reaches a response body. The real error is logged with the request ID.

GET /provisioning/blueprints/{blueprintId}/versions/{version}/slots

credential sessionCookiemodule provisioningoperation getProvisioningBlueprintSlotChoices

The slot picker — which accounts qualify for each slot, and why the others do not

MILESTONE_1 §11.3: "A slot may map only to an account with a live, capable agent; the slot picker says which accounts qualify and why the others do not." For every slot of the version, every account owned by a live team the reader belongs to — every live team's, for an administrator; an archived team leaves the picker — judged by the rules a run's submission holds its mapping to, in their order: enabled, of the slot's cloud, with an agent, that agent called in within the protocol's heartbeat timeout, holding the library release the slot's steps pin, and reporting outputs where a later step wires one. reason is the first rule it fails and detail the sentence a submission naming it would be refused with; both are absent when it qualifies. A run's slot accounts all belong to one team, so each account names its team.

One document per version, not a paged list: a version has at most 8 slots, and each judges the same accounts.

Responses

StatusBodyMeaning
200BlueprintSlotChoicesThe version's slots, each with the reader's accounts judged.
401ErrorResponseunauthorized — no credential, or one that is not valid.
403ErrorResponsemodule_not_entitled — RequireModule("provisioning") refused this before the handler ran, because the tenant's subscription does not include the provisioning module — or forbidden, when the module is entitled and the user's role or team scope does not permit the operation. Two codes because they are two mechanisms and two remedies: "upgrade your plan" and "ask your admin" are different problems, and the portal renders a locked-module upsell for the first and an access message for the second. On an unsafe request, also origin_not_trusted: the browser boundary refused the request's origin before anything else ran.
404ErrorResponsenot_found
500ErrorResponseinternal_error. The message is generic by design: internal error text never reaches a response body. The real error is logged with the request ID.

GET /provisioning/library-blueprints

credential sessionCookiemodule provisioningoperation listProvisioningLibraryBlueprints

The library's Blueprints

The Blueprints the platform's library carries (MILESTONE_1 §11.1: "Library Blueprints live in the library and are enabled the same way, which enables the templates they pin"), by slug — each with its identity, its graph as the library writes it (each step's template a library template at the hash it pins), its README, the templates it pins, and the tenant's Blueprint enabled from it, if there is one.

Parameters

NameInTypeDescription
pagequeryinteger1-based. Normalised server-side rather than rejected: < 1 becomes 1, and a page past the end is clamped to totalPages. Deliberately no minimum: a schema constraint would make the generated server reject page=0 before the handler could normalise it, which contradicts the normalisation this very description promises.
pageSizequeryintegerNormalised server-side, not rejected: < 1 becomes 100, > 500 becomes 500. No minimum or maximum, for the same reason as page — a constraint here would turn a clamp into a 400.

Responses

StatusBodyMeaning
200LibraryBlueprintPageA page of the library's Blueprints, by slug.
400ErrorResponseinvalid_request — a malformed parameter or body. The batch ingest endpoint uses invalid_batch instead, because a collector branches on it differently: a bad batch is a bug in the collector, while a bad request is usually a bug in the caller's query string.
401ErrorResponseunauthorized — no credential, or one that is not valid.
403ErrorResponsemodule_not_entitled — RequireModule("provisioning") refused this before the handler ran, because the tenant's subscription does not include the provisioning module — or forbidden, when the module is entitled and the user's role or team scope does not permit the operation. Two codes because they are two mechanisms and two remedies: "upgrade your plan" and "ask your admin" are different problems, and the portal renders a locked-module upsell for the first and an access message for the second. On an unsafe request, also origin_not_trusted: the browser boundary refused the request's origin before anything else ran.
500ErrorResponseinternal_error. The message is generic by design: internal error text never reaches a response body. The real error is logged with the request ID.

POST /provisioning/library-blueprints/{slug}/enable

credential sessionCookiemodule provisioningrole adminoperation enableProvisioningLibraryBlueprint

Enable a library Blueprint, with the templates it pins

Admin only, no body. In one transaction: every library template the Blueprint pins that the tenant has not enabled is enabled as enableProvisioningLibraryTemplate enables one (version 1 at the library's hash); one the tenant has enabled is wired at its newest version pinning that hash; then the Blueprint is created — its slug, name and description the library's slug, title and summary — and its version 1 published, held to every check a publish is. Either all of it is enabled or none of it is. Audited (provisioning.blueprint.created, naming the library slug, its identity and the templates enabled, then provisioning.blueprint.published).

Responses

StatusBodyMeaning
201BlueprintThe enabled Blueprint. Its latestVersion is 1.
400ErrorResponseinvalid_request — a malformed parameter or body. The batch ingest endpoint uses invalid_batch instead, because a collector branches on it differently: a bad batch is a bug in the collector, while a bad request is usually a bug in the caller's query string.
401ErrorResponseunauthorized — no credential, or one that is not valid.
403ErrorResponsemodule_not_entitled — RequireModule("provisioning") refused this before the handler ran, because the tenant's subscription does not include the provisioning module — or forbidden, when the module is entitled and the user's role or team scope does not permit the operation. Two codes because they are two mechanisms and two remedies: "upgrade your plan" and "ask your admin" are different problems, and the portal renders a locked-module upsell for the first and an access message for the second. On an unsafe request, also origin_not_trusted: the browser boundary refused the request's origin before anything else ran.
404ErrorResponsenot_found
409ErrorResponserequest_state_conflict — a Blueprint is enabled from this library Blueprint already, its slug or a pinned template's slug is taken, or a template it pins is enabled in the tenant but archived, or has no version pinning the hash the Blueprint pins (publish the library's version of it first). The message names the template.
500ErrorResponseinternal_error. The message is generic by design: internal error text never reaches a response body. The real error is logged with the request ID.

POST /provisioning/library-blueprints/{slug}/update

credential sessionCookiemodule provisioningrole adminoperation updateProvisioningLibraryBlueprint

Publish the library Blueprint's current version as the enabled Blueprint's next

Admin only, no body. When a library release changes a library Blueprint, the tenant's Blueprint enabled from it reads updateAvailable in listProvisioningLibraryBlueprints; this publishes the library's current version as that Blueprint's next version, wired to the tenant's templates exactly as enabling wires them (a pinned template not enabled is enabled with it; one enabled is used at its newest version pinning the library's hash). Nothing is published until an administrator asks. One transaction; audited as any publish (provisioning.blueprint.published, naming librarySha256).

Responses

StatusBodyMeaning
201BlueprintVersionThe Blueprint's new version.
400ErrorResponseinvalid_request — a malformed parameter or body. The batch ingest endpoint uses invalid_batch instead, because a collector branches on it differently: a bad batch is a bug in the collector, while a bad request is usually a bug in the caller's query string.
401ErrorResponseunauthorized — no credential, or one that is not valid.
403ErrorResponsemodule_not_entitled — RequireModule("provisioning") refused this before the handler ran, because the tenant's subscription does not include the provisioning module — or forbidden, when the module is entitled and the user's role or team scope does not permit the operation. Two codes because they are two mechanisms and two remedies: "upgrade your plan" and "ask your admin" are different problems, and the portal renders a locked-module upsell for the first and an access message for the second. On an unsafe request, also origin_not_trusted: the browser boundary refused the request's origin before anything else ran.
404ErrorResponsenot_found
409ErrorResponserequest_state_conflict — no Blueprint is enabled from this library Blueprint, it is archived, its latest version is the library's current one already, or a template it pins is enabled but archived or has no version pinning the hash the library Blueprint pins (publish the library's version of it first). The message names the template.
500ErrorResponseinternal_error. The message is generic by design: internal error text never reaches a response body. The real error is logged with the request ID.
provisioning-admin (4)

provisioning-admin

Agent registration tokens. Require the admin role, and behind RequireModule("provisioning").

GET /provisioning/agents

credential sessionCookiemodule provisioningoperation listProvisioningAgents

Registered agents

One agent per cloud account (D4). lastSeenAt, agentVersion and tofuVersion are what the last handshake reported, so an account whose agent has stopped calling is visible here as a stale timestamp rather than as requests that mysteriously never start.

Parameters

NameInTypeDescription
includeRevokedqueryboolean
pagequeryinteger1-based. Normalised server-side rather than rejected: < 1 becomes 1, and a page past the end is clamped to totalPages. Deliberately no minimum: a schema constraint would make the generated server reject page=0 before the handler could normalise it, which contradicts the normalisation this very description promises.
pageSizequeryintegerNormalised server-side, not rejected: < 1 becomes 100, > 500 becomes 500. No minimum or maximum, for the same reason as page — a constraint here would turn a clamp into a 400.

Responses

StatusBodyMeaning
200ProvisioningAgentPageA page of the agent tokens registered for this tenant.
400ErrorResponseinvalid_request — a malformed parameter or body. The batch ingest endpoint uses invalid_batch instead, because a collector branches on it differently: a bad batch is a bug in the collector, while a bad request is usually a bug in the caller's query string.
401ErrorResponseunauthorized — no credential, or one that is not valid.
403ErrorResponsemodule_not_entitled — RequireModule("provisioning") refused this before the handler ran, because the tenant's subscription does not include the provisioning module — or forbidden, when the module is entitled and the user's role or team scope does not permit the operation. Two codes because they are two mechanisms and two remedies: "upgrade your plan" and "ask your admin" are different problems, and the portal renders a locked-module upsell for the first and an access message for the second. On an unsafe request, also origin_not_trusted: the browser boundary refused the request's origin before anything else ran.
500ErrorResponseinternal_error. The message is generic by design: internal error text never reaches a response body. The real error is logged with the request ID.

POST /provisioning/agents

credential sessionCookiemodule provisioningrole adminoperation issueProvisioningAgentToken

Issue an agent token

Admin only. The token is bound to exactly one (tenant, account) at issue, which is what makes a claim account-scoped structurally instead of by a filter a handler could forget. The account is either cloud's: an AWS account, or a Google Cloud project, where the agent runs with the project's attached identity. The binding is the same for both, and so is everything a claim can reach.

The token value is returned once, here, and never again: only sha256(token) is stored. This is the one operator response in the provisioning API that carries a secret, and it is deliberately not reachable from any /provisioning/agent/* operation.

Audited as provisioning.agent.issue on the agent, with its account and name and never the token or its hash. The row is written in the tenant's own audit transaction, which commits first, from inside the control transaction that stores the token: an audit log that cannot be written refuses the issue and no token is stored, and a control commit that then fails leaves an audit row for an issue that did not happen, never an issue with no row.

Request body application/json

FieldTypeDescription
accountIdrequiredstringThe account the agent runs in, as the registry holds it: a 12-digit AWS account id, or a Google Cloud project id — the same two shapes AccountIdPath takes. It must be one of this tenant's accounts and enabled, or the answer is 409 account_not_managed.
namerequiredstringThe agent's name, as the Agents page lists it. Whitespace at either end is removed before it is stored, and what remains must not be empty: a name that is empty or only whitespace is 400 invalid_request, "name: must not be blank".

Responses

StatusBodyMeaning
201ProvisioningAgentTokenIssuedThe agent and its token. Store the token now; it cannot be read back.
400ErrorResponseinvalid_request — a malformed parameter or body. The batch ingest endpoint uses invalid_batch instead, because a collector branches on it differently: a bad batch is a bug in the collector, while a bad request is usually a bug in the caller's query string.
401ErrorResponseunauthorized — no credential, or one that is not valid.
403ErrorResponsemodule_not_entitled — RequireModule("provisioning") refused this before the handler ran, because the tenant's subscription does not include the provisioning module — or forbidden, when the module is entitled and the user's role or team scope does not permit the operation. Two codes because they are two mechanisms and two remedies: "upgrade your plan" and "ask your admin" are different problems, and the portal renders a locked-module upsell for the first and an access message for the second. On an unsafe request, also origin_not_trusted: the browser boundary refused the request's origin before anything else ran.
409ErrorResponseaccount_not_managed — the account is not in this tenant's registry or is disabled. An agent for an account nobody has enabled would poll forever for work that can never be created. An account only another tenant holds is not in this tenant's registry, and gets the same answer as one nobody holds.
413ErrorResponseThe request body exceeded the size the server will read. Declared because it is real: a body is otherwise read until the client stops sending, and one caller could exhaust the process. The cap is above any legitimate request — 500 items at the additionalInfo limit is roughly 160 MB — so reaching it means something is wrong with the sender.
415ErrorResponseunsupported_media_type — the body of a browser-facing route is not labelled application/json (a +json suffix and a charset parameter are fine). Refused before the body is read: a form post, or a "simple" cross-site request carrying JSON as text/plain to dodge the preflight, is not something this API reads. Bearer routes are not held to it.
500ErrorResponseinternal_error. The message is generic by design: internal error text never reaches a response body. The real error is logged with the request ID.

POST /provisioning/agents/install

credential sessionCookiemodule provisioningrole adminoperation installProvisioningAgent

Install an agent in an AWS account in one step

Admin only. Issues an agent token for one of this tenant's AWS accounts, exactly as issueProvisioningAgentToken does, and returns it inside an AWS CloudFormation quick-create link instead of on its own. The link opens the customer's own console on a stack (deploy/cloudformation/agent-runtime.yaml) that runs the pinned agent image on ECS Fargate in that account, on that account's credentials: the platform assumes no role there and never holds a write credential (PROVISIONING_SPEC.md §3). The token is the stack's NoEcho parameter and goes into the account's Secrets Manager.

The link carries a secret, and is returned once. Its token lapses an hour after issue unless an agent has registered with it by then; after registering it is an ordinary agent token, revoked from the Agents page like any other. Only sha256(token) is stored.

A dedicated account only (the owner, 2026-10-03): the request carries dedicatedAccount: true, the admin's confirmation that the account holds nothing but what the agent creates, and the confirmation is written to the audit log.

Request body application/json

FieldTypeDescription
accountIdrequiredstringThe AWS account the agent runs in: one of this tenant's accounts, enabled, or 409 account_not_managed. AWS only: the link is a CloudFormation stack.
dedicatedAccountrequiredbooleanThe admin's confirmation that the account holds nothing but what the agent creates (the owner, 2026-10-03). The one-step install is for a dedicated account only: in a shared account the agent could act through compute already there under a stronger identity. Must be true, or 400 invalid_request; recorded in the audit log. A shared account uses the manual install with a policy the customer scopes.
namerequiredstringThe agent's name, as for ProvisioningAgentTokenCreate.
regionrequiredstringThe AWS region the stack, and so the agent, runs in.

Responses

StatusBodyMeaning
201ProvisioningAgentInstallThe agent, and the link that installs it.
400ErrorResponseinvalid_request — a malformed parameter or body. The batch ingest endpoint uses invalid_batch instead, because a collector branches on it differently: a bad batch is a bug in the collector, while a bad request is usually a bug in the caller's query string.
401ErrorResponseunauthorized — no credential, or one that is not valid.
403ErrorResponsemodule_not_entitled — RequireModule("provisioning") refused this before the handler ran, because the tenant's subscription does not include the provisioning module — or forbidden, when the module is entitled and the user's role or team scope does not permit the operation. Two codes because they are two mechanisms and two remedies: "upgrade your plan" and "ask your admin" are different problems, and the portal renders a locked-module upsell for the first and an access message for the second. On an unsafe request, also origin_not_trusted: the browser boundary refused the request's origin before anything else ran.
409ErrorResponseaccount_not_managed — the account is not this tenant's or is disabled, as for issueProvisioningAgentToken. agent_install_unavailable — this deployment does not offer the install link; issue a token and install by hand.
413ErrorResponseThe request body exceeded the size the server will read. Declared because it is real: a body is otherwise read until the client stops sending, and one caller could exhaust the process. The cap is above any legitimate request — 500 items at the additionalInfo limit is roughly 160 MB — so reaching it means something is wrong with the sender.
415ErrorResponseunsupported_media_type — the body of a browser-facing route is not labelled application/json (a +json suffix and a charset parameter are fine). Refused before the body is read: a form post, or a "simple" cross-site request carrying JSON as text/plain to dodge the preflight, is not something this API reads. Bearer routes are not held to it.
500ErrorResponseinternal_error. The message is generic by design: internal error text never reaches a response body. The real error is logged with the request ID.

DELETE /provisioning/agents/{agentId}

credential sessionCookiemodule provisioningrole adminoperation revokeProvisioningAgentToken

Revoke an agent token

Admin only, and effective on the agent's next call: revokedAt is stamped and every subsequent request from that token is 401. A task the revoked agent still holds is not finished by it — its writes are refused and the scheduler reaps the task after heartbeatTimeoutSeconds, which returns the request to queued.

Idempotent: revoking an already-revoked token succeeds and does not move revokedAt. The row is kept, never deleted, because it is what a task's agentId refers to.

Audited once, as provisioning.agent.revoke on the agent, with its account and name, revokedAt and the session's steppedUpAt. The row is written in the tenant's own audit transaction, which commits first, from inside the control transaction that revokes: an audit log that cannot be written refuses the revocation, and a control commit that then fails leaves an audit row for a revocation that did not happen, never a revocation with no row. A repeat writes no second row.

Responses

StatusBodyMeaning
204—Revoked. Repeating this is not an error.
401ErrorResponseunauthorized — no credential, or one that is not valid.
403ErrorResponsemodule_not_entitled — RequireModule("provisioning") refused this before the handler ran, because the tenant's subscription does not include the provisioning module — or forbidden, when the module is entitled and the user's role or team scope does not permit the operation. Two codes because they are two mechanisms and two remedies: "upgrade your plan" and "ask your admin" are different problems, and the portal renders a locked-module upsell for the first and an access message for the second. On an unsafe request, also origin_not_trusted: the browser boundary refused the request's origin before anything else ran.
404ErrorResponsenot_found
500ErrorResponseinternal_error. The message is generic by design: internal error text never reaches a response body. The real error is logged with the request ID.
provisioning-agent (5)

provisioning-agent

The agent protocol. A product contract with third-party operators: it runs inside customers' AWS accounts, on their upgrade schedule, and a breaking change cannot be shipped by redeploying.

POST /provisioning/agent/claim

credential bearerAgentmodule provisioning1200 requests a minute per addressoperation claimProvisioningWork

Long-poll for work

Outbound-only, long-polled. No customer is ever asked for an inbound firewall rule, which is why this is a poll and not a webhook.

Replay safety. The agent generates claimId before its first attempt and reuses it on every retry of that attempt. Repeating a claimId returns the same work item, never a second one: a lost response must not cost the agent a claim it will then never heartbeat. A claimId that was used for a different work item is claim_conflict.

Fencing. A claim stamps the request's current attempt onto the task it creates, selecting the request FOR UPDATE SKIP LOCKED. It does not change it. The increment belongs to every event that invalidates a task — reap, cancel, retry — and happens in the same transaction as the invalidation (§6.3), so a task is fenced at the instant it is lost rather than at the convenience of whatever agent polls next. Every later write from this agent carries the stamped attempt, and every write whose attempt is not the request's current one is refused task_superseded. Two instances of one agent — a restart overlapping its predecessor, a duplicated deployment — is the case this defends, and it does happen.

Scope. The claim is account-scoped by the token, structurally. There is no account parameter to get wrong.

One shape, three actions. A claimed task carries an action. create and destroy are an apply. resolve is a git ref to turn into a commit SHA with the customer's own git credentials, which the platform does not have and will never ask for (D7); it is reported on the terminal status like any other outcome, which is why there is no resolve endpoint (§6.6). A fourth, adopt — an apply in steps: import, retag, or release from state — arrives with Import's requests, and is offered only to an agent that advertised adopt and plan-inspection. A fifth, stop — an action on an existing resource — is a create of a library hash in a state of its own, with its permitted effects, offered only to an agent that advertised stop and plan-inspection (agent-protocol.md §3.7).

What the claiming agent does decides what it is offered. A request that needs something of its agent — an adopt, or a step whose outputs another step consumes — is offered only to a claim whose version registered the capability for it; to any other it is invisible, and it waits, queued, without holding up the requests behind it (as a library request waits for a release that holds its hash).

Request body application/json

FieldTypeDescription
agentVersionrequiredstring
claimIdrequiredstring (uuid)Generated by the agent before its first attempt and reused on every retry of that attempt. This is the idempotency key for claiming: repeating it returns the same work item, never a second one. Minting it inside the retry loop is the mistake that turns one lost response into a task nobody heartbeats.
libraryReleaseintegerThe newest release of the built-in library this agent's image carries (MILESTONE_1 §8.1). Send it only if the agent runs library templates — it also advertises the library capability — and on every claim as well as at register: a task pinned to a library hash is offered only to a claim whose release holds that hash. Omitted, the agent is never offered a library task.
sealingKeyAgentSealingKeyThe public half of the agent's sealing key (agent-protocol.md §3.10): standard base64 of the 32 bytes of an X25519 public key. Sent at register beside sealed-values, which stores it, and on every claim, which hands only values sealed to it. The private half never leaves the agent.
tofuVersionrequiredstring
waitSecondsintegerHow long the agent is willing to hold the poll open. Clamped server-side to claimPollTimeoutSeconds; it is a hint, not a demand.

Responses

StatusBodyMeaning
200AgentTaskWork to do. Repeating the same claimId returns this same item, and the response is identical either way — the agent cannot tell whether its first attempt was lost, and must not need to.
204—Nothing to do. The long poll expired without work appearing; the agent polls again. Not an error, and not a reason to back off.
400ErrorResponseinvalid_request — a malformed parameter or body. The batch ingest endpoint uses invalid_batch instead, because a collector branches on it differently: a bad batch is a bug in the collector, while a bad request is usually a bug in the caller's query string.
401ErrorResponseunauthorized — no agent token, an unknown token, or a revoked token. Fails closed (§3.7): there is no degraded mode and no anonymous claim. An account removed from the tenant's accounts registry takes its agent tokens with it, so its agent is answered here too, on every call — and a new token is what it needs, since connecting the account again does not bring the old one back.
403ErrorResponseaccount_not_managed — the token is valid, but the account it is bound to is disabled in the tenant's accounts registry — or module_not_entitled, when the tenant's subscription no longer includes provisioning. Distinct from 401 on purpose: the agent's own credential is fine, so an operator should look at the account, not reissue a token. The agent waits it out, a minute or more at a time, and then takes the same step again — register again if that was refused, a new claim if a claim was — logging the reason each time; nothing is silently dropped. Both are answered by register and claim only (x-entitlement-access: start for the licence; the account's own check for account_not_managed). A task the agent already holds settles whatever the licence or the account says since: its heartbeat, logs and status keep answering (settle), so an apply that crossed the change reports its outcome rather than leaving cloud and platform state disagreeing. A claim replayed with the claimId that created it is answered with that task, as any replay is.
409ErrorResponseclaim_conflict — this claimId was already used for a different work item. The agent minted a key inside its retry loop instead of before it, or reused one across attempts. Not retryable: a new claimId is required.
413ErrorResponseThe request body exceeded the size the server will read. Declared because it is real: a body is otherwise read until the client stops sending, and one caller could exhaust the process. The cap is above any legitimate request — 500 items at the additionalInfo limit is roughly 160 MB — so reaching it means something is wrong with the sender.
426ErrorResponseagent_version_unsupported — the agent is below minAgentVersion and may not claim work. The message names the required version. Deliberately not a silent no-work 204: an operator whose agents have aged out sees requests that never start and nothing that says why. The agent logs this and keeps polling; nothing is dropped, and the moment it is upgraded it picks the queue up.
429ErrorResponseRate limited.
500ErrorResponseinternal_error. The message is generic by design: internal error text never reaches a response body. The real error is logged with the request ID.

POST /provisioning/agent/register

credential bearerAgentmodule provisioning1200 requests a minute per addressoperation registerProvisioningAgent

Version handshake and policy fetch

The first call an agent makes, and the only place it learns the operating parameters it must respect: how often to heartbeat, how long a claim may hang, how large a log batch may be, and how many attempts a request gets. Hard-coding any of those in the agent is how a fleet becomes impossible to tune.

Naturally idempotent — it stores the reported versions on the agent's token row and returns policy. Safe to call on every start, and on reconnect after a long outage.

Version policy is report-and-warn, and refuses only below a declared minimum (D16). An agent below minAgentVersion still registers, so that an operator can see it in the Agents page and know what to upgrade; it is claim that refuses it, with 426.

Request body application/json

FieldTypeDescription
agentVersionrequiredstring
capabilitiesarray of stringOptional feature flags an agent advertises, for forward compatibility. An unknown capability is ignored, never rejected: the fleet is upgraded by customers on their own schedule, so a newer agent must always be able to talk to an older platform. The platform stores them with this registration's agentVersion and acts on them only for claims that carry that same version, so two versions sharing one token during an upgrade are each asked only for what they said they do. Acted on today:
Capability Means The platform then
plan-inspection the agent inspects its saved plan against permittedEffects before it applies, and reports planInspection hands create and destroy tasks their permittedEffects, and records one succeeded only with an accepted inspection
adopt the agent runs an adopt task's three steps — import, retag, release — and reports the steps it recorded (MILESTONE_1 §9.5); agent 0.3.0 offers it adopt tasks, and only when it also advertises plan-inspection: an agent without both is never offered one
outputs after a create's successful apply the agent reads the outputs it was handed and reports them (MILESTONE_1 §11.4); agent 0.3.0 hands create tasks the outputs their version declares, and offers a request whose outputs another step needs only to an agent that advertises it
stop the agent runs a stop — an action on an existing resource — as a create of its library hash in a state of its own (agent-protocol.md §3.7) offers it stop tasks, and only when it also advertises plan-inspection: an agent without both is never offered one
generate the agent runs an adopt's generate step — tofu plan -generate-config-out for the imports, an absent optional companion dropped, every sensitive attribute a variable, a re-plan held to the import rules, nothing applied — and reports generated (agent-protocol.md §3.9); agent 0.3.28 offers it an adopt of a generated import, which records generate among what it needs, beside adopt and plan-inspection — its generate step and the import that follows, whose template is inline; advertise it only when the agent runs inline sources (the owner 2026-10-11)
sealed-values the agent holds a sealing key, sends its public half as sealingKey here and on every claim, opens the secret values a requester sealed to it for an adopt of a generated import, writes them only into a 0600 variables file for the attempt, and never logs, reports or digests them (agent-protocol.md §3.10); agent 0.3.29 offers it an adopt whose configuration needs secret values, which records sealed-values among what it needs, and only with values sealed to the key its claim names, for the request's current attempt; advertise it only with inline sources allowed
azure-resource-groups the agent's Azure runners are scoped to resource groups (PANORAMA_RUNNER_AZURE_RESOURCE_GROUPS): it runs a template of the tenant's own (git or inline) in Azure, and refuses before OpenTofu runs a request naming a group outside its runner's list, or none; advertise it only with the setting offers a request whose template is the tenant's own (git or inline, not one Import generated) for an Azure account — create, destroy, adopt or release — only to an agent that advertises it, recorded among what the request needs at submit and by a Blueprint's steps; the request page names the setting (agentSettingsNeeded), never a version (the owner 2026-10-27)
libraryReleaseintegerThe newest release of the built-in library this agent's image carries (MILESTONE_1 §8.1). Send it only if the agent runs library templates — it also advertises the library capability — and on every claim as well as at register: a task pinned to a library hash is offered only to a claim whose release holds that hash. Omitted, the agent is never offered a library task.
sealingKeyAgentSealingKeyThe public half of the agent's sealing key (agent-protocol.md §3.10): standard base64 of the 32 bytes of an X25519 public key. Sent at register beside sealed-values, which stores it, and on every claim, which hands only values sealed to it. The private half never leaves the agent.
tofuVersionrequiredstringThe OpenTofu binary pinned in this agent's image. OpenTofu rather than Terraform (D13): MPL-2.0, so a commercial provisioning product raises no BUSL "competitive offering" question, and existing Terraform templates run unchanged.

Responses

StatusBodyMeaning
200AgentRegistrationResultRegistered. accountId is echoed from the token, not accepted from the request — an agent that finds it disagrees with its own configuration is pointed at the wrong account and should stop.
400ErrorResponseinvalid_request — a malformed parameter or body. The batch ingest endpoint uses invalid_batch instead, because a collector branches on it differently: a bad batch is a bug in the collector, while a bad request is usually a bug in the caller's query string.
401ErrorResponseunauthorized — no agent token, an unknown token, or a revoked token. Fails closed (§3.7): there is no degraded mode and no anonymous claim. An account removed from the tenant's accounts registry takes its agent tokens with it, so its agent is answered here too, on every call — and a new token is what it needs, since connecting the account again does not bring the old one back.
403ErrorResponseaccount_not_managed — the token is valid, but the account it is bound to is disabled in the tenant's accounts registry — or module_not_entitled, when the tenant's subscription no longer includes provisioning. Distinct from 401 on purpose: the agent's own credential is fine, so an operator should look at the account, not reissue a token. The agent waits it out, a minute or more at a time, and then takes the same step again — register again if that was refused, a new claim if a claim was — logging the reason each time; nothing is silently dropped. Both are answered by register and claim only (x-entitlement-access: start for the licence; the account's own check for account_not_managed). A task the agent already holds settles whatever the licence or the account says since: its heartbeat, logs and status keep answering (settle), so an apply that crossed the change reports its outcome rather than leaving cloud and platform state disagreeing. A claim replayed with the claimId that created it is answered with that task, as any replay is.
413ErrorResponseThe request body exceeded the size the server will read. Declared because it is real: a body is otherwise read until the client stops sending, and one caller could exhaust the process. The cap is above any legitimate request — 500 items at the additionalInfo limit is roughly 160 MB — so reaching it means something is wrong with the sender.
429ErrorResponseRate limited.
500ErrorResponseinternal_error. The message is generic by design: internal error text never reaches a response body. The real error is logged with the request ID.

POST /provisioning/agent/tasks/{taskId}/heartbeat

credential bearerAgentmodule provisioning1200 requests a minute per addressoperation heartbeatProvisioningTask

Liveness, and the cancellation channel

Sent every heartbeatIntervalSeconds for each task the agent holds. A task whose heartbeatAt falls behind heartbeatTimeoutSeconds is reaped: in one transaction the task becomes abandoned, the request's attempt is incremented — which is what fences the abandoned task, immediately, with no window in which its writes still pass (§6.3) — and the request returns to queued, unless it has used up maxAttempts, in which case it fails with failureClass = timeout.

Cancellation rides this response. cancelRequested is how an agent learns a human asked it to stop; there is no separate polled endpoint, because the agent is already calling this on a known interval and one fewer endpoint is one fewer thing a third-party implementer can get wrong. On seeing it the agent stops at the next safe point and reports cancelled, and it does not destroy what has already been created.

Naturally idempotent. It is also fenced: a heartbeat carrying a stale attempt is task_superseded, which is precisely how a process that has not noticed it lost its task finds out — and so is a heartbeat for a task the platform ended without moving the attempt: the reap of a request's last attempt, or of a cancel. A claim that takes over the lease of a task that stopped heartbeating ends that task the same way, in its own transaction. The heartbeat an agent sends immediately before apply is its confirmation that the task is still its own; task_superseded there means apply nothing.

Request body application/json

FieldTypeDescription
attemptrequiredintegerThe fence.
stagestring

Responses

StatusBodyMeaning
200AgentHeartbeatResultLiveness recorded. Act on cancelRequested.
400ErrorResponseinvalid_request — a malformed parameter or body. The batch ingest endpoint uses invalid_batch instead, because a collector branches on it differently: a bad batch is a bug in the collector, while a bad request is usually a bug in the caller's query string.
401ErrorResponseunauthorized — no agent token, an unknown token, or a revoked token. Fails closed (§3.7): there is no degraded mode and no anonymous claim. An account removed from the tenant's accounts registry takes its agent tokens with it, so its agent is answered here too, on every call — and a new token is what it needs, since connecting the account again does not bring the old one back.
403ErrorResponseaccount_not_managed — the token is valid, but the account it is bound to is disabled in the tenant's accounts registry — or module_not_entitled, when the tenant's subscription no longer includes provisioning. Distinct from 401 on purpose: the agent's own credential is fine, so an operator should look at the account, not reissue a token. The agent waits it out, a minute or more at a time, and then takes the same step again — register again if that was refused, a new claim if a claim was — logging the reason each time; nothing is silently dropped. Both are answered by register and claim only (x-entitlement-access: start for the licence; the account's own check for account_not_managed). A task the agent already holds settles whatever the licence or the account says since: its heartbeat, logs and status keep answering (settle), so an apply that crossed the change reports its outcome rather than leaving cloud and platform state disagreeing. A claim replayed with the claimId that created it is answered with that task, as any replay is.
404ErrorResponsenot_found
409ErrorResponsetask_superseded — this task's attempt is not the request's current attempt, or the platform ended the task (abandoned): it was reaped, cancelled or retried, or ended by the claim that took over its lease after its heartbeats stopped, and this process has not noticed it lost it (§6.3). Every agent write is fenced this way, so a late write from a dead or duplicated process changes nothing. The agent's correct response is to abandon the task and go back to claiming, not to retry: retrying identically fails identically.
413ErrorResponseThe request body exceeded the size the server will read. Declared because it is real: a body is otherwise read until the client stops sending, and one caller could exhaust the process. The cap is above any legitimate request — 500 items at the additionalInfo limit is roughly 160 MB — so reaching it means something is wrong with the sender.
429ErrorResponseRate limited.
500ErrorResponseinternal_error. The message is generic by design: internal error text never reaches a response body. The real error is logged with the request ID.

POST /provisioning/agent/tasks/{taskId}/logs

credential bearerAgentmodule provisioningbody at most 8.388608e+06 bytesoperation appendProvisioningTaskLogs

Append a batch of log lines

Lines are scrubbed by the agent before they are sent — the platform stores what it is given and gets no second chance. Scrubbing is exact for every variable the version's schema marks sensitive, and pattern based for AWS key IDs, assignment forms and PEM blocks (PROVISIONING_SPEC.md §7.5). It is deliberately not the donor's bare 40-character base64 pattern, which ate hashes, ARNs and resource IDs.

Replay safety has two independent layers, and both are needed. The Idempotency-Key header makes a repeated batch return the stored acknowledgement — the same counts, unchanged — without writing. The (taskId, seq) primary key makes a repeated line impossible even if it arrives in a differently-shaped batch, which is what happens when an agent restarts mid-flush and re-chunks its buffer.

seq is agent-assigned and monotonic per task, and it is the ordering the portal renders. Gaps are permitted and mean lines were dropped by the agent's own buffer under pressure; they are not an error and must not stall the stream.

Parameters

NameInTypeDescription
Idempotency-KeyrequiredheaderstringStable for the lifetime of one batch, across every retry of it. Generate it before the retry loop: a key minted per attempt makes a retry look like new data, which is the one mistake this header exists to prevent. Repeating a key with the same batch returns the stored acknowledgement and writes nothing; repeating it with a different batch is idempotency_key_reuse. Over-length is 400 invalid_request, enforced by the server and not merely declared — a longer key reaches the database as an index-size error inside the transaction, which would report a malformed request as the 500 an agent retries.

Request body application/json

FieldTypeDescription
attemptrequiredintegerThe fence.
linesrequiredarray of AgentLogLineAt most maxLogBatchLines from the policy; 500 is the ceiling this contract allows regardless.

Responses

StatusBodyMeaning
200AgentLogBatchAcceptedStored, or the stored acknowledgement replayed unchanged.
400ErrorResponseinvalid_request — the body is not a batch, idempotency_key_required when the header is missing, one line's message exceeds maxLogLineBytes, or one line's createdAt is more than a day behind the platform's clock or an hour ahead of it — a clock that far off has no partition to land in. A line the platform will not take as sent is a record, not a size: an agent that has halved its batch down to that one line replaces it with a note and moves on, rather than repeating the refusal forever with every later line held behind it. The message names the offending line index and field and never echoes the value: an error body must not become the exfiltration channel that the scrubber exists to close.
401ErrorResponseunauthorized — no agent token, an unknown token, or a revoked token. Fails closed (§3.7): there is no degraded mode and no anonymous claim. An account removed from the tenant's accounts registry takes its agent tokens with it, so its agent is answered here too, on every call — and a new token is what it needs, since connecting the account again does not bring the old one back.
403ErrorResponseaccount_not_managed — the token is valid, but the account it is bound to is disabled in the tenant's accounts registry — or module_not_entitled, when the tenant's subscription no longer includes provisioning. Distinct from 401 on purpose: the agent's own credential is fine, so an operator should look at the account, not reissue a token. The agent waits it out, a minute or more at a time, and then takes the same step again — register again if that was refused, a new claim if a claim was — logging the reason each time; nothing is silently dropped. Both are answered by register and claim only (x-entitlement-access: start for the licence; the account's own check for account_not_managed). A task the agent already holds settles whatever the licence or the account says since: its heartbeat, logs and status keep answering (settle), so an apply that crossed the change reports its outcome rather than leaving cloud and platform state disagreeing. A claim replayed with the claimId that created it is answered with that task, as any replay is.
404ErrorResponsenot_found
409ErrorResponsetask_superseded (the fence, or a task that finished more than fifteen minutes ago: its log is closed) or idempotency_key_reuse (this key was used for a different batch). Neither is retryable. A task keeps at most 64 MiB of message; the line that would cross it is kept as a note saying so, and later lines are acknowledged without being stored.
413ErrorResponseinvalid_request — more than maxLogBatchLines lines in one batch, or a body over the route's byte limit. Split the batch and send again; the seqs are unchanged, so the halves land exactly once.
429ErrorResponseRate limited.
500ErrorResponseinternal_error. The message is generic by design: internal error text never reaches a response body. The real error is logged with the request ID.

POST /provisioning/agent/tasks/{taskId}/status

credential bearerAgentmodule provisioningoperation reportProvisioningTaskStatus

Report a stage or a terminal state

The one write that moves a task, and through it the request.

Replay safety. The target state plus attempt is the key. Setting the state the task already has returns 200 with the stored response, byte for byte, and writes nothing — the agent cannot tell a replay from a first attempt, and must not need to. Setting a different terminal state after a terminal state is task_state_conflict — that is a bug in the agent, not a retry, and silently accepting it would let a lost response rewrite an outcome somebody has already been notified about.

Fencing. attempt must equal the request's current attempt or the write is refused task_superseded (§6.3). The attempt an agent holds goes stale when the task is reaped, cancelled or retried, and it goes stale in that transaction — not at some later claim.

Resolutions report here too. A task with action: resolve finishes by reporting succeeded with gitSha, and the handler writes templateVersions.gitSha and resolveState: resolved in the same transaction as the request's terminal state (§6.6). gitSha is write-once: the same SHA again is a replay and returns the stored response; a different SHA for an already-resolved version is resolution_conflict and the stored SHA does not move. A failed resolution with failureClass: configuration sets resolveState: failed and stores the agent's own scrubbed git error.

Partial applies. A failure that created something reports partialApply = true with the stateBackend location. Nothing is ever auto-destroyed (D10): infrastructure that exists and is not tracked is a problem for a human, and quietly destroying it is a worse one.

Outputs (MILESTONE_1 §11.4). A create handed outputs reports the ones it found on its terminal succeeded report, in outputs, and names in withheldOutputs the ones it found and did not report. They are stored per attempt, write-once: the same outputs again is a replay and returns the stored response; different ones are outputs_conflict, and the recorded ones do not move. A report naming an output the task was not handed, or a value not of its declared type, is invalid_request.

Adopt steps (MILESTONE_1 §9.5). An adopt task says, in step, which step a report records: imported on the running report its import step makes once the import is committed, tagged or released on its terminal succeeded. Any other task reporting a step is invalid_request.

Failure classes are six, and the pair that earns its keep is configuration against infrastructure: "the template author must fix this" and "your cloud account must be fixed" go to different people, and collapsing them sends every failure to the wrong one.

Request body application/json

FieldTypeDescription
agentVersionrequiredstring
attemptrequiredintegerThe fence, from the claimed task. A value that is not the request's current attempt is task_superseded, and that is the answer, not an error to retry past.
createdCountinteger
destroyedCountinteger
failureClassProvisionFailureClassRequired when state is failed. Pick honestly: configuration sends the report to the template's author, infrastructure sends it to whoever owns the cloud account, and getting it wrong wastes the time of somebody who cannot fix it. restored is the platform's alone, and a report of it is 400.
failureMessagestringAlready scrubbed when it is sent. The platform stores it verbatim and shows it to humans; it has no second chance to redact.
finalLogSeqinteger (int64)Terminal reports only. The log-completion watermark: the highest seq this agent has assigned for the task, which is the last line it will ever send — -1 when it buffered none. Send it on every terminal report, even though the lines behind it may still be in your buffer: the reference agent reports first and drains afterwards, and the platform accepts the drain after the task is terminal. The platform stores it once, in the transaction that closes the task, and the operator's log read (getProvisioningTaskLogs) reports the log complete only when a line at this seq has been paged out — never from "the task is terminal and the page was short", which is how a viewer used to miss the tail. If the line never arrives (a drain that failed, a buffer that was lost), the read reports incomplete after a bounded grace rather than waiting forever. Absent, the platform falls back to logLineCount - 1 when logLineCount is positive (every reference agent has sent it), and otherwise calls the log unverified once the grace passes. Sending this field is what tells the difference between "all here" and "nobody said".
generatedAgentGeneratedThe terminal succeeded of an adopt's generate step only, with step generated and the re-plan's accepted planInspection. The platform stores it as a private template version and the request moves to pending_approval on it (AgentGenerated); from any other report it is refused.
gitShastringresolve only, and required when it succeeds. The commit the ref resolved to, from git ls-remote. SHA-1 or SHA-256, because git repositories exist in both object formats and a 40-character-only rule would lock out the newer ones. Write-once. The same SHA again is a replay and returns the stored response; a different SHA for an already-resolved version is 409 resolution_conflict and the stored value does not move — it would otherwise silently change what a pending approval is about to approve.
logLineCountinteger (int64)Every line the agent buffered for this task, dropped ones included (Rule 3), so the portal can show a loss as a number. Kept in the permanent summary.
outputsobjectThe terminal succeeded of a create handed outputs (MILESTONE_1 §11.4). Every output it was handed that tofu output -json found, keyed by name, of the type it was handed — a tuple of strings is a list(string), an object of strings a map(string). Never one OpenTofu marks sensitive: that one's name goes in withheldOutputs. Nor one the log scrubber's rules would change, whose name goes there too: an output is reported as it is or not at all. At most 32, and at most 16 KiB between them, counted as the canonical JSON encoding of each value; one that does not fit is withheld. Recorded per attempt, write-once: the same outputs again for the same attempt is a replay; different ones are outputs_conflict. A request's outputs are those of the attempt that succeeded, and they are what a later step consumes.
overriddenTagsarray of stringThe stamp keys the template's own provider defaults also set literally, which the stamp then won. A tenant's tag policy may require keys the template's author also writes — they are the tenant's own vocabulary, not the panorama: namespace — and the stamp is merged last, so the request's value is the one the resource carries. These are the keys whose template value did not survive, reported so the request page can say which rather than leaving the author to find out from a plan. Keys a template computes are invisible to the parser and are not listed, the same limit the stamp itself has (§9). Omitted when nothing was overridden.
partialApplybooleanThe apply failed after creating something. Report it truthfully even when it is embarrassing: the alternative is infrastructure nobody knows exists. Nothing is auto-destroyed on the strength of this.
planDigeststringsha256 of the plan output. Kept in the permanent summary.
planInspectionAgentPlanInspectionTerminal reports of a task handed permittedEffects. The verdict of the plan against them, stored with the task. A succeeded report of such a task without an accepted inspection is 400 invalid_request — the platform never records success without it — and so is an inspection that does not describe the task: sent for a task handed no effects, naming other effects, accepted while listing violations, or refused while listing none. A refused inspection is reported with failed and failureClass: configuration, and nothing was applied. An adopt reports one inspection per step it recorded: its import step's on the imported report, and the last step's on the terminal one.
provenanceTaggedByTypeobjectBeside provenanceTaggedCount, on a create's and an adopt's tagged report (agent 0.3.0). The same count per OpenTofu resource type — {"aws_vpc": 1, "aws_subnet": 4, "aws_iam_role": 1} — read from the same tofu show -json. Each key is a resource type as a resource block names it (an identifier of at most 128 characters), each count at least 1, and the counts sum to provenanceTaggedCount; anything else is 400 invalid_request. A type with nothing stamped is not named. Drift compares with the inventory only the types a registered collector emits and reads the tags of, kind by kind — the inventory collects no roles, log groups, route tables or listeners, and finds Lambda functions without their tags. Omit it and the create is recorded with its total alone, and drift does not judge it at all, saying so on the resource (driftScope.judged false), since nothing says which of its resources the inventory could see.
provenanceTaggedCountintegerResources carrying panorama:request_id that the agent counted in state after a successful apply, by reading tofu show -json locally — no extra cloud API call. It becomes expectedCount, the baseline the drift diff measures against. Zero on a create that produced resources means the stamp did not land: the row is stored provenanceTagged = false and excluded from drift with a visible badge, never silently omitted.
stagestring
stageDurationsMsobject
staterequiredProvisionTaskStateOne attempt at one request. abandoned is what the reaper writes when a task's heartbeat goes stale — distinct from failed, because nobody reported a failure: the process stopped talking, and what it did before it stopped is unknown. One of claimed, running, succeeded, failed, cancelled, abandoned.
stateBackendstringWhere this task's Terraform state lives, so an operator can act on a partial apply. A location, never contents and never a credential — the platform does not read state (D3).
stepAgentAdoptStepRecordedadopt only (MILESTONE_1 §9.5). The step this report records: imported on a running report, with the import step's accepted planInspection and its stateBackend; tagged or released on the terminal succeeded. Report imported and have it answered before the retag begins: a task whose imported report was never accepted fails, rather than retagging a resource the platform does not know is in state. A report of any other task carrying one is invalid_request. generated on the terminal succeeded of a generate step, with generated.
tofuVersionrequiredstring
withheldOutputsarray of stringThe outputs this create was handed and found, and did not report — marked sensitive, not of the declared type, one the scrubber would change, or past the size limit. A handed output in neither list was not found. Names only, never a value.

Responses

StatusBodyMeaning
200AgentTaskStatusAcceptedRecorded, or the stored response replayed unchanged. There is nothing in the body that distinguishes the two, deliberately.
400ErrorResponseinvalid_request — a malformed parameter or body. The batch ingest endpoint uses invalid_batch instead, because a collector branches on it differently: a bad batch is a bug in the collector, while a bad request is usually a bug in the caller's query string.
401ErrorResponseunauthorized — no agent token, an unknown token, or a revoked token. Fails closed (§3.7): there is no degraded mode and no anonymous claim. An account removed from the tenant's accounts registry takes its agent tokens with it, so its agent is answered here too, on every call — and a new token is what it needs, since connecting the account again does not bring the old one back.
403ErrorResponseaccount_not_managed — the token is valid, but the account it is bound to is disabled in the tenant's accounts registry — or module_not_entitled, when the tenant's subscription no longer includes provisioning. Distinct from 401 on purpose: the agent's own credential is fine, so an operator should look at the account, not reissue a token. The agent waits it out, a minute or more at a time, and then takes the same step again — register again if that was refused, a new claim if a claim was — logging the reason each time; nothing is silently dropped. Both are answered by register and claim only (x-entitlement-access: start for the licence; the account's own check for account_not_managed). A task the agent already holds settles whatever the licence or the account says since: its heartbeat, logs and status keep answering (settle), so an apply that crossed the change reports its outcome rather than leaving cloud and platform state disagreeing. A claim replayed with the claimId that created it is answered with that task, as any replay is.
404ErrorResponsenot_found
409ErrorResponsetask_superseded (the fence rejected this attempt), task_state_conflict (a different terminal state was already recorded), resolution_conflict (a different gitSha is already recorded for the version this resolve task pins) or outputs_conflict (a replayed terminal report whose outputs or withheldOutputs differ from the ones already recorded for this attempt). Never repeat the same request after any of them — the write will fail identically forever, and repeating it builds a hot loop against a platform that is behaving correctly.
413ErrorResponseThe request body exceeded the size the server will read. Declared because it is real: a body is otherwise read until the client stops sending, and one caller could exhaust the process. The cap is above any legitimate request — 500 items at the additionalInfo limit is roughly 160 MB — so reaching it means something is wrong with the sender.
429ErrorResponseRate limited.
500ErrorResponseinternal_error. The message is generic by design: internal error text never reaches a response body. The real error is logged with the request ID.
ai (13)

ai

The AI reviewer — the reviews a channel opened and the reviewer's answers. Authenticated by an agent token and never by a session, and no schema reachable from any of these operations carries a cloud credential — a test walks this document to assert it, rather than trusting the sentence.

GET /ai/reviewer/changes

credential bearerReviewoperation getReviewerChanges

The change feed in a window, for the reviewer

As GET /changes, tenant-wide.

Parameters

NameInTypeDescription
fromquerystring (date-time)
toquerystring (date-time)
kindqueryarray of ChangeKind
accountIdqueryarray of stringRepeatable. Any of the given accounts.
resourceKeyquerystring
resourceTypequerystring
cursorquerystring
pageSizequeryintegerNormalised server-side, not rejected: < 1 becomes 100, > 500 becomes 500. No minimum or maximum, for the same reason as page — a constraint here would turn a clamp into a 400.

Responses

StatusBodyMeaning
200ChangeFeedPageThe page.
400ErrorResponseinvalid_request — a malformed parameter or body. The batch ingest endpoint uses invalid_batch instead, because a collector branches on it differently: a bad batch is a bug in the collector, while a bad request is usually a bug in the caller's query string.
401ErrorResponseunauthorized — no credential, or one that is not valid.
403ErrorResponseforbidden — authenticated, but the user's role does not permit this. Distinct from module_not_entitled, which is about the tenant's subscription rather than the user. On an unsafe request, also origin_not_trusted: the browser boundary refused the request's origin before anything else ran.
500ErrorResponseinternal_error. The message is generic by design: internal error text never reaches a response body. The real error is logged with the request ID.

GET /ai/reviewer/cost/anomalies

credential bearerReviewoperation listReviewerCostAnomalies

Cost anomalies, for the reviewer

As GET /cost/anomalies, tenant-wide.

Parameters

NameInTypeDescription
statusqueryarray of CostAnomalyStatus
accountIdqueryarray of stringRepeatable. Any of the given accounts.
pagequeryinteger1-based. Normalised server-side rather than rejected: < 1 becomes 1, and a page past the end is clamped to totalPages. Deliberately no minimum: a schema constraint would make the generated server reject page=0 before the handler could normalise it, which contradicts the normalisation this very description promises.
pageSizequeryintegerNormalised server-side, not rejected: < 1 becomes 100, > 500 becomes 500. No minimum or maximum, for the same reason as page — a constraint here would turn a clamp into a 400.

Responses

StatusBodyMeaning
200CostAnomalyPageThe page.
400ErrorResponseinvalid_request — a malformed parameter or body. The batch ingest endpoint uses invalid_batch instead, because a collector branches on it differently: a bad batch is a bug in the collector, while a bad request is usually a bug in the caller's query string.
401ErrorResponseunauthorized — no credential, or one that is not valid.
403ErrorResponseforbidden — authenticated, but the user's role does not permit this. Distinct from module_not_entitled, which is about the tenant's subscription rather than the user. On an unsafe request, also origin_not_trusted: the browser boundary refused the request's origin before anything else ran.
500ErrorResponseinternal_error. The message is generic by design: internal error text never reaches a response body. The real error is logged with the request ID.

GET /ai/reviewer/cost/anomaly

credential bearerReviewoperation getReviewerCostAnomaly

The cost anomaly this review is of

The one anomaly an anomaly review is about, as GET /cost/anomalies shows a row, in whatever status it is in now — a member may have acknowledged or dismissed it before the reviewer ran, and twenty newer ones may have opened since. The reviewer's subject read: a verdict on an anomaly rests on this row.

It takes no parameters. The anomaly is the review's own subject, so a token cannot be pointed at another one. It is read under the review's audience, as every reviewer read is: an anomaly the audience no longer sees answers 404, exactly as one that does not exist. A review of any other subject kind is refused.

Responses

StatusBodyMeaning
200CostAnomalyThe anomaly.
400ErrorResponseThis review is not of a cost anomaly. error.code is invalid_request.
401ErrorResponseunauthorized — no credential, or one that is not valid.
403ErrorResponseforbidden — authenticated, but the user's role does not permit this. Distinct from module_not_entitled, which is about the tenant's subscription rather than the user. On an unsafe request, also origin_not_trusted: the browser boundary refused the request's origin before anything else ran.
404ErrorResponsenot_found
500ErrorResponseinternal_error. The message is generic by design: internal error text never reaches a response body. The real error is logged with the request ID.

GET /ai/reviewer/cost/forecast

credential bearerReviewoperation getReviewerCostForecast

The cost forecast, for the reviewer

As GET /cost/forecast, for the tenant or one account.

Parameters

NameInTypeDescription
accountIdquerystring
monthquerystring

Responses

StatusBodyMeaning
200CostForecastListThe forecast.
400ErrorResponseinvalid_request — a malformed parameter or body. The batch ingest endpoint uses invalid_batch instead, because a collector branches on it differently: a bad batch is a bug in the collector, while a bad request is usually a bug in the caller's query string.
401ErrorResponseunauthorized — no credential, or one that is not valid.
403ErrorResponseforbidden — authenticated, but the user's role does not permit this. Distinct from module_not_entitled, which is about the tenant's subscription rather than the user. On an unsafe request, also origin_not_trusted: the browser boundary refused the request's origin before anything else ran.
500ErrorResponseinternal_error. The message is generic by design: internal error text never reaches a response body. The real error is logged with the request ID.

GET /ai/reviewer/impact

credential bearerReviewoperation getReviewerImpact

The investigation this review is of

The whole investigation GET /estate/impact answers for a person, for this review's own anchor: the resource the change happened to, its neighbours, what changed in the window either side — status transitions, performance deviations after against before, settled cost per day — and each panel's statement of what it lacks.

It takes no parameters. The anchor is the review's, kept when the review was opened, so a token cannot be pointed at another investigation. A review of any other subject kind is refused: an alert's evidence is the resource reads.

Responses

StatusBodyMeaning
200ImpactThe investigation.
400ErrorResponseThis review is not of an investigation. error.code is invalid_request.
401ErrorResponseunauthorized — no credential, or one that is not valid.
403ErrorResponseforbidden — authenticated, but the user's role does not permit this. Distinct from module_not_entitled, which is about the tenant's subscription rather than the user. On an unsafe request, also origin_not_trusted: the browser boundary refused the request's origin before anything else ran.
404ErrorResponsenot_found
500ErrorResponseinternal_error. The message is generic by design: internal error text never reaches a response body. The real error is logged with the request ID.

GET /ai/reviewer/resource

credential bearerReviewoperation getReviewerResource

One resource, for the reviewer

The resource as GET /resources/detail answers it, with its recent snapshots, transitions and last change. No acknowledgement is hidden — the reviewer sees what a member acknowledged and why.

Parameters

NameInTypeDescription
accountIdrequiredquerystring
regionrequiredquerystring
systemrequiredquerystring
servicerequiredquerystring
resourceTyperequiredquerystring
resourceIdrequiredquerystring

Responses

StatusBodyMeaning
200ResourceDetailThe resource.
401ErrorResponseunauthorized — no credential, or one that is not valid.
403ErrorResponseforbidden — authenticated, but the user's role does not permit this. Distinct from module_not_entitled, which is about the tenant's subscription rather than the user. On an unsafe request, also origin_not_trusted: the browser boundary refused the request's origin before anything else ran.
404ErrorResponsenot_found
500ErrorResponseinternal_error. The message is generic by design: internal error text never reaches a response body. The real error is logged with the request ID.

GET /ai/reviewer/review

credential bearerReviewoperation getReviewerReview

The review the token names

The review itself, as GET /ai/reviews lists it — its subject, account and resource key, its summary and when it was requested. The reviewer's first read.

Responses

StatusBodyMeaning
200AIReviewThe review.
401ErrorResponseunauthorized — no credential, or one that is not valid.
403ErrorResponseforbidden — authenticated, but the user's role does not permit this. Distinct from module_not_entitled, which is about the tenant's subscription rather than the user. On an unsafe request, also origin_not_trusted: the browser boundary refused the request's origin before anything else ran.
500ErrorResponseinternal_error. The message is generic by design: internal error text never reaches a response body. The real error is logged with the request ID.

GET /ai/reviewer/snapshots

credential bearerReviewoperation listReviewerSnapshots

A resource's observations, for the reviewer

As GET /resources/snapshots.

Parameters

NameInTypeDescription
accountIdrequiredquerystring
regionrequiredquerystring
systemrequiredquerystring
servicerequiredquerystring
resourceTyperequiredquerystring
resourceIdrequiredquerystring
cursorquerystring
pageSizequeryinteger

Responses

StatusBodyMeaning
200SnapshotPageThe page.
400ErrorResponseinvalid_request — a malformed parameter or body. The batch ingest endpoint uses invalid_batch instead, because a collector branches on it differently: a bad batch is a bug in the collector, while a bad request is usually a bug in the caller's query string.
401ErrorResponseunauthorized — no credential, or one that is not valid.
403ErrorResponseforbidden — authenticated, but the user's role does not permit this. Distinct from module_not_entitled, which is about the tenant's subscription rather than the user. On an unsafe request, also origin_not_trusted: the browser boundary refused the request's origin before anything else ran.
404ErrorResponsenot_found
500ErrorResponseinternal_error. The message is generic by design: internal error text never reaches a response body. The real error is logged with the request ID.

GET /ai/reviewer/snapshots/diff

credential bearerReviewoperation getReviewerSnapshotDiff

The difference between two observations, for the reviewer

As GET /resources/snapshots/diff, sensitive fields suppressed the same way.

Parameters

NameInTypeDescription
accountIdrequiredquerystring
regionrequiredquerystring
systemrequiredquerystring
servicerequiredquerystring
resourceTyperequiredquerystring
resourceIdrequiredquerystring
fromrequiredqueryinteger (int64)
torequiredqueryinteger (int64)

Responses

StatusBodyMeaning
200SnapshotDiffThe difference.
400ErrorResponseinvalid_request — a malformed parameter or body. The batch ingest endpoint uses invalid_batch instead, because a collector branches on it differently: a bad batch is a bug in the collector, while a bad request is usually a bug in the caller's query string.
401ErrorResponseunauthorized — no credential, or one that is not valid.
403ErrorResponseforbidden — authenticated, but the user's role does not permit this. Distinct from module_not_entitled, which is about the tenant's subscription rather than the user. On an unsafe request, also origin_not_trusted: the browser boundary refused the request's origin before anything else ran.
404ErrorResponsenot_found
500ErrorResponseinternal_error. The message is generic by design: internal error text never reaches a response body. The real error is logged with the request ID.

GET /ai/reviewer/timeline

credential bearerReviewoperation getReviewerTimeline

Status transitions in a window, for the reviewer

As GET /timeline, tenant-wide, acknowledgements shown.

Parameters

NameInTypeDescription
fromquerystring (date-time)
toquerystring (date-time)
accountIdqueryarray of stringRepeatable. Any of the given accounts.
servicequeryarray of stringRepeatable.
statusqueryarray of TransitionStatusRepeatable. On /timeline this filters on the destination status, where deleted is meaningful; on resource endpoints it filters live health, where it is not.
cursorquerystring
pageSizequeryintegerNormalised server-side, not rejected: < 1 becomes 100, > 500 becomes 500. No minimum or maximum, for the same reason as page — a constraint here would turn a clamp into a 400.

Responses

StatusBodyMeaning
200TimelinePageThe page.
400ErrorResponseinvalid_request — a malformed parameter or body. The batch ingest endpoint uses invalid_batch instead, because a collector branches on it differently: a bad batch is a bug in the collector, while a bad request is usually a bug in the caller's query string.
401ErrorResponseunauthorized — no credential, or one that is not valid.
403ErrorResponseforbidden — authenticated, but the user's role does not permit this. Distinct from module_not_entitled, which is about the tenant's subscription rather than the user. On an unsafe request, also origin_not_trusted: the browser boundary refused the request's origin before anything else ran.
500ErrorResponseinternal_error. The message is generic by design: internal error text never reaches a response body. The real error is logged with the request ID.

GET /ai/reviews

credential sessionCookieoperation listAIReviews

The AI reviews of a resource or a subject

The reviews opened for this tenant, newest first — every one, or those of a resource, a subject kind, or one subject. Each carries the answer when it came, and says when it did not.

A page of many rows reads them once and indexes them by resource: naming accountId and resourceKey is a row's read, and naming neither is a page's.

Within the reader's reach, whatever is named: an administrator reads the tenant's reviews, and a member reads those of their teams' accounts plus any they asked for themselves. A review of an account outside them is not listed and is not refused — the answer is what the reader may see, as the Alerts list is.

Parameters

NameInTypeDescription
accountIdquerystring
resourceKeyquerystringregion/system/service/type/id
subjectKindquerystring enum
subjectIdquerystringNeeds subjectKind.
limitqueryinteger

Responses

StatusBodyMeaning
200AIReviewListThe reviews.
400ErrorResponseinvalid_request — a malformed parameter or body. The batch ingest endpoint uses invalid_batch instead, because a collector branches on it differently: a bad batch is a bug in the collector, while a bad request is usually a bug in the caller's query string.
401ErrorResponseunauthorized — no credential, or one that is not valid.
500ErrorResponseinternal_error. The message is generic by design: internal error text never reaches a response body. The real error is logged with the request ID.

POST /ai/reviews

credential sessionCookierole engineeroperation requestAIReviews

Ask the AI reviewer about what is selected

Opens a review per item and puts its claim check on the operator's queue — the same door the ai-reviewer notification channel uses, opened by a person instead of by the notifier.

The item names a resource; the server names the subject. An alert item becomes the resource's latest status transition, which is exactly what the notifier would have sent, so asking about an alert already handed over refreshes that review rather than opening a second one. An impact item becomes an investigation: the anchor travels with the review, and the reviewer reads it back through its own token.

The answer is the rows as they were opened — nothing is answered yet. GET /ai/reviews is where the answers arrive.

Request body application/json

FieldTypeDescription
itemsrequiredarray of AIReviewRequestItem

Responses

StatusBodyMeaning
201AIReviewListThe reviews opened.
400ErrorResponseinvalid_request — a malformed parameter or body. The batch ingest endpoint uses invalid_batch instead, because a collector branches on it differently: a bad batch is a bug in the collector, while a bad request is usually a bug in the caller's query string.
401ErrorResponseunauthorized — no credential, or one that is not valid.
403ErrorResponseforbidden — authenticated, but the user's role does not permit this. Distinct from module_not_entitled, which is about the tenant's subscription rather than the user. On an unsafe request, also origin_not_trusted: the browser boundary refused the request's origin before anything else ran.
404ErrorResponsenot_found
409ErrorResponseThis deployment has no AI reviewer queue, so there is nothing to ask. error.code is reviewer_unavailable.
500ErrorResponseinternal_error. The message is generic by design: internal error text never reaches a response body. The real error is logged with the request ID.

POST /ai/reviews/{reviewId}/answer

credential bearerReviewbody at most 65536 bytesoperation answerAIReview

The reviewer's answer to one review

The AI reviewer answers the review its token names; the path's id must be that review's. Every finding's evidence must name a row this tenant can see — a resource by its account and key, a transition or a cost anomaly by id — or the whole answer is refused. The same answer twice is a no-op; a different answer to an answered review is a conflict. The answer lands on the subject and in the inbox of the tenant's administrators (the subject's owners once ownership resolution is wired), and every string keeps correlation apart from cause.

Parameters

NameInTypeDescription
reviewIdrequiredpathstring (uuid)

Request body application/json

FieldTypeDescription
confidencestring enumThe model's own estimate, not a measured one; the portal labels it so. One of low, medium, high.
evidencearray of AIReviewEvidenceThe rows the answer as a whole rests on: the review's subject as the reviewer read it. A nothing_stands_out answer to any subject but a channel test must name the subject among them — an alert's transition or resource, an anomaly by its id, an impact review's resource — or it is refused: an all-clear that read nothing is not a review. Each row is checked as a finding's evidence is, and counts toward the same limit of 50 across the answer. They are checked when the answer is accepted and are not kept.
findingsarray of AIReviewFinding
modelrequiredstringThe model that reviewed, as the reviewer names it.
summaryrequiredstring
verdictrequiredstring enum One of nothing_stands_out, findings, could_not_review.

Responses

StatusBodyMeaning
200AIReviewThe review, answered.
400ErrorResponseinvalid_request — a malformed parameter or body. The batch ingest endpoint uses invalid_batch instead, because a collector branches on it differently: a bad batch is a bug in the collector, while a bad request is usually a bug in the caller's query string.
401ErrorResponseunauthorized — no credential, or one that is not valid.
403ErrorResponseforbidden — authenticated, but the user's role does not permit this. Distinct from module_not_entitled, which is about the tenant's subscription rather than the user. On an unsafe request, also origin_not_trusted: the browser boundary refused the request's origin before anything else ran.
404ErrorResponsenot_found
409ErrorResponseThe review was already answered differently.
500ErrorResponseinternal_error. The message is generic by design: internal error text never reaches a response body. The real error is logged with the request ID.
cost (19)

cost

The Cost module (COST_SPEC.md): AWS spend, attributed to the teams that caused it, joined to the inventory Estate owns. Behind RequireModule("cost"). Every money-bearing response is an array of {currency, amount} — never a scalar, never two currencies added together (C7) — and amount is a decimal string, because a JSON number is a float and money is not.

GET /cost/anomalies

credential sessionCookie or apiTokenmodule costoperation listCostAnomalies

Detected anomalies

Spend that left its baseline (§9): a relative deviation above the tenant's sensitivity and an absolute delta above the floor, per (scope, service), newest first. Each is also an event in the platform outbox, delivered once per suppression window.

Parameters

NameInTypeDescription
statusqueryarray of CostAnomalyStatusRepeatable. Defaults to open.
accountIdqueryarray of stringRepeatable. Any of the given accounts.
teamIdqueryarray of string (uuid)Repeatable. Narrows within the caller's team scope, never widens it; a team the caller may not see yields no rows, not an error.
pagequeryinteger1-based. Normalised server-side rather than rejected: < 1 becomes 1, and a page past the end is clamped to totalPages. Deliberately no minimum: a schema constraint would make the generated server reject page=0 before the handler could normalise it, which contradicts the normalisation this very description promises.
pageSizequeryintegerNormalised server-side, not rejected: < 1 becomes 100, > 500 becomes 500. No minimum or maximum, for the same reason as page — a constraint here would turn a clamp into a 400.
formatquerystring enumcsv answers the same read as a file: the same filters, the same scope, the same rows, one line per amount and currency — a key with spend in two currencies is two lines, and no line is a total. A text cell that begins with =, +, -, @, a tab or a carriage return is written with a leading ', so a name or a tag value cannot become a spreadsheet formula; an amount is written as the database's decimal, its sign kept. The export of a view is its URL with format=csv, so a browser navigating to it downloads the file.

Responses

StatusBodyMeaning
200CostAnomalyPageA page of anomalies. As format=csv, the whole filtered list rather than a page — page and pageSize are ignored — newest first, up to its first 50,000 anomalies, with the columns id, scopeType, scopeId, scopeLabel, service, windowStart, windowEnd, currency, baseline, observed, deviationPct, severity, status, detectedAt, resolvedAt, resolvedBy (resolvedBy the actor's id): one line per anomaly, its baseline and observed in its one currency. A longer list ends with a line whose first cell begins with # and says how many of how many the file holds.
400ErrorResponseinvalid_request — a malformed parameter or body. The batch ingest endpoint uses invalid_batch instead, because a collector branches on it differently: a bad batch is a bug in the collector, while a bad request is usually a bug in the caller's query string.
401ErrorResponseunauthorized — no credential, or one that is not valid.
403ErrorResponsemodule_not_entitled — RequireModule("cost") refused this before the handler ran (the first real consumer; the portal renders the locked-module upsell) — or forbidden, when the module is entitled and the user's role does not permit the operation. On an unsafe request, also origin_not_trusted: the browser boundary refused the request's origin before anything else ran.
429ErrorResponseRate limited.
500ErrorResponseinternal_error. The message is generic by design: internal error text never reaches a response body. The real error is logged with the request ID.

PATCH /cost/anomalies/{anomalyId}

credential sessionCookiemodule costrole engineeroperation updateCostAnomaly

Acknowledge or dismiss an anomaly

acknowledged means somebody is looking; dismissed means it was expected. Either closes it. An anomaly cannot be reopened through this endpoint — the detector opens a new one if the spend is still out of line on the next window.

Request body application/json

FieldTypeDescription
statusrequiredstring enum One of acknowledged, dismissed.

Responses

StatusBodyMeaning
200CostAnomalyThe anomaly as it now stands.
400ErrorResponseinvalid_request — a malformed parameter or body. The batch ingest endpoint uses invalid_batch instead, because a collector branches on it differently: a bad batch is a bug in the collector, while a bad request is usually a bug in the caller's query string.
401ErrorResponseunauthorized — no credential, or one that is not valid.
403ErrorResponsemodule_not_entitled — RequireModule("cost") refused this before the handler ran (the first real consumer; the portal renders the locked-module upsell) — or forbidden, when the module is entitled and the user's role does not permit the operation. On an unsafe request, also origin_not_trusted: the browser boundary refused the request's origin before anything else ran.
404ErrorResponsenot_found
413ErrorResponseThe request body exceeded the size the server will read. Declared because it is real: a body is otherwise read until the client stops sending, and one caller could exhaust the process. The cap is above any legitimate request — 500 items at the additionalInfo limit is roughly 160 MB — so reaching it means something is wrong with the sender.
415ErrorResponseunsupported_media_type — the body of a browser-facing route is not labelled application/json (a +json suffix and a charset parameter are fine). Refused before the body is read: a form post, or a "simple" cross-site request carrying JSON as text/plain to dodge the preflight, is not something this API reads. Bearer routes are not held to it.
500ErrorResponseinternal_error. The message is generic by design: internal error text never reaches a response body. The real error is logged with the request ID.

GET /cost/breakdown

credential sessionCookie or apiTokenmodule costoperation getCostBreakdown

One level of grouping, with drill-down keys

Rows for the chosen grouping, largest first in the tenant's default currency ordering, each with the key a client passes back as a filter to drill one level further (a team's services, a service's accounts). untagged appears as its own row under groupBy=team — visible, never dropped (§7.1 step 3).

Parameters

NameInTypeDescription
fromquerystring (date)First day of the range, inclusive. Defaults to the first of the current month. from after to is invalid_request.
toquerystring (date)Last day of the range, inclusive. Defaults to today.
figurequeryCostFigureWhich money figure (C1). Amortized is what makes chargeback defensible — a reserved-instance purchase is a flat line rather than a spike on the team that did not make it — and is the default; unblended is what the invoice says.
groupByrequiredquerystring enum
accountIdqueryarray of stringRepeatable. Any of the given accounts.
teamIdqueryarray of string (uuid)Repeatable. Narrows within the caller's team scope, never widens it; a team the caller may not see yields no rows, not an error.
servicequeryarray of stringRepeatable.
limitqueryintegerRows to return before the rest is summed into an other row. Normalised: < 1 becomes 20, > 200 becomes 200.
formatquerystring enumcsv answers the same read as a file: the same filters, the same scope, the same rows, one line per amount and currency — a key with spend in two currencies is two lines, and no line is a total. A text cell that begins with =, +, -, @, a tab or a carriage return is written with a leading ', so a name or a tag value cannot become a spreadsheet formula; an amount is written as the database's decimal, its sign kept. The export of a view is its URL with format=csv, so a browser navigating to it downloads the file.

Responses

StatusBodyMeaning
200CostBreakdownThe breakdown. As format=csv, the same rows with the columns from, to, figure, groupBy, key, label, currency, amount, allocatedAmount — one line per row and currency, the other row included, no total line.
400ErrorResponseinvalid_request — a malformed parameter or body. The batch ingest endpoint uses invalid_batch instead, because a collector branches on it differently: a bad batch is a bug in the collector, while a bad request is usually a bug in the caller's query string.
401ErrorResponseunauthorized — no credential, or one that is not valid.
403ErrorResponsemodule_not_entitled — RequireModule("cost") refused this before the handler ran (the first real consumer; the portal renders the locked-module upsell) — or forbidden, when the module is entitled and the user's role does not permit the operation. On an unsafe request, also origin_not_trusted: the browser boundary refused the request's origin before anything else ran.
429ErrorResponseRate limited.
500ErrorResponseinternal_error. The message is generic by design: internal error text never reaches a response body. The real error is logged with the request ID.

GET /cost/explanation

credential sessionCookiemodule costoperation getCostExplanation

What moved between two windows

A change in spend decomposed into its movers: per currency, the movers sum exactly to the delta, never converted (C7). The input is either an anomaly — its window is the current one, the same length immediately before it the previous one, and its scope and service narrow the read — or a window (from, to) with an optional previous one, which defaults to the same length immediately before.

An anomaly in any status is explained: an acknowledged, dismissed or suppressed anomaly is out of the inbox, not out of reach. One the reader cannot see is 404, as in the list.

Read from the rollups, so the figures reconcile with /cost/summary for the same windows. The grain is resource where the bill's lines the rollup build attributed to the read's scope (by the same tag, owner and tier as the rollup) are every line of each rollup figure read, from that figure's own build — so a team that holds spend by tag reads its own lines and never another team's — or where the read holds every line of each account it reads (the whole tenant with no teamId, or accounts each owned by a team of the read with no attribution tag in the period and nothing allocated); in both cases a bill (the Cost & Usage Report, a Google Cloud billing export, an Azure export) is the money source of every day read, names resources, and its lines sum to exactly each rollup figure read (per day, account, service and currency). Whose a line is is decided by the build or the scope, never from a sum. service otherwise, for the whole answer, never a mix: a day Cost Explorer owns, an allocated share, or a tagged group whose report rows carry no line tags yet is the service grain. At the resource grain each mover is a resource, or a service's lines that name none (withoutResource), and charge types other than usage are adjustment movers of their own; for the largest movers the owner and the changes on the resource in the current window (from changesFrom) are read beside the figures, on every request and in no version. Days after the settled day are cut and listed; days with no row are listed and the coverage is incomplete. dataVersion is the newest rebuild the figures were computed from. version names the answer itself: it changes with anything that changes the answer — a rebuild, a newly settled day, the method — so two answers with one dataVersion still have two versions. Every answer is kept under the reader's scope, its windows and its version; pass version back and that answer is served, with superseded saying it is no longer the current one. A version never computed for this scope and these windows gets the current figures, superseded.

An administrator reads any scope; a member reads their teams', and a teamId outside them is 403 forbidden.

Parameters

NameInTypeDescription
anomalyIdquerystring (uuid)Explain this anomaly. Not with from/to.
fromquerystring (date)The current window's first day.
toquerystring (date)The current window's last day, at most 366 days after from.
previousFromquerystring (date)The previous window's first day; with previousTo.
previousToquerystring (date)The previous window's last day; with previousFrom.
teamIdquerystring (uuid)Narrow to one team, within the reader's scope.
accountIdquerystringNarrow to one account.
versionquerystringThe version an earlier answer carried; that answer is served, and superseded says whether it is still current.
figurequeryCostFigureWhich money figure (C1). Amortized is what makes chargeback defensible — a reserved-instance purchase is a flat line rather than a spike on the team that did not make it — and is the default; unblended is what the invoice says.

Responses

StatusBodyMeaning
200CostExplanationThe explanation.
400ErrorResponseinvalid_request — a malformed parameter or body. The batch ingest endpoint uses invalid_batch instead, because a collector branches on it differently: a bad batch is a bug in the collector, while a bad request is usually a bug in the caller's query string.
401ErrorResponseunauthorized — no credential, or one that is not valid.
403ErrorResponsemodule_not_entitled — RequireModule("cost") refused this before the handler ran (the first real consumer; the portal renders the locked-module upsell) — or forbidden, when the module is entitled and the user's role does not permit the operation. On an unsafe request, also origin_not_trusted: the browser boundary refused the request's origin before anything else ran.
404ErrorResponsenot_found
500ErrorResponseinternal_error. The message is generic by design: internal error text never reaches a response body. The real error is logged with the request ID.

GET /cost/forecast

credential sessionCookie or apiTokenmodule costoperation getCostForecast

The month-end forecast for a scope

The approved forecast (the trailing median, its parameters approved by the owner on 2026-09-12), computed daily after the rollups. For a scope — the tenant, a team or an account — and a month, one forecast per currency, from the newest day computed. Amortized, per currency, unconverted, and a forecast: it never changes the actuals.

state is not_enough_history below 14 settled days. lowConfidence flags a scope whose backtest at the nearest checkpoint has fewer than three scored months, or a 90th-percentile error above 25%.

An administrator reads any scope. A member reads their own teams' only: a team they cannot see is an empty list, and the tenant and account scopes are 403 forbidden.

Parameters

NameInTypeDescription
scoperequiredqueryCostForecastScope
teamIdquerystring (uuid)The team, with scope=team.
accountIdquerystringThe account, with scope=account.
monthquerystringThe month, YYYY-MM; the current month when absent.
currencyquerystringOne currency; every currency the scope spends in when absent.

Responses

StatusBodyMeaning
200CostForecastListThe scope's forecasts for the month.
400ErrorResponseinvalid_request — a malformed parameter or body. The batch ingest endpoint uses invalid_batch instead, because a collector branches on it differently: a bad batch is a bug in the collector, while a bad request is usually a bug in the caller's query string.
401ErrorResponseunauthorized — no credential, or one that is not valid.
403ErrorResponsemodule_not_entitled — RequireModule("cost") refused this before the handler ran (the first real consumer; the portal renders the locked-module upsell) — or forbidden, when the module is entitled and the user's role does not permit the operation. On an unsafe request, also origin_not_trusted: the browser boundary refused the request's origin before anything else ran.
429ErrorResponseRate limited.
500ErrorResponseinternal_error. The message is generic by design: internal error text never reaches a response body. The real error is logged with the request ID.

GET /cost/kubernetes

credential sessionCookie or apiTokenmodule costoperation getCostKubernetes

EKS node spend attributed to namespaces and workloads

Kubernetes cost attribution: each cluster's node line items for the window, split across the workloads that ran on the nodes by their CPU and memory requests, with the idle share nobody requested as its own row. Computed daily from the inventory's nodes and pods and the nodes' line items; a read never calls a cluster. A cluster's rows sum to its nodes' spend.

Node spend here is per-resource evidence (§8), not a total of the account's spend: it is what the nodes' line items said. Scoped to the caller's teams' accounts.

A cluster is accountId + region + cluster — the inventory's identity for an EKS cluster — so two clusters named alike in two regions are two clusters here, apart from discovery to display. cluster and region narrow to a name and a region; both together to one cluster.

The attribution uses the pods and nodes the collector last saw, applied to each day of the trailing window (the accepted current-placement approximation, COST_SPEC.md): a change of placement mid-window shows as a step at the next collection, not a pro-rated day. Daily placement history is a later improvement, and no figure here claims it.

Parameters

NameInTypeDescription
fromquerystring (date)First day of the range, inclusive. Defaults to the first of the current month. from after to is invalid_request.
toquerystring (date)Last day of the range, inclusive. Defaults to today.
figurequeryCostFigureWhich money figure (C1). Amortized is what makes chargeback defensible — a reserved-instance purchase is a flat line rather than a spike on the team that did not make it — and is the default; unblended is what the invoice says.
accountIdqueryarray of stringRepeatable. Any of the given accounts.
clusterquerystringA cluster name; with region, one cluster.
regionquerystringThe cluster's region, as the inventory holds it.

Responses

StatusBodyMeaning
200CostKubernetesThe attribution.
400ErrorResponseinvalid_request — a malformed parameter or body. The batch ingest endpoint uses invalid_batch instead, because a collector branches on it differently: a bad batch is a bug in the collector, while a bad request is usually a bug in the caller's query string.
401ErrorResponseunauthorized — no credential, or one that is not valid.
403ErrorResponsemodule_not_entitled — RequireModule("cost") refused this before the handler ran (the first real consumer; the portal renders the locked-module upsell) — or forbidden, when the module is entitled and the user's role does not permit the operation. On an unsafe request, also origin_not_trusted: the browser boundary refused the request's origin before anything else ran.
429ErrorResponseRate limited.
500ErrorResponseinternal_error. The message is generic by design: internal error text never reaches a response body. The real error is logged with the request ID.

GET /cost/reconciliation

credential sessionCookiemodule costoperation getCostReconciliation

Where every amount of the window sits, and what would move it

Spend reconciliation (COST_SPEC §10). Every amount of the scope's authoritative total sits in exactly one of three places — attributed to a team (by tag or by account), allocated by a shared-cost rule, untagged — and the three sum to the total per currency, COST_SPEC §7.2's invariant. The places are read from the same rollup every other aggregation closes against.

Two breakdowns cut the same rows again and are never places: inventory coverage (what the inventory knows of the resource behind an amount; stale is a bucket whose collector is late and is never reported as missing; unknown is the amount with no resource behind it) and chargeTypes (usage, credit, refund, tax, fee, commitment, other). Each sums to the same total the places do; their money is under figures, not amounts, because it is the same money again.

Each row is one (account, service, currency), with its place amounts, both breakdowns, the moves that would clear its unhealthy values and, beside untagged spend, the owners the resolver infers for its billed resources. The figure is always amortized.

Parameters

NameInTypeDescription
fromquerystring (date)The window's first day. Defaults with to to the month to date.
toquerystring (date)The window's last day, inclusive.

Responses

StatusBodyMeaning
200CostReconciliationThe reconciliation.
400ErrorResponseinvalid_request — a malformed parameter or body. The batch ingest endpoint uses invalid_batch instead, because a collector branches on it differently: a bad batch is a bug in the collector, while a bad request is usually a bug in the caller's query string.
401ErrorResponseunauthorized — no credential, or one that is not valid.
403ErrorResponsemodule_not_entitled — RequireModule("cost") refused this before the handler ran (the first real consumer; the portal renders the locked-module upsell) — or forbidden, when the module is entitled and the user's role does not permit the operation. On an unsafe request, also origin_not_trusted: the browser boundary refused the request's origin before anything else ran.
500ErrorResponseinternal_error. The message is generic by design: internal error text never reaches a response body. The real error is logged with the request ID.

GET /cost/report-schedules

credential sessionCookiemodule costoperation listCostReportSchedules

Scheduled reports, and the teams the caller may add one to

A team's monthly report — its spend by service and the resources its tag policy is not satisfied by — sent to one of the team's own notification channels. items is every schedule of the caller's teams (every team's for an administrator), each with canEdit and its newest delivery; teams is the teams the caller may write a schedule for — the teams they lead, or every team for an administrator — each with its own enabled channels a report can go to (email, Slack, Teams or webhook).

A month's report is sent once the month has ended and two days have passed for its last days' spend to arrive. Email attaches the breakdown and the tag violations as CSV files — GET /cost/breakdown and GET /cost/tag-violations with format=csv, the same files, each cut at 3 MiB with a last line saying how many lines of how many it holds and where the whole export is; every other channel gets the links. Sent by the notify role when PORTAL_URL is set.

Responses

StatusBodyMeaning
200CostReportScheduleListThe schedules and the teams the caller may write.
401ErrorResponseunauthorized — no credential, or one that is not valid.
403ErrorResponsemodule_not_entitled — RequireModule("cost") refused this before the handler ran (the first real consumer; the portal renders the locked-module upsell) — or forbidden, when the module is entitled and the user's role does not permit the operation. On an unsafe request, also origin_not_trusted: the browser boundary refused the request's origin before anything else ran.
500ErrorResponseinternal_error. The message is generic by design: internal error text never reaches a response body. The real error is logged with the request ID.

POST /cost/report-schedules

credential sessionCookiemodule costrole vieweroperation createCostReportSchedule

Schedule a team's monthly report

Written by a lead of the team or an administrator; forbidden otherwise. The channel must be one of the team's own — never the organisation's default, which would send one team's spend to everyone — and of a kind a report goes to: email, Slack, Teams or webhook (invalid_request names which rule). One schedule per team and channel: a second is 409 report_schedule_exists. Audited as cost.report_schedule.create.

Request body application/json

FieldTypeDescription
channelIdrequiredstring (uuid)
enabledboolean
teamIdrequiredstring (uuid)

Responses

StatusBodyMeaning
201CostReportScheduleThe schedule.
400ErrorResponseinvalid_request — a malformed parameter or body. The batch ingest endpoint uses invalid_batch instead, because a collector branches on it differently: a bad batch is a bug in the collector, while a bad request is usually a bug in the caller's query string.
401ErrorResponseunauthorized — no credential, or one that is not valid.
403ErrorResponsemodule_not_entitled — RequireModule("cost") refused this before the handler ran (the first real consumer; the portal renders the locked-module upsell) — or forbidden, when the module is entitled and the user's role does not permit the operation. On an unsafe request, also origin_not_trusted: the browser boundary refused the request's origin before anything else ran.
409ErrorResponsereport_schedule_exists — the team already sends this report to that channel.
413ErrorResponseThe request body exceeded the size the server will read. Declared because it is real: a body is otherwise read until the client stops sending, and one caller could exhaust the process. The cap is above any legitimate request — 500 items at the additionalInfo limit is roughly 160 MB — so reaching it means something is wrong with the sender.
415ErrorResponseunsupported_media_type — the body of a browser-facing route is not labelled application/json (a +json suffix and a charset parameter are fine). Refused before the body is read: a form post, or a "simple" cross-site request carrying JSON as text/plain to dodge the preflight, is not something this API reads. Bearer routes are not held to it.
500ErrorResponseinternal_error. The message is generic by design: internal error text never reaches a response body. The real error is logged with the request ID.

PATCH /cost/report-schedules/{scheduleId}

credential sessionCookiemodule costrole vieweroperation updateCostReportSchedule

Change a report's channel, or switch it on or off

By a lead of the schedule's team or an administrator; a schedule outside the caller's teams is not_found, one of a team they do not lead forbidden. The new channel follows the create's rules. A delivery already owed goes to the channel it was owed to, and only while that channel is still the team's. Audited as cost.report_schedule.update.

Request body application/json

FieldTypeDescription
channelIdstring (uuid)
enabledboolean

Responses

StatusBodyMeaning
200CostReportScheduleThe schedule as it now stands.
400ErrorResponseinvalid_request — a malformed parameter or body. The batch ingest endpoint uses invalid_batch instead, because a collector branches on it differently: a bad batch is a bug in the collector, while a bad request is usually a bug in the caller's query string.
401ErrorResponseunauthorized — no credential, or one that is not valid.
403ErrorResponsemodule_not_entitled — RequireModule("cost") refused this before the handler ran (the first real consumer; the portal renders the locked-module upsell) — or forbidden, when the module is entitled and the user's role does not permit the operation. On an unsafe request, also origin_not_trusted: the browser boundary refused the request's origin before anything else ran.
404ErrorResponsenot_found
409ErrorResponsereport_schedule_exists — the team already sends this report to that channel.
413ErrorResponseThe request body exceeded the size the server will read. Declared because it is real: a body is otherwise read until the client stops sending, and one caller could exhaust the process. The cap is above any legitimate request — 500 items at the additionalInfo limit is roughly 160 MB — so reaching it means something is wrong with the sender.
415ErrorResponseunsupported_media_type — the body of a browser-facing route is not labelled application/json (a +json suffix and a charset parameter are fine). Refused before the body is read: a form post, or a "simple" cross-site request carrying JSON as text/plain to dodge the preflight, is not something this API reads. Bearer routes are not held to it.
500ErrorResponseinternal_error. The message is generic by design: internal error text never reaches a response body. The real error is logged with the request ID.

DELETE /cost/report-schedules/{scheduleId}

credential sessionCookiemodule costrole vieweroperation deleteCostReportSchedule

Stop a team's report

By a lead of the schedule's team or an administrator. The schedule's deliveries go with it, a delivery still owed included. Audited as cost.report_schedule.delete.

Responses

StatusBodyMeaning
204—Deleted.
401ErrorResponseunauthorized — no credential, or one that is not valid.
403ErrorResponsemodule_not_entitled — RequireModule("cost") refused this before the handler ran (the first real consumer; the portal renders the locked-module upsell) — or forbidden, when the module is entitled and the user's role does not permit the operation. On an unsafe request, also origin_not_trusted: the browser boundary refused the request's origin before anything else ran.
404ErrorResponsenot_found
500ErrorResponseinternal_error. The message is generic by design: internal error text never reaches a response body. The real error is logged with the request ID.

POST /cost/report-schedules/{scheduleId}/send

credential sessionCookiemodule costrole vieweroperation sendCostReport

Send the report now, for the month to date

By a lead of the schedule's team or an administrator. The month to date goes to the schedule's channel on the notify role's next cycle. Once a day: a second request the same day (UTC) is the same delivery, answered queued: false. A switched-off schedule is invalid_request. Audited as cost.report_schedule.send.

Responses

StatusBodyMeaning
202CostReportSendWhether this request queued the report.
400ErrorResponseinvalid_request — a malformed parameter or body. The batch ingest endpoint uses invalid_batch instead, because a collector branches on it differently: a bad batch is a bug in the collector, while a bad request is usually a bug in the caller's query string.
401ErrorResponseunauthorized — no credential, or one that is not valid.
403ErrorResponsemodule_not_entitled — RequireModule("cost") refused this before the handler ran (the first real consumer; the portal renders the locked-module upsell) — or forbidden, when the module is entitled and the user's role does not permit the operation. On an unsafe request, also origin_not_trusted: the browser boundary refused the request's origin before anything else ran.
404ErrorResponsenot_found
500ErrorResponseinternal_error. The message is generic by design: internal error text never reaches a response body. The real error is logged with the request ID.

GET /cost/resources

credential sessionCookie or apiTokenmodule costoperation listCostResources

Per-resource cost, joined to the inventory

The 14-day per-resource view (§4, C4) joined to the Estate inventory on the platform resource key — names, types and status come from the inventory, with zero cost-side discovery. windowDays is in every response because the limitation is stated, not buried.

filter=orphaned is cost-without-resource: spend in the window on a key the inventory has tombstoned or never held. filter=unattributed is resource-without-cost: live inventory rows in managed accounts with no per-resource spend in the window. These are the two views no single-tool competitor can produce, and both say 14 days.

Each day's evidence is one source's — the Cost & Usage Report's resource rows when the day has any, Cost Explorer's sample otherwise (cost.AuthorityCTE, the same day-by-day rule the totals follow) — so a resource both sources report on a day is counted once, never twice.

An account no source supplies per-resource rows for — Cost Explorer's resource-level granularity switched off and no report carrying resource IDs — has no rows here and is listed in unavailableAccounts with the resource_granularity_unavailable reason; one that no source has been pulled from yet is not_yet_ingested. Documented states, never errors.

Parameters

NameInTypeDescription
figurequeryCostFigureWhich money figure (C1). Amortized is what makes chargeback defensible — a reserved-instance purchase is a flat line rather than a spike on the team that did not make it — and is the default; unblended is what the invoice says.
filterquerystring enum
accountIdqueryarray of stringRepeatable. Any of the given accounts.
servicequeryarray of stringRepeatable.
queryquerystringSubstring match on the resource id and name.
pagequeryinteger1-based. Normalised server-side rather than rejected: < 1 becomes 1, and a page past the end is clamped to totalPages. Deliberately no minimum: a schema constraint would make the generated server reject page=0 before the handler could normalise it, which contradicts the normalisation this very description promises.
pageSizequeryintegerNormalised server-side, not rejected: < 1 becomes 100, > 500 becomes 500. No minimum or maximum, for the same reason as page — a constraint here would turn a clamp into a 400.
formatquerystring enumcsv answers the same read as a file: the same filters, the same scope, the same rows, one line per amount and currency — a key with spend in two currencies is two lines, and no line is a total. A text cell that begins with =, +, -, @, a tab or a carriage return is written with a leading ', so a name or a tag value cannot become a spreadsheet formula; an amount is written as the database's decimal, its sign kept. The export of a view is its URL with format=csv, so a browser navigating to it downloads the file.

Responses

StatusBodyMeaning
200CostResourcePageA page of resources with their spend. As format=csv, the whole filtered list rather than a page — page and pageSize are ignored — up to its first 50,000 resources, with the columns from, to, figure, filter, accountId, resourceKey, resourceName, service, region, resourceType, resourceId, inventoryState, status, lastSeenAt, mapped, currency, amount: one line per resource and currency, and one with no currency or amount for a resource with no spend. A longer list ends with a line whose first cell begins with # and says how many of how many the file holds.
400ErrorResponseinvalid_request — a malformed parameter or body. The batch ingest endpoint uses invalid_batch instead, because a collector branches on it differently: a bad batch is a bug in the collector, while a bad request is usually a bug in the caller's query string.
401ErrorResponseunauthorized — no credential, or one that is not valid.
403ErrorResponsemodule_not_entitled — RequireModule("cost") refused this before the handler ran (the first real consumer; the portal renders the locked-module upsell) — or forbidden, when the module is entitled and the user's role does not permit the operation. On an unsafe request, also origin_not_trusted: the browser boundary refused the request's origin before anything else ran.
429ErrorResponseRate limited.
500ErrorResponseinternal_error. The message is generic by design: internal error text never reaches a response body. The real error is logged with the request ID.

GET /cost/savings/claims

credential sessionCookiemodule costoperation listSavingsClaims

Savings claims, each with its current verdict

Every claim the caller's team scope allows: a team's claims to its members, and to an administrator every claim, the tenant-wide ones included. The newest change first.

A claim says a change saved money; its verdict says what the per-resource evidence measured and how far it supports the claim, in one of five words: verified, partly_verified, reduced_cause_uncertain (a reduction was observed and nothing compared it), not_yet_measurable (the after-window has not settled, or a window has no day with evidence), no_saving_measured. Nothing is extrapolated, and no currency is converted.

Parameters

NameInTypeDescription
teamIdqueryarray of string (uuid)Repeatable. Narrows within the caller's team scope, never widens it; a team the caller may not see yields no rows, not an error.

Responses

StatusBodyMeaning
200SavingsClaimListThe claims.
401ErrorResponseunauthorized — no credential, or one that is not valid.
403ErrorResponsemodule_not_entitled — RequireModule("cost") refused this before the handler ran (the first real consumer; the portal renders the locked-module upsell) — or forbidden, when the module is entitled and the user's role does not permit the operation. On an unsafe request, also origin_not_trusted: the browser boundary refused the request's origin before anything else ran.
500ErrorResponseinternal_error. The message is generic by design: internal error text never reaches a response body. The real error is logged with the request ID.

POST /cost/savings/claims

credential sessionCookiemodule costrole engineeroperation createSavingsClaim

Claim a saving for a change

A manual claim: the day of the change, what was done, the resources it touched — a list, or a tag match — and optionally a control set of resources it did not touch. Both sets are resolved and frozen now: later tag changes and new matches move nothing in or out. A resource deleted by the change is a member; one gone before the before-window began is not.

A non-administrator names one of their own teams and may freeze only resources in that team's accounts; forbidden otherwise. Only an administrator makes a claim without a team. The first verdict is computed before this answers; the daily cost.verify_savings job computes it again, and a different answer is a new version.

A claim may instead name the succeeded action it came from (actionRequestId): the action's target, day and team are then the claim's.

invalid_request — the set names a resource that is not in the claim's accounts, matches nothing, or is both a list and a tag; the action is not a succeeded action of the caller's teams, or the claim names its own set, day or another team beside it; the control overlaps the claimed set; the change is dated in the future; or the window is outside 1–90 days. The message names which.

Request body application/json

FieldTypeDescription
actionRequestIdstring (uuid)The succeeded action the claim came from.
changedOnstring (date)The day of the change; it is in neither window. Required unless the claim is over an action.
claimedSavingsSetSpecA list of resources, or a tag match — exactly one of them.
controlSavingsSetSpecA list of resources, or a tag match — exactly one of them.
descriptionstringWhat was done. Required unless the claim is over an action.
recommendationSavingsRecommendationRefThe Trusted Advisor recommendation the claim came from, by the three names GET /cost/recommendations gives it. It must be an open recommendation in the claim's accounts; its check name and AWS's estimate are copied onto the claim.
teamIdstring (uuid)The team the claim is for; omitted, a tenant-wide claim (administrators only), or the action's team.
windowDaysintegerEach window's length; 14 when omitted.

Responses

StatusBodyMeaning
201SavingsClaimDetailThe claim, its frozen sets and its first verdict.
400ErrorResponseinvalid_request — a malformed parameter or body. The batch ingest endpoint uses invalid_batch instead, because a collector branches on it differently: a bad batch is a bug in the collector, while a bad request is usually a bug in the caller's query string.
401ErrorResponseunauthorized — no credential, or one that is not valid.
403ErrorResponsemodule_not_entitled — RequireModule("cost") refused this before the handler ran (the first real consumer; the portal renders the locked-module upsell) — or forbidden, when the module is entitled and the user's role does not permit the operation. On an unsafe request, also origin_not_trusted: the browser boundary refused the request's origin before anything else ran.
404ErrorResponsenot_found
413ErrorResponseThe request body exceeded the size the server will read. Declared because it is real: a body is otherwise read until the client stops sending, and one caller could exhaust the process. The cap is above any legitimate request — 500 items at the additionalInfo limit is roughly 160 MB — so reaching it means something is wrong with the sender.
415ErrorResponseunsupported_media_type — the body of a browser-facing route is not labelled application/json (a +json suffix and a charset parameter are fine). Refused before the body is read: a form post, or a "simple" cross-site request carrying JSON as text/plain to dodge the preflight, is not something this API reads. Bearer routes are not held to it.
500ErrorResponseinternal_error. The message is generic by design: internal error text never reaches a response body. The real error is logged with the request ID.

GET /cost/savings/claims/{claimId}

credential sessionCookiemodule costoperation getSavingsClaim

One claim with its frozen sets and every version of its verdict

versions is every verdict ever computed for the claim, newest first. A recomputation whose answer differs — a late billing correction inside either window, the after-window settling — is a new version; the earlier ones are kept with their figures and marked supersededAt, never overwritten. A claim outside the caller's team scope is 404.

Responses

StatusBodyMeaning
200SavingsClaimDetailThe claim.
401ErrorResponseunauthorized — no credential, or one that is not valid.
403ErrorResponsemodule_not_entitled — RequireModule("cost") refused this before the handler ran (the first real consumer; the portal renders the locked-module upsell) — or forbidden, when the module is entitled and the user's role does not permit the operation. On an unsafe request, also origin_not_trusted: the browser boundary refused the request's origin before anything else ran.
404ErrorResponsenot_found
500ErrorResponseinternal_error. The message is generic by design: internal error text never reaches a response body. The real error is logged with the request ID.

GET /cost/savings/ledger

credential sessionCookiemodule costoperation getSavingsLedger

The savings ledger, per team or per declared app

Every claim the caller's team scope allows (slice 3), each with its current verdict and decomposition, cut by team or by declared app, with totals per currency that count every amount once. Read from the verdicts when asked; nothing is stored for it.

Totals add, per currency and never across, the amortized figure's observed reduction and the verified amount of each counted claim, side by side and never added to each other, each over the claim's own after-window. Nothing is annualised or extrapolated. A claim not yet measurable has no figure and adds nothing.

Overlaps. Two claims overlap when they froze a claimed resource in common and their after-windows share a day. Among the measured claims, in the order of the change, a claim counts unless it overlaps one already counted; one that does keeps its own figures, names the claims it overlaps, and is in no total. This is decided over the whole organisation's claims, so a team's total is the same whoever reads it; overlapsWith lists the overlapping claims the caller may read and overlapsElsewhere counts the others.

By app, a claim is in every declared app one of its claimed resources is (on the resource's last-known record, a resource the change deleted included), whole or part; only counted claims wholly in the app enter its totals. Apps may overlap one another, so their totals do not add up to the caller's.

AWS's estimate (claim.recommendation) is shown beside the measurement, monthly, as Trusted Advisor gave it when the claim was made; it is in no total.

Parameters

NameInTypeDescription
groupByquerystring enum

Responses

StatusBodyMeaning
200SavingsLedgerThe ledger.
400ErrorResponseinvalid_request — a malformed parameter or body. The batch ingest endpoint uses invalid_batch instead, because a collector branches on it differently: a bad batch is a bug in the collector, while a bad request is usually a bug in the caller's query string.
401ErrorResponseunauthorized — no credential, or one that is not valid.
403ErrorResponsemodule_not_entitled — RequireModule("cost") refused this before the handler ran (the first real consumer; the portal renders the locked-module upsell) — or forbidden, when the module is entitled and the user's role does not permit the operation. On an unsafe request, also origin_not_trusted: the browser boundary refused the request's origin before anything else ran.
500ErrorResponseinternal_error. The message is generic by design: internal error text never reaches a response body. The real error is logged with the request ID.

GET /cost/summary

credential sessionCookie or apiTokenmodule costoperation getCostSummary

Totals for a period, with the previous period beside them

Spend for the range by currency, the same length of range immediately before it, and the change between them — per currency, because a delta across two currencies is not a number. Also the split across the three attribution tiers (§7.1), which always sums to the total: that is the reconciliation invariant, and it is asserted for this endpoint and every other aggregation.

Parameters

NameInTypeDescription
fromquerystring (date)First day of the range, inclusive. Defaults to the first of the current month. from after to is invalid_request.
toquerystring (date)Last day of the range, inclusive. Defaults to today.
figurequeryCostFigureWhich money figure (C1). Amortized is what makes chargeback defensible — a reserved-instance purchase is a flat line rather than a spike on the team that did not make it — and is the default; unblended is what the invoice says.
accountIdqueryarray of stringRepeatable. Any of the given accounts.
teamIdqueryarray of string (uuid)Repeatable. Narrows within the caller's team scope, never widens it; a team the caller may not see yields no rows, not an error.

Responses

StatusBodyMeaning
200CostSummaryThe period's totals.
400ErrorResponseinvalid_request — a malformed parameter or body. The batch ingest endpoint uses invalid_batch instead, because a collector branches on it differently: a bad batch is a bug in the collector, while a bad request is usually a bug in the caller's query string.
401ErrorResponseunauthorized — no credential, or one that is not valid.
403ErrorResponsemodule_not_entitled — RequireModule("cost") refused this before the handler ran (the first real consumer; the portal renders the locked-module upsell) — or forbidden, when the module is entitled and the user's role does not permit the operation. On an unsafe request, also origin_not_trusted: the browser boundary refused the request's origin before anything else ran.
429ErrorResponseRate limited.
500ErrorResponseinternal_error. The message is generic by design: internal error text never reaches a response body. The real error is logged with the request ID.

GET /cost/trend

credential sessionCookie or apiTokenmodule costoperation getCostTrend

A daily or monthly series, grouped

One series per group — team, service or account — each a list of {date, amounts} points. A day with no spend is a point with an empty amounts array rather than a missing point, so a chart's x-axis is the range and not the data.

Parameters

NameInTypeDescription
fromquerystring (date)First day of the range, inclusive. Defaults to the first of the current month. from after to is invalid_request.
toquerystring (date)Last day of the range, inclusive. Defaults to today.
figurequeryCostFigureWhich money figure (C1). Amortized is what makes chargeback defensible — a reserved-instance purchase is a flat line rather than a spike on the team that did not make it — and is the default; unblended is what the invoice says.
groupByrequiredquerystring enum
granularityquerystring enum
accountIdqueryarray of stringRepeatable. Any of the given accounts.
teamIdqueryarray of string (uuid)Repeatable. Narrows within the caller's team scope, never widens it; a team the caller may not see yields no rows, not an error.

Responses

StatusBodyMeaning
200CostTrendThe series.
400ErrorResponseinvalid_request — a malformed parameter or body. The batch ingest endpoint uses invalid_batch instead, because a collector branches on it differently: a bad batch is a bug in the collector, while a bad request is usually a bug in the caller's query string.
401ErrorResponseunauthorized — no credential, or one that is not valid.
403ErrorResponsemodule_not_entitled — RequireModule("cost") refused this before the handler ran (the first real consumer; the portal renders the locked-module upsell) — or forbidden, when the module is entitled and the user's role does not permit the operation. On an unsafe request, also origin_not_trusted: the browser boundary refused the request's origin before anything else ran.
429ErrorResponseRate limited.
500ErrorResponseinternal_error. The message is generic by design: internal error text never reaches a response body. The real error is logged with the request ID.
cost-settings (21)

cost-settings

Attribution rules, team tag values, anomaly sensitivity, budgets. Writes require the admin role; changing a rule or a tag value re-attributes all history (C3), and the response says so.

GET /cost/azure-billing-exports

credential sessionCookiemodule costoperation listCostAzureBillingExports

Azure Cost Management exports, with each one's last pull

Every Cost Management export the tenant has configured: the subscription whose reader reads it, the storage account, container and directory it is written to, the directory of its AmortizedCost export when it is the ActualCost + AmortizedCost pair rather than FOCUS, the cap on one blob, and the last pull's outcome. The pull is the job cost.azure_billing, run once a day per Azure subscription as that subscription's reader, so lastPull is the subscription's latest run record; one run reads every export of its subscription, and its message names the export a refusal is about. lastPull is absent until the subscription has been pulled once.

Responses

StatusBodyMeaning
200CostAzureBillingExportListThe exports, by subscription.
401ErrorResponseunauthorized — no credential, or one that is not valid.
403ErrorResponsemodule_not_entitled — RequireModule("cost") refused this before the handler ran (the first real consumer; the portal renders the locked-module upsell) — or forbidden, when the module is entitled and the user's role does not permit the operation. On an unsafe request, also origin_not_trusted: the browser boundary refused the request's origin before anything else ran.
500ErrorResponseinternal_error. The message is generic by design: internal error text never reaches a response body. The real error is logged with the request ID.

POST /cost/azure-billing-exports

credential sessionCookiemodule costrole adminoperation createCostAzureBillingExport

Configure an Azure export

The subscription must be an Azure subscription of the organisation — the job runs only for those. The names follow Azure's own rules, and the directories are paths inside the container. The blob cap is 1 GiB unless given. The same export twice is 409 invalid_request. Audited as cost.azure_billing_export.create. Configuring an export reads nothing at once: the next daily pull of the subscription does, and only once the export reader role is assigned on the container.

Request body application/json

FieldTypeDescription
containerrequiredstring
directoryrequiredstring
maxBlobBytesinteger (int64)
pairDirectorystring
storageAccountrequiredstring
subscriptionIdrequiredstring (uuid)

Responses

StatusBodyMeaning
201CostAzureBillingExportConfigured.
400ErrorResponseinvalid_request — a malformed parameter or body. The batch ingest endpoint uses invalid_batch instead, because a collector branches on it differently: a bad batch is a bug in the collector, while a bad request is usually a bug in the caller's query string.
401ErrorResponseunauthorized — no credential, or one that is not valid.
403ErrorResponsemodule_not_entitled — RequireModule("cost") refused this before the handler ran (the first real consumer; the portal renders the locked-module upsell) — or forbidden, when the module is entitled and the user's role does not permit the operation. On an unsafe request, also origin_not_trusted: the browser boundary refused the request's origin before anything else ran.
409ErrorResponseinvalid_request — that export is already configured.
413ErrorResponseThe request body exceeded the size the server will read. Declared because it is real: a body is otherwise read until the client stops sending, and one caller could exhaust the process. The cap is above any legitimate request — 500 items at the additionalInfo limit is roughly 160 MB — so reaching it means something is wrong with the sender.
415ErrorResponseunsupported_media_type — the body of a browser-facing route is not labelled application/json (a +json suffix and a charset parameter are fine). Refused before the body is read: a form post, or a "simple" cross-site request carrying JSON as text/plain to dodge the preflight, is not something this API reads. Bearer routes are not held to it.
500ErrorResponseinternal_error. The message is generic by design: internal error text never reaches a response body. The real error is logged with the request ID.

PATCH /cost/azure-billing-exports/{exportId}

credential sessionCookiemodule costrole adminoperation updateCostAzureBillingExport

Change where an Azure export is, or its blob cap

It asks for the password (x-step-up), as a budget's update does: a different place points the pull somewhere else, and a lower cap can stop it. pairDirectory set to the empty string makes the export a FOCUS export again. Rows already read stay; the next pull replaces its windows from the export as it now stands. Audited as cost.azure_billing_export.update, with the export before and after.

Request body application/json

FieldTypeDescription
containerstring
directorystring
maxBlobBytesinteger (int64)
pairDirectorystring
storageAccountstring

Responses

StatusBodyMeaning
200CostAzureBillingExportThe export as it now stands.
400ErrorResponseinvalid_request — a malformed parameter or body. The batch ingest endpoint uses invalid_batch instead, because a collector branches on it differently: a bad batch is a bug in the collector, while a bad request is usually a bug in the caller's query string.
401ErrorResponseunauthorized — no credential, or one that is not valid.
403ErrorResponsemodule_not_entitled — RequireModule("cost") refused this before the handler ran (the first real consumer; the portal renders the locked-module upsell) — or forbidden, when the module is entitled and the user's role does not permit the operation. On an unsafe request, also origin_not_trusted: the browser boundary refused the request's origin before anything else ran.
404ErrorResponsenot_found
409ErrorResponseinvalid_request — another export is already configured there.
413ErrorResponseThe request body exceeded the size the server will read. Declared because it is real: a body is otherwise read until the client stops sending, and one caller could exhaust the process. The cap is above any legitimate request — 500 items at the additionalInfo limit is roughly 160 MB — so reaching it means something is wrong with the sender.
415ErrorResponseunsupported_media_type — the body of a browser-facing route is not labelled application/json (a +json suffix and a charset parameter are fine). Refused before the body is read: a form post, or a "simple" cross-site request carrying JSON as text/plain to dodge the preflight, is not something this API reads. Bearer routes are not held to it.
500ErrorResponseinternal_error. The message is generic by design: internal error text never reaches a response body. The real error is logged with the request ID.

DELETE /cost/azure-billing-exports/{exportId}

credential sessionCookiemodule costrole adminoperation deleteCostAzureBillingExport

Stop reading an Azure export

The pull stops reading it. What was read stays — spend is history, and the cost retention removes it on its own schedule. Audited as cost.azure_billing_export.delete.

Responses

StatusBodyMeaning
204—Removed.
401ErrorResponseunauthorized — no credential, or one that is not valid.
403ErrorResponsemodule_not_entitled — RequireModule("cost") refused this before the handler ran (the first real consumer; the portal renders the locked-module upsell) — or forbidden, when the module is entitled and the user's role does not permit the operation. On an unsafe request, also origin_not_trusted: the browser boundary refused the request's origin before anything else ran.
404ErrorResponsenot_found
500ErrorResponseinternal_error. The message is generic by design: internal error text never reaches a response body. The real error is logged with the request ID.

GET /cost/budgets

credential sessionCookie or apiTokenmodule costoperation listCostBudgets

Team budgets, with month-to-date progress

Every budget the caller's team scope allows, with the current period's amortized spend in the budget's currency beside the amount. Spend in a currency the budget is not denominated in is not converted (C7): it is listed under otherCurrencies so the mismatch is visible.

Parameters

NameInTypeDescription
teamIdqueryarray of string (uuid)Repeatable. Narrows within the caller's team scope, never widens it; a team the caller may not see yields no rows, not an error.

Responses

StatusBodyMeaning
200CostBudgetListThe budgets.
401ErrorResponseunauthorized — no credential, or one that is not valid.
403ErrorResponsemodule_not_entitled — RequireModule("cost") refused this before the handler ran (the first real consumer; the portal renders the locked-module upsell) — or forbidden, when the module is entitled and the user's role does not permit the operation. On an unsafe request, also origin_not_trusted: the browser boundary refused the request's origin before anything else ran.
429ErrorResponseRate limited.
500ErrorResponseinternal_error. The message is generic by design: internal error text never reaches a response body. The real error is logged with the request ID.

POST /cost/budgets

credential sessionCookiemodule costrole adminoperation createCostBudget

Create a team's monthly budget

One budget per team per period; a second is 409 budget_exists. Thresholds are percentages of amount, and each fires once per period (§9) through the platform outbox to the team's channel.

Request body application/json

FieldTypeDescription
amountrequiredMoneyOne amount in one currency. The only way money appears in this document. TestAllMoneyIsCurrencyQualified walks every response schema and fails on any amount-shaped property that is not inside this object or an array of it, because "a scalar total" is the one contract mistake that cannot be retrofitted once a client has summed two currencies (C7). amount is a decimal as a string, never a JSON number: numeric(20,10) does not survive a float, and a total that reconciles to the cent is the product.
enabledboolean
periodstring enum One of monthly.
teamIdrequiredstring (uuid)
thresholdsarray of integer

Responses

StatusBodyMeaning
201CostBudgetCreated.
400ErrorResponseinvalid_request — a malformed parameter or body. The batch ingest endpoint uses invalid_batch instead, because a collector branches on it differently: a bad batch is a bug in the collector, while a bad request is usually a bug in the caller's query string.
401ErrorResponseunauthorized — no credential, or one that is not valid.
403ErrorResponsemodule_not_entitled — RequireModule("cost") refused this before the handler ran (the first real consumer; the portal renders the locked-module upsell) — or forbidden, when the module is entitled and the user's role does not permit the operation. On an unsafe request, also origin_not_trusted: the browser boundary refused the request's origin before anything else ran.
404ErrorResponsenot_found
409ErrorResponsebudget_exists — this team already has a monthly budget; edit that one instead.
413ErrorResponseThe request body exceeded the size the server will read. Declared because it is real: a body is otherwise read until the client stops sending, and one caller could exhaust the process. The cap is above any legitimate request — 500 items at the additionalInfo limit is roughly 160 MB — so reaching it means something is wrong with the sender.
415ErrorResponseunsupported_media_type — the body of a browser-facing route is not labelled application/json (a +json suffix and a charset parameter are fine). Refused before the body is read: a form post, or a "simple" cross-site request carrying JSON as text/plain to dodge the preflight, is not something this API reads. Bearer routes are not held to it.
500ErrorResponseinternal_error. The message is generic by design: internal error text never reaches a response body. The real error is logged with the request ID.

GET /cost/budgets/{budgetId}

credential sessionCookiemodule costoperation getCostBudget

One budget with its progress

Responses

StatusBodyMeaning
200CostBudgetThe budget.
401ErrorResponseunauthorized — no credential, or one that is not valid.
403ErrorResponsemodule_not_entitled — RequireModule("cost") refused this before the handler ran (the first real consumer; the portal renders the locked-module upsell) — or forbidden, when the module is entitled and the user's role does not permit the operation. On an unsafe request, also origin_not_trusted: the browser boundary refused the request's origin before anything else ran.
404ErrorResponsenot_found
500ErrorResponseinternal_error. The message is generic by design: internal error text never reaches a response body. The real error is logged with the request ID.

PATCH /cost/budgets/{budgetId}

credential sessionCookiemodule costrole adminoperation updateCostBudget

Change a budget's amount, currency, thresholds or enabled flag

Changing the currency is currency_mismatch while the current period already holds notifications in the old one: a threshold that fired at 80% of 1,000 USD cannot be reinterpreted as 80% of 1,000 EUR.

It asks for the password (x-step-up): enabled: false stops the budget alerting, as deleting it would. Any other change asks too; it is the same operation.

Request body application/json

FieldTypeDescription
amountMoneyOne amount in one currency. The only way money appears in this document. TestAllMoneyIsCurrencyQualified walks every response schema and fails on any amount-shaped property that is not inside this object or an array of it, because "a scalar total" is the one contract mistake that cannot be retrofitted once a client has summed two currencies (C7). amount is a decimal as a string, never a JSON number: numeric(20,10) does not survive a float, and a total that reconciles to the cent is the product.
enabledboolean
thresholdsarray of integer

Responses

StatusBodyMeaning
200CostBudgetThe budget as it now stands.
400ErrorResponseinvalid_request — a malformed parameter or body. The batch ingest endpoint uses invalid_batch instead, because a collector branches on it differently: a bad batch is a bug in the collector, while a bad request is usually a bug in the caller's query string.
401ErrorResponseunauthorized — no credential, or one that is not valid.
403ErrorResponsemodule_not_entitled — RequireModule("cost") refused this before the handler ran (the first real consumer; the portal renders the locked-module upsell) — or forbidden, when the module is entitled and the user's role does not permit the operation. On an unsafe request, also origin_not_trusted: the browser boundary refused the request's origin before anything else ran.
404ErrorResponsenot_found
409ErrorResponsecurrency_mismatch — see the description.
413ErrorResponseThe request body exceeded the size the server will read. Declared because it is real: a body is otherwise read until the client stops sending, and one caller could exhaust the process. The cap is above any legitimate request — 500 items at the additionalInfo limit is roughly 160 MB — so reaching it means something is wrong with the sender.
415ErrorResponseunsupported_media_type — the body of a browser-facing route is not labelled application/json (a +json suffix and a charset parameter are fine). Refused before the body is read: a form post, or a "simple" cross-site request carrying JSON as text/plain to dodge the preflight, is not something this API reads. Bearer routes are not held to it.
500ErrorResponseinternal_error. The message is generic by design: internal error text never reaches a response body. The real error is logged with the request ID.

DELETE /cost/budgets/{budgetId}

credential sessionCookiemodule costrole adminoperation deleteCostBudget

Remove a budget

Its notification history goes with it.

Responses

StatusBodyMeaning
204—Removed.
401ErrorResponseunauthorized — no credential, or one that is not valid.
403ErrorResponsemodule_not_entitled — RequireModule("cost") refused this before the handler ran (the first real consumer; the portal renders the locked-module upsell) — or forbidden, when the module is entitled and the user's role does not permit the operation. On an unsafe request, also origin_not_trusted: the browser boundary refused the request's origin before anything else ran.
404ErrorResponsenot_found
500ErrorResponseinternal_error. The message is generic by design: internal error text never reaches a response body. The real error is logged with the request ID.

GET /cost/gcp-billing-exports

credential sessionCookiemodule costoperation listCostGcpBillingExports

Google Cloud billing exports, with each one's last pull

Every billing account whose BigQuery billing export the tenant has configured: the export's table, the byte cap every query of it carries, and the last pull's outcome. The pull is the job cost.gcp_billing, run once a day per Google Cloud project as that project's reader; an export is read by the project its table is in, so lastPull is that project's latest run record. One run reads every export of its project, and a refusal stops it, so a run refused for one billing account is the last pull of the others in that project too: its message names the billing account it is about. lastPull is absent until the project has been pulled once — the project not being connected to the tenant is the usual reason.

Responses

StatusBodyMeaning
200CostGcpBillingExportListThe exports, by billing account.
401ErrorResponseunauthorized — no credential, or one that is not valid.
403ErrorResponsemodule_not_entitled — RequireModule("cost") refused this before the handler ran (the first real consumer; the portal renders the locked-module upsell) — or forbidden, when the module is entitled and the user's role does not permit the operation. On an unsafe request, also origin_not_trusted: the browser boundary refused the request's origin before anything else ran.
500ErrorResponseinternal_error. The message is generic by design: internal error text never reaches a response body. The real error is logged with the request ID.

POST /cost/gcp-billing-exports

credential sessionCookiemodule costrole adminoperation createCostGcpBillingExport

Configure a billing account's export

One export per billing account; a second is 409 invalid_request. The table is project.dataset.table with BigQuery's own characters, held to the same rule the query builder holds it to, because the name is interpolated into the query. The byte cap is 10 GiB unless given. Audited as cost.gcp_billing_export.create. Configuring an export reads nothing at once: the next daily pull of the table's project does, and only if that project is connected.

Request body application/json

FieldTypeDescription
billingAccountIdrequiredstring
maxBytesBilledinteger (int64)
tableNamerequiredstring

Responses

StatusBodyMeaning
201CostGcpBillingExportConfigured.
400ErrorResponseinvalid_request — a malformed parameter or body. The batch ingest endpoint uses invalid_batch instead, because a collector branches on it differently: a bad batch is a bug in the collector, while a bad request is usually a bug in the caller's query string.
401ErrorResponseunauthorized — no credential, or one that is not valid.
403ErrorResponsemodule_not_entitled — RequireModule("cost") refused this before the handler ran (the first real consumer; the portal renders the locked-module upsell) — or forbidden, when the module is entitled and the user's role does not permit the operation. On an unsafe request, also origin_not_trusted: the browser boundary refused the request's origin before anything else ran.
409ErrorResponseinvalid_request — the billing account already has an export.
413ErrorResponseThe request body exceeded the size the server will read. Declared because it is real: a body is otherwise read until the client stops sending, and one caller could exhaust the process. The cap is above any legitimate request — 500 items at the additionalInfo limit is roughly 160 MB — so reaching it means something is wrong with the sender.
415ErrorResponseunsupported_media_type — the body of a browser-facing route is not labelled application/json (a +json suffix and a charset parameter are fine). Refused before the body is read: a form post, or a "simple" cross-site request carrying JSON as text/plain to dodge the preflight, is not something this API reads. Bearer routes are not held to it.
500ErrorResponseinternal_error. The message is generic by design: internal error text never reaches a response body. The real error is logged with the request ID.

PATCH /cost/gcp-billing-exports/{billingAccountId}

credential sessionCookiemodule costrole adminoperation updateCostGcpBillingExport

Change an export's table or byte cap

It asks for the password (x-step-up), as a budget's update does: a different table points the pull somewhere else, and a lower cap can stop it. Rows already read stay; the next pull replaces its windows from the table as it now stands. Audited as cost.gcp_billing_export.update, with the export before and after.

Request body application/json

FieldTypeDescription
maxBytesBilledinteger (int64)
tableNamestring

Responses

StatusBodyMeaning
200CostGcpBillingExportThe export as it now stands.
400ErrorResponseinvalid_request — a malformed parameter or body. The batch ingest endpoint uses invalid_batch instead, because a collector branches on it differently: a bad batch is a bug in the collector, while a bad request is usually a bug in the caller's query string.
401ErrorResponseunauthorized — no credential, or one that is not valid.
403ErrorResponsemodule_not_entitled — RequireModule("cost") refused this before the handler ran (the first real consumer; the portal renders the locked-module upsell) — or forbidden, when the module is entitled and the user's role does not permit the operation. On an unsafe request, also origin_not_trusted: the browser boundary refused the request's origin before anything else ran.
404ErrorResponsenot_found
413ErrorResponseThe request body exceeded the size the server will read. Declared because it is real: a body is otherwise read until the client stops sending, and one caller could exhaust the process. The cap is above any legitimate request — 500 items at the additionalInfo limit is roughly 160 MB — so reaching it means something is wrong with the sender.
415ErrorResponseunsupported_media_type — the body of a browser-facing route is not labelled application/json (a +json suffix and a charset parameter are fine). Refused before the body is read: a form post, or a "simple" cross-site request carrying JSON as text/plain to dodge the preflight, is not something this API reads. Bearer routes are not held to it.
500ErrorResponseinternal_error. The message is generic by design: internal error text never reaches a response body. The real error is logged with the request ID.

DELETE /cost/gcp-billing-exports/{billingAccountId}

credential sessionCookiemodule costrole adminoperation deleteCostGcpBillingExport

Stop reading a billing account's export

The pull stops reading it. What was read stays — spend is history, and the cost retention removes it on its own schedule. Audited as cost.gcp_billing_export.delete.

Responses

StatusBodyMeaning
204—Removed.
401ErrorResponseunauthorized — no credential, or one that is not valid.
403ErrorResponsemodule_not_entitled — RequireModule("cost") refused this before the handler ran (the first real consumer; the portal renders the locked-module upsell) — or forbidden, when the module is entitled and the user's role does not permit the operation. On an unsafe request, also origin_not_trusted: the browser boundary refused the request's origin before anything else ran.
404ErrorResponsenot_found
500ErrorResponseinternal_error. The message is generic by design: internal error text never reaches a response body. The real error is logged with the request ID.

GET /cost/recommendations

credential sessionCookie or apiTokenmodule costoperation listCostRecommendations

Trusted Advisor's cost recommendations, per account

AWS's cost-optimisation checks, one row per flagged resource, largest estimated saving first. The savings are AWS's estimates and are presented as such. Where a check names a resource the key derivation can map, resourceKey links to the inventory.

Trusted Advisor needs Business or Enterprise support on the account; an account without it is listed under unavailableAccounts with support_plan_required — a documented state, never an error. Scoped to the caller's teams' accounts.

Parameters

NameInTypeDescription
accountIdqueryarray of stringRepeatable. Any of the given accounts.
pagequeryinteger1-based. Normalised server-side rather than rejected: < 1 becomes 1, and a page past the end is clamped to totalPages. Deliberately no minimum: a schema constraint would make the generated server reject page=0 before the handler could normalise it, which contradicts the normalisation this very description promises.
pageSizequeryintegerNormalised server-side, not rejected: < 1 becomes 100, > 500 becomes 500. No minimum or maximum, for the same reason as page — a constraint here would turn a clamp into a 400.
formatquerystring enumcsv answers the same read as a file: the same filters, the same scope, the same rows, one line per amount and currency — a key with spend in two currencies is two lines, and no line is a total. A text cell that begins with =, +, -, @, a tab or a carriage return is written with a leading ', so a name or a tag value cannot become a spreadsheet formula; an amount is written as the database's decimal, its sign kept. The export of a view is its URL with format=csv, so a browser navigating to it downloads the file.

Responses

StatusBodyMeaning
200CostRecommendationPageThe recommendations. As format=csv, the whole filtered list rather than a page — page and pageSize are ignored — in the list's order, up to its first 50,000 recommendations, with the columns accountId, checkId, checkName, resourceId, resourceKey, region, status, currency, estimatedMonthlySavings, firstSeenAt, observedAt, metadata (metadata the check's fields as key=value, sorted by key and joined by ; ): one line per recommendation, AWS's estimate in its currency. unavailableAccounts is the JSON's alone. A longer list ends with a line whose first cell begins with # and says how many of how many the file holds.
400ErrorResponseinvalid_request — a malformed parameter or body. The batch ingest endpoint uses invalid_batch instead, because a collector branches on it differently: a bad batch is a bug in the collector, while a bad request is usually a bug in the caller's query string.
401ErrorResponseunauthorized — no credential, or one that is not valid.
403ErrorResponsemodule_not_entitled — RequireModule("cost") refused this before the handler ran (the first real consumer; the portal renders the locked-module upsell) — or forbidden, when the module is entitled and the user's role does not permit the operation. On an unsafe request, also origin_not_trusted: the browser boundary refused the request's origin before anything else ran.
429ErrorResponseRate limited.
500ErrorResponseinternal_error. The message is generic by design: internal error text never reaches a response body. The real error is logged with the request ID.

GET /cost/settings

credential sessionCookiemodule costoperation getCostSettings

Attribution rules, tag values, sensitivity, and ingestion health

Everything the Settings page shows: the tag keys in priority order, the messy tag values mapped onto teams, anomaly sensitivity, the display default, per-account ingestion state — backfill progress, the resource-granularity flag, the last error, the pass A / pass B discrepancy — the count of per-resource identifiers the key derivation could not map (§5.4), and whether a rebuild is in flight.

Responses

StatusBodyMeaning
200CostSettingsThe settings.
401ErrorResponseunauthorized — no credential, or one that is not valid.
403ErrorResponsemodule_not_entitled — RequireModule("cost") refused this before the handler ran (the first real consumer; the portal renders the locked-module upsell) — or forbidden, when the module is entitled and the user's role does not permit the operation. On an unsafe request, also origin_not_trusted: the browser boundary refused the request's origin before anything else ran.
500ErrorResponseinternal_error. The message is generic by design: internal error text never reaches a response body. The real error is logged with the request ID.

PUT /cost/settings

credential sessionCookiemodule costrole adminoperation updateCostSettings

Replace the attribution rules, tag values and sensitivity

A whole-document replace, because rules are an ordered list and a partial update of an ordering is ambiguous. Changing the rules or the tag values enqueues a rebuild of every rollup month (§7.3, C3); the response reports rebuildEnqueued. A second change while a rebuild is already running is 409 attribution_rebuild_in_progress: let the first finish, or its result would describe rules that no longer exist.

Request body application/json

FieldTypeDescription
allocationRulesarray of CostAllocationRuleThe whole list, in order. Omitted, the rules are left as they are. Changing them re-attributes history like a rule change (§7.3): the allocation runs inside the rebuild.
anomalyrequiredCostAnomalySettings
attributionRulesrequiredarray of CostAttributionRule
curReportsarray of CostCurReportThe whole set, like the rules: an account absent from the list has its report configuration removed. Omitted, the configuration is left as it is. Changing a report enqueues no rebuild — the next pull replaces the periods it reads, and the rebuild follows the data.
defaultFigurerequiredCostFigure One of amortized, unblended.
teamTagValuesrequiredarray of CostTeamTagValue

Responses

StatusBodyMeaning
200CostSettingsThe settings as they now stand.
400ErrorResponseinvalid_request — a malformed parameter or body. The batch ingest endpoint uses invalid_batch instead, because a collector branches on it differently: a bad batch is a bug in the collector, while a bad request is usually a bug in the caller's query string.
401ErrorResponseunauthorized — no credential, or one that is not valid.
403ErrorResponsemodule_not_entitled — RequireModule("cost") refused this before the handler ran (the first real consumer; the portal renders the locked-module upsell) — or forbidden, when the module is entitled and the user's role does not permit the operation. On an unsafe request, also origin_not_trusted: the browser boundary refused the request's origin before anything else ran.
409ErrorResponseattribution_rebuild_in_progress — a rebuild from the previous change is still running.
413ErrorResponseThe request body exceeded the size the server will read. Declared because it is real: a body is otherwise read until the client stops sending, and one caller could exhaust the process. The cap is above any legitimate request — 500 items at the additionalInfo limit is roughly 160 MB — so reaching it means something is wrong with the sender.
415ErrorResponseunsupported_media_type — the body of a browser-facing route is not labelled application/json (a +json suffix and a charset parameter are fine). Refused before the body is read: a form post, or a "simple" cross-site request carrying JSON as text/plain to dodge the preflight, is not something this API reads. Bearer routes are not held to it.
500ErrorResponseinternal_error. The message is generic by design: internal error text never reaches a response body. The real error is logged with the request ID.

GET /cost/tag-compliance

credential sessionCookie or apiTokenmodule costoperation getTagCompliance

What the policy says about the estate, with the money behind it

Five disjoint classes — compliant, missing_required_key, value_not_allowed, exempt, unknown_coverage — each with the month-to-date amortized spend behind it, per currency, summing to the scope's authoritative total.

unknown_coverage will often be the largest. Compliance is a property of a resource and its tags; the authoritative total is per account and day and names no resource at all (COST_SPEC §7.2). So this class holds every amount with no resource behind it to read tags from, plus resources of a type nothing reads tags for. It means "we cannot see these resources' tags", never "your tagging is bad", and it is never folded into compliant.

discrepancy names a currency whose per-resource detail exceeded the authoritative total, which can happen on a day whose money source and resource source are different reports. The classes are capped at the total and this says so, rather than showing a negative share.

Parameters

NameInTypeDescription
fromquerystring (date)The window's first day. Defaults with to to the month to date.
toquerystring (date)The window's last day, inclusive.
accountIdqueryarray of string

Responses

StatusBodyMeaning
200TagComplianceBreakdownThe breakdown.
400ErrorResponseinvalid_request — a malformed parameter or body. The batch ingest endpoint uses invalid_batch instead, because a collector branches on it differently: a bad batch is a bug in the collector, while a bad request is usually a bug in the caller's query string.
401ErrorResponseunauthorized — no credential, or one that is not valid.
403ErrorResponsemodule_not_entitled — RequireModule("cost") refused this before the handler ran (the first real consumer; the portal renders the locked-module upsell) — or forbidden, when the module is entitled and the user's role does not permit the operation. On an unsafe request, also origin_not_trusted: the browser boundary refused the request's origin before anything else ran.
429ErrorResponseRate limited.
500ErrorResponseinternal_error. The message is generic by design: internal error text never reaches a response body. The real error is logged with the request ID.

GET /cost/tag-compliance/groups

credential sessionCookiemodule costoperation getTagComplianceGroups

The same classes, cut by account, team or declared app

The scope's compliance re-cut per subject: by account, by the owning team — the verdict, with the unowned as their own subject — or by declared app.

A re-cut, and its own operation for that reason. These amounts are the same money the flat classes hold, counted again per subject, so they do not sum to the scope's total and must not be added to it. A declared app's cut cannot reconcile at all: a resource may belong to several apps or to none, and an app has no authoritative spend of its own to close against. /cost/tag-compliance is the figure that reconciles; this is the breakdown beneath it.

Parameters

NameInTypeDescription
fromquerystring (date)
toquerystring (date)
accountIdqueryarray of string
groupByrequiredquerystring enum

Responses

StatusBodyMeaning
200TagComplianceGroupListThe subjects, sorted by label.
400ErrorResponseinvalid_request — a malformed parameter or body. The batch ingest endpoint uses invalid_batch instead, because a collector branches on it differently: a bad batch is a bug in the collector, while a bad request is usually a bug in the caller's query string.
401ErrorResponseunauthorized — no credential, or one that is not valid.
403ErrorResponsemodule_not_entitled — RequireModule("cost") refused this before the handler ran (the first real consumer; the portal renders the locked-module upsell) — or forbidden, when the module is entitled and the user's role does not permit the operation. On an unsafe request, also origin_not_trusted: the browser boundary refused the request's origin before anything else ran.
500ErrorResponseinternal_error. The message is generic by design: internal error text never reaches a response body. The real error is logged with the request ID.

GET /cost/tag-policy

credential sessionCookiemodule costoperation getTagPolicy

The tenant's tag policy

Which tag keys a resource must carry, which values each key admits, and which resource types the policy does not apply to.

A tenant that has not written a policy is answered with one derived from the attribution keys in their priority order (COST_SPEC §7.1), and declared is false. The editor must not present that as something somebody chose.

The team key's allowed values always include team_tag_values, whatever the policy stores: a value this page called allowed and attribution could not map to a team would be a resource compliant here and invisible to the money.

Responses

StatusBodyMeaning
200TagPolicyThe policy.
401ErrorResponseunauthorized — no credential, or one that is not valid.
403ErrorResponsemodule_not_entitled — RequireModule("cost") refused this before the handler ran (the first real consumer; the portal renders the locked-module upsell) — or forbidden, when the module is entitled and the user's role does not permit the operation. On an unsafe request, also origin_not_trusted: the browser boundary refused the request's origin before anything else ran.
500ErrorResponseinternal_error. The message is generic by design: internal error text never reaches a response body. The real error is logged with the request ID.

PUT /cost/tag-policy

credential sessionCookiemodule costrole adminoperation updateTagPolicy

Replace the tag policy

A whole-document replace, as the attribution rules are: a policy is a set and a partial update of a set is ambiguous.

This changes no figure. A policy says whether a resource is tagged the way the tenant asked; attribution says whose spend it is. Changing the policy is audited as cost.tag_policy.update and enqueues no rebuild — unlike cost_attribution_rules, whose change re-attributes every month (§7.3).

Request body application/json

FieldTypeDescription
exemptResourceTypesrequiredarray of string
requiredKeysrequiredarray of TagPolicyKey

Responses

StatusBodyMeaning
200TagPolicyThe policy as it now stands.
400ErrorResponseinvalid_request — a malformed parameter or body. The batch ingest endpoint uses invalid_batch instead, because a collector branches on it differently: a bad batch is a bug in the collector, while a bad request is usually a bug in the caller's query string.
401ErrorResponseunauthorized — no credential, or one that is not valid.
403ErrorResponsemodule_not_entitled — RequireModule("cost") refused this before the handler ran (the first real consumer; the portal renders the locked-module upsell) — or forbidden, when the module is entitled and the user's role does not permit the operation. On an unsafe request, also origin_not_trusted: the browser boundary refused the request's origin before anything else ran.
413ErrorResponseThe request body exceeded the size the server will read. Declared because it is real: a body is otherwise read until the client stops sending, and one caller could exhaust the process. The cap is above any legitimate request — 500 items at the additionalInfo limit is roughly 160 MB — so reaching it means something is wrong with the sender.
415ErrorResponseunsupported_media_type — the body of a browser-facing route is not labelled application/json (a +json suffix and a charset parameter are fine). Refused before the body is read: a form post, or a "simple" cross-site request carrying JSON as text/plain to dodge the preflight, is not something this API reads. Bearer routes are not held to it.
500ErrorResponseinternal_error. The message is generic by design: internal error text never reaches a response body. The real error is logged with the request ID.

GET /cost/tag-violations

credential sessionCookie or apiTokenmodule costoperation listTagViolations

The resources the tag policy is not satisfied by

Every resource in violation, with what it is worth, every fault it carries and the value the product can justify proposing. Biggest spend first, so the list opens on what is worth fixing.

A proposal is made only where it can be justified: the team key, from the resolved owner, and only when the policy admits that value. Nothing else is inferable, and a proposal nobody can justify is worse than none.

unowned=true is the card's unowned view — the verdict joined to this money — as a filter of this same list rather than a second read that could disagree with it.

Parameters

NameInTypeDescription
fromquerystring (date)
toquerystring (date)
accountIdqueryarray of string
unownedquerybooleanOnly the resources the resolver could not give an owner.
limitqueryinteger
formatquerystring enumcsv answers the same read as a file: the same filters, the same scope, the same rows, one line per amount and currency — a key with spend in two currencies is two lines, and no line is a total. A text cell that begins with =, +, -, @, a tab or a carriage return is written with a leading ', so a name or a tag value cannot become a spreadsheet formula; an amount is written as the database's decimal, its sign kept. The export of a view is its URL with format=csv, so a browser navigating to it downloads the file.

Responses

StatusBodyMeaning
200TagViolationListThe resources in violation. As format=csv, the whole filtered list rather than its first limit — limit is ignored — in the list's order, up to its first 50,000 resources, with the columns from, to, accountId, resourceKey, class, ownState, faults, proposed, currency, amount: one line per resource and currency. faults is each fault as kind key or kind key=value, and proposed each proposal as key=value, sorted and joined by ; , so a resource's faults never multiply its lines. A longer list ends with a line whose first cell begins with # and says how many of how many the file holds.
400ErrorResponseinvalid_request — a malformed parameter or body. The batch ingest endpoint uses invalid_batch instead, because a collector branches on it differently: a bad batch is a bug in the collector, while a bad request is usually a bug in the caller's query string.
401ErrorResponseunauthorized — no credential, or one that is not valid.
403ErrorResponsemodule_not_entitled — RequireModule("cost") refused this before the handler ran (the first real consumer; the portal renders the locked-module upsell) — or forbidden, when the module is entitled and the user's role does not permit the operation. On an unsafe request, also origin_not_trusted: the browser boundary refused the request's origin before anything else ran.
429ErrorResponseRate limited.
500ErrorResponseinternal_error. The message is generic by design: internal error text never reaches a response body. The real error is logged with the request ID.
Schemas (507)

Schemas

Every object the API reads or writes. A field marked required is always present.

AIReview

FieldTypeDescription
accountIdstring
anchorAIReviewAnchorAn impact review's question beside its resource: the time the investigation is anchored on, the window either side and how far the neighbours go. Present on an impact review and on no other.
answerSummarystring
answeredAtstring (date-time)
confidencestring enum One of low, medium, high.
expiresAtrequiredstring (date-time)When the request's token stops answering.
findingsrequiredarray of AIReviewFinding
idrequiredstring (uuid)
modelstring
requestedAtrequiredstring (date-time)
resourceKeystring
subjectIdrequiredstring
subjectKindrequiredstring enum One of alert, anomaly, test, impact.
summaryrequiredstringThe one line the reviewer was handed.
verdictstring enum One of nothing_stands_out, findings, could_not_review.

AIReviewAnchor

An impact review's question beside its resource: the time the investigation is anchored on, the window either side and how far the neighbours go. Present on an impact review and on no other.

FieldTypeDescription
atrequiredstring (date-time)
depthrequiredinteger
windowrequiredstring enum One of 1h, 2h, 6h, 24h.

AIReviewAnswer

FieldTypeDescription
confidencestring enumThe model's own estimate, not a measured one; the portal labels it so. One of low, medium, high.
evidencearray of AIReviewEvidenceThe rows the answer as a whole rests on: the review's subject as the reviewer read it. A nothing_stands_out answer to any subject but a channel test must name the subject among them — an alert's transition or resource, an anomaly by its id, an impact review's resource — or it is refused: an all-clear that read nothing is not a review. Each row is checked as a finding's evidence is, and counts toward the same limit of 50 across the answer. They are checked when the answer is accepted and are not kept.
findingsarray of AIReviewFinding
modelrequiredstringThe model that reviewed, as the reviewer names it.
summaryrequiredstring
verdictrequiredstring enum One of nothing_stands_out, findings, could_not_review.

AIReviewEvidence

A row a finding rests on; it must be one this tenant can see.

FieldTypeDescription
idrequiredstringA resource's account/region/system/service/type/id; a transition's or an anomaly's id.
kindrequiredstring enum One of resource, transition, anomaly.

AIReviewFinding

FieldTypeDescription
detailstring
evidencearray of AIReviewEvidence
titlerequiredstring

AIReviewList

FieldTypeDescription
itemsrequiredarray of AIReview

AIReviewRequest

What to ask the reviewer about. One to ten items: each names a resource, and the server decides the subject — an alert item becomes the resource's latest status transition, which is the subject the notifier sends for the same alert, and an impact item becomes the investigation anchored where the person was looking.

FieldTypeDescription
itemsrequiredarray of AIReviewRequestItem

AIReviewRequestItem

FieldTypeDescription
accountIdrequiredstring
atstring (date-time)An impact item's anchor. Omitted, the resource's latest status change is the anchor, as the Impact page's own read does it.
depthintegerHow far an impact item's neighbours go; 1 when omitted.
kindrequiredstring enum One of alert, impact.
resourceKeyrequiredstringregion/system/service/type/id
windowstring enumAn impact item's window either side of the anchor; ±2h when omitted. One of 1h, 2h, 6h, 24h.

APIToken

A read token, everything but the token.

FieldTypeDescription
createdAtrequiredstring (date-time)
expiresAtrequiredstring (date-time)
idrequiredstring (uuid)
lastUsedAtstring (date-time)
namerequiredstring
revokedAtstring (date-time)

APITokenCreate

FieldTypeDescription
expiresInDaysintegerDays until the token stops working, at most the organisation's longest (/org/api-token-settings, 90 unless an administrator changed it). When absent, 90, or that longest when it is shorter.
namerequiredstringWhat the token is for; how a list tells two apart. Must not be blank.

APITokenIssued

FieldTypeDescription
apiTokenrequiredAPITokenA read token, everything but the token.
tokenrequiredstringThe read token, panorama_read_ and 43 characters. Shown here once.

APITokenList

FieldTypeDescription
itemsrequiredarray of APIToken

APITokenSettings

FieldTypeDescription
maxExpiresInDaysrequiredintegerThe longest life, in days, of a read token issued from now on.

Account

externalId is deliberately absent from every response. It is half of the confused-deputy defence on the cross-account role, and an API that hands it out has given away that half.

FieldTypeDescription
accountIdrequiredstring
azureClientIdstringThe client id of the app registration an Azure subscription is read as; absent for the other clouds. An id, not a secret: no secret exists for it.
azureTenantIdstringAn Azure subscription's directory (tenant) id, where the app the platform signs in as is registered; absent for the other clouds.
cloudtrailAttributionbooleanWhether an administrator turned CloudTrail attribution on for this account: the platform then reads the account's CloudTrail event history (cloudtrail:LookupEvents, under the reader role) for each configuration change, to name the calls that may have made it. Off by default; always off for a Google Cloud project or an Azure subscription. Always sent; a client reads it absent as off.
createdAtrequiredstring (date-time)
enabledrequiredbooleanNew accounts default to false. Discovery registers everything it finds; collecting from an unapproved account is both surprising and expensive.
environmentrequiredstring enum One of production, staging, sdlc, shared, unknown.
googleServiceAccountstringA Google Cloud project's reader service account, which the platform impersonates; absent for an AWS account.
isManagementrequiredbooleanWhether this is the organisation's management account — the one discovery assumes a role in to enumerate the rest. At most one per tenant. Registered like any other account and then marked, so discovery needs nothing the registry does not already hold.
namerequiredstring
regionsrequiredarray of string
roleArnrequiredstring
systemrequiredstring enumThe account's cloud. A Google Cloud project is registered as an account, accountId its project id; an Azure subscription, accountId its subscription id. Either one's roleArn is empty. One of aws, gcp, azure.

AccountCollection

How this account is being read. Absent for a disabled account: nothing is scheduled for it, which a status of no-data would misreport as a collector that has never answered.

FieldTypeDescription
lastSuccessAtstring (date-time)The most recent successful collection of any module, absent where none has succeeded.
modulesrequiredarray of AccountModuleStateEvery module scheduled for this account, worst cell per module, in the scheduler's order.
stalestModuleAccountModuleState
statusrequiredstring enumThe worst cell of this account's account × region × module grid. One of ok, error, slow, no-data.
worstModuleAccountModuleState

AccountList

FieldTypeDescription
itemsrequiredarray of Account

AccountModuleState

FieldTypeDescription
lastSuccessAtstring (date-time)
modulerequiredstring
regionstringThe region of the cell this state came from; global for a per-account module.
statusrequiredstring enum One of ok, error, slow, no-data.

AccountOnboarding

What an administrator has to do in the customer's cloud, and the values to do it with. externalId is present for an AWS account and is the one secret this contract ever returns. Every operation whose answer can carry it is marked x-step-up — the two registrations and the onboarding read — and the middleware's TestEveryAnswerCarryingTheExternalIdAsksForAStepUp holds that by the property rather than by a route's name, so a new operation answering with one inherits the rule.

FieldTypeDescription
certificateThumbprintstringAzure only, for a deployment off AWS: the SHA-256 thumbprint of the certificate the app registration must hold, base64url, as Entra ID shows it.
externalIdstringThe external id the customer's trust policy must demand. AWS only.
federationAudiencestringAzure only: the audience the federated credential trusts, api://AzureADTokenExchange/<this organisation's id>. The issuer and subject are this deployment's for every organisation it serves; the audience is this organisation's alone, and the deployment asks for no other when it reads this organisation's subscriptions — so another organisation registering the same app cannot read through it (the per-tenant binding AWS's external id gives an AWS account). Absent when the deployment signs in with a certificate.
federationIssuerstringAzure only: the issuer the app registration's federated credential trusts — this deployment's own token's, read from a token it made for itself, so it is the one the sign-in presents. Absent when the deployment signs in with a certificate.
federationSubjectstringAzure only: the subject the federated credential trusts, this deployment's AWS role's ARN as its token names it.
readerRolestringThe custom role to bind to that service account (Google Cloud), or to assign the app at the subscription (Azure).
roleArnstringThe role ARN this deployment will assume. AWS only.
roleNamestringThe role the customer creates. AWS only.
serviceAccountstringThe reader service account to grant impersonation on. Google Cloud only.
stepsrequiredarray of OnboardingStepThe steps in order, each with the command to run where one exists. Prose the page shows as it is: the contract decides what onboarding says, so a portal and a third-party client cannot disagree about it.
systemrequiredstring enumThe account's cloud. One of aws, gcp, azure.
templatestringThe CloudFormation template to deploy, by its path in the product's own distribution. Not a URL: this deployment does not serve the template, and inventing a link to something it cannot answer for would be worse than naming the file. AWS only.
workloadIdentityProviderstringThe provider this deployment signs in through. Google Cloud only.

AccountOverview

One account as the Estate module's accounts page reads it: the registry row, how its collection is going, and what it holds.

FieldTypeDescription
accountrequiredAccountexternalId is deliberately absent from every response. It is half of the confused-deputy defence on the cross-account role, and an API that hands it out has given away that half.
collectionAccountCollectionHow this account is being read. Absent for a disabled account: nothing is scheduled for it, which a status of no-data would misreport as a collector that has never answered.
regionDiscoveryAccountRegionDiscoveryA Google Cloud project's region discovery (MILESTONE_1 §13.4): when it last ran and how many regions it found in use. The regions themselves are the account's regions, which discovery only adds to. A resource in a region the project had never used appears after the next discovery, not the next collection — this is what says when that was. Absent for an AWS account.
resourcesrequiredAccountResourcesWhat this account holds, counted from the live inventory.

AccountOverviewList

FieldTypeDescription
itemsrequiredarray of AccountOverview

AccountRegionCount

FieldTypeDescription
countrequiredinteger
regionrequiredstring

AccountRegionDiscovery

A Google Cloud project's region discovery (MILESTONE_1 §13.4): when it last ran and how many regions it found in use. The regions themselves are the account's regions, which discovery only adds to. A resource in a region the project had never used appears after the next discovery, not the next collection — this is what says when that was. Absent for an AWS account.

FieldTypeDescription
lastRunAtstring (date-time)When the latest discovery finished; absent while none has.
messagestringWhat the latest discovery could not read, when Google refused part of it — an API the project has not enabled, a permission the reader lacks — or why it failed.
regionsFoundintegerHow many regions the latest finished discovery found in use.
statusrequiredstring enumThe latest discovery: ok when it finished, error when it failed, no-data when it has never run. One of ok, error, no-data.

AccountRegistration

An AWS account an administrator is connecting.

FieldTypeDescription
accountIdrequiredstringThe twelve-digit AWS account id.
environmentrequiredstring enum One of production, staging, sdlc, shared, unknown.
isManagementbooleanWhether this is the organisation's management account — the one discovery enumerates through. At most one per tenant.
namerequiredstring
regionsarray of stringThe regions to collect in. Empty means every region the account has.
roleNamestringThe name of the reader role in the customer's account. Defaults to the template's own panorama-reader; a customer whose naming standard forbids that says so here, and the onboarding material names what they chose.

AccountResources

What this account holds, counted from the live inventory.

FieldTypeDescription
byRegionrequiredarray of AccountRegionCountThe regions this account's resources are actually in, most first.
byStatusrequiredarray of AccountStatusCount
resourceCountrequiredinteger

AccountStatusCount

FieldTypeDescription
countrequiredinteger
statusrequiredStatusThe health of something that currently exists. Derived once at ingest and stored, never recomputed at read time. deleted is absent, and that is the point: a deleted resource is tombstoned, not given a status, so a live resource carrying status: deleted is an impossible value. Keeping one enum for both made that impossible value contract-valid everywhere. unknown is a resource the inventory knows exists, or was recently active, whose health it cannot observe — found only from its metrics or its name. It is neither healthy nor a problem: never green, not on the Alerts page, and counted as not checked on the public page. One of failed, degraded, operational, unknown.

AccountUpdate

Only the fields present are changed.

FieldTypeDescription
cloudtrailAttributionbooleanTurn CloudTrail attribution on or off for this account. On asks the platform to call cloudtrail:LookupEvents in the account, which the reader role grants; a stack older than that answers each lookup permission_denied. Turning it on for a Google Cloud project or an Azure subscription is a 400.
enabledboolean
environmentstring enum One of production, staging, sdlc, shared, unknown.
isManagementbooleanMark (or unmark) this account as the organisation's management account. Marking a second one is a 409: there is at most one.
namestring
regionsarray of string
roleArnstringCorrect the reader role's ARN when it differs from the convention discovery assumed (role/panorama-reader). The external id cannot be set here: it is per tenant, generated once by panorama admin add-management-account, and inherited by every account discovery registers.

AccountVerification

FieldTypeDescription
assumedRolebooleanWhether the role assumption itself succeeded.
checksrequiredarray of AccountVerificationCheck
okrequiredbooleanTrue only if every check that ran passed. A check in skipped did not run, and ok says nothing about it.
regionsarray of stringA Google Cloud project's regions as region discovery found them in use during this verification, each now among the account's regions. Absent for an AWS account, and when discovery did not run.
skippedarray of AccountVerificationSkipThe checks verification did not run, each with why — a module the tenant is not licensed for is not probed (closing the note: a verification that skipped every module's probe used to read as passed). Absent when nothing was skipped.

AccountVerificationCheck

FieldTypeDescription
detailstringWhat to grant, when the check failed. "Access denied" without saying what is missing turns onboarding into guesswork.
okrequiredboolean
permissionrequiredstring

AccountVerificationSkip

FieldTypeDescription
checkrequiredstring
reasonrequiredstring

AcknowledgeRequest

A resource's alert to acknowledge, and why. The note is required.

FieldTypeDescription
accountIdrequiredstring
noterequiredstring
regionrequiredstring
resourceIdrequiredstring
resourceTyperequiredstring
servicerequiredstring
systemrequiredstring

Acknowledgement

An active acknowledgement, with the resource it covers.

FieldTypeDescription
accountIdrequiredstring
acknowledgedAtrequiredstring (date-time)
acknowledgedByrequiredActorRefWho did something, for display and for audit. Not User: that carries the tenant and the role of the session, and an audit record naming a role that has since changed would be a lie about the past.
noterequiredstring
regionrequiredstring
resourceIdrequiredstring
resourceNamerequiredstring
resourceTyperequiredstring
servicerequiredstring
statusrequiredStatusThe health of something that currently exists. Derived once at ingest and stored, never recomputed at read time. deleted is absent, and that is the point: a deleted resource is tombstoned, not given a status, so a live resource carrying status: deleted is an impossible value. Keeping one enum for both made that impossible value contract-valid everywhere. unknown is a resource the inventory knows exists, or was recently active, whose health it cannot observe — found only from its metrics or its name. It is neither healthy nor a problem: never green, not on the Alerts page, and counted as not checked on the public page. One of failed, degraded, operational, unknown.
systemrequiredstring
transitionIdrequiredinteger (int64)

AcknowledgementList

FieldTypeDescription
itemsrequiredarray of Acknowledgement

AcknowledgementTarget

A resource, by its natural key.

FieldTypeDescription
accountIdrequiredstring
regionrequiredstring
resourceIdrequiredstring
resourceTyperequiredstring
servicerequiredstring
systemrequiredstring

ActorRef

Who did something, for display and for audit. Not User: that carries the tenant and the role of the session, and an audit record naming a role that has since changed would be a lie about the past.

FieldTypeDescription
emailrequiredstring
idrequiredstring (uuid)
namestringThe name the person gave themselves, when they gave one.

AdminOverview

FieldTypeDescription
accountsrequiredobject
collectionrequiredobjectThe enabled accounts by their collectors' worst state, as the collector matrix rolls it up.
licencerequiredEntitlements
recentrequiredarray of AuditEntryThe latest audit entries, newest first.
ssorequiredobject
usersrequiredobject

AgentAdoptStep

A step of an adopt task (MILESTONE_1 §9.3.3, §9.5):

  • import — import blocks for the declared addresses, then a plan that may import them and do nothing else: no create, update, replacement or destroy. The accepted plan is applied as an import-only commit — state gains the resources, the cloud is not touched — and the step is reported imported before anything more happens. It is followed, in the same task, by retag.
  • retag — the provenance stamp written as it is for a create, then a plan whose only permitted effect is the tag (or label) properties of the declared addresses. Reported tagged on the terminal succeeded.
  • release — the declared addresses removed from state (state rm) and nothing else, leaving the resources standing. Reported released on the terminal succeeded.
  • generate — the task's inlineSource is the server's skeleton (the provider configuration and nothing else); import blocks for the declared addresses, then tofu plan -generate-config-out. An optional import the provider finds no object for is dropped and the plan run again; any other error fails the step with OpenTofu's words. Every attribute the provider marks sensitive becomes a variable with no default. The skeleton and the generated configuration, as one file, are planned again with the imports and held to the import rules: nothing but the declared imports. Nothing is applied and no state is written. Reported generated, with generated, on the terminal succeeded. When no agent of the account generates, the platform renders the configuration from the inventory (slice 4), and the first agent that does is handed that as inlineSource, every import required: its source already declares the resources, so it generates nothing — it plans the configuration as handed under the import rules and reports those bytes as generated.

Type: string enum

AgentAdoptStepRecorded

What a status report of an adopt task records: imported on the running report its import step makes once the import is committed to state, with that step's planInspection — a step is never recorded without an accepted inspection; tagged, released or — a generate step's, with generated — generated on the terminal succeeded. A report of generated, or one carrying a configuration, from any other step is refused.

Type: string enum

AgentClaimRequest

FieldTypeDescription
agentVersionrequiredstring
claimIdrequiredstring (uuid)Generated by the agent before its first attempt and reused on every retry of that attempt. This is the idempotency key for claiming: repeating it returns the same work item, never a second one. Minting it inside the retry loop is the mistake that turns one lost response into a task nobody heartbeats.
libraryReleaseintegerThe newest release of the built-in library this agent's image carries (MILESTONE_1 §8.1). Send it only if the agent runs library templates — it also advertises the library capability — and on every claim as well as at register: a task pinned to a library hash is offered only to a claim whose release holds that hash. Omitted, the agent is never offered a library task.
sealingKeyAgentSealingKeyThe public half of the agent's sealing key (agent-protocol.md §3.10): standard base64 of the 32 bytes of an X25519 public key. Sent at register beside sealed-values, which stores it, and on every claim, which hands only values sealed to it. The private half never leaves the agent.
tofuVersionrequiredstring
waitSecondsintegerHow long the agent is willing to hold the poll open. Clamped server-side to claimPollTimeoutSeconds; it is a hint, not a demand.

AgentGenerated

The configuration an adopt's generate step produced, on its terminal succeeded with step generated: the server's skeleton and what tofu plan -generate-config-out wrote, one file, exactly as the step re-planned it. Every attribute the provider marks sensitive is var.<name>, and its variable is declared sensitive = true with no default: a sensitive value is never in hcl. The platform stores hcl as the inline source of the next version of the import's private template, pinned by sha256, and the request moves to pending_approval on it. Refused, with nothing written (400): a digest that is not hcl's, an address the step was not handed or out of its order, a required import dropped or any handed import neither kept nor dropped. A configuration with sensitiveVariables is stored when it carries placeholders for every one of them (agent 0.3.29); without them the request fails as configuration, naming them. With valuesNeeded, the step's planInspection is the refused one, and is accepted only when every violation is an update or replacement of a kept address that one of them accounts for: the request then asks for those values through the secret channel. A re-plan of a configuration the platform rendered (slice 4) reports the bytes it was handed, every address, nothing dropped and no variable; anything else is refused (400).

FieldTypeDescription
addressesrequiredarray of stringThe imports hcl holds, one resource block each, in the task's order.
droppedrequiredarray of stringThe optional imports dropped because the provider found no object for them.
hclrequiredstring
placeholdersobjectWhat OpenTofu wrote in each sensitive attribute's place, by variable — null, {} or [], never a value: what later steps plan with where no value is supplied.
sensitiveVariablesrequiredarray of stringThe variables the sensitive attributes became, sorted. Names only: each is declared in hcl with no default, and its value is asked for when the version is used.
sha256requiredstringThe hex SHA-256 of hcl's bytes.
valuesNeededarray of AgentValueNeededThe sensitive variables whose placeholder the re-plan would replace or update the resource for, sorted: a secret the provider reads back. Their values are asked for, sealed to the account's agents, and the import step's plan with them must import and change nothing.

AgentHeartbeat

FieldTypeDescription
attemptrequiredintegerThe fence.
stagestring

AgentHeartbeatResult

FieldTypeDescription
cancelRequestedrequiredbooleanA human asked this task to stop. Stop at the next safe point and report cancelled; do not destroy what has already been created — that decision belongs to an operator looking at a partial apply, not to an agent reacting to a boolean. This rides the heartbeat rather than getting its own polled endpoint because the agent is already calling this on a known interval, and one fewer endpoint is one fewer thing a third-party implementer can get wrong.
heartbeatIntervalSecondsrequiredintegerRepeated here so the platform can retune a running fleet without waiting for every agent to re-register.

AgentImport

One resource an adopt acts on: the address the template declares and the identifier OpenTofu imports it by, finished on the server from the one resource being imported (MILESTONE_1 §9.4). An agent writes an import block of them — to = <address> as the expression it is, id = "<id>" through an encoder — and refuses an address that is not a managed resource of the root module rather than writing it.

FieldTypeDescription
addressrequiredstring
idrequiredstring
optionalbooleanA generate step's alone. A companion whose existence the inventory could not confirm: when the provider reports no object for its id, the agent drops it, generates again without it and names it in generated.dropped. Never sent on another step, which acts on every address it names; an agent refuses a task that does.

AgentLogBatch

FieldTypeDescription
attemptrequiredintegerThe fence.
linesrequiredarray of AgentLogLineAt most maxLogBatchLines from the policy; 500 is the ceiling this contract allows regardless.

AgentLogBatchAccepted

The acknowledgement stored against this batch's Idempotency-Key. Repeating the key returns this same body, unchanged and without writing — including stored, which keeps reporting what the batch wrote the first time rather than the nothing it wrote this time. There is no replayed flag for the same reason AgentTaskStatusAccepted has none.

FieldTypeDescription
duplicatesrequiredintegerLines whose seq was already stored. Not an error: it is what a re-chunked retry looks like, and the count is here so an agent can notice it is retrying more than it thinks.
highestSeqrequiredinteger (int64)The highest seq stored for this task as of this batch.
storedrequiredintegerLines this batch wrote. A stored value, so a replay reports the original number.

AgentLogLine

FieldTypeDescription
createdAtrequiredstring (date-time)When the agent wrote the line. Within a day behind the platform's clock and an hour ahead of it, or the batch is refused with 400.
levelrequiredstring enum One of debug, info, warn, error.
messagerequiredstringAlready scrubbed. Exact-value redaction of every sensitive variable, plus AWS key IDs, assignment forms and PEM blocks (§7.5). Deliberately not the donor's bare 40-character base64 pattern: it ate hashes, ARNs and resource IDs, and a log that redacts the resource ID is a log nobody can debug from.
seqrequiredinteger (int64)Agent-assigned, monotonic per task, and half of the replay defence: (taskId, seq) is a primary key, so a line that arrives twice in two differently-shaped batches still lands once. That happens whenever an agent restarts mid-flush and re-chunks its buffer.
stagerequiredstring

AgentOutputWanted

One output a create reports after a successful apply (MILESTONE_1 §11.4): its name, as the template's output block spells it, and the variables schema type its value must have.

FieldTypeDescription
namerequiredstring
typerequiredstring enum One of string, number, boolean, list(string), map(string).

AgentPermittedEffect

One permitted operation — on any address, or only on the addresses named; and for an update, on any property, or only under the property-path prefixes in paths (a path is the dotted attribute path, tags.Owner; a prefix permits everything under it).

FieldTypeDescription
addressesarray of string
operationrequiredstring enum One of create, update, replace, delete, import, forget, move.
pathsarray of string

AgentPermittedEffects

What an approved plan may do: every change the saved plan holds must be permitted by a rule, or nothing is applied (MILESTONE_1 §9.3.1). A change is read off OpenTofu's resource_changes, and its effects are its operations: create, update and delete from its actions; replace for a delete and a create of one address, in either order; import when it imports an existing object; forget when a removed block drops it from state; move when a moved block gives it a new address. A no-op has no effect, and a data source may always be read.

For the two actions that exist, a create may create, update, replace and delete — a retry of a partial create touches what its first attempt left — and never import, forget or move; a destroy may delete and nothing else, unless its library version's template declares the protections its destroy turns off first: then it may also update each declared address, narrowed to its declared path, and the agent applies that update before it plans the destroy (agent-protocol.md §3.8).

Refuse what you cannot read. An operation, a field or a rule this agent does not understand refuses the plan: a constraint that is ignored is a constraint that does not hold.

FieldTypeDescription
digestrequiredstringThe sha256 of the rules. Echo it, verbatim, as planInspection.effectsDigest: it is how a stored inspection says which effects it was checked against.
rulesrequiredarray of AgentPermittedEffect

AgentPlaceholder

What OpenTofu wrote in a sensitive attribute's place, spelled as the JSON it is: null, {} or []. Never a value.

Type: string enum

AgentPlanInspection

What the agent found when it inspected its saved plan against the permitted effects, before apply. Addresses, operations and property paths only: never a value — a plan holds the variables' values, sensitive ones included, and none of them belongs here.

FieldTypeDescription
acceptedrequiredbooleanEvery change the plan holds is permitted, and the apply may run.
changesrequiredintegerHow many resource changes the plan held, no-ops included.
effectsDigestrequiredstringThe digest of the permittedEffects inspected against, echoed verbatim.
violationsarray of AgentPlanViolationWhat the plan would have done that no rule permits; at least one when accepted is false, none when it is true. At most fifty: a plan refused for more lists the first fifty, and the refusal stands on any one of them.

AgentPlanViolation

FieldTypeDescription
addressrequiredstringThe resource address the change is to, as OpenTofu writes it.
operationrequiredstringOne of AgentPermittedEffect's operations, or the action OpenTofu reported when it is none of them (a data source's that is not a read, an action this agent does not know).
pathsarray of stringFor an update, the changed property paths no rule permits.
reasonrequiredstringWhy it is refused, in words; already scrubbed.

AgentPolicy

The operating parameters an agent must respect. Served rather than hard-coded: a fleet whose intervals are compiled in cannot be tuned without a customer-scheduled upgrade.

FieldTypeDescription
claimPollTimeoutSecondsrequiredintegerThe longest a claim will hang before returning 204. The agent should use a client timeout comfortably above this, or it will abandon every idle poll and look like a reconnect storm.
heartbeatIntervalSecondsrequiredintegerHow often to heartbeat each held task. Default 30.
heartbeatTimeoutSecondsrequiredintegerAfter this much silence the scheduler reaps the task. Published so an agent can tell how much slack it has before a slow apply's heartbeat gap costs it the task. Default 300.
maxAttemptsrequiredintegerAfter this many attempts a request fails rather than returning to queued. Default 3.
maxLogBatchLinesrequiredinteger
maxLogLineBytesrequiredinteger
minAgentVersionrequiredstringBelow this, claim returns 426 and this agent does no work. The only hard refusal in the version policy.

AgentRegistration

The version handshake. Note what is not here: no tenant, no account, no credential of any kind. Both identities come from the token (§3.6), so there is nothing in this body for a compromised agent to claim to be.

FieldTypeDescription
agentVersionrequiredstring
capabilitiesarray of stringOptional feature flags an agent advertises, for forward compatibility. An unknown capability is ignored, never rejected: the fleet is upgraded by customers on their own schedule, so a newer agent must always be able to talk to an older platform. The platform stores them with this registration's agentVersion and acts on them only for claims that carry that same version, so two versions sharing one token during an upgrade are each asked only for what they said they do. Acted on today:
Capability Means The platform then
plan-inspection the agent inspects its saved plan against permittedEffects before it applies, and reports planInspection hands create and destroy tasks their permittedEffects, and records one succeeded only with an accepted inspection
adopt the agent runs an adopt task's three steps — import, retag, release — and reports the steps it recorded (MILESTONE_1 §9.5); agent 0.3.0 offers it adopt tasks, and only when it also advertises plan-inspection: an agent without both is never offered one
outputs after a create's successful apply the agent reads the outputs it was handed and reports them (MILESTONE_1 §11.4); agent 0.3.0 hands create tasks the outputs their version declares, and offers a request whose outputs another step needs only to an agent that advertises it
stop the agent runs a stop — an action on an existing resource — as a create of its library hash in a state of its own (agent-protocol.md §3.7) offers it stop tasks, and only when it also advertises plan-inspection: an agent without both is never offered one
generate the agent runs an adopt's generate step — tofu plan -generate-config-out for the imports, an absent optional companion dropped, every sensitive attribute a variable, a re-plan held to the import rules, nothing applied — and reports generated (agent-protocol.md §3.9); agent 0.3.28 offers it an adopt of a generated import, which records generate among what it needs, beside adopt and plan-inspection — its generate step and the import that follows, whose template is inline; advertise it only when the agent runs inline sources (the owner 2026-10-11)
sealed-values the agent holds a sealing key, sends its public half as sealingKey here and on every claim, opens the secret values a requester sealed to it for an adopt of a generated import, writes them only into a 0600 variables file for the attempt, and never logs, reports or digests them (agent-protocol.md §3.10); agent 0.3.29 offers it an adopt whose configuration needs secret values, which records sealed-values among what it needs, and only with values sealed to the key its claim names, for the request's current attempt; advertise it only with inline sources allowed
azure-resource-groups the agent's Azure runners are scoped to resource groups (PANORAMA_RUNNER_AZURE_RESOURCE_GROUPS): it runs a template of the tenant's own (git or inline) in Azure, and refuses before OpenTofu runs a request naming a group outside its runner's list, or none; advertise it only with the setting offers a request whose template is the tenant's own (git or inline, not one Import generated) for an Azure account — create, destroy, adopt or release — only to an agent that advertises it, recorded among what the request needs at submit and by a Blueprint's steps; the request page names the setting (agentSettingsNeeded), never a version (the owner 2026-10-27)
libraryReleaseintegerThe newest release of the built-in library this agent's image carries (MILESTONE_1 §8.1). Send it only if the agent runs library templates — it also advertises the library capability — and on every claim as well as at register: a task pinned to a library hash is offered only to a claim whose release holds that hash. Omitted, the agent is never offered a library task.
sealingKeyAgentSealingKeyThe public half of the agent's sealing key (agent-protocol.md §3.10): standard base64 of the 32 bytes of an X25519 public key. Sent at register beside sealed-values, which stores it, and on every claim, which hands only values sealed to it. The private half never leaves the agent.
tofuVersionrequiredstringThe OpenTofu binary pinned in this agent's image. OpenTofu rather than Terraform (D13): MPL-2.0, so a commercial provisioning product raises no BUSL "competitive offering" question, and existing Terraform templates run unchanged.

AgentRegistrationResult

FieldTypeDescription
accountIdrequiredstringEchoed from the token, never accepted from the request. An agent that finds this disagrees with its own configuration is pointed at the wrong account and should stop rather than proceed.
agentIdrequiredstring (uuid)
policyrequiredAgentPolicyThe operating parameters an agent must respect. Served rather than hard-coded: a fleet whose intervals are compiled in cannot be tuned without a customer-scheduled upgrade.
versionStatusrequiredAgentVersionStatusReport and warn; refuse only below the declared minimum (D16). outdated still claims work — an operator upgrades a fleet over weeks, and a platform that stops the fleet the day it ships a release is a platform nobody upgrades. One of supported, outdated, unsupported.

AgentSealedValue

One secret value, sealed to the claiming agent's key.

FieldTypeDescription
sealedrequiredstringStandard base64 of HPKE's encapsulated key (32 bytes) and the ciphertext: the value, of at most 64 KiB, and a 16-byte tag.
variablerequiredstring

AgentSealingKey

The public half of the agent's sealing key (agent-protocol.md §3.10): standard base64 of the 32 bytes of an X25519 public key. Sent at register beside sealed-values, which stores it, and on every claim, which hands only values sealed to it. The private half never leaves the agent.

Type: string

AgentTask

One claimed task: everything the agent needs to do exactly the approved work, and nothing else.

What is not here is the design. No cloud credential: the agent runs with ambient credentials — instance profile, IRSA, task role — and the platform has none to send. No git credential: a create or destroy against a git source arrives already resolved to a commit SHA, and the resolve that produced it used the customer's own credentials, on the customer's own network. No state backend configuration: state is customer-owned (D3) and the agent configures its own, then reports where it put it. No template body for a git source: the agent clones it itself, at the pinned SHA.

One shape, not a tagged union. action says what to do, and it is the same action the request carries — a resolution is a request (§6.6), so it needs no work-item kind of its own. An earlier draft multiplexed two kinds through a kind discriminator and invented a surrogate versionId for the second; both are gone, because the second kind was a mechanism the data model did not have.

Fields that are conditional on action are marked below. OpenAPI 3.0 cannot express the condition, so it is stated rather than encoded: an agent must read action first.

An adopt task (MILESTONE_1 §9.5; the action joins ProvisionAction with Import's requests) is an apply whose plan may do one thing per step: import the declared addresses into state, then retag them with the provenance stamp, or — on its own — release them from state. It carries the step it begins at, the imports it acts on and the permittedEffects every step is held to, and it is offered only to an agent that advertised adopt and plan-inspection under the version it claims with. Its generate step stands alone: the configuration OpenTofu generates for the imports, re-planned and never applied; it is offered only to an agent that also advertised generate, and the platform hands none yet.

A stop task is an action on an existing resource, and in every field a create of a library hash: the library slug and hash, the variables the server filled from the target (instance_id, region), no resourceId, createRequestId or stateBackend — its state is its own, laid out by the request's id as a create's is — and always permittedEffects, which allow creating the one aws_ec2_instance_state.this and nothing else. It leaves nothing behind, so a partialApply reported for it is not recorded. It is offered only to an agent that advertised stop and plan-inspection under the version it claims with.

FieldTypeDescription
accountIdrequiredstringThe target account, which is also the account the token is bound to. Sent for logging and for an agent's own sanity check, not as something to act on: a claim is account-scoped structurally and this can never be an account this agent may not touch.
actionrequiredProvisionActionWhat a request asks for, and PROVISIONING_SPEC.md §4.2's action CHECK constraint exactly. create and destroy are the lifecycle (D5). There is no in-place update in v1: changing a resource is a destroy and a create, and each gets its own approval. A lifecycle with three verbs and one of them partial is worse than one with two that are complete. resolve is not a lifecycle verb. It is git ref resolution modelled as a request (§6.6) so that it inherits the claim, the fence, the replay rules, the heartbeat, the reap and the tenant scoping wholesale instead of growing a second copy of each. It is created only by publishing a git-sourced version, never by POST /provisioning/requests, and it is the one action that skips pending_approval: approval governs infrastructure, and resolving a ref creates none. adopt is Import's second step (MILESTONE_1 §9.3): a recorded import brought under its template version — imported into a new state with a plan that may import its declared addresses and nothing else, then stamped with a plan that may change their tags and nothing else. Submitted from a recorded import (importId); the approval names both steps. The agent protocol for it: AgentTask.step and imports, and the steps a status report records. stop is an action on an existing resource: one resource the inventory sees, named whole as target, acted on by a library template of the action's kind and approved as a create is. It is asked for at POST /provisioning/actions, never at POST /provisioning/requests; it leaves nothing behind — no provisioned resource, never partialApply — and a failed one is asked for again rather than retried. One of create, destroy, resolve, adopt, stop.
attemptrequiredintegerThe fence, stamped from the request at claim time and not incremented by it. Send it back on every subsequent write for this task — status, logs and heartbeat — and expect task_superseded the moment the task is reaped, cancelled or retried, which is the moment it stops being current rather than the moment somebody else claims.
claimIdrequiredstring (uuid)Echoed from the claim, so a replayed claim is recognisable as one — and so that an agent which lost a response can prove the task it is holding is the one it asked for rather than a second one.
claimedAtrequiredstring (date-time)
createRequestIdstring (uuid)destroy, and an adopt task that begins at retag or release. The request whose apply created what this destroys — for an adopt, the adopt request whose import put the resource in state. It is the value of the panorama:request_id stamp on the live resources, and provenanceTags on a destroy carries it rather than this request's own id, so a destroy never proposes re-tagging what it is about to remove. An agent that lays state out by request id derives the create's location from it.
deadlineSecondsintegerThe budget for the whole task. Exceeding it is failureClass: timeout, reported by the agent — the platform's own reaper is the backstop for an agent that cannot report at all.
gitRefstringresolve only, and the thing to resolve: a branch, a tag or any other ref. Absent for create and destroy, which are pinned to a SHA precisely so that a moving ref cannot decide what runs.
gitShastringcreate and destroy only. The pinned commit, and the source identity the agent must execute. Checking out anything else — a branch head, a newer tag — is a protocol violation: it would run code no approver ever saw.
gitUrlstringFor git. create/destroy: clone --depth 1 and check out gitSha. resolve: git ls-remote this, and nothing more.
importsarray of AgentImportadopt only, and required there. The addresses the template version declares importable and the identifier each is imported by, finished on the server (MILESTONE_1 §9.4) — the agent never derives an identifier. import writes one import block per entry into panorama_imports.tf; release removes exactly these addresses from state, where state holds them; retag acts on no other. generate writes them the same way and generates the configuration for them: the primary resource and its companions, any of them optional, at least one not. Only a generate step's imports may be optional, and its addresses carry no index.
inlineSha256string
inlineSourcestringFor inline: the exact bytes to write into the workspace. Verify inlineSha256 over them before running anything.
librarySha256stringFor library, and the source identity: look it up in the library your own image carries, verify the bytes hash to it, and lay them out. No code travels with a library task (MILESTONE_1 §8.1): a hash your library does not hold fails the task as configuration, and is never fetched from anywhere.
librarySlugstringFor library: the library template's slug, for the log and for the agent's own sanity check. What runs is librarySha256.
namerequiredstring
outputsarray of AgentOutputWantedcreate only, and only for an agent that registered the outputs capability under the version it claims with (MILESTONE_1 §11.4). The outputs the pinned version declares, less any it declares sensitive: after a successful apply, read them (tofu output -json), and report on the terminal succeeded the ones found in outputs — never one OpenTofu marks sensitive, which is named in withheldOutputs instead. Absent, report none.
permittedEffectsAgentPermittedEffectscreate and destroy, and only for an agent that registered the plan-inspection capability under the version it claims with; always on an adopt. What the plan may do, as the approval pinned it. Inspect the saved plan against it after plan and before apply: a change no rule permits refuses the apply, and nothing is applied. Report the verdict as planInspection on the terminal status — a task handed effects is recorded succeeded only with an accepted inspection that echoes their digest. An adopt's effects cover every step it may run, and each step is held to the rules of its own operation: import to the import rules, retag to the update rules, release to the forget rules — so the import step's plan may import and nothing else, whatever the retag is permitted. Absent, the agent was not asked to inspect, and applies as an agent without the capability always has. A replayed claim returns the same effects, whatever has changed since.
placeholdersobjectAn adopt or destroy of a generated configuration with sensitive variables. Each sensitive variable that no value is supplied for, and what OpenTofu wrote in its attribute's place — null, {} or [], spelled as those strings: write each as that JSON value beside variables, so a plan never stops for a required variable. Never a name variables or sealedValues gives.
provenanceTagsobjectcreate, destroy and adopt. The tags to stamp onto everything the apply creates, written into a generated ccc_provenance_override.tf before init — an override merged into each provider "aws" configuration the template declares, its own default_tags kept and these merged last. Always includes panorama:request_id. Absent for resolve, which creates nothing to stamp. An adopt writes them in its retag step alone: the import step's plan may import and do nothing else, and a stamp would be an update. This is the whole identity mechanism (D11): one writer, so there is no key derived independently by two parties to disagree — the failure PLATFORM.md §3 calls its highest-consequence line is removed by construction rather than defended against.
requestIdrequiredstring (uuid)
resourceIdstring (uuid)destroy only. The provisionedResources row being torn down.
runnerSlugstringThe runner the task runs as, when the agent runs each template as its own runner role or runner account (PANORAMA_RUNNER_ROLE_ARN_PREFIX, PANORAMA_RUNNER_SERVICE_ACCOUNT_PROJECT): the runner's name, and the segment of a Google state prefix that runner may reach. It is templateSlug for every template but a generated one, whose slug is generated-<import id> and names no runner; a generated template's task names the runner of its resource type, generated-<type key with dots as hyphens> (generated-aws-s3-bucket), which holds the iam of every Library template that adopts the type. The server is its one writer: an agent never derives a runner from a generated template's slug, and one that receives no runnerSlug (an older platform) uses templateSlug.
sealedValuesarray of AgentSealedValueAn adopt's import or retag of a generated import whose configuration needs secret values, and only to an agent that registered sealed-values (agent-protocol.md §3.10). One per variable generated.valuesNeeded named: the value a requester sealed in their browser to the key this claim named (sealingKey), for this request, this attempt, this agent and this variable. Open each with your private key — HPKE base mode, DHKEM(X25519, HKDF-SHA256), HKDF-SHA256, AES-128-GCM, info cloudpanorama secret channel v1, the associated data panorama-sealed-value/v1 and the four fields, each after a newline — and write the values into a 0600 variables file of the attempt, never into generated.auto.tfvars; add each to your scrubber's exact values; report no planDigest; remove the file after the last apply. A value that does not open fails the task as configuration, naming the variable. The platform stores only the sealed form, deletes it when the task ends, and never returns it on any other read.
sourceTyperequiredTemplateSourceTypeWhere a template's code comes from, and therefore what its source identity is: a commit SHA for git, a content sha256 for inline, and for library the sha256 of a built-in library template's files (MILESTONE_1 §8.1), whose bytes the agent carries in its own image. Whichever it is, it is pinned at publish and never updated. One of git, inline, library.
stateBackendstringdestroy, and an adopt task that begins at retag or release. Where the create's state lives — or the adopted state, as the import step reported it — exactly as the agent that ran it reported it (D3: state is customer-owned; the platform stores the address and never the state). The destroying agent configures that backend and runs destroy against it — destroying against any other state is how a destroy removes the wrong thing or nothing. Absent when the creating agent never reported one, in which case the agent must fail the task as configuration rather than guess. An adopt task that begins at import carries none: its state is new, laid out by its own request id as a create's is.
stepAgentAdoptStepadopt only, and required there (MILESTONE_1 §9.5). The step this task begins at. import is followed, in the same task, by retag; retag, release and generate stand alone. The platform hands the first step its request has not recorded, so a task reaped after its import was recorded resumes at retag and never imports twice.
taskIdrequiredstring (uuid)
templateIdrequiredstring (uuid)
templateSlugrequiredstring
templateVersionrequiredintegerWith templateId, the whole identity of the version: template_versions is keyed (tenant_id, template_id, version) and has no surrogate id. An earlier draft of this contract invented one for a resolve endpoint that no longer exists.
variablesobjectcreate, destroy and adopt. The validated variable values, unmasked, including the ones marked sensitive — the template needs them and this is the only response that carries them. Write them to generated.auto.tfvars through an HCL encoder, never by string concatenation. Empty for resolve, which runs no template. A destroy's are its create's, exactly as the create was given them, so the destroy plans the configuration the create applied; write them the same way. The keys are exactly the names variablesSchema declares. The server validated the request against that schema before the request existed, so nothing else can be here — and an agent should check, because a key it cannot find in the schema means it is talking to something that is not this platform. That is the closed key space, and it is closed by a sibling field rather than by this schema because OpenAPI 3.0 has no way to say it: propertyNames is a 3.1 keyword, the pinned redocly rejects the document outright when it appears, and neither kin-openapi nor either generator enforces it. Declaring it would be a rule that reads as a constraint and enforces nothing. A credential-shaped key is not, and cannot be, refused here, and that is a decision rather than an oversight. §8.1's own worked example declares db_password, aws_db_instance names its variable password, and a name rule strong enough to refuse aws_secret_access_key refuses both — which would make this module unable to provision the database template §4.1 uses as its example. A name rule would not stop the value either: the same secret in a variable called pw passes any blocklist. What §3.1 forbids is a platform-defined field that could carry a cloud credential, and that is what the scan of this document enforces. These values are the customer's own template inputs, travelling to the customer's own account, typed by §8.1's five types and no others. Where one is genuinely secret the schema marks it sensitive, and it is then masked in the UI, masked in variablesRedacted, and exact-value redacted from every stored log line (§7.5) — which is a defence that acts on the value, where the risk actually is.
variablesSchemaVariablesSchemacreate, destroy and adopt. The pinned version's schema. The agent needs it twice: for the HCL type of each value, and for the set of sensitive names whose exact values must be redacted from every log line it sends. Absent for resolve.

AgentTaskStatusAccepted

The stored result of this task's status. A repeat of a report already recorded returns this same body, byte for byte, without writing: there is deliberately no field saying "you are replaying", because a response that differs on a replay is not a replay, and an agent that can see the difference will eventually branch on it.

FieldTypeDescription
requestStaterequiredProvisionRequestStateWhere the request landed. Worth reading: a terminal task on a request that went back to queued means this attempt was already superseded.
taskIdrequiredstring (uuid)
taskStaterequiredProvisionTaskStateOne attempt at one request. abandoned is what the reaper writes when a task's heartbeat goes stale — distinct from failed, because nobody reported a failure: the process stopped talking, and what it did before it stopped is unknown. One of claimed, running, succeeded, failed, cancelled, abandoned.

AgentTaskStatusReport

Report a stage transition or a terminal outcome. The counts and the digest are only meaningful on a terminal report and are ignored otherwise.

This is also where a resolve task reports its answer, in gitSha (§6.6). A resolution has no endpoint of its own, so it has no replay rule of its own either: the target state plus attempt is the key here as it is for every other task.

FieldTypeDescription
agentVersionrequiredstring
attemptrequiredintegerThe fence, from the claimed task. A value that is not the request's current attempt is task_superseded, and that is the answer, not an error to retry past.
createdCountinteger
destroyedCountinteger
failureClassProvisionFailureClassRequired when state is failed. Pick honestly: configuration sends the report to the template's author, infrastructure sends it to whoever owns the cloud account, and getting it wrong wastes the time of somebody who cannot fix it. restored is the platform's alone, and a report of it is 400.
failureMessagestringAlready scrubbed when it is sent. The platform stores it verbatim and shows it to humans; it has no second chance to redact.
finalLogSeqinteger (int64)Terminal reports only. The log-completion watermark: the highest seq this agent has assigned for the task, which is the last line it will ever send — -1 when it buffered none. Send it on every terminal report, even though the lines behind it may still be in your buffer: the reference agent reports first and drains afterwards, and the platform accepts the drain after the task is terminal. The platform stores it once, in the transaction that closes the task, and the operator's log read (getProvisioningTaskLogs) reports the log complete only when a line at this seq has been paged out — never from "the task is terminal and the page was short", which is how a viewer used to miss the tail. If the line never arrives (a drain that failed, a buffer that was lost), the read reports incomplete after a bounded grace rather than waiting forever. Absent, the platform falls back to logLineCount - 1 when logLineCount is positive (every reference agent has sent it), and otherwise calls the log unverified once the grace passes. Sending this field is what tells the difference between "all here" and "nobody said".
generatedAgentGeneratedThe terminal succeeded of an adopt's generate step only, with step generated and the re-plan's accepted planInspection. The platform stores it as a private template version and the request moves to pending_approval on it (AgentGenerated); from any other report it is refused.
gitShastringresolve only, and required when it succeeds. The commit the ref resolved to, from git ls-remote. SHA-1 or SHA-256, because git repositories exist in both object formats and a 40-character-only rule would lock out the newer ones. Write-once. The same SHA again is a replay and returns the stored response; a different SHA for an already-resolved version is 409 resolution_conflict and the stored value does not move — it would otherwise silently change what a pending approval is about to approve.
logLineCountinteger (int64)Every line the agent buffered for this task, dropped ones included (Rule 3), so the portal can show a loss as a number. Kept in the permanent summary.
outputsobjectThe terminal succeeded of a create handed outputs (MILESTONE_1 §11.4). Every output it was handed that tofu output -json found, keyed by name, of the type it was handed — a tuple of strings is a list(string), an object of strings a map(string). Never one OpenTofu marks sensitive: that one's name goes in withheldOutputs. Nor one the log scrubber's rules would change, whose name goes there too: an output is reported as it is or not at all. At most 32, and at most 16 KiB between them, counted as the canonical JSON encoding of each value; one that does not fit is withheld. Recorded per attempt, write-once: the same outputs again for the same attempt is a replay; different ones are outputs_conflict. A request's outputs are those of the attempt that succeeded, and they are what a later step consumes.
overriddenTagsarray of stringThe stamp keys the template's own provider defaults also set literally, which the stamp then won. A tenant's tag policy may require keys the template's author also writes — they are the tenant's own vocabulary, not the panorama: namespace — and the stamp is merged last, so the request's value is the one the resource carries. These are the keys whose template value did not survive, reported so the request page can say which rather than leaving the author to find out from a plan. Keys a template computes are invisible to the parser and are not listed, the same limit the stamp itself has (§9). Omitted when nothing was overridden.
partialApplybooleanThe apply failed after creating something. Report it truthfully even when it is embarrassing: the alternative is infrastructure nobody knows exists. Nothing is auto-destroyed on the strength of this.
planDigeststringsha256 of the plan output. Kept in the permanent summary.
planInspectionAgentPlanInspectionTerminal reports of a task handed permittedEffects. The verdict of the plan against them, stored with the task. A succeeded report of such a task without an accepted inspection is 400 invalid_request — the platform never records success without it — and so is an inspection that does not describe the task: sent for a task handed no effects, naming other effects, accepted while listing violations, or refused while listing none. A refused inspection is reported with failed and failureClass: configuration, and nothing was applied. An adopt reports one inspection per step it recorded: its import step's on the imported report, and the last step's on the terminal one.
provenanceTaggedByTypeobjectBeside provenanceTaggedCount, on a create's and an adopt's tagged report (agent 0.3.0). The same count per OpenTofu resource type — {"aws_vpc": 1, "aws_subnet": 4, "aws_iam_role": 1} — read from the same tofu show -json. Each key is a resource type as a resource block names it (an identifier of at most 128 characters), each count at least 1, and the counts sum to provenanceTaggedCount; anything else is 400 invalid_request. A type with nothing stamped is not named. Drift compares with the inventory only the types a registered collector emits and reads the tags of, kind by kind — the inventory collects no roles, log groups, route tables or listeners, and finds Lambda functions without their tags. Omit it and the create is recorded with its total alone, and drift does not judge it at all, saying so on the resource (driftScope.judged false), since nothing says which of its resources the inventory could see.
provenanceTaggedCountintegerResources carrying panorama:request_id that the agent counted in state after a successful apply, by reading tofu show -json locally — no extra cloud API call. It becomes expectedCount, the baseline the drift diff measures against. Zero on a create that produced resources means the stamp did not land: the row is stored provenanceTagged = false and excluded from drift with a visible badge, never silently omitted.
stagestring
stageDurationsMsobject
staterequiredProvisionTaskStateOne attempt at one request. abandoned is what the reaper writes when a task's heartbeat goes stale — distinct from failed, because nobody reported a failure: the process stopped talking, and what it did before it stopped is unknown. One of claimed, running, succeeded, failed, cancelled, abandoned.
stateBackendstringWhere this task's Terraform state lives, so an operator can act on a partial apply. A location, never contents and never a credential — the platform does not read state (D3).
stepAgentAdoptStepRecordedadopt only (MILESTONE_1 §9.5). The step this report records: imported on a running report, with the import step's accepted planInspection and its stateBackend; tagged or released on the terminal succeeded. Report imported and have it answered before the retag begins: a task whose imported report was never accepted fails, rather than retagging a resource the platform does not know is in state. A report of any other task carrying one is invalid_request. generated on the terminal succeeded of a generate step, with generated.
tofuVersionrequiredstring
withheldOutputsarray of stringThe outputs this create was handed and found, and did not report — marked sensitive, not of the declared type, one the scrubber would change, or past the size limit. A handed output in neither list was not found. Names only, never a value.

AgentValueNeeded

A sensitive variable whose value a generated configuration needs, and the attribute it stands for.

FieldTypeDescription
addressrequiredstring
attributerequiredstring
variablerequiredstring

AgentVersionStatus

Report and warn; refuse only below the declared minimum (D16). outdated still claims work — an operator upgrades a fleet over weeks, and a platform that stops the fleet the day it ships a release is a platform nobody upgrades.

Type: string enum

AppGroup

One application: one value of one attribution tag key.

FieldTypeDescription
healthrequiredStatusCountsComputed over the full filtered set, not the current page.
provisionedintegerHow many of the group's live resources Provisioning created. Absent when the tenant does not hold Provisioning.
spendarray of MoneyMonth-to-date amortized spend for the tag value, one entry per currency, from each day's authoritative source — the Cost & Usage Report where it is ingested, Cost Explorer otherwise; the same attribution the cost views make. Absent when the tenant does not hold Spend; empty when it does and no source reported any.
tagKeyrequiredstring
tagValuerequiredstring

AppGroups

FieldTypeDescription
groupsrequiredarray of AppGroup
periodrequiredobjectThe month-to-date range spend covers, inclusive.
tagKeysrequiredarray of stringThe attribution tag keys the groups came from, in priority order; empty when none are configured.

AttentionItem

One thing that needs someone (§4.3). Every item links to the filtered page that explains it — a number a person cannot follow is a number they cannot act on.

FieldTypeDescription
countintegerHow many; absent where the item is not a count.
kindrequiredstring enum One of alerts.failed, alerts.degraded, estate.unknown, collection.failing, collection.stale, cost.anomaly, cost.budget, provisioning.awaiting_you, provisioning.expiring, provisioning.failed, provisioning.drift, agents.offline.
linkrequiredstringThe portal address that shows exactly these rows.
severityrequiredstring enum One of destructive, warning, neutral.
sincestring (date-time)The oldest of what it counts.
titlerequiredstringOne sentence, in the product's words.

AuditActor

The account that acted; absent for the platform's own changes and the CLI's.

FieldTypeDescription
displayNamestringThe name the person gave themselves, while the account exists and they gave one.
emailstringThe account's email, while the account exists.
idrequiredstring (uuid)

AuditEntry

FieldTypeDescription
actionrequiredstringDotted, as auth.identity_provider.update or notify.channel.create.
actorAuditActorThe account that acted; absent for the platform's own changes and the CLI's.
afterobjectThe subject after the change, as the audit recorded it.
atrequiredstring (date-time)
beforeobjectThe subject before the change, as the audit recorded it.
idrequiredinteger (int64)
subjectIdrequiredstring
subjectTyperequiredstring

AuditLogPage

FieldTypeDescription
entriesrequiredarray of AuditEntry
nextCursorstringPresent while older entries follow; pass it back as cursor.

Blueprint

A catalog object whose versions are graphs of template steps (MILESTONE_1 §11.1). Created by an administrator; archived, never deleted.

FieldTypeDescription
archivedAtstring (date-time)
createdAtrequiredstring (date-time)
createdByrequiredActorRefWho did something, for display and for audit. Not User: that carries the tenant and the role of the session, and an audit record naming a role that has since changed would be a lie about the past.
descriptionrequiredstring
idrequiredstring (uuid)
latestVersionintegerAbsent until a version is published.
librarySlugstringThe library Blueprint it was enabled from, when it was one.
namerequiredstring
slugrequiredstring

BlueprintBinding

What a run's one approval binds (MILESTONE_1 §11.3): what is knowable when it is given, and nothing else. The run's variables are bound by digest as well as shown, so a sensitive value is bound without being written here.

FieldTypeDescription
blueprintrequiredobject
createRunIdstring (uuid)A destroy run's binding only — the create run it takes down.
kindrequiredstring enum One of create, destroy.
stepsrequiredarray of BlueprintBoundStep
teamIdrequiredstring (uuid)
variablesSha256requiredstring
versionrequiredinteger One of 1.

BlueprintBoundInput

One input as the approval binds it: a literal's value; a run variable's name and value (masked where sensitive); or, for an input wired from an upstream output, the expression — producer — and the consumer's constraints, and no value.

FieldTypeDescription
constraintsBlueprintInputConstraintsEvery rule the consumer variable holds a value to — what a value resolved from the upstream output is validated against before its step is created, exactly as a requester's value is.
fromrequiredstring enum One of literal, variable, output.
indexintegerThe element of a list(string) run variable this instance takes — its slot position — when the run variable numbers the instances of a step that runs once per account. Absent otherwise.
producerobject
runVariablestring
valueVariableValueOne variable value, in one of the five types PROVISIONING_SPEC.md §8.1 allows and in no other. That list is the entire vocabulary until a customer asks for more, and a value outside it is a template the validator will reject anyway. Written as a oneOf even though AgentTask deliberately avoids one for its work item: a union at a leaf costs a third-party generator nothing it does not already pay — a generator that cannot express it falls back to "any", which is precisely what an unconstrained schema gives it — while a oneOf at the top of a work item makes the whole response awkward to read. The value here is that the map carrying these is no longer free-form: an arbitrary nested blob does not validate, so there is nowhere for one to hide.
variablerequiredstring

BlueprintBoundStep

FieldTypeDescription
accountIdrequiredstring
inputsrequiredarray of BlueprintBoundInputEmpty in a destroy run, whose steps run with what their creates ran with.
needsarray of stringThe steps it waits for: in a create run those it needs; in a destroy run — the graph reversed — those that needed it, whose destroys go first.
resourceobjectA destroy run's step only: the provisioned resource its create left, that create's request, and the digest of the variables it ran with, which the destroy runs with too. Absent on a destroy run's step that never applied, which is skipped.
slotIndexrequiredinteger
steprequiredstring
templaterequiredobject

BlueprintCreate

FieldTypeDescription
descriptionstring
namerequiredstring
slugrequiredstring

BlueprintDecision

FieldTypeDescription
bindingSha256stringRequired to approve: the digest of the binding the approver read. A binding that moved since is refused (409 binding_changed).
commentstring
decisionrequiredstring enum One of approved, rejected.

BlueprintGraph

A version's document (MILESTONE_1 §11.1): its slots, the run's variables schema, and its steps. At most 8 slots and 32 steps.

FieldTypeDescription
nonOverlappingCidrsarray of stringRun variables, each a string or list(string) and none sensitive, whose values are address ranges that must not overlap (§11.6): a run is refused when a value is not a range written as its range, such as 10.0.0.0/16, or when two of them overlap.
slotsrequiredarray of BlueprintSlot
stepsrequiredarray of BlueprintStep
variablesVariablesSchemaStored on the version and immutable with it. The portal generates the request form from this; the agent uses it to write generated.auto.tfvars as proper HCL through an encoder, never by string concatenation, because hand-rolled escaping is how tfvars injection happens.

BlueprintInput

Where one variable of a step's template gets its value: exactly one of a run variable (var), a literal, or an upstream step's declared output (output, written step.output, or step.output[n] for one element of a list output, the step named in needs). On a step that runs once per account of a slot, a list(string) run variable wired into a string variable gives each instance the element at its slot position, and a run must give one element per account. A string output wired into a list(string) variable arrives as a list of that one value. step.output[slot], on a step that runs once per account, takes the element at the instance's own slot position, so each account receives its own element of a single producer's list.

FieldTypeDescription
literalVariableValueOne variable value, in one of the five types PROVISIONING_SPEC.md §8.1 allows and in no other. That list is the entire vocabulary until a customer asks for more, and a value outside it is a template the validator will reject anyway. Written as a oneOf even though AgentTask deliberately avoids one for its work item: a union at a leaf costs a third-party generator nothing it does not already pay — a generator that cannot express it falls back to "any", which is precisely what an unconstrained schema gives it — while a oneOf at the top of a work item makes the whole response awkward to read. The value here is that the map carrying these is no longer free-form: an arbitrary nested blob does not validate, so there is nowhere for one to hide.
outputstring
varstring

BlueprintInputConstraints

Every rule the consumer variable holds a value to — what a value resolved from the upstream output is validated against before its step is created, exactly as a requester's value is.

FieldTypeDescription
enumarray of VariableValue
maxnumber
maxItemsinteger
maxLengthinteger
minnumber
minItemsinteger
minLengthinteger
patternstring
requiredboolean
sensitiveboolean
typerequiredstring enum One of string, number, boolean, list(string), map(string).

BlueprintPage

FieldTypeDescription
itemsrequiredarray of Blueprint
pagerequiredinteger
pageSizerequiredinteger
totalrequiredinteger (int64)
totalPagesrequiredinteger

BlueprintRun

FieldTypeDescription
bindingBlueprintBindingWhat a run's one approval binds (MILESTONE_1 §11.3): what is knowable when it is given, and nothing else. The run's variables are bound by digest as well as shown, so a sensitive value is bound without being written here.
bindingSha256stringThe binding's digest, which an approval names.
blueprintIdrequiredstring (uuid)
commentrequiredstring
createRunIdstring (uuid)A destroy run's create run.
decidedAtstring (date-time)
decidedByActorRefWho did something, for display and for audit. Not User: that carries the tenant and the role of the session, and an audit record naming a role that has since changed would be a lie about the past.
decisionstring enum One of approved, rejected.
destroyRunIdsarray of string (uuid)A create run's destroy runs, newest first; absent when it has none.
expiresAtrequiredstring (date-time)The approval deadline.
failureClassstring enumWhy a failed run stopped: a step's request failed, or a value wired into a step was not reported or broke a constraint. A failed run that was then cancelled keeps it. One of step_failed, output_missing, output_rejected.
failureMessagestringEvery step that stopped the run, and why.
idrequiredstring (uuid)
kindrequiredstring enum One of create, destroy.
mayApproveOwnbooleanOn a single run's read: true when the reader asked for this pending_approval run and may approve it themselves, nobody else being able to. Absent from the list.
namerequiredstring
requestedAtrequiredstring (date-time)
requestedByrequiredActorRefWho did something, for display and for audit. Not User: that carries the tenant and the role of the session, and an audit record naming a role that has since changed would be a lie about the past.
selfApprovedbooleanPresent once the run is decided: whether its requester approved it themselves, because nobody else could.
slotAccountsrequiredobject
staterequiredBlueprintRunStateMILESTONE_1 §11.2's machine, enforced by a trigger: pending_approval → rejected | expired | withdrawn | running; running → succeeded | failed | cancelling; cancelling → cancelled; failed → running (resume) | cancelling (cancel). One of pending_approval, rejected, expired, withdrawn, running, succeeded, failed, cancelling, cancelled.
stepsrequiredarray of BlueprintRunStep
teamIdrequiredstring (uuid)
terminalAtstring (date-time)
variablesRedactedrequiredobjectThe run's variables, a sensitive value masked.
versionrequiredinteger

BlueprintRunCreate

FieldTypeDescription
blueprintIdrequiredstring (uuid)
namerequiredstringThe run's name; each step's request is named after it.
slotAccountsrequiredobjectEvery slot of the version to its accounts.
teamIdstring (uuid)Optional; when given, it must be the team that owns every slot account.
variablesobject
versionrequiredinteger

BlueprintRunPage

FieldTypeDescription
itemsrequiredarray of BlueprintRun
pagerequiredinteger
pageSizerequiredinteger
totalrequiredinteger (int64)
totalPagesrequiredinteger

BlueprintRunState

MILESTONE_1 §11.2's machine, enforced by a trigger: pending_approval → rejected | expired | withdrawn | running; running → succeeded | failed | cancelling; cancelling → cancelled; failed → running (resume) | cancelling (cancel).

Type: string enum

BlueprintRunStep

FieldTypeDescription
accountIdrequiredstring
failureClassstringA failed step's request's failure class.
failureMessagestringWhy the step stopped: its request's failure message, or — for output_missing and output_rejected — the input, the expression and the rule, never the value; for skipped, why there was nothing to destroy.
requestIdstring (uuid)The step's request, once it exists.
requestStateProvisionRequestStatePROVISIONING_SPEC.md §5, exactly. approved is deliberately not a state. Approving writes the approvals row and sets queued in one transaction; a separate approved state could only ever be observed as a limbo where a request is blessed but not runnable, and every reader would have to treat the two identically. cancelling is the only non-terminal state a human can ask for and not immediately get: the agent learns about it on its next heartbeat and moves the request to cancelled itself. One of pending_approval, rejected, expired, withdrawn, queued, running, cancelling, cancelled, succeeded, failed.
resolvedAtstring (date-time)
resolvedInputsobjectThe values the advance wired into the step from upstream outputs, by consumer variable, a sensitive one masked. Absent for a step started at approval or not yet resolved.
slotIndexrequiredintegerThe account's position in its slot; 0 for a slot that takes one.
staterequiredBlueprintRunStepStatewaiting — not started; started — its request exists, and requestState says where it has got to; succeeded and failed — its request ended so (a cancelled request is a failed step); output_rejected and output_missing — never created, because a value wired into it broke a constraint or was not reported; skipped — a destroy run's step that never applied, or whose resource was already destroyed. One of waiting, started, succeeded, failed, output_rejected, output_missing, skipped.
stepIdrequiredstring

BlueprintRunStepState

waiting — not started; started — its request exists, and requestState says where it has got to; succeeded and failed — its request ended so (a cancelled request is a failed step); output_rejected and output_missing — never created, because a value wired into it broke a constraint or was not reported; skipped — a destroy run's step that never applied, or whose resource was already destroyed.

Type: string enum

BlueprintSlot

A place a run maps to accounts: exactly one, or — many — one or more, each running its own instance of the slot's steps.

FieldTypeDescription
cloudrequiredTemplateCloudThe cloud a template makes its resources in, as the account registry names it (Account.system): a library template's (MILESTONE_1 §8.1) or one of the tenant's own. azure is a library template's, one Import generated for an Azure resource, and — since then slice 6 — one of the tenant's own and a copy published to the catalog. A request for a template of the tenant's own in an Azure account waits for an agent whose Azure runners are scoped to resource groups (capability azure-resource-groups). One of aws, gcp, azure.
manyboolean
namerequiredstring

BlueprintSlotAccount

FieldTypeDescription
accountIdrequiredstring
detailstringThe sentence a submission mapping the account to this slot would be refused with.
namerequiredstringThe registry's name for the account.
qualifiesrequiredboolean
reasonstring enumThe first rule the account fails for this slot, absent when it qualifies: account_disabled; wrong_cloud (the slot is another cloud's); no_agent; agent_not_live (its agent has not called in within the heartbeat timeout); library_release_missing (its agent does not hold the library release the slot's steps pin); capability_missing (its agent does not report outputs, and a later step wires one; or, in an Azure slot whose steps run a template of the tenant's own, its Azure runners are not scoped to resource groups — detail names the setting). One of account_disabled, wrong_cloud, no_agent, agent_not_live, library_release_missing, capability_missing.
teamIdrequiredstring (uuid)The team that owns the account.

BlueprintSlotChoice

FieldTypeDescription
accountsrequiredarray of BlueprintSlotAccountThe reader's teams' accounts, by account id, each judged for this slot.
slotrequiredBlueprintSlotA place a run maps to accounts: exactly one, or — many — one or more, each running its own instance of the slot's steps.

BlueprintSlotChoices

FieldTypeDescription
slotsrequiredarray of BlueprintSlotChoiceThe version's slots, in its order.

BlueprintStep

One template version applied in one slot. A step on a slot that takes several accounts sets forEachSlot and runs once per account. Its output wired into a step on the same slot feeds that slot position's instance; wired anywhere else it arrives as the list of every instance's value, and only a string output into a list(string) variable can take it.

FieldTypeDescription
forEachSlotboolean
idrequiredstring
inputsobject
needsarray of stringThe steps whose success this step waits for. Acyclic.
slotrequiredstring
templaterequiredBlueprintTemplateRefA step's template version. identity is its source identity — the git commit, the inline sha256 or the library hash — which the platform writes at publish; a publisher may name it, and naming another is refused.

BlueprintTemplateRef

A step's template version. identity is its source identity — the git commit, the inline sha256 or the library hash — which the platform writes at publish; a publisher may name it, and naming another is refused.

FieldTypeDescription
idrequiredstring (uuid)
identitystring
versionrequiredinteger

BlueprintVersion

One published, immutable version.

FieldTypeDescription
blueprintIdrequiredstring (uuid)
graphrequiredBlueprintGraphA version's document (MILESTONE_1 §11.1): its slots, the run's variables schema, and its steps. At most 8 slots and 32 steps.
graphSha256requiredstringThe sha256 of the version's canonical document, which a run's binding names.
publishedAtrequiredstring (date-time)
publishedByrequiredActorRefWho did something, for display and for audit. Not User: that carries the tenant and the role of the session, and an audit record naming a role that has since changed would be a lie about the past.
versionrequiredinteger

BlueprintVersionPage

FieldTypeDescription
itemsrequiredarray of BlueprintVersion
pagerequiredinteger
pageSizerequiredinteger
totalrequiredinteger (int64)
totalPagesrequiredinteger

ChangeAlert

FieldTypeDescription
accountIdrequiredstring
appIdstring (uuid)The declared app the subscription is narrowed to; absent is none.
appNamestringThat app's name as it is now, archived or not; present with appId.
entriesrequiredarray of DiffEntry
hourlyLimitrequiredinteger
idrequiredinteger (int64)
occurredAtrequiredstring (date-time)
predicaterequiredChangeSubscriptionPredicate
regionrequiredstring
resourceIdrequiredstring
resourceNamestring
resourceTyperequiredstring
servicerequiredstring
subscriptionIdrequiredstring (uuid)
systemrequiredstring
teamIdstring (uuid)The subscription's team; absent is the whole tenant's.
throttledrequiredbooleanThe alert that said the subscription reached its hourly limit; the rest of that hour was not sent.

ChangeAlertList

FieldTypeDescription
itemsrequiredarray of ChangeAlertNewest first, at most 100.
sincerequiredstring (date-time)

ChangeAttribution

Who may have made a configuration change: the CloudTrail events that name the resource with a write call between the change's two observations, for an AWS account with CloudTrail attribution on. Every candidate is shown and none is chosen; one candidate is still a candidate. state is the lookup's outcome — matched, no_match, truncated (the one page read had more events behind it, so the candidates may be incomplete; never reported as no_match), permission_denied, unavailable (throttled, failed or waited past a day), beyond_lookup_window (older than CloudTrail's 90 days) — or, with no lookup behind it, pending, not_opted_in or not_looked_up (the change was recorded before the opt-in). summary says the state in one sentence. Only an AWS resource's change carries one: CloudTrail records no other cloud's calls and no Kubernetes workload's.

FieldTypeDescription
candidatesrequiredarray of ChangeAttributionCandidate
lookedUpAtstring (date-time)
staterequiredstring enum One of pending, matched, no_match, truncated, permission_denied, unavailable, beyond_lookup_window, not_opted_in, not_looked_up.
summaryrequiredstring

ChangeAttributionCandidate

One CloudTrail event that may be the change: its id, name and time, who made the call, and why it matched. Never the event's request, response or source address.

FieldTypeDescription
atrequiredstring (date-time)
basisrequiredobject
eventIdrequiredstring
eventNamerequiredstring
principalrequiredstringThe caller's ARN, else the user name CloudTrail gives.
principalTypestringCloudTrail's userIdentity type: IAMUser, AssumedRole, AWSService, …

ChangeEvent

One row of the change feed. id is <kind>:<source id> — stable across reads and pages. at is the event's own time.

FieldTypeDescription
accountIdstringThe account, where the source has one.
actorActorRefWho did something, for display and for audit. Not User: that carries the tenant and the role of the session, and an audit record naming a role that has since changed would be a lie about the past.
appIdstring (uuid)The declared app a marker names.
atrequiredstring (date-time)
attributionChangeAttributionWho may have made a config_change of an AWS resource; absent on every other row.
detailrequiredobjectThe source's own fields, by kind.
evidencerequiredChangeEvidenceWhere the row's source can be read in full.
idrequiredstring
kindrequiredChangeKindOne source's kind of event in the change feed. One of config_change, status_transition, acknowledgement, provisioning_apply, provisioning_destroy, provisioning_partial, cost_anomaly, budget_threshold, external_marker, ownership_change, ai_review, provisioning_import, instance_lifecycle.
resourceKeystringThe resource key (region/system/service/type/id), where the source has one.
summaryrequiredstringWhat happened, in one line. Never why.
teamIdstring (uuid)The team, for a team-scoped event.

ChangeEvidence

Where the row's source can be read in full.

FieldTypeDescription
apirequiredstringThe API read behind the row.
hrefrequiredstringA portal path, or a marker's own URL.

ChangeFeedPage

FieldTypeDescription
itemsrequiredarray of ChangeEvent
nextCursorstringPresent when another page follows; opaque.
truncatedrequiredarray of ChangeKindThe kinds whose source had more rows in this window than one page carries; their contribution was capped.

ChangeKind

One source's kind of event in the change feed.

Type: string enum

ChangeMarker

FieldTypeDescription
accountIdstring
appIdstring (uuid)
atrequiredstring (date-time)
createdAtrequiredstring (date-time)
idrequiredstring (uuid)
idempotencyKeyrequiredstring
labelrequiredstring
resourceKeysrequiredarray of string
urlrequiredstring

ChangeMarkerCreate

FieldTypeDescription
accountIdstringThe account the marker is about; omitted, the marker is the tenant's.
appIdstring (uuid)The declared app the marker is about.
atrequiredstring (date-time)When it happened, by the poster's clock.
idempotencyKeyrequiredstring
labelrequiredstring
resourceKeysarray of stringThe resources it touched, as keys under accountId.
urlstringA link back to the poster's own record.

ChangeSubscription

FieldTypeDescription
accountIdsrequiredarray of string
appIdstring (uuid)The declared app the scope is narrowed to; absent is none.
channelIdsrequiredarray of string (uuid)The chosen channels; empty is the team's routing.
createdAtrequiredstring (date-time)
createdByrequiredstring (uuid)
hourlyLimitrequiredinteger
idrequiredstring (uuid)
predicaterequiredChangeSubscriptionPredicate
resourceTypesrequiredarray of ChangeSubscriptionType
teamIdstring (uuid)

ChangeSubscriptionInput

FieldTypeDescription
accountIdsarray of string
appIdstring (uuid)One live declared app (GET /apps/declared): the subscription then covers the app's members alone — its tag matches and included resources, less the excluded — and its team is the app's (absent teamId takes it; another is a 400). A lead subscribes to their own team's apps.
channelIdsarray of string (uuid)Where its alerts go: Slack or webhook channels of the tenant — a lead's, its team's own or the tenant's default ones. Absent or empty is the team's routing.
hourlyLimitinteger
predicaterequiredChangeSubscriptionPredicate
resourceTypesarray of ChangeSubscriptionTypeAbsent or empty is every type; a details path needs at least one.
teamIdstring (uuid)Absent is the whole tenant, an administrator's alone.

ChangeSubscriptionList

FieldTypeDescription
itemsrequiredarray of ChangeSubscription

ChangeSubscriptionPredicate

FieldTypeDescription
oprequiredstring enum One of changed, added, removed, equals.
pathrequiredstringstatus, state, tags.<key> or additionalInfo.<field> — a field of every named type's descriptor.
valuestringThe new value, for equals only.

ChangeSubscriptionPreview

FieldTypeDescription
matchedrequiredintegerChanges the draft matched.
matchesrequiredarray of ChangeSubscriptionPreviewMatchThe newest 50.
partialrequiredbooleanThe day held more changes than one preview reads.
sincerequiredstring (date-time)
throttledrequiredintegerOf them, those past the hourly limit: not sent.

ChangeSubscriptionPreviewMatch

FieldTypeDescription
accountIdrequiredstring
entriesrequiredarray of DiffEntry
observedAtrequiredstring (date-time)
regionrequiredstring
resourceIdrequiredstring
resourceTyperequiredstring
servicerequiredstring
systemrequiredstring
throttledrequiredboolean

ChangeSubscriptionType

FieldTypeDescription
resourceTyperequiredstring
servicerequiredstring
systemrequiredstring

CloudIdentity

How this deployment signs in to each cloud. home is the cloud whose identity the platform holds; each entry of clouds says whether the deployment reaches that cloud and how.

FieldTypeDescription
cloudsrequiredarray of CloudSignInEvery cloud this build carries, in the product's order (AWS, Google Cloud, Azure).
homerequiredstring enum One of aws, gcp, azure.

CloudSignIn

FieldTypeDescription
cloudrequiredstring enum One of aws, gcp, azure.
fromstring enumThe cloud whose identity a federated sign-in starts from; absent otherwise. One of aws, gcp, azure.
signInrequiredstring enumhome, the home itself; federation, a token the home issues, trusted by this cloud (from names the home); certificate, Azure's certificate credential under an AWS home, which is not federation from the home; none, a cloud this deployment does not reach — the portal does not offer it. One of home, federation, certificate, none.

CollectionQueued

What collect-now queued.

FieldTypeDescription
jobsrequiredintegerHow many jobs were queued — one per module per region.
modulesrequiredarray of stringThe modules queued, each once however many regions it covers.

CollectionRunBucket

The unit of reconciliation: (account, region, system, service, resourceType). The account comes from the run, so it is not repeated here.

FieldTypeDescription
regionrequiredstring
resourceTyperequiredstring
servicerequiredstring
systemrequiredstring

CollectionRunRegistered

FieldTypeDescription
bucketsrequiredarray of RegisteredBucketEach registered bucket with the generation this run claimed.
runIdrequiredstring (uuid)

CollectionRunRegistration

FieldTypeDescription
accountIdrequiredstring
bucketsrequiredarray of CollectionRunBucketMust not be empty. A module that declares no buckets can neither store nor finalize, which is a wiring bug worth failing loudly. Every bucket's region must equal the run's region, and the set must contain no duplicates. Both are 400 invalid_request, and both are rejected rather than tidied up because each is a claim on a generation: a duplicate would bump one bucket's fence twice and supersede the run's own registration, and a bucket in another region would supersede whichever run legitimately owns that region — after which finalize there would tombstone live resources. global is a region like any other here: a global bucket belongs to a run whose own region is global.
modulerequiredstring
regionrequiredstring
runIdrequiredstring (uuid)Generated by the collector before its first attempt, and reused on every retry. This is the idempotency key for registration.

CollectorMatrix

FieldTypeDescription
accountsrequiredarray of CollectorMatrixAccountPer-account roll-up, worst status wins.
columnsrequiredarray of CollectorMatrixColumnEvery collector the grid can have a column for — each module this deployment schedules per account — once, in the scheduler's order, with the cloud it runs for and the category of what it reads (MILESTONE_1 §6). An entry's module names its column here. The grid's columns differ per cloud, and a module's name cannot say which: k8s.eks.workload runs for an AWS account, and the cost and performance reads carry no cloud in their names at all. So the portal draws one block per cloud and groups each block's columns by category from this list, never from the name.
entriesrequiredarray of MatrixEntry

CollectorMatrixAccount

FieldTypeDescription
accountIdrequiredstring
overallStatusrequiredstring enum One of ok, error, slow, no-data.

CollectorMatrixColumn

FieldTypeDescription
categoryResourceCategoryWhat kind of thing this collector reads: the ResourceCategory it declares beside itself. Absent for a read that collects no resource type — the cost and performance reads — which belongs to no category; it is never other, which is the bucket for legacy template categories.
modulerequiredstring
systemrequiredstringThe cloud of the accounts this collector runs for — one of Account.system's values. The scheduler pairs a module only with accounts of its own cloud.

CollectorRun

FieldTypeDescription
accountIdrequiredstring
durationMsinteger (int64)
errorMessagestring
finishedAtstring (date-time)
modulerequiredstring
regionrequiredstring
resourcesAttemptedinteger (int64)
resourcesStoredinteger (int64)
runIdrequiredstring (uuid)
startedAtrequiredstring (date-time)
staterequiredstring enum One of running, finalized, failed.
tombstonedinteger (int64)

CollectorRunList

FieldTypeDescription
itemsrequiredarray of CollectorRun

CostAllocationRule

A shared-cost allocation rule: the services whose UNCLAIMED spend — the untagged tier, after attribution — is split across teams, and how. Tried in list order; the first rule naming a service takes its rows. What a tag claimed is never allocated.

FieldTypeDescription
enabledrequiredboolean
fixedSharesobjectFor fixed: team id → share. Any positive numbers; they are normalised.
idstring (uuid)
methodrequiredstring enumproportional — in proportion to each team's direct spend in the month, per currency (a currency with no direct spend stays untagged); even — equally across every live team; fixed — by fixedShares, normalised. One of proportional, even, fixed.
namerequiredstring
servicesrequiredarray of stringCost Explorer SERVICE names, as the breakdown spells them.

CostAnomaly

FieldTypeDescription
baselinerequiredMoneyOne amount in one currency. The only way money appears in this document. TestAllMoneyIsCurrencyQualified walks every response schema and fails on any amount-shaped property that is not inside this object or an array of it, because "a scalar total" is the one contract mistake that cannot be retrofitted once a client has summed two currencies (C7). amount is a decimal as a string, never a JSON number: numeric(20,10) does not survive a float, and a total that reconciles to the cent is the product.
detectedAtrequiredstring (date-time)
deviationPctrequirednumber
idrequiredstring (uuid)
observedrequiredMoneyOne amount in one currency. The only way money appears in this document. TestAllMoneyIsCurrencyQualified walks every response schema and fails on any amount-shaped property that is not inside this object or an array of it, because "a scalar total" is the one contract mistake that cannot be retrofitted once a client has summed two currencies (C7). amount is a decimal as a string, never a JSON number: numeric(20,10) does not survive a float, and a total that reconciles to the cent is the product.
resolvedAtstring (date-time)
resolvedByActorRefWho did something, for display and for audit. Not User: that carries the tenant and the role of the session, and an audit record naming a role that has since changed would be a lie about the past.
scopeIdrequiredstringAn account id, or a team id.
scopeLabelstringThe account's or team's name, for display.
scopeTyperequiredstring enum One of account, team.
servicerequiredstringEmpty for a scope-wide anomaly.
severityrequiredstring enum One of low, medium, high.
statusrequiredCostAnomalyStatus One of open, acknowledged, dismissed.
windowEndrequiredstring (date)
windowStartrequiredstring (date)

CostAnomalyPage

FieldTypeDescription
inNoTeamrequiredbooleanThe reader is not an administrator and belongs to no team, so the page is empty for that reason; as CostReadMeta.inNoTeam. On the reviewer's route the reader is the review's audience.
itemsrequiredarray of CostAnomaly
pagerequiredinteger
pageSizerequiredinteger
totalrequiredinteger (int64)
totalPagesrequiredinteger

CostAnomalySettings

FieldTypeDescription
minDeltarequiredMoneyOne amount in one currency. The only way money appears in this document. TestAllMoneyIsCurrencyQualified walks every response schema and fails on any amount-shaped property that is not inside this object or an array of it, because "a scalar total" is the one contract mistake that cannot be retrofitted once a client has summed two currencies (C7). amount is a decimal as a string, never a JSON number: numeric(20,10) does not survive a float, and a total that reconciles to the cent is the product.
sensitivityPctrequirednumberThe relative deviation from baseline that counts.
suppressionHoursrequiredinteger

CostAnomalyStatus

Type: string enum

CostAnomalyUpdate

FieldTypeDescription
statusrequiredstring enum One of acknowledged, dismissed.

CostAttributionAmounts

FieldTypeDescription
amountsrequiredarray of Money
attributionrequiredCostAttributionTierWhich step of the attribution chain (§7.1) attributed a row: a tag value mapped to a team, the account's owning team, a shared-cost allocation rule (spend nobody claimed, split across teams by a rule the tenant set), or nothing — untagged, visible and never dropped. The four tiers sum to the total. One of tag, team_account, allocated, untagged.

CostAttributionRule

FieldTypeDescription
priorityrequiredinteger
tagKeyrequiredstring

CostAttributionTier

Which step of the attribution chain (§7.1) attributed a row: a tag value mapped to a team, the account's owning team, a shared-cost allocation rule (spend nobody claimed, split across teams by a rule the tenant set), or nothing — untagged, visible and never dropped. The four tiers sum to the total.

Type: string enum

CostAzureBillingExport

Where one Azure Cost Management export is written, and the cap on one blob of it. Nothing here is a credential: the export is read as the subscription's registered reader, with the export reader role assigned on the container.

FieldTypeDescription
containerrequiredstring
directoryrequiredstringThe export's directory in the container: FOCUS, or the ActualCost export of a pair.
idrequiredstring (uuid)
lastPullCostAzureBillingPullThe latest run record of the job cost.azure_billing for the export's subscription that started after the export was last saved — created or changed — and absent until one has: a pull before the save read nothing of it. refused is a pull the customer has to unblock — the role not assigned on the container, a blob over the cap, half a pair — and message says which and what to change; failed is anything else, retried by the schedule.
maxBlobBytesrequiredinteger (int64)The most bytes one blob may hold; over it, the pull is refused and says so.
pairDirectorystringThe AmortizedCost export's directory, when the export is the pair; absent for FOCUS.
storageAccountrequiredstring
subscriptionIdrequiredstring (uuid)
updatedAtrequiredstring (date-time)

CostAzureBillingExportCreate

FieldTypeDescription
containerrequiredstring
directoryrequiredstring
maxBlobBytesinteger (int64)
pairDirectorystring
storageAccountrequiredstring
subscriptionIdrequiredstring (uuid)

CostAzureBillingExportList

FieldTypeDescription
itemsrequiredarray of CostAzureBillingExport

CostAzureBillingExportUpdate

FieldTypeDescription
containerstring
directorystring
maxBlobBytesinteger (int64)
pairDirectorystring
storageAccountstring

CostAzureBillingPull

The latest run record of the job cost.azure_billing for the export's subscription that started after the export was last saved — created or changed — and absent until one has: a pull before the save read nothing of it. refused is a pull the customer has to unblock — the role not assigned on the container, a blob over the cap, half a pair — and message says which and what to change; failed is anything else, retried by the schedule.

FieldTypeDescription
endedAtstring (date-time)
messagestring
rowsWrittenintegerRows the run wrote, over every export of the subscription.
startedAtrequiredstring (date-time)
staterequiredstring enum One of running, succeeded, refused, failed.

CostBreakdown

FieldTypeDescription
figurerequiredCostFigure One of amortized, unblended.
groupByrequiredstring enum One of team, service, account.
inNoTeamrequiredbooleanTrue when the reader is not an administrator and belongs to no team. A member reads their teams' spend, so for them this read covers nothing: every figure in it is empty because of who is reading, not because nothing was spent, and the portal says so instead of drawing a zero. False for an administrator, who reads the whole organisation whatever their teams, and for a member of a team — one that spent nothing too, whose zero is real. The teamId filter does not change it: it is about the reader, not the question. The anomalies page, the budget list, the recommendations page and the tag-compliance breakdown carry the same field with the same meaning.
periodrequiredCostPeriod
rebuildInProgressrequiredboolean
rebuiltAtstring (date-time)When the rollups these figures come from were last built.
rowsrequiredarray of CostBreakdownRow
totalsrequiredarray of MoneyThe sum of every row including other, per currency.

CostBreakdownRow

FieldTypeDescription
allocatedAmountsarray of MoneyThe part of amounts that arrived by a shared-cost allocation rule, per currency. Present whenever any did, so a team can always see what was direct and what was allocated; the two are never merged silently.
amountsrequiredarray of MoneyThe row's whole figure, per currency, allocation included.
keyrequiredstringPass it back as the matching filter to drill down. other and untagged are not drillable.
labelrequiredstring
sharePctnumberThis row's share of the total in the row's leading currency, when the total has exactly one currency. Absent otherwise — a share of two currencies is not a number.

CostBudget

FieldTypeDescription
amountrequiredMoneyOne amount in one currency. The only way money appears in this document. TestAllMoneyIsCurrencyQualified walks every response schema and fails on any amount-shaped property that is not inside this object or an array of it, because "a scalar total" is the one contract mistake that cannot be retrofitted once a client has summed two currencies (C7). amount is a decimal as a string, never a JSON number: numeric(20,10) does not survive a float, and a total that reconciles to the cent is the product.
createdAtrequiredstring (date-time)
enabledrequiredboolean
idrequiredstring (uuid)
periodrequiredstring enum One of monthly.
progressCostBudgetProgressThe current period against the budget, in the budget's currency.
teamIdrequiredstring (uuid)
teamNamerequiredstring
thresholdsrequiredarray of integerPercentages of amount, each firing once per period.
updatedAtrequiredstring (date-time)

CostBudgetCreate

FieldTypeDescription
amountrequiredMoneyOne amount in one currency. The only way money appears in this document. TestAllMoneyIsCurrencyQualified walks every response schema and fails on any amount-shaped property that is not inside this object or an array of it, because "a scalar total" is the one contract mistake that cannot be retrofitted once a client has summed two currencies (C7). amount is a decimal as a string, never a JSON number: numeric(20,10) does not survive a float, and a total that reconciles to the cent is the product.
enabledboolean
periodstring enum One of monthly.
teamIdrequiredstring (uuid)
thresholdsarray of integer

CostBudgetList

FieldTypeDescription
inNoTeamrequiredbooleanThe reader is not an administrator and belongs to no team, so the list is empty for that reason; as CostReadMeta.inNoTeam.
itemsrequiredarray of CostBudget

CostBudgetProgress

The current period against the budget, in the budget's currency.

FieldTypeDescription
otherCurrenciesarray of MoneyThe team's spend this period in currencies other than the budget's — not converted (C7), listed so the mismatch is seen.
pctrequirednumberspent / amount × 100.
periodStartrequiredstring (date)
spentrequiredMoneyOne amount in one currency. The only way money appears in this document. TestAllMoneyIsCurrencyQualified walks every response schema and fails on any amount-shaped property that is not inside this object or an array of it, because "a scalar total" is the one contract mistake that cannot be retrofitted once a client has summed two currencies (C7). amount is a decimal as a string, never a JSON number: numeric(20,10) does not survive a float, and a total that reconciles to the cent is the product.
thresholdsCrossedrequiredarray of integerThe thresholds that have already fired this period.

CostBudgetUpdate

FieldTypeDescription
amountMoneyOne amount in one currency. The only way money appears in this document. TestAllMoneyIsCurrencyQualified walks every response schema and fails on any amount-shaped property that is not inside this object or an array of it, because "a scalar total" is the one contract mistake that cannot be retrofitted once a client has summed two currencies (C7). amount is a decimal as a string, never a JSON number: numeric(20,10) does not survive a float, and a total that reconciles to the cent is the product.
enabledboolean
thresholdsarray of integer

CostCloudAmounts

FieldTypeDescription
amountsrequiredarray of Money
systemrequiredstring enumThe cloud: a registered account's own system; a Google billing account's own place (support, fees, rounding — never an account row) is gcp; a place an Azure export wrote that day (the billing account's or profile's own, or a subscription the registry no longer holds) is azure; spend of any other account the registry no longer holds is aws, the cloud of every other source. One of aws, gcp, azure.

CostCurReport

Where one account's Cost & Usage Report lives, and the role that reads it. The report is read under reportRoleArn — a role the account deploys from cost-report-role.yaml, scoped to this bucket and prefix — never under the inventory reader role, which reads no object content. Nothing here is a credential. lastAssemblyId and lastPeriodStart are the most recent generation ingested, read-only.

FieldTypeDescription
accountIdrequiredstring
bucketrequiredstring
bucketRegionrequiredstringS3 does not follow a request to the wrong region; the client is built for this one.
curOnlybooleanSkip this account's daily Cost Explorer pulls — the aggregate, tag and per-resource passes, each billed per request — while this report covers them. A preference, not a promise: a pull is skipped only while every day before today that it would write already has this report's own rows of its kind (money rows for the aggregate pull, resource rows for the per-resource one), which the cost module takes over Cost Explorer's anyway; otherwise it runs as before and the job logs the first day without them. Omitted on an update, it is false — the list is the whole set.
enabledrequiredboolean
lastAssemblyIdstring
lastPeriodStartstring (date)
prefixrequiredstring
reportNamerequiredstring
reportRoleArnstringThe ARN of the report role in this account. Blank until the customer has deployed it: a pull is then refused with a reason that names the template, and POST /admin/accounts/{accountId}/verify reports it as the missing check. Must be an IAM role ARN when set.

CostExplanation

A change between two windows and the movers that make it up.

FieldTypeDescription
anomalyIdstring (uuid)
changesFromstring (date)The first day the movers' changes were read from: the current window's first, or ninety days before its end when it is longer (the change feed's widest read). Absent when no change was read — the service grain, or no resource mover.
comparablerequiredCostExplanationComparability
coveragerequiredstring enum One of complete, incomplete.
currenciesrequiredarray of CostExplanationCurrency
currentrequiredCostExplanationWindowBoth days inclusive, after the cut at the settled day.
dataVersionstring (date-time)The newest rebuild among the rows read; absent when none was.
figurerequiredCostFigure One of amortized, unblended.
grainrequiredstring enumWhat a mover is; one grain per explanation. One of resource, service.
inNoTeamrequiredbooleanAs CostReadMeta.inNoTeam.
methodVersionrequiredstringThe method the figures were computed with.
missingDaysrequiredarray of string (date)Days of either window with no row.
previousrequiredCostExplanationWindowBoth days inclusive, after the cut at the settled day.
rebuildInProgressrequiredbooleanAn attribution rebuild is queued or running (§7.3), so the movers and a team's share may move until it finishes; as CostReadMeta.rebuildInProgress.
rebuiltAtstring (date-time)When the last attribution rebuild finished; absent if none has; as CostKubernetes.rebuiltAt.
settledThroughrequiredstring (date)
supersededrequiredbooleanThe request's version is not the current answer; false when none was given.
unsettledDaysrequiredarray of string (date)Days of either window after the settled day, cut from it.
versionstringThis answer's identity, a hash of everything it says: pass it back as version to be served this answer again. Absent when no row was read.

CostExplanationChange

FieldTypeDescription
atrequiredstring (date-time)
kindrequiredstring
resourcerequiredstring
summaryrequiredstring

CostExplanationComparability

FieldTypeDescription
currentDaysrequiredinteger
equalLengthrequiredboolean
previousDaysrequiredinteger
weekdaysDifferrequiredbooleanThe windows hold a different mix of weekdays.

CostExplanationCurrency

One currency's change; its movers sum to delta exactly.

FieldTypeDescription
currencyrequiredstring
currentrequiredstring
currentPerDayrequiredstring
deltarequiredstring
deltaPerDayrequiredstring
moversrequiredarray of CostExplanationMover
previousrequiredstring
previousPerDayrequiredstring

CostExplanationMover

FieldTypeDescription
accountIdrequiredstring
changesInWindowrequiredarray of CostExplanationChangeWhat changed on the resource during the current window — shown beside the money, never as its cause.
classrequiredstring enum One of new, gone, amount_moved, adjustment.
currencyrequiredstring
currentrequiredstring
deltarequiredstring
moreChangesbooleanchangesInWindow is not every change on the resource: the feed had more, or this mover is past the largest a request reads beside. The resource's own page has them all.
ownerOwnershipWho owns the resource, from the one resolver, derived when the row is read. state is the one-word answer; team, confidence, source and evidence are the winning tier's; reason says why there is no owner although a tier answered — a conflict between two matches of equal reach, or an archived team; tried lists the tiers that said nothing; candidates is every tier's answer, best first — the evidence one click away, and the discrepancy when a lower tier disagrees. Ownership explains and proposes: it never widens what a member sees, and never touches attribution. import (MILESTONE_1 §9.2) is a recorded import's owning team: explicit, as a declared owner is, and second only to a resource override — the correction a person makes stays the last word. Its evidence names the import and the template version.
previousrequiredstring
resourcestringAt the resource grain only: the bill's own identifier of the resource.
resourceKeystringThe inventory key (region/system/service/type/id) resource maps to, when it maps to one; the account is accountId.
servicerequiredstring
withoutResourcebooleanAt the resource grain, this service's lines that name no resource — tax, a fee, support — as one mover.

CostExplanationWindow

Both days inclusive, after the cut at the settled day.

FieldTypeDescription
fromrequiredstring (date)
torequiredstring (date)

CostFigure

Type: string enum

CostForecast

FieldTypeDescription
actualToDaterequiredMoneyOne amount in one currency. The only way money appears in this document. TestAllMoneyIsCurrencyQualified walks every response schema and fails on any amount-shaped property that is not inside this object or an array of it, because "a scalar total" is the one contract mistake that cannot be retrofitted once a client has summed two currencies (C7). amount is a decimal as a string, never a JSON number: numeric(20,10) does not survive a float, and a total that reconciles to the cent is the product.
asOfrequiredstring (date)
backtestrequiredCostForecastBacktestThe scope's own backtest, at the checkpoint nearest the forecast's day.
computedAtrequiredstring (date-time)
daysRemainingrequiredinteger
highMoneyOne amount in one currency. The only way money appears in this document. TestAllMoneyIsCurrencyQualified walks every response schema and fails on any amount-shaped property that is not inside this object or an array of it, because "a scalar total" is the one contract mistake that cannot be retrofitted once a client has summed two currencies (C7). amount is a decimal as a string, never a JSON number: numeric(20,10) does not survive a float, and a total that reconciles to the cent is the product.
lowMoneyOne amount in one currency. The only way money appears in this document. TestAllMoneyIsCurrencyQualified walks every response schema and fails on any amount-shaped property that is not inside this object or an array of it, because "a scalar total" is the one contract mistake that cannot be retrofitted once a client has summed two currencies (C7). amount is a decimal as a string, never a JSON number: numeric(20,10) does not survive a float, and a total that reconciles to the cent is the product.
lowConfidencerequiredboolean
methodrequiredstring
projectionMoneyOne amount in one currency. The only way money appears in this document. TestAllMoneyIsCurrencyQualified walks every response schema and fails on any amount-shaped property that is not inside this object or an array of it, because "a scalar total" is the one contract mistake that cannot be retrofitted once a client has summed two currencies (C7). amount is a decimal as a string, never a JSON number: numeric(20,10) does not survive a float, and a total that reconciles to the cent is the product.
settledThroughrequiredstring (date)The last day counted; the two days after it are still arriving.
staterequiredstring enumready with a projection; not_enough_history below 14 settled days, with none. One of ready, not_enough_history.

CostForecastBacktest

The scope's own backtest, at the checkpoint nearest the forecast's day.

FieldTypeDescription
checkpointrequiredintegerThe day of the month the backtest was read at, 7, 14 or 21.
p90ApePctnumber (float)The 90th-percentile absolute percentage error there; absent with no scored month. At most 1,000,000,000: a month whose credits all but cancel its spend gives an error without bound, and it is shown at that ceiling, the forecast already low-confidence.
scoredMonthsrequiredinteger

CostForecastList

FieldTypeDescription
forecastsrequiredarray of CostForecastOne per currency, from the newest day computed; empty when none has been computed.
monthrequiredstring
scoperequiredCostForecastScopeWhat a forecast is for. One of tenant, team, account.
scopeIdrequiredstringThe team's or the account's id; empty for the tenant.

CostForecastScope

What a forecast is for.

Type: string enum

CostGcpBillingExport

Where one Google Cloud billing account's BigQuery billing export is, and the byte cap every query of it carries. Nothing here is a credential: the export is read as its project's registered reader, which needs roles/bigquery.jobUser on the project and roles/bigquery.dataViewer on the dataset.

FieldTypeDescription
billingAccountIdrequiredstring
lastPullCostGcpBillingPullThe latest run record of the job cost.gcp_billing for the export's project that started after the export was last saved — created or changed — and absent until one has: a pull before the save read nothing of it. refused is a pull the customer has to unblock — over the byte cap, or the reader lacking a grant — and message says which and what to change; failed is anything else, retried by the schedule.
maxBytesBilledrequiredinteger (int64)The most bytes one query may scan; over it, the pull is refused and says so.
tableNamerequiredstringproject.dataset.table, the detailed usage cost export.
updatedAtrequiredstring (date-time)

CostGcpBillingExportCreate

FieldTypeDescription
billingAccountIdrequiredstring
maxBytesBilledinteger (int64)
tableNamerequiredstring

CostGcpBillingExportList

FieldTypeDescription
itemsrequiredarray of CostGcpBillingExport

CostGcpBillingExportUpdate

FieldTypeDescription
maxBytesBilledinteger (int64)
tableNamestring

CostGcpBillingPull

The latest run record of the job cost.gcp_billing for the export's project that started after the export was last saved — created or changed — and absent until one has: a pull before the save read nothing of it. refused is a pull the customer has to unblock — over the byte cap, or the reader lacking a grant — and message says which and what to change; failed is anything else, retried by the schedule.

FieldTypeDescription
endedAtstring (date-time)
messagestring
rowsWrittenintegerRows the run wrote, over every export of the project.
startedAtrequiredstring (date-time)
staterequiredstring enum One of running, succeeded, refused, failed.

CostIngestionState

FieldTypeDescription
accountIdrequiredstring
backfillCompleteboolean
backfillCursorstring (date)
backfillFromstring (date)
lastCompleteDatestring (date)
lastErrorstring
lastRunAtstring (date-time)
passDiscrepancyMoneyOne amount in one currency. The only way money appears in this document. TestAllMoneyIsCurrencyQualified walks every response schema and fails on any amount-shaped property that is not inside this object or an array of it, because "a scalar total" is the one contract mistake that cannot be retrofitted once a client has summed two currencies (C7). amount is a decimal as a string, never a JSON number: numeric(20,10) does not survive a float, and a total that reconciles to the cent is the product.
resourceGranularityAvailablebooleanAbsent until the first resource pull has asked.
sourcerequiredstring enum One of ce_aggregate, ce_tag, ce_resource, cur, trusted_advisor.
supportPlanAvailablebooleanFor trusted_advisor: whether the account's support plan includes it. Absent until the first pull.

CostInventoryState

What the inventory says about the key the spend is against: a live row, a tombstoned one (spend continuing after deletion — the cost-without-resource finding), or nothing at all (missing).

Type: string enum

CostKubernetes

FieldTypeDescription
clustersrequiredarray of CostKubernetesCluster
figurerequiredCostFigure One of amortized, unblended.
inNoTeamrequiredbooleanTrue when the reader is not an administrator and belongs to no team. A member reads their teams' spend, so for them this read covers nothing: every figure in it is empty because of who is reading, not because nothing was spent, and the portal says so instead of drawing a zero. False for an administrator, who reads the whole organisation whatever their teams, and for a member of a team — one that spent nothing too, whose zero is real. The teamId filter does not change it: it is about the reader, not the question. The anomalies page, the budget list, the recommendations page and the tag-compliance breakdown carry the same field with the same meaning.
periodrequiredCostPeriod
rebuildInProgressrequiredboolean
rebuiltAtstring (date-time)When the rollups these figures come from were last built.
rowsrequiredarray of CostKubernetesRowLargest first, across clusters; filter by cluster for one.

CostKubernetesCluster

FieldTypeDescription
accountIdrequiredstring
clusterrequiredstring
idleSpendrequiredarray of MoneyThe part no pod requested, per currency.
nodeCountrequiredintegerNodes in the inventory on the last day computed.
nodeSpendrequiredarray of MoneyThe cluster's node line items over the window, per currency: what the rows sum to.
regionrequiredstringThe cluster's region: with accountId and cluster, its identity. Empty for rows stored before the region was kept whose cluster the inventory no longer holds — history of a cluster that is gone, not attributed to a region.

CostKubernetesRow

FieldTypeDescription
accountIdrequiredstring
clusterrequiredstring
namespacerequiredstringEmpty for the idle row.
regionrequiredstringThe cluster's region; with accountId and cluster, which cluster this row belongs to.
sharePctnumberThis row's share of the cluster's node spend, in the row's leading currency, when the cluster has one currency.
spendrequiredarray of Money
workloadKindrequiredstringDeployment, StatefulSet, DaemonSet, CronJob, Job, Pod — or idle.
workloadNamerequiredstring

CostPeriod

FieldTypeDescription
fromrequiredstring (date)
torequiredstring (date)

CostReadMeta

Carried by every read. rebuildInProgress is C3 said out loud: a rule change is re-attributing history and yesterday's number may move. figure echoes what was asked for.

FieldTypeDescription
figurerequiredCostFigure One of amortized, unblended.
inNoTeamrequiredbooleanTrue when the reader is not an administrator and belongs to no team. A member reads their teams' spend, so for them this read covers nothing: every figure in it is empty because of who is reading, not because nothing was spent, and the portal says so instead of drawing a zero. False for an administrator, who reads the whole organisation whatever their teams, and for a member of a team — one that spent nothing too, whose zero is real. The teamId filter does not change it: it is about the reader, not the question. The anomalies page, the budget list, the recommendations page and the tag-compliance breakdown carry the same field with the same meaning.
rebuildInProgressrequiredboolean
rebuiltAtstring (date-time)When the rollups these figures come from were last built.

CostRecommendation

FieldTypeDescription
accountIdrequiredstring
checkIdrequiredstring
checkNamerequiredstring
estimatedMonthlySavingsMoneyOne amount in one currency. The only way money appears in this document. TestAllMoneyIsCurrencyQualified walks every response schema and fails on any amount-shaped property that is not inside this object or an array of it, because "a scalar total" is the one contract mistake that cannot be retrofitted once a client has summed two currencies (C7). amount is a decimal as a string, never a JSON number: numeric(20,10) does not survive a float, and a total that reconciles to the cent is the product.
firstSeenAtstring (date-time)
metadatarequiredobjectThe check's columns, by the names Trusted Advisor gives them.
observedAtrequiredstring (date-time)
regionstring
resourceIdrequiredstringTrusted Advisor's identifier for the flagged resource.
resourceKeystringThe inventory key, when the check names a resource the derivation can map.
statusrequiredstring enum One of ok, warning, error.

CostRecommendationPage

FieldTypeDescription
inNoTeamrequiredbooleanThe reader is not an administrator and belongs to no team, so the page is empty for that reason; as CostReadMeta.inNoTeam.
itemsrequiredarray of CostRecommendation
pagerequiredinteger
pageSizerequiredinteger
totalrequiredinteger (int64)
totalPagesrequiredinteger
unavailableAccountsrequiredarray of CostRecommendationUnavailableAccountAccounts with no recommendations and why: no Trusted Advisor on the support plan, or not pulled yet.

CostRecommendationUnavailableAccount

FieldTypeDescription
accountIdrequiredstring
reasonrequiredstring enum One of support_plan_required, not_yet_ingested.

CostReconciliation

The three places, the two breakdowns and the rows. Every list names every value, zero included, so nothing is hidden by absence.

FieldTypeDescription
chargeTypesrequiredarray of ReconciliationChargeType
coveragerequiredarray of ReconciliationCoverage
inNoTeamrequiredbooleanThe reader is not an administrator and belongs to no team, so everything is empty for that reason; as CostReadMeta.inNoTeam.
placesrequiredarray of ReconciliationPlace
rebuildInProgressrequiredbooleanAn attribution rebuild is queued or running (§7.3), so the places and a team's share may move until it finishes; as CostReadMeta.rebuildInProgress.
rebuiltAtstring (date-time)When the last attribution rebuild finished; absent if none has; as CostKubernetes.rebuiltAt.
rowsrequiredarray of ReconciliationRow
totalsrequiredarray of MoneyThe scope's authoritative net spend, signed; the places, the coverage and the charge types each sum to it.
windowrequiredCostPeriod

CostReportDelivery

A schedule's newest delivery. period is 2026-09 for a month's report and on-demand/2026-10-23 for one asked for. No sentAt and no droppedReason is still owed; attempts counts the tries. droppedReason says why nothing was sent: the schedule or its channel was switched off, or the channel was no longer the team's.

FieldTypeDescription
attemptsrequiredinteger
createdAtrequiredstring (date-time)
droppedReasonstring enum One of schedule_disabled, destination_disabled, destination_not_the_teams.
periodrequiredstring
sentAtstring (date-time)

CostReportDestination

FieldTypeDescription
idrequiredstring (uuid)
kindrequiredstring enum One of email, slack, teams, webhook.
namerequiredstring

CostReportSchedule

FieldTypeDescription
cadencerequiredstring enum One of monthly.
canEditrequiredbooleanWhether the caller may change it — a lead of its team, or an administrator.
channelIdrequiredstring (uuid)
channelKindrequiredstring enum One of email, slack, teams, webhook.
channelNamerequiredstring
createdAtrequiredstring (date-time)
enabledrequiredboolean
idrequiredstring (uuid)
lastDeliveryCostReportDeliveryA schedule's newest delivery. period is 2026-09 for a month's report and on-demand/2026-10-23 for one asked for. No sentAt and no droppedReason is still owed; attempts counts the tries. droppedReason says why nothing was sent: the schedule or its channel was switched off, or the channel was no longer the team's.
teamIdrequiredstring (uuid)
teamNamerequiredstring
updatedAtrequiredstring (date-time)

CostReportScheduleCreate

FieldTypeDescription
channelIdrequiredstring (uuid)
enabledboolean
teamIdrequiredstring (uuid)

CostReportScheduleList

FieldTypeDescription
itemsrequiredarray of CostReportSchedule
teamsrequiredarray of CostReportTeam

CostReportScheduleUpdate

FieldTypeDescription
channelIdstring (uuid)
enabledboolean

CostReportSend

FieldTypeDescription
periodrequiredstring
queuedrequiredbooleanFalse when today's report was already asked for.

CostReportTeam

FieldTypeDescription
channelsrequiredarray of CostReportDestination
idrequiredstring (uuid)
namerequiredstring

CostResource

FieldTypeDescription
accountIdrequiredstring
amountsrequiredarray of MoneySpend in the window, per currency.
inventoryStaterequiredCostInventoryStateWhat the inventory says about the key the spend is against: a live row, a tombstoned one (spend continuing after deletion — the cost-without-resource finding), or nothing at all (missing). One of live, tombstoned, missing.
lastSeenAtstring (date-time)The inventory's last sighting, when it has a row.
mappedbooleanWhether the identifier the source reported mapped to a platform key (§5.4). Unmapped rows are attribution gaps in disguise and are counted in settings.
regionstring
resourceIdstring
resourceKeyrequiredstringThe platform resource key (PLATFORM.md §3), or the raw identifier the source reported when it could not be mapped.
resourceNamestringFrom the inventory. Absent when the inventory has no row.
resourceTypestring
servicerequiredstring
statusstringThe inventory's status word, when it has a row.
systemstring

CostResourcePage

FieldTypeDescription
figurerequiredCostFigure One of amortized, unblended.
filterrequiredstring enum One of all, orphaned, unattributed.
inNoTeamrequiredbooleanTrue when the reader is not an administrator and belongs to no team. A member reads their teams' spend, so for them this read covers nothing: every figure in it is empty because of who is reading, not because nothing was spent, and the portal says so instead of drawing a zero. False for an administrator, who reads the whole organisation whatever their teams, and for a member of a team — one that spent nothing too, whose zero is real. The teamId filter does not change it: it is about the reader, not the question. The anomalies page, the budget list, the recommendations page and the tag-compliance breakdown carry the same field with the same meaning.
itemsrequiredarray of CostResource
pagerequiredinteger
pageSizerequiredinteger
rebuildInProgressrequiredboolean
rebuiltAtstring (date-time)When the rollups these figures come from were last built.
totalrequiredinteger (int64)
totalPagesrequiredinteger
unavailableAccountsrequiredarray of CostResourceUnavailableAccountAccounts with no usable per-resource evidence and why. resource_granularity_unavailable: no source the account is pulled from carries resource-level rows — resource granularity is not enabled in Cost Explorer preferences, and any Cost & Usage Report ingested has no resource IDs. A report with resource IDs is enough on its own. not_yet_ingested: no source has been pulled yet. Documented states, never errors.
windowrequiredCostPeriod
windowDaysrequiredintegerAlways 14 in v1 (§4): Cost Explorer's per-resource window. Rendered beside every per-resource figure, because claiming more than the data supports is how the donors lost trust.

CostResourceUnavailableAccount

FieldTypeDescription
accountIdrequiredstring
reasonrequiredstring enum One of resource_granularity_unavailable, not_yet_ingested.

CostSeries

FieldTypeDescription
keyrequiredstringThe group's identity: a team id, a service name, an account id — or untagged for the unattributed remainder under groupBy=team.
labelrequiredstring
pointsrequiredarray of CostTrendPoint

CostSettings

FieldTypeDescription
allocationRulesrequiredarray of CostAllocationRuleShared-cost allocation rules in the order they are tried.
anomalyrequiredCostAnomalySettings
attributionRulesrequiredarray of CostAttributionRule
curReportsrequiredarray of CostCurReportPer account: where its Cost & Usage Report is, when one is configured.
defaultFigurerequiredCostFigure One of amortized, unblended.
ingestionrequiredarray of CostIngestionState
rebuildEnqueuedbooleanOn the response to a PUT that changed the rules or the tag values.
rebuildInProgressrequiredboolean
teamTagValuesrequiredarray of CostTeamTagValue
unmappedIdentifiersrequiredintegerPer-resource rows whose identifier the key derivation could not map (§5.4), in the current window.

CostSettingsUpdate

FieldTypeDescription
allocationRulesarray of CostAllocationRuleThe whole list, in order. Omitted, the rules are left as they are. Changing them re-attributes history like a rule change (§7.3): the allocation runs inside the rebuild.
anomalyrequiredCostAnomalySettings
attributionRulesrequiredarray of CostAttributionRule
curReportsarray of CostCurReportThe whole set, like the rules: an account absent from the list has its report configuration removed. Omitted, the configuration is left as it is. Changing a report enqueues no rebuild — the next pull replaces the periods it reads, and the rebuild follows the data.
defaultFigurerequiredCostFigure One of amortized, unblended.
teamTagValuesrequiredarray of CostTeamTagValue

CostSummary

FieldTypeDescription
byAttributionrequiredarray of CostAttributionAmountsThe three tiers, each per currency. Their sum is totals, to the cent — the reconciliation invariant (§7.2).
byCloudrequiredarray of CostCloudAmountsThe period's spend split by the cloud each account is in, each cloud per currency and never converted. Per currency, the clouds add up to totals. A cloud with no spend in the period is absent, never a zero.
deltarequiredarray of MoneyDelta
figurerequiredCostFigure One of amortized, unblended.
inNoTeamrequiredbooleanTrue when the reader is not an administrator and belongs to no team. A member reads their teams' spend, so for them this read covers nothing: every figure in it is empty because of who is reading, not because nothing was spent, and the portal says so instead of drawing a zero. False for an administrator, who reads the whole organisation whatever their teams, and for a member of a team — one that spent nothing too, whose zero is real. The teamId filter does not change it: it is about the reader, not the question. The anomalies page, the budget list, the recommendations page and the tag-compliance breakdown carry the same field with the same meaning.
periodrequiredCostPeriod
previousPeriodrequiredCostPeriod
previousTotalsrequiredarray of Money
rebuildInProgressrequiredboolean
rebuiltAtstring (date-time)When the rollups these figures come from were last built.
totalsrequiredarray of MoneyPer currency. A single-currency tenant sees one element.

CostTeamTagValue

FieldTypeDescription
tagValuerequiredstringMatched case-insensitively.
teamIdrequiredstring (uuid)

CostTrend

FieldTypeDescription
figurerequiredCostFigure One of amortized, unblended.
granularityrequiredstring enum One of daily, monthly.
groupByrequiredstring enum One of team, service, account.
inNoTeamrequiredbooleanTrue when the reader is not an administrator and belongs to no team. A member reads their teams' spend, so for them this read covers nothing: every figure in it is empty because of who is reading, not because nothing was spent, and the portal says so instead of drawing a zero. False for an administrator, who reads the whole organisation whatever their teams, and for a member of a team — one that spent nothing too, whose zero is real. The teamId filter does not change it: it is about the reader, not the question. The anomalies page, the budget list, the recommendations page and the tag-compliance breakdown carry the same field with the same meaning.
periodrequiredCostPeriod
rebuildInProgressrequiredboolean
rebuiltAtstring (date-time)When the rollups these figures come from were last built.
seriesrequiredarray of CostSeries

CostTrendPoint

FieldTypeDescription
amountsrequiredarray of MoneyPer currency; empty on a day with no spend.
daterequiredstring (date)The day, or the first of the month for monthly granularity.

Dashboard

The start page (MILESTONE_1 §4.2). Every section beyond attention is optional twice over: absent when the viewer may not have it, and present with unavailable: true when it could not be read in time. The two are different answers and the page says different things about them.

FieldTypeDescription
attentionrequiredarray of AttentionItemWhat needs someone now, ranked; empty is a sentence, not a panel.
changesDashboardChangesThe change feed's newest entries, in the feed's own schema.
collectionDashboardCollectionHow collection is going, in the words /accounts/overview uses for one account, counted across them.
estateDashboardEstateWhat the estate is, by cloud and by category (the taxonomy), from one grouped read so the panels cannot disagree.
firstRunDashboardFirstRunThe first-run checklist on the start page (MILESTONE_1 §7.1, §4.4 "Getting started"): the steps GET /first-run answers, in the same schema. Absent once every step is done or the viewer has dismissed it (§4.2) — it leaves the page by itself — and present with unavailable: true when it could not be read.
generatedAtrequiredstring (date-time)
hottestDashboardHottestThe hottest resources (MILESTONE_1 §4.4, §12.5): the busiest three of each saturation group over the last 24 hours, grouped by category and by defining signal, each with its raw value and unit — GET /performance/hottest?kind=saturation&limit=3's groups. No order across groups. Absent unless the viewer's licence carries Estate and the performance capability; empty groups when nothing is measured.
provisioningDashboardProvisioningWhat is waiting on this person, what is moving, and what has gone wrong lately.
spendDashboardSpendMonth to date against the same point last month, the forecast, and how many budgets are at risk — per currency, never summed across them (COST_SPEC). Team-scoped for a member exactly as /cost/summary is.

DashboardCategoryCount

FieldTypeDescription
categoryrequiredResourceCategoryWhat kind of thing a resource type is (MILESTONE_1 §3). One taxonomy, declared once beside the collector that emits the type, and the thing the catalog, the filters, the Dashboard's blocks and the hottest lists all group by — so none of them carries a list of types of its own. other is the last bucket: a legacy templates.category value outside the set lists under it. Nothing the platform emits is other — every resource type resolves to one of the seven real categories — so a client may show it only where template categories appear, and never when it is empty. PerformanceSection is the same vocabulary, narrowed: a Performance page exists only for a category with measured types. One of compute, networking, databases, messaging, serverless, storage, security_and_identity, other.
degradedrequiredinteger
failedrequiredinteger
operationalrequiredinteger
resourceCountrequiredinteger
unknownrequiredinteger

DashboardChanges

The change feed's newest entries, in the feed's own schema.

FieldTypeDescription
itemsarray of ChangeEvent
unavailableDashboardUnavailableThis section could not be read inside its own deadline, or its repository refused. The rest of the page is unaffected; nothing here is a fault of the viewer's.

DashboardCloudCount

FieldTypeDescription
accountCountrequiredintegerHow many accounts the viewer can see in this cloud — from the registry, not from what they hold. An account connected this morning, one whose collection legitimately found nothing, and one whose last resource was tombstoned are all connected, and all of them are invisible to a count folded out of the inventory. Registered, enabled or not: an account whose collection is paused is still one this tenant has connected. A cloud whose accounts hold nothing has a block of its own, with no resources in it.
degradedrequiredinteger
failedrequiredinteger
operationalrequiredinteger
resourceCountrequiredinteger
systemrequiredstring
unknownrequiredinteger

DashboardCollection

How collection is going, in the words /accounts/overview uses for one account, counted across them.

FieldTypeDescription
errorinteger
noDatainteger
okinteger
slowinteger
unavailableDashboardUnavailableThis section could not be read inside its own deadline, or its repository refused. The rest of the page is unaffected; nothing here is a fault of the viewer's.
worstarray of DashboardWorstAccountThe accounts in the worst state, worst first.

DashboardEstate

What the estate is, by cloud and by category (the taxonomy), from one grouped read so the panels cannot disagree.

FieldTypeDescription
byCategoryarray of DashboardCategoryCount
byCloudarray of DashboardCloudCount
collectedAtstring (date-time)The newest successful collection among the buckets of the accounts this viewer can see, which is what §4.3's empty attention band says beside "nothing needs attention": a page that says nothing is wrong while the inventory is a week old has said nothing useful. Read from the accounts and their fences rather than from the inventory, because a bucket that succeeds with nothing in it still ran — a page that answers "nothing has been collected yet" the minute after a successful empty collection is worse than one that says nothing. Absent means never, and only while this section is present and byCloud names at least one account: a section that is absent or unavailable, or a viewer whose teams hold no account at all, says nothing about freshness either way. That is why this is here rather than on the page.
countsDashboardStatusCountsHow many resources are at each status. A count, not an amount.
unavailableDashboardUnavailableThis section could not be read inside its own deadline, or its repository refused. The rest of the page is unaffected; nothing here is a fault of the viewer's.

DashboardFirstRun

The first-run checklist on the start page (MILESTONE_1 §7.1, §4.4 "Getting started"): the steps GET /first-run answers, in the same schema. Absent once every step is done or the viewer has dismissed it (§4.2) — it leaves the page by itself — and present with unavailable: true when it could not be read.

FieldTypeDescription
stepsarray of FirstRunStep
unavailableDashboardUnavailableThis section could not be read inside its own deadline, or its repository refused. The rest of the page is unaffected; nothing here is a fault of the viewer's.

DashboardForecast

Month-end, as the forecast reads it; absent where there is not enough settled history to say.

FieldTypeDescription
highrequiredstring
lowrequiredstring
midrequiredstring

DashboardHottest

The hottest resources (MILESTONE_1 §4.4, §12.5): the busiest three of each saturation group over the last 24 hours, grouped by category and by defining signal, each with its raw value and unit — GET /performance/hottest?kind=saturation&limit=3's groups. No order across groups. Absent unless the viewer's licence carries Estate and the performance capability; empty groups when nothing is measured.

FieldTypeDescription
fromstring (date-time)
groupsarray of PerformanceHottestGroup
tostring (date-time)
unavailableDashboardUnavailableThis section could not be read inside its own deadline, or its repository refused. The rest of the page is unaffected; nothing here is a fault of the viewer's.
windowPerformanceHottestWindowThe spans hottest reads: the last hour, at the stored five minutes, or the last 24 hours. "Hottest now" is about the present; the charts keep the wider windows. One of 1h, 24h.

DashboardProvisioning

What is waiting on this person, what is moving, and what has gone wrong lately.

FieldTypeDescription
awaitingYouintegerRequests in pending_approval this viewer may decide — the approvals queue's total, their own included exactly when nobody else could approve it.
driftMissinginteger
driftUnprovisionedinteger
failedLast7dinteger
inFlightinteger
linksDashboardProvisioningLinksWhere each count leads: the requests list, filtered by the same parameters the count was taken with, built once on the server so the number and the page it opens cannot disagree. Portal paths; the list page forwards their query to GET /provisioning/requests unchanged. "Awaiting you" is the approvals queue, which asks the list awaitingMyDecision=true and, from the band's expiring item, its expiresBefore bound (MILESTONE_1 §4.3).
unavailableDashboardUnavailableThis section could not be read inside its own deadline, or its repository refused. The rest of the page is unaffected; nothing here is a fault of the viewer's.

DashboardSpend

Month to date against the same point last month, the forecast, and how many budgets are at risk — per currency, never summed across them (COST_SPEC). Team-scoped for a member exactly as /cost/summary is.

FieldTypeDescription
currenciesarray of DashboardSpendCurrency
unavailableDashboardUnavailableThis section could not be read inside its own deadline, or its repository refused. The rest of the page is unaffected; nothing here is a fault of the viewer's.

DashboardSpendCurrency

FieldTypeDescription
budgetsAtRiskrequiredintegerBudgets over 100 % or forecast to exceed.
currencyrequiredstring
forecastDashboardForecastMonth-end, as the forecast reads it; absent where there is not enough settled history to say.
monthToDaterequiredstringA decimal string; money is never a float.
previousToDaterequiredstringThe same day of the previous month, so the comparison is like for like.

DashboardStatusCounts

How many resources are at each status. A count, not an amount.

FieldTypeDescription
degradedrequiredinteger
failedrequiredinteger
operationalrequiredinteger
unknownrequiredinteger

DashboardUnavailable

This section could not be read inside its own deadline, or its repository refused. The rest of the page is unaffected; nothing here is a fault of the viewer's.

Type: boolean

DashboardWorstAccount

FieldTypeDescription
accountIdrequiredstring
lastSuccessAtstring (date-time)
namestring
statusrequiredstring enum One of ok, slow, error, no-data.

DeclaredApp

FieldTypeDescription
archivedAtstring (date-time)
canEditrequiredbooleanWhether the viewer may change it: their platform role clears the engineer floor, they are a lead of the owning team or an admin, and it is not archived. The same two axes as canCreate.
createdAtrequiredstring (date-time)
descriptionstring
excluderequiredarray of ResourceRef
healthrequiredStatusCountsComputed over the full filtered set, not the current page.
idrequiredstring (uuid)
includerequiredarray of ResourceRef
matchesrequiredarray of TagMatch
moneyDeclaredAppMoneyAn app's money for the period, at one grain, named (platform/docs/declared-apps.md §2). resource: the members' own rows from the Cost & Usage Report, exact, when every account-day of the period with money has the report as its evidence source. tag otherwise: the matched tags' attributed amounts, the tag-defined page's figure for each, summed — and overlapResources says how many members hit more than one match (their money may be counted twice) while explicitOutsideTagGrain counts the added resources that carry no matched tag and the excluded ones that do (the tag grain cannot see either). Never both grains in one period.
namerequiredstring
provisionedintegerHow many of the app's live members Provisioning created, over the viewer's accounts. Absent without the module.
slugrequiredstring
teamrequiredTeamRefA team named where only its identity matters — an app's owner, or a team a picker offers. Team is the org model's full row, with the counts the settings page shows; this is the two fields everything else needs.
updatedAtrequiredstring (date-time)

DeclaredAppDefinition

What a lead declares. At least one tag match or one added resource. A resource that is both added and excluded is refused.

FieldTypeDescription
descriptionstring
excludearray of ResourceRefResources kept out although a match hits them.
includearray of ResourceRefResources added by hand.
matchesrequiredarray of TagMatch
namerequiredstring
teamIdrequiredstring (uuid)

DeclaredAppDetail

FieldTypeDescription
apprequiredDeclaredApp
membersrequiredarray of DeclaredAppMember
ownableTeamsrequiredarray of TeamRefThe open teams this viewer may give this app to, by the same rule as the list's — so the editor opened here can move an app between teams, and offers only the moves PUT accepts. Empty when the viewer may not write at all, including a platform viewer who leads a team; app.canEdit is the check for this app as it stands.
trendarray of DeclaredAppTrendMonthSix calendar months ending with the current one, each at its own grain. Absent without Spend.

DeclaredAppList

FieldTypeDescription
appsrequiredarray of DeclaredApp
canCreaterequiredbooleanWhether the viewer may declare an app. Both axes, never one: the platform role must clear the engineer floor every write route carries, and they must be an admin or the lead of an open team. The two are disjoint (PLATFORM.md §4) — a platform viewer can lead a team, and answering on the team rule alone would offer a write the route refuses with 403. Exactly ownableTeams being non-empty: a lead whose only team has been closed may not declare either, because every team the write would accept is gone.
ownableTeamsrequiredarray of TeamRefThe open teams this viewer may give an app to — every one for an admin, the ones they lead otherwise — by name, and empty for a platform viewer whatever their team roles. The editor offers these and nothing else, so nobody is shown a team the write would refuse: not a lead offered another team, not a closed team (409 team_archived), and not a reader offered any. Empty exactly when canCreate is false.
periodrequiredobjectThe month-to-date range spend covers, inclusive.

DeclaredAppMember

FieldTypeDescription
accountIdrequiredstring
lastSeenAtrequiredstring (date-time)
namerequiredstring
regionrequiredstring
resourceIdrequiredstring
resourceTyperequiredstring
servicerequiredstring
statusrequiredStatusThe health of something that currently exists. Derived once at ingest and stored, never recomputed at read time. deleted is absent, and that is the point: a deleted resource is tombstoned, not given a status, so a live resource carrying status: deleted is an impossible value. Keeping one enum for both made that impossible value contract-valid everywhere. unknown is a resource the inventory knows exists, or was recently active, whose health it cannot observe — found only from its metrics or its name. It is neither healthy nor a problem: never green, not on the Alerts page, and counted as not checked on the public page. One of failed, degraded, operational, unknown.
systemrequiredstring
viarequiredstring enumWhy it is in — a tag match, or an explicit addition. One of tag, added.

DeclaredAppMoney

An app's money for the period, at one grain, named (platform/docs/declared-apps.md §2). resource: the members' own rows from the Cost & Usage Report, exact, when every account-day of the period with money has the report as its evidence source. tag otherwise: the matched tags' attributed amounts, the tag-defined page's figure for each, summed — and overlapResources says how many members hit more than one match (their money may be counted twice) while explicitOutsideTagGrain counts the added resources that carry no matched tag and the excluded ones that do (the tag grain cannot see either). Never both grains in one period.

FieldTypeDescription
amountsrequiredarray of Money
explicitOutsideTagGrainrequiredinteger
grainrequiredstring enum One of resource, tag.
overlapResourcesrequiredinteger
sourcerequiredstring enum One of cur, ce, legacy, mixed, none.

DeclaredAppTrendMonth

FieldTypeDescription
moneyrequiredDeclaredAppMoneyAn app's money for the period, at one grain, named (platform/docs/declared-apps.md §2). resource: the members' own rows from the Cost & Usage Report, exact, when every account-day of the period with money has the report as its evidence source. tag otherwise: the matched tags' attributed amounts, the tag-defined page's figure for each, summed — and overlapResources says how many members hit more than one match (their money may be counted twice) while explicitOutsideTagGrain counts the added resources that carry no matched tag and the excluded ones that do (the tag grain cannot see either). Never both grains in one period.
monthrequiredstring (date)The first day of the month.

DiffEntry

One changed path, older to newer. Paths follow the descriptor's field keys: status, state, tags.<key>, additionalInfo.<key>, nested by ., a list element by [i]. A set member's entry sits under the set's own path with the member as old (removed) or new (added). A redacted entry is a field the descriptor marks sensitive: its values are not carried, here or anywhere else.

FieldTypeDescription
kindrequiredstring enum One of added, removed, changed.
newobject
oldobject
pathrequiredstring
redactedboolean

DiscoveryResult

updated and unchanged are separate on purpose. Discovery re-runs routinely, and "did anything actually change?" is the question an operator has — collapsing them into one "already known" count destroys the only interesting part of a repeat run.

FieldTypeDescription
accountsarray of Account
createdrequiredintegerNewly registered, all with enabled = false.
foundrequiredintegerAccounts returned by Organizations.
unchangedrequiredintegerAlready known and identical.
updatedrequiredintegerAlready known, and some field differed.

DriftMissing

Provisioned, tagged, and no longer in the inventory. Severity is the gap, not a computed score: "4 of 7 gone" is what an operator acts on.

Only what the inventory can see is compared: the create's stamped resources of types a registered collector emits and reads the tags of, kind by kind. A create whose agent reported a total alone is never a finding (resource.driftScope.judged false).

FieldTypeDescription
expectedCountrequiredintegerThe stamped resources drift compares, resource.driftScope.compared — not resource.expectedCount, which is everything the create stamped.
gaprequiredintegerexpectedCount - observedCount, always positive here.
lastObservedAtstring (date-time)
observedCountrequiredintegerHow many of those the inventory still sees: live rows in that account, of the compared kinds, whose tags carry panorama:request_id = createRequestId and which are not tombstoned — counted kind by kind, and never more of a kind than the create stamped of it, so a stamped row of one kind (an instance's root volume) covers nothing of another's gap.
resourcerequiredProvisionedResourceWhat a create produced, and the anchor of the provenance badge on the inventory side. A create that stopped part-way has one too, partial, so what it made can be destroyed from the platform. Or, a resource Import recorded (origin: imported, management: recorded): nothing was applied for it, so createRequestId, destroyRequestId, stateBackend, provenanceTagged, overriddenTags, expectedCount, driftScope, status and destroyedAt are absent, id is the import's, teamId is the team it names as owner, createdAt is when it was recorded, and import says which resource it is. Every other row has createRequestId, provenanceTagged, expectedCount and status.

DriftMissingPage

FieldTypeDescription
itemsrequiredarray of DriftMissing
pagerequiredinteger
pageSizerequiredinteger
totalrequiredinteger (int64)
totalPagesrequiredinteger

DriftNotCompared

FieldTypeDescription
countrequiredinteger
reasonrequiredstring enumnot_collected — no registered collector emits the type. tags_not_read — a collector emits it but finds it without its tags, so the stamp cannot be read on it (Lambda functions, found from their metrics). One of not_collected, tags_not_read.
typerequiredstringThe OpenTofu resource type, as the agent reported it (aws_iam_role).

DriftScope

Which of a provisioned resource's stamped resources the drift diff compares with the inventory (PROVISIONING_SPEC §10).

A create's agent reports how many resources it stamped of each OpenTofu type. Drift compares only the types a registered collector emits and reads the tags of — the stamp is a tag — kind by kind, and names every other type here with the reason, rather than calling what it cannot see missing.

FieldTypeDescription
comparedintegerJudged only. The stamped resources drift compares: the baseline a provisioned-but-missing finding measures against. Zero when the inventory can see none of them — a Lambda function, its roles and its log group.
judgedrequiredbooleanFalse when the agent that created it reported how many resources it stamped and not of which types — an agent before 0.3.0. Drift then judges nothing for it: it cannot tell which of them the inventory can see, and "missing" would be a guess.
notComparedarray of DriftNotComparedJudged only. Every stamped type drift does not compare, sorted by type, with how many of it and why. Empty when it compares them all.

DriftUnprovisioned

A live inventory resource of a type this tenant's provisioning has produced before, carrying no panorama:request_id. Somebody made it by hand.

FieldTypeDescription
accountIdrequiredstring
firstSeenAtrequiredstring (date-time)
importIdstring (uuid)The live import that recorded it — only on a list asked with imported=true.
lastSeenAtrequiredstring (date-time)
regionrequiredstring
resourceIdrequiredstring
resourceNamestring
resourceTyperequiredstring
servicerequiredstring
systemrequiredstring

DriftUnprovisionedPage

FieldTypeDescription
itemsrequiredarray of DriftUnprovisioned
pagerequiredinteger
pageSizerequiredinteger
totalrequiredinteger (int64)
totalPagesrequiredinteger

Entitlements

FieldTypeDescription
capabilitiesrequiredarray of stringFeatures the licence carries beside its modules: sso (Enterprise) and performance (Professional and Enterprise); later scim. Locked with the modules at expiry.
enabledAccountCountrequiredinteger
licenseestringWho the licence was issued to, when the row came from a licence.
maxAccountsintegerAbsent means unlimited.
modulesrequiredarray of stringUnlocked modules, now. A tenant with no row has none — unless the deployment runs with ENTITLEMENTS_UNRESTRICTED, the development exception, in which case it has every module this build serves and state says unrestricted.
staterequiredstring enumThe one word the portal turns into a banner. unrestricted: no row, development exception. none: no row, nothing unlocked. licensed: valid. expiring: within thirty days of validUntil, or within the last third of the licence's term (validUntil minus its issue date) when that is shorter — a thirty-day trial warns for its last ten days; a row with no issue date keeps thirty — a banner, not a lock. lapsed: past validUntil; every module is locked and collection continues for fourteen days. paused: past that grace; collection has stopped. Nothing is ever deleted because of a lapse. One of unrestricted, none, licensed, expiring, lapsed, paused.
tierstringThe licence's tier id (essentials, professional, enterprise), when the row came from a licence. An id, never a name: the name is the brand's.
validUntilstring (date-time)Checked at read time, not by a scheduled job: an expired row behaves as unentitled the moment it is read.

ErrorResponse

FieldTypeDescription
errorrequiredobject

FailRunRequest

resourcesAttempted is here as well as on finalize, and that is the whole point of it. A run that finalizes successfully has resources_stored equal to what it sent by construction — ingest is atomic per batch — so the counters can only ever differ on a run that did NOT finish. Reporting the count only at finalize would leave it zero for exactly the failures it exists to describe.

What it cannot detect: a worker that dies without calling either endpoint. That run stays running until cleanup reaps it, with resources_attempted at zero; it is identified by its state and the reaper's error_message, not by the counters.

FieldTypeDescription
reasonrequiredstringWhy the run failed, for the admin matrix. Recorded verbatim, so it must not contain credentials or raw provider payloads. Clients truncate to this length before sending. The server truncates rather than rejecting — uniquely among the body constraints here. This is the call that closes a run against a late batch and a late finalize, and refusing it because an error string ran long would leave the run open for the whole stale-run window over something cosmetic.
resourcesAttemptedrequiredintegerResources the enumeration observed before it failed, or 0 if it never got that far. SET on the run, never added to. The bound is the column's: resources_attempted is a PostgreSQL int, and a larger value would fail inside the transaction as a 500 for what is a malformed request. Out of range is 400 invalid_request.

FieldDescriptor

FieldTypeDescription
badgeMapobjectRaw value to badge tone, for format: badge.
columnsarray of FieldDescriptorColumn descriptors, for format: table.
formatrequiredFieldFormatThe renderer implements exactly these and falls back to json for anything it does not recognise, so a backend that adds a format cannot break the UI. One of text, number, bytes, datetime, relativeTime, boolean, badge, list, table, json, link.
keyrequiredstringA path into the resource, including additionalInfo. prefixes.
labelrequiredstring
unitstring
volatilebooleanPresent and true when the field moves with use rather than configuration (a queue's depth): the configuration diff never carries it, so a change subscription cannot name it.

FieldFormat

The renderer implements exactly these and falls back to json for anything it does not recognise, so a backend that adds a format cannot break the UI.

Type: string enum

FieldGroup

FieldTypeDescription
fieldsrequiredarray of FieldDescriptor
titlerequiredstring

FinalizeRequest

Added 2026-08-19. collection_runs.resources_attempted previously had no writer at all, so the admin matrix would have reported zero forever.

The count belongs here because only the collector knows it: how many resources its enumeration actually saw, before anything was chunked or sent. Divergence from resources_stored then means what is worth knowing — chunks that never landed, because a batch was rejected or the run died mid-send.

It is deliberately not an increment. Finalize is replay-safe, and a counter that grew on every retry would report a run collecting more each time its response was lost.

Reported to /fail as well, because a run that finalizes successfully has resources_stored equal to what it sent by construction: the counters can only differ on a run that did not finish.

FieldTypeDescription
resourcesAttemptedrequiredintegerResources the enumeration observed. SET on the run, never added to, so a replayed finalize leaves it unchanged. The bound is the column's — a PostgreSQL int. Out of range is 400 invalid_request.

FinalizeResponse

FieldTypeDescription
alreadyFinalizedbooleanTrue when this call replayed an earlier finalize.
runIdrequiredstring (uuid)
tombstonedrequiredinteger (int64)Resources this run did not observe and therefore logically deleted. On a replayed finalize this is the stored count, not a fresh one.

Finding

One finding, projected at read time. Its identity is (source, sourceId, scope); scope is the resource key, or account:<id> / team:<id> for a finding about a scope. owner absent is the unowned group. Priority is three components, never one score.

FieldTypeDescription
accountIdstring
appsarray of FindingAppThe live declared apps the finding's resource is in, by the apps' own rule — a matched tag or an added resource, and not excluded — ordered by name. Absent for a finding about an account or a team, or about a key the inventory no longer holds. One resource may be in several apps; an app is not part of what makes a finding change materially.
changedSinceExceptedrequiredbooleanA live exception exists, but the finding changed materially since it was taken.
exceptionFindingException
faultsarray of TagFaultA tag_policy finding's faults, in the policy's key order — the same faults the tag compliance read lists for the resource. Absent for every other source.
firstSeenrequiredstring (date-time)
labelstringWhat the source calls it when the scope alone does not say — a check's name, a create's name, a bucket.
ownerTeamRefA team named where only its identity matters — an app's owner, or a team a picker offers. Team is the org model's full row, with the counts the settings page shows; this is the two fields everything else needs.
priorityrequiredFindingPriority
scoperequiredstring
sourcerequiredFindingSourceWhere a finding comes from, each read from its own table:
  • cost_anomaly — an open spend anomaly; scope its account or team.
  • unknown_status — a live resource whose status is unknown; scope the resource.
  • trusted_advisor — a recommendation Trusted Advisor flags (warning or error); sourceId is <account>/<check>/<resource>, scope the resource where the check names one the inventory maps, else the account; the figure is AWS's estimate and label the check's name.
  • cost_without_resource — spend in the last 14 days on a resource the inventory looks for and no longer has, or never had; scope the key.
  • drift_missing — a provisioned create the inventory sees fewer of than were stamped; sourceId the provisioned resource, scope its team, label its name.
  • drift_unprovisioned — a live resource of a kind provisioning produces that nobody provisioned; scope the resource.
  • stale_bucket — a bucket nothing has collected inside its window; sourceId <account>/<region>/<system>/<service>/<type>, scope the account, label the bucket.
  • tag_policy — a live resource with spend in the last 14 days whose tags the tenant's tag policy is not satisfied by, as the tag compliance read's violations list it; scope the resource, faults what to fix, the figure its spend. A change to the resource's tags is a new occurrence, which no earlier exception covers.
  • action_unverified — a provisioning action whose recorded verdict is unverified: its window passed and what a complete collection saw of its target was never the state the action reaches; sourceId the request, scope the team that asked (the request's own scope), label the request's name, accountId the target's account. Open until a complete collection registered after the action sees the target in that state, or a newer succeeded action of the same kind on the same target speaks for it; the recorded verdict itself never changes.
  • action_not_yet_observable — the same for a verdict of not_yet_observable: the window passed with no complete collection seeing the target at all.
One of cost_anomaly, unknown_status, trusted_advisor, cost_without_resource, drift_missing, drift_unprovisioned, stale_bucket, tag_policy, action_unverified, action_not_yet_observable.
sourceIdrequiredstring

FindingApp

A declared app a finding's resource is in.

FieldTypeDescription
idrequiredstring (uuid)
namerequiredstring

FindingCost

The source's figure, in its own currency.

FieldTypeDescription
amountrequiredstringA decimal string, never a float.
currencyrequiredstring

FindingException

FieldTypeDescription
createdAtrequiredstring (date-time)
createdByrequiredstring (uuid)
expiresAtrequiredstring (date-time)
idrequiredstring (uuid)
reasonrequiredstring
revokedAtstring (date-time)
revokedBystring (uuid)
scoperequiredstring
sourcerequiredFindingSourceWhere a finding comes from, each read from its own table:
  • cost_anomaly — an open spend anomaly; scope its account or team.
  • unknown_status — a live resource whose status is unknown; scope the resource.
  • trusted_advisor — a recommendation Trusted Advisor flags (warning or error); sourceId is <account>/<check>/<resource>, scope the resource where the check names one the inventory maps, else the account; the figure is AWS's estimate and label the check's name.
  • cost_without_resource — spend in the last 14 days on a resource the inventory looks for and no longer has, or never had; scope the key.
  • drift_missing — a provisioned create the inventory sees fewer of than were stamped; sourceId the provisioned resource, scope its team, label its name.
  • drift_unprovisioned — a live resource of a kind provisioning produces that nobody provisioned; scope the resource.
  • stale_bucket — a bucket nothing has collected inside its window; sourceId <account>/<region>/<system>/<service>/<type>, scope the account, label the bucket.
  • tag_policy — a live resource with spend in the last 14 days whose tags the tenant's tag policy is not satisfied by, as the tag compliance read's violations list it; scope the resource, faults what to fix, the figure its spend. A change to the resource's tags is a new occurrence, which no earlier exception covers.
  • action_unverified — a provisioning action whose recorded verdict is unverified: its window passed and what a complete collection saw of its target was never the state the action reaches; sourceId the request, scope the team that asked (the request's own scope), label the request's name, accountId the target's account. Open until a complete collection registered after the action sees the target in that state, or a newer succeeded action of the same kind on the same target speaks for it; the recorded verdict itself never changes.
  • action_not_yet_observable — the same for a verdict of not_yet_observable: the window passed with no complete collection seeing the target at all.
One of cost_anomaly, unknown_status, trusted_advisor, cost_without_resource, drift_missing, drift_unprovisioned, stale_bucket, tag_policy, action_unverified, action_not_yet_observable.
sourceIdrequiredstring

FindingExceptionCreate

FieldTypeDescription
expiresAtrequiredstring (date-time)
reasonrequiredstring
scoperequiredstring
sourcerequiredFindingSourceWhere a finding comes from, each read from its own table:
  • cost_anomaly — an open spend anomaly; scope its account or team.
  • unknown_status — a live resource whose status is unknown; scope the resource.
  • trusted_advisor — a recommendation Trusted Advisor flags (warning or error); sourceId is <account>/<check>/<resource>, scope the resource where the check names one the inventory maps, else the account; the figure is AWS's estimate and label the check's name.
  • cost_without_resource — spend in the last 14 days on a resource the inventory looks for and no longer has, or never had; scope the key.
  • drift_missing — a provisioned create the inventory sees fewer of than were stamped; sourceId the provisioned resource, scope its team, label its name.
  • drift_unprovisioned — a live resource of a kind provisioning produces that nobody provisioned; scope the resource.
  • stale_bucket — a bucket nothing has collected inside its window; sourceId <account>/<region>/<system>/<service>/<type>, scope the account, label the bucket.
  • tag_policy — a live resource with spend in the last 14 days whose tags the tenant's tag policy is not satisfied by, as the tag compliance read's violations list it; scope the resource, faults what to fix, the figure its spend. A change to the resource's tags is a new occurrence, which no earlier exception covers.
  • action_unverified — a provisioning action whose recorded verdict is unverified: its window passed and what a complete collection saw of its target was never the state the action reaches; sourceId the request, scope the team that asked (the request's own scope), label the request's name, accountId the target's account. Open until a complete collection registered after the action sees the target in that state, or a newer succeeded action of the same kind on the same target speaks for it; the recorded verdict itself never changes.
  • action_not_yet_observable — the same for a verdict of not_yet_observable: the window passed with no complete collection seeing the target at all.
One of cost_anomaly, unknown_status, trusted_advisor, cost_without_resource, drift_missing, drift_unprovisioned, stale_bucket, tag_policy, action_unverified, action_not_yet_observable.
sourceIdrequiredstring

FindingGroup

FieldTypeDescription
findingsrequiredarray of Finding
ownerTeamRefA team named where only its identity matters — an app's owner, or a team a picker offers. Team is the org model's full row, with the counts the settings page shows; this is the two fields everything else needs.

FindingList

FieldTypeDescription
exceptedrequiredarray of FindingFindings hidden by a live exception, until it expires or is revoked.
groupsrequiredarray of FindingGroupBy owning team, teams by name; the group without owner (unowned) last.

FindingPriority

FieldTypeDescription
costFindingCostThe source's figure, in its own currency.
exposurerequiredstring enumFrom the collectors' posture fields; unknown when they say nothing. One of public, not_public, unknown.
importancerequiredstring enumin_path when a live load balancer targets the resource; else carries_traffic when a stored traffic signal of the resource (its requests, or a load balancer's connections or flows) was above zero in the 14 days ending today (UTC); otherwise unknown, never unimportant. Sorted in that order. One of in_path, carries_traffic, unknown.

FindingSettings

FieldTypeDescription
isDefaultrequiredboolean
maxExceptionDaysrequiredinteger
updatedAtstring (date-time)
updatedBystring (uuid)

FindingSettingsUpdate

FieldTypeDescription
maxExceptionDaysrequiredinteger

FindingSource

Where a finding comes from, each read from its own table:

  • cost_anomaly — an open spend anomaly; scope its account or team.
  • unknown_status — a live resource whose status is unknown; scope the resource.
  • trusted_advisor — a recommendation Trusted Advisor flags (warning or error); sourceId is <account>/<check>/<resource>, scope the resource where the check names one the inventory maps, else the account; the figure is AWS's estimate and label the check's name.
  • cost_without_resource — spend in the last 14 days on a resource the inventory looks for and no longer has, or never had; scope the key.
  • drift_missing — a provisioned create the inventory sees fewer of than were stamped; sourceId the provisioned resource, scope its team, label its name.
  • drift_unprovisioned — a live resource of a kind provisioning produces that nobody provisioned; scope the resource.
  • stale_bucket — a bucket nothing has collected inside its window; sourceId <account>/<region>/<system>/<service>/<type>, scope the account, label the bucket.
  • tag_policy — a live resource with spend in the last 14 days whose tags the tenant's tag policy is not satisfied by, as the tag compliance read's violations list it; scope the resource, faults what to fix, the figure its spend. A change to the resource's tags is a new occurrence, which no earlier exception covers.
  • action_unverified — a provisioning action whose recorded verdict is unverified: its window passed and what a complete collection saw of its target was never the state the action reaches; sourceId the request, scope the team that asked (the request's own scope), label the request's name, accountId the target's account. Open until a complete collection registered after the action sees the target in that state, or a newer succeeded action of the same kind on the same target speaks for it; the recorded verdict itself never changes.
  • action_not_yet_observable — the same for a verdict of not_yet_observable: the window passed with no complete collection seeing the target at all.

Type: string enum

FirstRun

The setup checklist of one person (MILESTONE_1 §7.1): the steps for their platform role and the tenant's licence, in the checklist's order, done steps included — a step that is done stays, ticked, with when.

FieldTypeDescription
completerequiredbooleanEvery step here is done. The Dashboard stops carrying the checklist then.
stepsrequiredarray of FirstRunStep

FirstRunChange

A change to the person's first-run record alone. Each field sent changes that field, by the field's own rule; a field left out keeps what is recorded. It never touches the display preferences or their confirmation, which only a preferences save changes: the checklist's Dismiss, and the tour's version, change the checklist and nothing a tab might hold stale.

FieldTypeDescription
checklistDismissedbooleantrue dismisses the checklist: the server records its own time, and a dismissal already recorded keeps its time. false brings the checklist back.
tourSeenVersionintegerThe version of the tour (§7.2) the person has now seen, as the portal numbers it. The server never lowers it: a version above the one recorded replaces it, and one at or below it — sent by a tab an earlier release loaded, or by one that read the record before another tab raised it — keeps the one recorded, which the answer carries.

FirstRunStep

One step of the checklist. done is recorded state, read at request time from the table that holds it; nothing a person clicks here can set it.

FieldTypeDescription
donerequiredboolean
doneAtstring (date-time)When the record that makes the step true was made, the earliest where several do; absent while the step is not done. It is read from the state, so it moves when the state does — a budget deleted and another made, an account disabled. Per step: when the account was registered; when the earliest-connected account that has read with every collector finished those reads, from the run history, which is kept a week by default (CLEANUP_RUN_RETENTION) — after that, the oldest read on record; when the second active user was added; when the account was given to its team; when the channel's test was delivered; when the budget was made; when the agent's credential was issued (the platform keeps an agent's latest call, not its first); when the version was resolved; when the request was made; when the viewer joined the team; the first save of the preferences.
idrequiredstring enumThe step, as MILESTONE_1 §7.1's table lists them. "For" is the platform roles a step is for — those roles exactly, not a floor — and "Needs" the modules of which the licence must serve one.
Id Done when For Needs
connect_account an enabled account exists admin Estate
first_read an enabled account has a successful read on record of every collector of its cloud admin Estate
invite_teammate a second active user exists admin —
create_team a team holds an account admin Spend or Provisioning
alert_channel an enabled notification channel's last test was delivered admin Estate
set_budget a budget exists admin Spend
install_agent an agent that is not revoked has registered, for any account admin Provisioning
enable_template the catalog has a requestable version: resolved, with a variables schema, of a template that is not archived admin Provisioning
first_request the viewer has made a create or destroy request engineer Provisioning
join_team the viewer is a member of a team engineer, viewer Spend or Provisioning
confirm_preferences the viewer has saved their preferences once (preferencesConfirmedAt); keeping the browser's own counts all —
One of connect_account, first_read, invite_teammate, create_team, alert_channel, set_budget, install_agent, enable_template, first_request, join_team, confirm_preferences.
linkrequiredstringThe portal page that does the step.

GeneratedTemplateCopy

What a generated configuration is published to the catalog as: the new template's slug, name, category and description.

FieldTypeDescription
categoryrequiredTemplateCategoryWhat a template creates, in the one taxonomy (MILESTONE_1 §3). The same vocabulary as ResourceCategory without other: that bucket is where a value written before the set existed lists, not something a new template may declare. A template stored with a category outside this set — written before the taxonomy, or by an older client — is read back as other; nothing rewrites the row, and category=other on the catalog finds exactly those. One of compute, networking, databases, messaging, serverless, storage, security_and_identity.
descriptionstring
namerequiredstring
slugrequiredstringThe new template's slug. A slug that names one of the Library's runners is refused (400): a git or inline template runs as the runner its slug names, so a Library template's slug, a slug beginning generated-, negative-control, and a slug on one of their Google service accounts are reserved.

HealthStatus

FieldTypeDescription
statusrequiredstring

IdentityProvider

FieldTypeDescription
boundAccountsrequiredintegerAccounts bound to a provider's subject. While any is bound to another issuer, saving this one is refused.
clientIdrequiredstring
clientSecretSetrequiredbooleanThe secret is stored and never returned.
defaultRolerequiredstring enum One of viewer, engineer, admin.
emailDomainsrequiredarray of string
enforcedrequiredboolean
groupsClaimrequiredstringThe ID-token claim the person's groups are read from at sign-in; groups unless named.
issuerrequiredstring
redirectUrirequiredstringThe callback address to register at the provider, for this host.
updatedAtrequiredstring (date-time)

IdentityProviderInput

FieldTypeDescription
clientIdrequiredstring
clientSecretstringRequired the first time; absent keeps the stored one.
defaultRolerequiredstring enum One of viewer, engineer, admin.
emailDomainsrequiredarray of string
enforcedrequiredboolean
groupsClaimstringThe claim the groups are read from; empty or absent keeps groups.
issuerrequiredstring

Impact

FieldTypeDescription
anchorrequiredobject
limitsrequiredobjectRetention and resolution, per panel.
methodrequiredstringThe page's one sentence of method; correlation in time is not a proven cause.
nodesrequiredarray of ImpactNodeThe anchor first (depth 0), then the affected neighbours, then the rest.
topologyrequiredobject
windowrequiredobject

ImpactNode

FieldTypeDescription
affectedrequiredbooleanA transition or a deviation inside the window — changed in the window, nothing more.
costDaysrequiredobject
depthrequiredinteger
performancerequiredobject
reasonsrequiredarray of string
relationsrequiredarray of object
resourcerequiredImpactResourceA resource as the impact page names it, the same fields the neighbours read gives.
statusrequiredobject

ImpactResource

A resource as the impact page names it, the same fields the neighbours read gives.

FieldTypeDescription
accountIdrequiredstring
namerequiredstring
regionrequiredstring
resourceIdrequiredstring
resourceTyperequiredstring
servicerequiredstring
statusrequiredstring
systemrequiredstring

ImportDeclaration

A template version's declaration that it can take over an existing resource (MILESTONE_1 §9.4): the one resource type it imports, every address it imports with the identifier each derives from that one resource, and the variables the resource's inventory record fills in. Identifiers and pre-fills are evaluated on the server; the agent is handed finished identifiers.

FieldTypeDescription
addressesrequiredarray of object
prefillarray of object
resourceTyperequiredstring

ImportGeneration

What generating a template for one resource would import: the addresses and identifiers a generate step would be handed, the gaps of the companion table, the agent version that generates and whether an agent of the account can now.

FieldTypeDescription
agentReadyrequiredbooleanAn enabled agent of the resource's account advertises generating, which it does only when it runs inline sources. False: an adopt of a generated import waits, queued, until one does.
agentVersionrequiredstringThe first agent version that generates a template.
gapsrequiredarray of stringWhat the companion table does not cover for this type, in a reader's words.
importsrequiredarray of AgentImportThe resource first, then its known companions; an optional one is left out when the cloud has no such object.

ImportPrefill

What the inventory knows for an adopt (MILESTONE_1 §9.4): the variables the resource's record fills in, for the requester to confirm in the ordinary generated form, and the identifier each declared address would be imported by. A value the record lacks is left out for the requester to fill.

FieldTypeDescription
importsrequiredarray of AgentImport
variablesrequiredobject

InboxEntry

FieldTypeDescription
createdAtrequiredstring (date-time)
headerrequiredstring
idrequiredinteger (int64)
kindrequiredstringThe notification's kind, as a webhook receives it — estate.status_transition, cost.anomaly, provisioning.task_failed.
linesrequiredarray of string
readAtstring (date-time)When the person marked it read; absent while unread.

InboxPage

FieldTypeDescription
itemsrequiredarray of InboxEntry
morerequiredbooleanWhether older entries follow this page.
unreadrequiredintegerHow many of the person's entries are unread, across every page.

InboxRead

FieldTypeDescription
allbooleanEvery entry of the person's, whatever ids says.
idsarray of integer (int64)

InboxUnread

FieldTypeDescription
unreadrequiredinteger

Investigation

The investigator's answer for one symptom.

FieldTypeDescription
candidatesrequiredarray of InvestigationCandidate
methodrequiredstringThe page's one sentence of method; a candidate is correlated in time, not a proven cause.
missingrequiredarray of objectWhat the investigation lacks, each a row.
symptomrequiredobject
topologyrequiredobjectHow the neighbours were read, as the impact page says it.
windowrequiredobject

InvestigationCandidate

One change that may explain the symptom. id is the feed row's (<kind>:<source id>); lead is seconds before the symptom, negative when reported after it inside the skew.

FieldTypeDescription
actorActorRefWho did something, for display and for audit. Not User: that carries the tenant and the role of the session, and an audit record naming a role that has since changed would be a lie about the past.
atrequiredstring (date-time)
attributedrequiredbooleanA person is recorded behind the change.
confidencerequiredstringIn words: how the candidate is tied to the symptom's resource.
evidencerequiredarray of objectEvery row the candidate rests on; a finding may cite nothing else. An attribution row is a configuration change's CloudTrail lookup: label its summary, detail the ChangeAttribution.
idrequiredstring
kindrequiredChangeKindOne source's kind of event in the change feed. One of config_change, status_transition, acknowledgement, provisioning_apply, provisioning_destroy, provisioning_partial, cost_anomaly, budget_threshold, external_marker, ownership_change, ai_review, provisioning_import, instance_lifecycle.
leadrequiredinteger
rankrequiredinteger
scorerequirednumber
summaryrequiredstringThe feed row's line. Never why.
whenrequiredstringIn words: "4 minutes before".
whererequiredobjectWhere the change happened relative to the symptom's resource.
wouldRaiserequiredarray of string

LibraryBlueprint

A Blueprint the platform's library carries (MILESTONE_1 §11.1, §8.1). graph is blueprint.json as the library writes it: a version's document whose steps name {"library": <template slug>, "sha256": <hash>} as their template; enabling writes each as the tenant's template version.

FieldTypeDescription
enabledBlueprintA catalog object whose versions are graphs of template steps (MILESTONE_1 §11.1). Created by an administrator; archived, never deleted.
graphrequiredobject
readmerequiredstringThe Blueprint's README, Markdown.
sha256requiredstringIts identity, over its three files.
slugrequiredstring
summaryrequiredstring
templatesrequiredarray of LibraryBlueprintPinThe library templates its steps pin, each once, in step order.
titlerequiredstring
updateAvailablebooleanPresent with enabled: the enabled Blueprint is not archived and its latest version was not published from this library Blueprint's sha256 — updateProvisioningLibraryBlueprint publishes it as the next version.

LibraryBlueprintPage

FieldTypeDescription
itemsrequiredarray of LibraryBlueprint
pagerequiredinteger
pageSizerequiredinteger
totalrequiredinteger (int64)
totalPagesrequiredinteger

LibraryBlueprintPin

FieldTypeDescription
sha256requiredstring
slugrequiredstring

LibraryClone

FieldTypeDescription
namestringIts name, when not the library's title.
slugrequiredstringThe new template's slug. A slug that names one of the Library's runners is refused (400): a git or inline template runs as the runner its slug names, so a Library template's slug, a slug beginning generated-, negative-control, and a slug on one of their Google service accounts are reserved.

LibraryEnable

FieldTypeDescription
slugstringThe template's slug, when not the library's. Another slug is held to the rule a created template's is: one that names one of the Library's runners — another Library template's slug, a slug beginning generated-, negative-control, or a slug on one of their Google service accounts — is refused (400).

LibraryEnabled

The tenant's template enabled from this library slug, and its latest version — which may pin an older hash than the library's.

FieldTypeDescription
agentVersionrequiredstringThe first agent version carrying release, or empty when this platform's library no longer names the release.
archivedAtstring (date-time)
releaserequiredinteger
runnableAccountsrequiredarray of stringThe enabled accounts in this version's cloud — its hash's meta.json's, which may not be the library's newer version's — whose agent can run it.
sha256requiredstring
templateIdrequiredstring (uuid)
templateSlugrequiredstring
versionrequiredinteger

LibraryFileDiff

FieldTypeDescription
pathrequiredstring
unifiedrequiredstringA unified diff, three lines of context.

LibraryProvenance

How a library template's resources carry the provenance stamp: the provider's default tags on AWS, labels on Google (MILESTONE_1 §10.2), or neither for a resource that can carry none.

Type: string enum

LibraryTemplate

One library template as the library stands at the platform's latest release: meta.json's fields, the version the library carries, where it can run, and the tenant's own state.

FieldTypeDescription
agentVersionrequiredstringThe first agent version that carries release: what an account's agent needs to run this version.
categoryrequiredTemplateCategoryWhat a template creates, in the one taxonomy (MILESTONE_1 §3). The same vocabulary as ResourceCategory without other: that bucket is where a value written before the set existed lists, not something a new template may declare. A template stored with a category outside this set — written before the taxonomy, or by an older client — is read back as other; nothing rewrites the row, and category=other on the catalog finds exactly those. One of compute, networking, databases, messaging, serverless, storage, security_and_identity.
cloudrequiredTemplateCloudThe cloud a template makes its resources in, as the account registry names it (Account.system): a library template's (MILESTONE_1 §8.1) or one of the tenant's own. azure is a library template's, one Import generated for an Azure resource, and — since then slice 6 — one of the tenant's own and a copy published to the catalog. A request for a template of the tenant's own in an Azure account waits for an agent whose Azure runners are scoped to resource groups (capability azure-resource-groups). One of aws, gcp, azure.
costNoterequiredstring
enabledLibraryEnabledThe tenant's template enabled from this library slug, and its latest version — which may pin an older hash than the library's.
iamrequiredarray of stringThe least-privilege statements the agent's role needs (§8.2.7).
importablerequiredbooleanWhether the template declares how an existing resource is brought under it — an import in its meta.json (MILESTONE_1 §9.4).
provenancerequiredLibraryProvenanceHow a library template's resources carry the provenance stamp: the provider's default tags on AWS, labels on Google (MILESTONE_1 §10.2), or neither for a resource that can carry none. One of tags, labels, none.
providersrequiredobjectProvider name to version constraint.
releaserequiredintegerThe first library release that carried sha256.
resourceTypesrequiredarray of stringWhat it creates, <cloud>.<service>.<type>.
retiredrequiredbooleanThe library no longer maintains it. An enabled one stays enabled and runnable; it is not enabled anew.
retiredInintegerThe release that retired it.
runnableAccountsrequiredarray of stringThe tenant's enabled accounts in the template's cloud whose agent last reported a library release that holds sha256. An account of another cloud is never listed, whatever its agent holds.
sha256requiredstringThe version the library now carries — its identity. An update names it as the publish's librarySha256.
slugrequiredstring
summaryrequiredstring
titlerequiredstring
updateAvailablebooleanAn administrator's notice, present for administrators only: the tenant enabled this template and its latest version pins another hash than the one the library now carries.

LibraryTemplateDetail

FieldTypeDescription
agentVersionrequiredstringThe first agent version that carries release: what an account's agent needs to run this version.
categoryrequiredTemplateCategoryWhat a template creates, in the one taxonomy (MILESTONE_1 §3). The same vocabulary as ResourceCategory without other: that bucket is where a value written before the set existed lists, not something a new template may declare. A template stored with a category outside this set — written before the taxonomy, or by an older client — is read back as other; nothing rewrites the row, and category=other on the catalog finds exactly those. One of compute, networking, databases, messaging, serverless, storage, security_and_identity.
cloudrequiredTemplateCloudThe cloud a template makes its resources in, as the account registry names it (Account.system): a library template's (MILESTONE_1 §8.1) or one of the tenant's own. azure is a library template's, one Import generated for an Azure resource, and — since then slice 6 — one of the tenant's own and a copy published to the catalog. A request for a template of the tenant's own in an Azure account waits for an agent whose Azure runners are scoped to resource groups (capability azure-resource-groups). One of aws, gcp, azure.
costNoterequiredstring
diffarray of LibraryFileDiffFor an administrator, when updateAvailable: what changed from the tenant's latest version to the library's, one unified diff per file that differs. Absent when this platform's library no longer holds the tenant's version, so the change cannot be shown; the portal publishes an update only once its diff has been read and shown, naming the sha256 it showed (round 3).
enabledLibraryEnabledThe tenant's template enabled from this library slug, and its latest version — which may pin an older hash than the library's.
iamrequiredarray of stringThe least-privilege statements the agent's role needs (§8.2.7).
importablerequiredbooleanWhether the template declares how an existing resource is brought under it — an import in its meta.json (MILESTONE_1 §9.4).
provenancerequiredLibraryProvenanceHow a library template's resources carry the provenance stamp: the provider's default tags on AWS, labels on Google (MILESTONE_1 §10.2), or neither for a resource that can carry none. One of tags, labels, none.
providersrequiredobjectProvider name to version constraint.
readmerequiredstringThe template's README, Markdown.
releaserequiredintegerThe first library release that carried sha256.
resourceTypesrequiredarray of stringWhat it creates, <cloud>.<service>.<type>.
retiredrequiredbooleanThe library no longer maintains it. An enabled one stays enabled and runnable; it is not enabled anew.
retiredInintegerThe release that retired it.
runnableAccountsrequiredarray of stringThe tenant's enabled accounts in the template's cloud whose agent last reported a library release that holds sha256. An account of another cloud is never listed, whatever its agent holds.
sha256requiredstringThe version the library now carries — its identity. An update names it as the publish's librarySha256.
slugrequiredstring
sourcerequiredstringThe library's main.tf for sha256.
summaryrequiredstring
titlerequiredstring
updateAvailablebooleanAn administrator's notice, present for administrators only: the tenant enabled this template and its latest version pins another hash than the one the library now carries.
variablesSchemarequiredVariablesSchemaStored on the version and immutable with it. The portal generates the request form from this; the agent uses it to write generated.auto.tfvars as proper HCL through an encoder, never by string concatenation, because hand-rolled escaping is how tfvars injection happens.

LibraryTemplatePage

FieldTypeDescription
itemsrequiredarray of LibraryTemplate
pagerequiredinteger
pageSizerequiredinteger
releaserequiredintegerThe newest library release this platform carries; 0 before the first.
totalrequiredinteger (int64)
totalPagesrequiredinteger

LoginRequest

FieldTypeDescription
emailrequiredstringThe longest address RFC 5321 permits. Deliberately NOT format: email. Syntax beyond "not blank" is not validated — an address that an over-strict pattern rejects is a customer who cannot log in — and declaring a format the server does not enforce is a contract that lies to whoever generates a client from it.
passwordrequiredstring (password)Bounded because verifying it is an argon2 derivation over whatever was sent, and this endpoint is unauthenticated.
tenantSlugstringOptional. Needed only when the request host does not identify a tenant, because an email address alone does not identify a user. A DNS label, because it is a subdomain: acme is acme.cloudpanorama.example. Matched case-insensitively.

MarkerCredential

A marker credential, everything but the token.

FieldTypeDescription
accountIdsrequiredarray of stringThe accounts it may mark; empty is any.
appIdsrequiredarray of string (uuid)The declared apps it may mark; empty is any.
createdAtrequiredstring (date-time)
idrequiredstring (uuid)
lastUsedAtstring (date-time)
namerequiredstring
revokedAtstring (date-time)

MarkerCredentialCreate

FieldTypeDescription
accountIdsarray of string
appIdsarray of string (uuid)
namerequiredstringThe credential's name, as the Teams & access page lists it. Whitespace at either end is removed before it is stored, and what remains must not be empty: a name that is empty or only whitespace is 400 invalid_request, "name: must not be blank".

MarkerCredentialIssued

FieldTypeDescription
credentialrequiredMarkerCredentialA marker credential, everything but the token.
tokenrequiredstring (password)Returned once and never again: only sha256(token) is stored.

MarkerCredentialList

FieldTypeDescription
itemsrequiredarray of MarkerCredential

MatrixEntry

One cell per (account, region, module) — the unit the scheduler actually runs. A cell per (account, module) would let one healthy region hide a failed or never-collected one, which is precisely the silent gap this grid exists to expose.

FieldTypeDescription
accountIdrequiredstring
durationMsinteger (int64)
errorMessagestring
lastRunAtstring (date-time)
modulerequiredstring
regionrequiredstring
resourcesAttemptedinteger (int64)
resourcesStoredinteger (int64)
statusrequiredstring enumslow is duration greater than three times the trailing ten-run average. no-data is no run within three cadence intervals — the state a silently dead collector produces. One of ok, error, slow, no-data.

Money

One amount in one currency. The only way money appears in this document. TestAllMoneyIsCurrencyQualified walks every response schema and fails on any amount-shaped property that is not inside this object or an array of it, because "a scalar total" is the one contract mistake that cannot be retrofitted once a client has summed two currencies (C7).

amount is a decimal as a string, never a JSON number: numeric(20,10) does not survive a float, and a total that reconciles to the cent is the product.

FieldTypeDescription
amountrequiredstring
currencyrequiredstring

MoneyDelta

The change between two periods, per currency. pct is absent when the previous period had no spend in this currency — a change from zero is not a percentage.

FieldTypeDescription
amountrequiredstringCurrent minus previous.
currencyrequiredstring
pctnumberPercentage change, when the previous period is non-zero.

NeighbourEdge

FieldTypeDescription
categoryrequiredstring enumImpact reads only the operational edges. One of structural, operational.
confidencerequiredstring enum One of observed, declared, inferred.
evidencerequiredobjectWhat the edge rests on: the collector field, the tag, the request, the app's rule, the resolver's candidate.
fromrequiredstring
kindrequiredNeighbourEdgeKindOne kind of relationship. One of contains, runs_on, fronts, guards, provisioned_by, belongs_to, owned_by.
torequiredstring

NeighbourEdgeKind

One kind of relationship.

Type: string enum

NeighbourNode

One thing in the graph. id is stable across reads: a resource's account and key, or a request's, template version's, app's or team's id.

FieldTypeDescription
depthrequiredintegerEdges from the root: 0 the root, 1 a neighbour, 2 a neighbour's neighbour.
detailrequiredobjectThe node's own fields: a request's state, a template version's source identity, an app's or team's slug.
idrequiredstring
kindrequiredstring enum One of resource, request, template_version, app, team.
labelrequiredstring
resourceobjectThe resource behind a resource node.

NotReadyStatus

FieldTypeDescription
reasonrequiredstringNames the cause without naming a credential, host or database role.
statusrequiredstring

NotificationChannel

A destination, redacted. The target is never here; targetHost, targetHint and targetFingerprint are what an administrator uses to recognise a channel and to see that a rotation changed it.

FieldTypeDescription
createdAtrequiredstring (date-time)
enabledrequiredboolean
idrequiredstring (uuid)
kindrequiredNotificationChannelKindslack posts {"text": …} to an incoming webhook; webhook posts the message as JSON with its kind, for a receiver that routes on it; teams posts an Adaptive Card, within Teams' size limit, to a Microsoft Teams Workflows HTTP trigger URL — under the same destination policy as a webhook; pagerduty sends Events API v2 events — trigger on a failed or degraded resource or a budget threshold, acknowledge on an acknowledgement, resolve on recovery — one incident per tenant, kind and resource; its target is the integration's 32-character routing key, and the endpoint is PagerDuty's; email sends each message as text and HTML to its target, a comma-separated list of at most twenty plain addresses, through the operator's SMTP relay — never a relay the tenant names; refused when the deployment has none; ai-reviewer hands every item to the operator's AI reviewer queue as a small claim-check message — its target is the deployment's queue, set by the platform, never given by the tenant. One of slack, webhook, ai-reviewer, teams, pagerduty, email.
lastTestNotificationChannelTestThe most recent test delivery, if any since the last rotation.
namerequiredstring
targetFingerprintrequiredstringsha256 of the target, hex. Two channels with the same fingerprint point at the same place.
targetHintrequiredstringThe last few characters of the destination's path, for recognition; empty for a row that predates the surface.
targetHostrequiredstringThe destination's host.
teamIdstring (uuid)The team whose events this channel receives; absent for the tenant default.
teamNamestring
updatedAtrequiredstring (date-time)

NotificationChannelCreate

FieldTypeDescription
enabledboolean
kindrequiredNotificationChannelKindslack posts {"text": …} to an incoming webhook; webhook posts the message as JSON with its kind, for a receiver that routes on it; teams posts an Adaptive Card, within Teams' size limit, to a Microsoft Teams Workflows HTTP trigger URL — under the same destination policy as a webhook; pagerduty sends Events API v2 events — trigger on a failed or degraded resource or a budget threshold, acknowledge on an acknowledgement, resolve on recovery — one incident per tenant, kind and resource; its target is the integration's 32-character routing key, and the endpoint is PagerDuty's; email sends each message as text and HTML to its target, a comma-separated list of at most twenty plain addresses, through the operator's SMTP relay — never a relay the tenant names; refused when the deployment has none; ai-reviewer hands every item to the operator's AI reviewer queue as a small claim-check message — its target is the deployment's queue, set by the platform, never given by the tenant. One of slack, webhook, ai-reviewer, teams, pagerduty, email.
namerequiredstring
targetrequiredstringAn https URL this deployment will post to. Never read back.
teamIdstring (uuid)Scope the channel to a team; omit for the tenant default.

NotificationChannelKind

slack posts {"text": …} to an incoming webhook; webhook posts the message as JSON with its kind, for a receiver that routes on it; teams posts an Adaptive Card, within Teams' size limit, to a Microsoft Teams Workflows HTTP trigger URL — under the same destination policy as a webhook; pagerduty sends Events API v2 events — trigger on a failed or degraded resource or a budget threshold, acknowledge on an acknowledgement, resolve on recovery — one incident per tenant, kind and resource; its target is the integration's 32-character routing key, and the endpoint is PagerDuty's; email sends each message as text and HTML to its target, a comma-separated list of at most twenty plain addresses, through the operator's SMTP relay — never a relay the tenant names; refused when the deployment has none; ai-reviewer hands every item to the operator's AI reviewer queue as a small claim-check message — its target is the deployment's queue, set by the platform, never given by the tenant.

Type: string enum

NotificationChannelList

FieldTypeDescription
itemsrequiredarray of NotificationChannel

NotificationChannelTest

FieldTypeDescription
atrequiredstring (date-time)
errorClassstring enumWhy it did not deliver, without the destination's words or its URL. One of dns, tls, timeout, network, redirect, private_address, invalid_target, refused.
httpStatusintegerThe destination's status code, when it answered.
outcomerequiredstring enum One of delivered, refused, unreachable, blocked.

NotificationChannelUpdate

FieldTypeDescription
enabledboolean
namestring
targetstringA new target — the rotation. Checked like a new one; the last test is cleared.
teamIdstring (uuid)Move the channel to a team.
tenantDefaultbooleantrue takes the channel out of its team and makes it a tenant default.

NotificationChoices

What reaches the person's own inbox.

FieldTypeDescription
alertsrequiredbooleanStatus alerts on the accounts the person may see — their teams', or every account for an administrator. An account no team owns reaches the administrators alone.
anomaliesrequiredbooleanCost anomalies on the person's teams and their teams' accounts; every one, for an administrator.
budgetsrequiredbooleanBudget thresholds of the person's teams; every team's, for an administrator.
digestrequiredbooleanThe weekly digest of each of the person's own teams: the team's spend, budgets, anomalies and alerts, with links back. Only their teams' — an administrator's too — and only while they are in the team. Sent only where the operator has set PORTAL_URL and the organisation has Spend.
myRequestsrequiredbooleanTheir own provisioning requests — approvals, successes and failures.

OnboardingStep

FieldTypeDescription
commandstringThe exact command, where the step has one.
detailrequiredstring
titlerequiredstring

OrgUser

FieldTypeDescription
breakGlassbooleanAn administrator who keeps a password under enforced single sign-on.
createdAtrequiredstring (date-time)
disabledAtstring (date-time)When the account was deactivated; absent while it is active. A deactivated user cannot log in and holds no session.
displayNamestringThe name the person gave themselves; absent while they gave none.
emailrequiredstring
idrequiredstring (uuid)
rolerequiredstring enumThe platform role. Says nothing about teams. One of viewer, engineer, admin.
teamsrequiredarray of TeamMembership

OrgUserCreate

FieldTypeDescription
emailrequiredstring
rolerequiredstring enum One of viewer, engineer, admin.

OrgUserCreated

FieldTypeDescription
temporaryPasswordrequiredstringShown once. Not stored in plaintext, not logged, not shown again: a lost one is replaced with a password reset.
userrequiredOrgUser

OrgUserList

FieldTypeDescription
itemsrequiredarray of OrgUser

OrgUserUpdate

FieldTypeDescription
breakGlassbooleanAn administrator who keeps a password when the tenant enforces single sign-on. Only an administrator can be one.
disabledbooleantrue deactivates the account; false reactivates it.
rolestring enum One of viewer, engineer, admin.

OwnAPITokenList

FieldTypeDescription
itemsrequiredarray of APIToken
maxExpiresInDaysrequiredintegerThe longest life, in days, the organisation allows a new read token.

OwnSession

FieldTypeDescription
addressstringThe address the session began from, when it was recorded.
createdAtrequiredstring (date-time)
currentrequiredbooleanWhether this is the session making the request.
expiresAtrequiredstring (date-time)
idrequiredstring
lastSeenAtrequiredstring (date-time)
userAgentstringThe browser the session began in, when it was recorded.

OwnSessionList

FieldTypeDescription
itemsrequiredarray of OwnSession

OwnTeam

FieldTypeDescription
idrequiredstring (uuid)
namerequiredstring
rolerequiredstring enumThe person's role in the team. One of member, lead.
slugrequiredstring

OwnTeamList

FieldTypeDescription
itemsrequiredarray of OwnTeam

Ownership

Who owns the resource, from the one resolver, derived when the row is read. state is the one-word answer; team, confidence, source and evidence are the winning tier's; reason says why there is no owner although a tier answered — a conflict between two matches of equal reach, or an archived team; tried lists the tiers that said nothing; candidates is every tier's answer, best first — the evidence one click away, and the discrepancy when a lower tier disagrees. Ownership explains and proposes: it never widens what a member sees, and never touches attribution.

import (MILESTONE_1 §9.2) is a recorded import's owning team: explicit, as a declared owner is, and second only to a resource override — the correction a person makes stays the last word. Its evidence names the import and the template version.

FieldTypeDescription
candidatesrequiredarray of OwnershipCandidate
confidencerequiredstring enum One of explicit, derived, inferred, unknown.
evidenceobject
reasonstring
sourcerequiredstring enum One of override, import, provenance, tag, account, relationship, none.
staterequiredstring enum One of owned, unowned, conflict.
teamTeamRefA team named where only its identity matters — an app's owner, or a team a picker offers. Team is the org model's full row, with the counts the settings page shows; this is the two fields everything else needs.
triedrequiredarray of string enum

OwnershipCandidate

FieldTypeDescription
archivedboolean
confidencerequiredstring enum One of explicit, derived, inferred.
evidenceobject
rankrequiredinteger
sourcerequiredstring enum One of override, import, provenance, tag, account, relationship.
teamrequiredTeamRefA team named where only its identity matters — an app's owner, or a team a picker offers. Team is the org model's full row, with the counts the settings page shows; this is the two fields everything else needs.

OwnershipOverride

An explicit owner for one resource, for every resource carrying a tag value, or for every resource of an account; with who set it, why and until when. A revoked override keeps its row, marked.

FieldTypeDescription
accountIdstring
createdAtrequiredstring (date-time)
createdByrequiredstring (uuid)
expiredbooleanPast its expiry: listed, but no longer applying.
expiresAtstring (date-time)
idrequiredstring (uuid)
kindrequiredstring enum One of resource, tag, account.
reasonrequiredstring
resourceKeystringregion/system/service/type/id, for a resource override.
revokedAtstring (date-time)
revokedBystring (uuid)
tagKeystring
tagValuestring
teamrequiredTeamRefA team named where only its identity matters — an app's owner, or a team a picker offers. Team is the org model's full row, with the counts the settings page shows; this is the two fields everything else needs.

OwnershipOverrideInput

FieldTypeDescription
accountIdstring
expiresAtstring (date-time)
kindrequiredstring enum One of resource, tag, account.
reasonrequiredstring
resourceKeystring
tagKeystring
tagValuestring
teamIdrequiredstring (uuid)

OwnershipOverrideList

FieldTypeDescription
itemsrequiredarray of OwnershipOverride

PageInfo

FieldTypeDescription
pagerequiredinteger
pageSizerequiredinteger
totalrequiredinteger (int64)
totalPagesrequiredinteger

PasswordChange

FieldTypeDescription
currentPasswordrequiredstring
newPasswordrequiredstring

PasswordResetIssued

FieldTypeDescription
expiresAtrequiredstring (date-time)
tokenrequiredstringShown once; only its hash is stored.

PasswordResetRedeem

FieldTypeDescription
newPasswordrequiredstring
tokenrequiredstring

PerformanceAbsentReasons

Why a kind in absentKinds is missing, where the catalogue knows more than that the type publishes no such metric: a CloudFront distribution's origin latency is an additional metric the account turns on and pays for. A kind without a reason here has none to give.

FieldTypeDescription
errorsstring
latencystring
saturationstring

PerformanceHotResource

FieldTypeDescription
accountIdrequiredstring
latestAtrequiredstring (date-time)Its newest stored point in the window.
pressurenumber (double)value over the group's capacity, 0 to 1 (a maximum can touch 1); present only where the signal declares a capacity.
regionrequiredstring
resourceIdrequiredstring
resourceNamerequiredstring
resourceTyperequiredstring
servicerequiredstring
systemrequiredstring
typeLabelstringIts type's plural label, as PerformanceResourceSeries.typeLabel.
valuerequirednumber (double)The raw measurement, in the group's unit and statistic.

PerformanceHottest

FieldTypeDescription
fromrequiredstring (date-time)
groupsrequiredarray of PerformanceHottestGroupOne per category and defining signal with a leader, in category, kind and signal order. No order across them.
torequiredstring (date-time)
windowrequiredPerformanceHottestWindowThe spans hottest reads: the last hour, at the stored five minutes, or the last 24 hours. "Hottest now" is about the present; the charts keep the wider windows. One of 1h, 24h.

PerformanceHottestGroup

One ranking: the resources of a category measured by one defining signal, hottest first. Every leader's value is in this group's unit and statistic, and in no other group's.

FieldTypeDescription
capacitynumber (double)The signal's declared capacity in its unit — 100 for a utilisation in percent — when it declares one. Absent, no leader has a pressure.
categoryrequiredPerformanceSectionEstate's Performance sections; the catalogue places each resource type in one. One of compute, networking, databases, messaging, serverless, storage.
familyrequiredstringThe signal's family, as PerformanceSeries.family.
kindrequiredPerformanceKindWhat a series stands for. One of latency, errors, saturation.
leadersrequiredarray of PerformanceHotResource
metricrequiredstringThe cloud's name for the metric, as a chart labels it.
signalrequiredstringThe defining signal's catalogue name (ec2.cpu), as a series carries it.
statisticrequiredPerformanceHottestStatisticWhat a group's values are (MILESTONE_1 §12.5): max, the highest stored value in the window (saturation); p95, the 95th percentile of the stored five-minute values (latency) — the summary strip's p95 of the same series; perHour, a counted signal's total over the window per hour (errors that are counted); mean, the mean of the stored values (errors already a rate or a level). One of max, p95, perHour, mean.
typeLabelsrequiredarray of stringThe plural labels of the types among the leaders, from their render descriptors (the apart where the catalogue gives one).
unitrequiredstringThe cloud's unit for the metric (Percent, Seconds, Count).

PerformanceHottestStatistic

What a group's values are (MILESTONE_1 §12.5): max, the highest stored value in the window (saturation); p95, the 95th percentile of the stored five-minute values (latency) — the summary strip's p95 of the same series; perHour, a counted signal's total over the window per hour (errors that are counted); mean, the mean of the stored values (errors already a rate or a level).

Type: string enum

PerformanceHottestWindow

The spans hottest reads: the last hour, at the stored five minutes, or the last 24 hours. "Hottest now" is about the present; the charts keep the wider windows.

Type: string enum

PerformanceKind

What a series stands for.

Type: string enum

PerformanceKubernetesPlacement

Compute only: where the resource sits in Kubernetes. An EKS cluster names itself; a node its cluster, its managed node group or Karpenter node pool, and its EC2 instance; a pod its cluster, namespace, node and workload. An EC2 instance has one when the owner's rule places it in Kubernetes — rule says which part — and none when it is EC2 outside Kubernetes.

FieldTypeDescription
clusterrequiredstringThe cluster's name; empty for an instance only a Karpenter node pool tag places in Kubernetes.
instanceIdstringA node's EC2 instance.
namespacestringA pod's namespace.
nodestringA pod's node.
nodeGroupstringThe managed node group, when one is known.
nodePoolstringThe Karpenter node pool, when one is known.
rolerequiredstring enumWhat the resource is to the cluster: the cluster itself, a node, a pod, or a machine the rule places in it (an EC2 instance). The portal groups by it, and names no service. One of cluster, node, pod, machine.
rulestring enumAn EC2 instance's: which part of the rule placed it — its Auto Scaling group is a node group's, or it carries eks:cluster-name, kubernetes.io/cluster/<name> or karpenter.sh/nodepool. One of nodeGroupAutoScaling, eksClusterTag, kubernetesClusterTag, karpenterTag.
workloadstringA pod's workload. Container Insights reports the pods of one workload together, so a pod's series are its workload's.

PerformanceLiveWindow

The spans a pod's live read covers. A pod is read from CloudWatch when it is opened and nothing of it is stored, so it has no hourly rollup and no 30d.

Type: string enum

PerformanceLoad

A part's newest stored value of its first saturation series.

FieldTypeDescription
atrequiredstring (date-time)
metricrequiredstring
signalrequiredstring
unitrequiredstring
valuerequirednumber (double)

PerformancePodWindow

A pod's own series over a window, read from CloudWatch on demand.

FieldTypeDescription
containersCutrequiredintegerThe pod's containers past the first eight, whose series the read left out.
fromrequiredstring (date-time)
readAtrequiredstring (date-time)When CloudWatch was read for these series — now, or within the step before, whose answer is kept.
resourcerequiredPerformanceResourceSeries
stepSecondsrequiredinteger
torequiredstring (date-time)
windowrequiredPerformanceWindowThe span a read covers, ending at the current step. 1h, 24h and 7d are read from the stored five-minute points, kept 14 days; 30d, at a six-hour step, from their hourly rollup, kept 90 days by default, so its compare=previous reaches the 30 days before. One of 1h, 24h, 7d, 30d.

PerformancePoint

FieldTypeDescription
atrequiredstring (date-time)The start of the step the value covers.
maxnumber (double)The highest stored five-minute value in the step, where the step combines more than one.
minnumber (double)The lowest stored five-minute value in the step, where the step combines more than one.
valuerequirednumber (double)

PerformanceResourceSeries

FieldTypeDescription
absentKindsrequiredarray of PerformanceKindThe kinds this resource's type publishes no metric for; the view says so rather than showing another metric.
absentReasonsPerformanceAbsentReasonsWhy a kind in absentKinds is missing, where the catalogue knows more than that the type publishes no such metric: a CloudFront distribution's origin latency is an additional metric the account turns on and pays for. A kind without a reason here has none to give.
accountIdrequiredstring
hasTopologybooleanSet when /performance/topology draws this resource's parts: an EKS cluster's, a load balancer's. The portal links to the drawing by it, naming no service.
kubernetesPerformanceKubernetesPlacementCompute only: where the resource sits in Kubernetes. An EKS cluster names itself; a node its cluster, its managed node group or Karpenter node pool, and its EC2 instance; a pod its cluster, namespace, node and workload. An EC2 instance has one when the owner's rule places it in Kubernetes — rule says which part — and none when it is EC2 outside Kubernetes.
regionrequiredstring
resourceIdrequiredstring
resourceNamerequiredstring
resourceTyperequiredstring
sectionPerformanceSectionEstate's Performance sections; the catalogue places each resource type in one. One of compute, networking, databases, messaging, serverless, storage.
seriesrequiredarray of PerformanceSeries
servicerequiredstring
systemrequiredstring
typeLabelstringThe type's plural label, from its render descriptor (the same as /resource-types gives), which the portal groups a section by. An RDS instance of Aurora, DocumentDB or Neptune carries its engine's label instead, so the Databases section shows them apart.

PerformanceResourceWindow

FieldTypeDescription
fromrequiredstring (date-time)
resourcerequiredPerformanceResourceSeries
stepSecondsrequiredinteger
torequiredstring (date-time)
windowrequiredPerformanceWindowThe span a read covers, ending at the current step. 1h, 24h and 7d are read from the stored five-minute points, kept 14 days; 30d, at a six-hour step, from their hourly rollup, kept 90 days by default, so its compare=previous reaches the 30 days before. One of 1h, 24h, 7d, 30d.

PerformanceSection

Estate's Performance sections; the catalogue places each resource type in one.

Type: string enum

PerformanceSectionPage

FieldTypeDescription
fromrequiredstring (date-time)
nextCursorstringPresent when more resources follow; pass it back as cursor.
resourcesrequiredarray of PerformanceResourceSeries
sectionrequiredPerformanceSectionEstate's Performance sections; the catalogue places each resource type in one. One of compute, networking, databases, messaging, serverless, storage.
stepSecondsrequiredinteger
torequiredstring (date-time)
windowrequiredPerformanceWindowThe span a read covers, ending at the current step. 1h, 24h and 7d are read from the stored five-minute points, kept 14 days; 30d, at a six-hour step, from their hourly rollup, kept 90 days by default, so its compare=previous reaches the 30 days before. One of 1h, 24h, 7d, 30d.

PerformanceSeries

FieldTypeDescription
detailstringWhich of the resource's series of one metric this is, or what the metric counts where its name alone would mislead: a regional NAT gateway's zone; "client errors, throttling included" for API Gateway's 4xx.
familyrequiredstringThe series this one belongs with (MILESTONE_1 §12.2): its own signal for an average, and the average's for the peak beside it — ec2.cpu.max and elb.target_response_time.p95 are in the families ec2.cpu and elb.target_response_time. A reader draws a family as one chart: the average as the line, the maximum or the percentiles beside it. A peak's statistic says which it is.
kindrequiredPerformanceKindWhat a series stands for. One of latency, errors, saturation.
latestAtstring (date-time)The newest stored point's time, in the window or before it; absent when the series has no point stored at all. How old the series is: a budget that reaches an estate less often than once a run shows here.
metricrequiredstring
namespacerequiredstring
periodSecondsrequiredintegerHow often the cloud publishes this metric, and the period the collector asks for (MILESTONE_1 §12.3). 300 for everything published every five minutes; 86400 for a metric published once a day, which is what S3 publishes a bucket's size and object count as. A reader uses it to say what a point is — "a point a day" rather than "a point every 15 minutes" — and to expect a window with no point in it at all.
pointsrequiredarray of PerformancePointOne per step that has a stored point, oldest first. A step with none is left out, not zero.
previousarray of PerformancePointWith compare=previous: the series over the window before this one, at the same step, its points at that window's times; empty when nothing was stored then.
signalrequiredstringThe catalogue's name for the series, stable across releases (cpu, read_latency).
statisticrequiredstring
summaryPerformanceSeriesSummaryThe series' stored five-minute values over the window: their mean, lowest, highest and 95th percentile, and how many there are. Computed from what the Performance job stored, so it costs no CloudWatch read; a spike shorter than five minutes counts as its five-minute value. The 30d window's summary is the same values' mean, lowest, highest and count, exact from the hourly rollup, and has no p95: an hour keeps its mean, highest, lowest and count, which compose into a month's, and the percentiles of parts do not.
unitrequiredstringCloudWatch's unit for the metric (Percent, Seconds, Count).

PerformanceSeriesSummary

The series' stored five-minute values over the window: their mean, lowest, highest and 95th percentile, and how many there are. Computed from what the Performance job stored, so it costs no CloudWatch read; a spike shorter than five minutes counts as its five-minute value. The 30d window's summary is the same values' mean, lowest, highest and count, exact from the hourly rollup, and has no p95: an hour keeps its mean, highest, lowest and count, which compose into a month's, and the percentiles of parts do not.

FieldTypeDescription
countrequiredintegerThe five-minute values the summary is of.
maxrequirednumber (double)
meanrequirednumber (double)
minrequirednumber (double)
p95number (double)Absent from the 30d window's summary, which the hourly rollup cannot give one.

PerformanceSparkPoint

FieldTypeDescription
atrequiredstring (date-time)
valuerequirednumber (double)

PerformanceSparkline

FieldTypeDescription
keyrequiredstringThe row's key, as it was asked for.
kindrequiredPerformanceKindWhat a series stands for. One of latency, errors, saturation.
metricrequiredstring
pointsrequiredarray of PerformanceSparkPointOne per hour with a stored point, oldest first; an hour with none is left out, not zero.
signalrequiredstring
statisticrequiredstringThe signal's own statistic, which each hour's point combines by.
unitrequiredstring

PerformanceSparklines

FieldTypeDescription
fromrequiredstring (date-time)
itemsrequiredarray of PerformanceSparkline
stepSecondsrequiredinteger
torequiredstring (date-time)

PerformanceTopology

A resource's parts, for a drawing.

FieldTypeDescription
loadSincerequiredstring (date-time)The oldest a stored point may be and still be a part's load; a part with none since has no load.
rootrequiredPerformanceTopologyPart

PerformanceTopologyPart

FieldTypeDescription
childrenrequiredarray of PerformanceTopologyPart
cutrequiredintegerHow many of its children were left out past the caps.
kindrequiredstring enumWhat the part is: the cluster; a managed node group, a Karpenter node pool, or the nodes in neither (ungrouped); a node; the pods on no node the inventory holds (unscheduled); a pod; the load balancer; a target group; a target. One of cluster, nodeGroup, nodePool, ungrouped, node, unscheduled, pod, loadBalancer, targetGroup, target.
labelrequiredstringIts name — a resource's, a group's, or a target's id and port; empty for ungrouped and unscheduled.
loadPerformanceLoadA part's newest stored value of its first saturation series.
podsOnNodesLeftOutintegerA cluster's pods on the nodes its cap left out (cut): counted, not drawn, and never among the pods on no node the inventory holds.
reasonstringWhy a target is in its state, as the balancer gave it; or what could not be read of a balancer or a group.
resourcePerformanceTopologyResourceThe inventory resource a part is, for a link to its detail or its Performance.
statestringA target's health state, as the balancer gave it (healthy, unhealthy, draining, …); its status follows it.
statusStatusThe health of something that currently exists. Derived once at ingest and stored, never recomputed at read time. deleted is absent, and that is the point: a deleted resource is tombstoned, not given a status, so a live resource carrying status: deleted is an impossible value. Keeping one enum for both made that impossible value contract-valid everywhere. unknown is a resource the inventory knows exists, or was recently active, whose health it cannot observe — found only from its metrics or its name. It is neither healthy nor a problem: never green, not on the Alerts page, and counted as not checked on the public page. One of failed, degraded, operational, unknown.

PerformanceTopologyResource

The inventory resource a part is, for a link to its detail or its Performance.

FieldTypeDescription
accountIdrequiredstring
regionrequiredstring
resourceIdrequiredstring
resourceTyperequiredstring
servicerequiredstring
systemrequiredstring

PerformanceWindow

The span a read covers, ending at the current step. 1h, 24h and 7d are read from the stored five-minute points, kept 14 days; 30d, at a six-hour step, from their hourly rollup, kept 90 days by default, so its compare=previous reaches the 30 days before.

Type: string enum

Preferences

The person's portal preferences, applied by the portal; an absent field is the browser's own.

FieldTypeDescription
dateFormatstring enumiso 2026-09-15, dmy 15.09.2026, mdy 09/15/2026. One of iso, dmy, mdy.
firstRunPreferencesFirstRunWhat the person has done with the first-run checklist and the tour (MILESTONE_1 §7.3), as the server records it. A preferences save never changes it: one that leaves it out and one that carries it both keep what is recorded, so a page that knows nothing of the checklist cannot undo a dismissal, and a tab that read it long ago cannot put its old values back. It changes only through ProfileUpdate.firstRun (FirstRunChange).
numberFormatstring enumcommaDot 1,234.5, dotComma 1.234,5, spaceComma 1 234,5, plain 1234.5. One of commaDot, dotComma, spaceComma, plain.
preferencesConfirmedAtstring (date-time)When the person first saved their preferences (MILESTONE_1 §7.3), whatever the values — keeping the browser's own counts. It completes the first-run checklist's confirm_preferences step. The server records it. A save that carries it while none is recorded records the server's time, not the one sent; after that no save moves or removes it, whether it carries the field or not. The portal's preferences page sends it with every save.
themestring enum One of system, light, dark.
timeZonestringAn IANA time zone, such as Europe/Berlin.

PreferencesFirstRun

What the person has done with the first-run checklist and the tour (MILESTONE_1 §7.3), as the server records it. A preferences save never changes it: one that leaves it out and one that carries it both keep what is recorded, so a page that knows nothing of the checklist cannot undo a dismissal, and a tab that read it long ago cannot put its old values back. It changes only through ProfileUpdate.firstRun (FirstRunChange).

FieldTypeDescription
checklistDismissedAtstring (date-time)When the person dismissed the checklist; the Dashboard no longer carries it for them. Recorded with the server's time by firstRun.checklistDismissed: true, and kept through a dismissal sent again; false brings the checklist back.
tourSeenVersionintegerThe version of the tour (§7.2) the person has seen, as the portal numbers it, so a later release can offer a changed tour once. Never lowered: only firstRun.tourSeenVersion above it moves it.

ProfileUpdate

FieldTypeDescription
displayNamestring
firstRunFirstRunChangeA change to the person's first-run record alone. Each field sent changes that field, by the field's own rule; a field left out keeps what is recorded. It never touches the display preferences or their confirmation, which only a preferences save changes: the checklist's Dismiss, and the tour's version, change the checklist and nothing a tab might hold stale.
preferencesPreferencesThe person's portal preferences, applied by the portal; an absent field is the browser's own.

ProjectRegistration

A Google Cloud project an administrator is connecting.

FieldTypeDescription
environmentrequiredstring enum One of production, staging, sdlc, shared, unknown.
namerequiredstring
projectIdrequiredstringThe project id, not its number and not its display name.
serviceAccountstringThe reader service account the deployment impersonates. Defaults to the convention the reader-role documentation uses.

ProvisionAction

What a request asks for, and PROVISIONING_SPEC.md §4.2's action CHECK constraint exactly.

create and destroy are the lifecycle (D5). There is no in-place update in v1: changing a resource is a destroy and a create, and each gets its own approval. A lifecycle with three verbs and one of them partial is worse than one with two that are complete.

resolve is not a lifecycle verb. It is git ref resolution modelled as a request (§6.6) so that it inherits the claim, the fence, the replay rules, the heartbeat, the reap and the tenant scoping wholesale instead of growing a second copy of each. It is created only by publishing a git-sourced version, never by POST /provisioning/requests, and it is the one action that skips pending_approval: approval governs infrastructure, and resolving a ref creates none.

adopt is Import's second step (MILESTONE_1 §9.3): a recorded import brought under its template version — imported into a new state with a plan that may import its declared addresses and nothing else, then stamped with a plan that may change their tags and nothing else. Submitted from a recorded import (importId); the approval names both steps. The agent protocol for it: AgentTask.step and imports, and the steps a status report records.

stop is an action on an existing resource: one resource the inventory sees, named whole as target, acted on by a library template of the action's kind and approved as a create is. It is asked for at POST /provisioning/actions, never at POST /provisioning/requests; it leaves nothing behind — no provisioned resource, never partialApply — and a failed one is asked for again rather than retried.

Type: string enum

ProvisionActionObservation

One qualifying observation of an action's target. present: false is a tombstone: a finalized run's complete enumeration that did not see the target. snapshotId names the inventory snapshot when the observation is one; an unchanged observation, recorded only on the resource, has none. transitionId names a tombstone's deletion transition, which is kept for ever.

FieldTypeDescription
observedAtrequiredstring (date-time)
presentrequiredboolean
runIdstring (uuid)The collection run that observed it, which finalized.
snapshotIdinteger (int64)
statestringThe inventory state observed; absent for a tombstone.
transitionIdinteger (int64)

ProvisionActionVerification

A succeeded action only (the qualifying observation): whether the inventory has since seen the target in the state the action reaches (expected; stopped for a stop) — from a collection, never from the agent's report. A qualifying observation is one of the target made after actedAt by a collection run that finalized; a run that did not finish, or has not yet, proves nothing.

Judged on each read from the inventory's own records until it freezes, then recorded once and never changed: at the first qualifying observation in expected (verified), or once windowEndsAt has passed without one (unverified, not_yet_observable). A recorded verdict carries recordedAt and stands after the snapshot, run or log it names has aged out under its own retention; awaiting is never recorded.

verified — a qualifying observation saw the target in expected (the earliest, as observation); awaiting — not yet, and the target's collection is not yet late (windowEndsAt: actedAt plus the bucket's freshness window, the inventory's own "late"); unverified — the window has passed, and the target was observed after the action but never in expected; not_yet_observable — the window has passed with no complete collection of the target since the action. observation is the proving one when verified, and otherwise the newest qualifying one, if any.

FieldTypeDescription
actedAtrequiredstring (date-time)When the platform recorded the action's terminal report — the request's terminalAt.
expectedrequiredstringThe inventory state the action reaches.
methodrequiredstringThe rule that judged it, versioned.
observationProvisionActionObservationOne qualifying observation of an action's target. present: false is a tombstone: a finalized run's complete enumeration that did not see the target. snapshotId names the inventory snapshot when the observation is one; an unchanged observation, recorded only on the resource, has none. transitionId names a tombstone's deletion transition, which is kept for ever.
recordedAtstring (date-time)When the verdict was recorded. Absent while it is judged on read: awaiting, or frozen and not yet recorded by the next finalize or sweep.
statusrequiredstring enum One of awaiting, verified, unverified, not_yet_observable.
windowEndsAtrequiredstring (date-time)

ProvisionApproval

A request is decided exactly once, ever — approvals is keyed (tenant_id, request_id). Actor, decision, comment and timestamp are permanent and outlive log retention.

FieldTypeDescription
actorrequiredActorRefWho did something, for display and for audit. Not User: that carries the tenant and the role of the session, and an audit record naming a role that has since changed would be a lie about the past.
commentstring
decidedAtrequiredstring (date-time)
decisionrequiredstring enum One of approved, rejected.
selfApprovedrequiredbooleanThe requester approved their own request, because nobody else could. Always false on a rejection.

ProvisionDecision

FieldTypeDescription
bindingSha256stringThe request detail's bindingSha256 as the approver read it: what they are approving. Checked on every approval that names it — a request whose binding moved since is 409 binding_changed and nothing is decided — and required to approve an adopt of a generated import, whose configuration a regenerate replaces while it waits. Ignored on a rejection.
commentstringRecorded permanently and shown to the requester. Optional on approve; a rejection without one is technically allowed and practically unkind.
decisionrequiredstring enum One of approved, rejected.

ProvisionFailureClass

PROVISIONING_SPEC.md §7.4 — six classes, and PLATFORM.md §8 was amended on 2026-09-01 to match.

These are what an agent reports. A request can also fail restored, which only the platform writes (ProvisionRequestFailureClass).

Class Meaning Retried by the agent
retryable Network, throttling, transient provider error Yes, bounded, with jitter
permanent The operation cannot succeed as specified No
configuration Bad template, bad variables, missing provider config No — the message points at the template
infrastructure The cloud refused: quota, outage, IAM denial No — the message points at the account
timeout A stage exceeded its limit No
cancelled A human asked it to stop No

The pair that earns its keep is configuration against infrastructure. One means the template author must fix something, the other means the cloud account must be; collapsing them into "permanent" routes every failure to the wrong person, which is what the four-class version of this list did.

Type: string enum

ProvisionOutcomeSummary

One past request for the same template in the same account, for the approver's "has this worked here before" question.

FieldTypeDescription
failureClassProvisionRequestFailureClassWhy a request failed, as the platform records it: the six classes an agent reports (ProvisionFailureClass), and a seventh that only the platform writes (PROVISIONING_SPEC.md §7.4, amended 2026-09-12).
Class Meaning
restored The platform's database was restored from a backup while this request was queued, running or cancelling. What it did after the backup is unknown, and failureMessage names the backup and where the request's state is. It runs again only when an operator retries it.
An agent that reports restored, or anything outside ProvisionFailureClass, is refused with 400. One of retryable, permanent, configuration, infrastructure, timeout, cancelled, restored.
requestIdrequiredstring (uuid)
requestedAtrequiredstring (date-time)
requestedByrequiredActorRefWho did something, for display and for audit. Not User: that carries the tenant and the role of the session, and an audit record naming a role that has since changed would be a lie about the past.
staterequiredProvisionRequestStatePROVISIONING_SPEC.md §5, exactly. approved is deliberately not a state. Approving writes the approvals row and sets queued in one transaction; a separate approved state could only ever be observed as a limbo where a request is blessed but not runnable, and every reader would have to treat the two identically. cancelling is the only non-terminal state a human can ask for and not immediately get: the agent learns about it on its next heartbeat and moves the request to cancelled itself. One of pending_approval, rejected, expired, withdrawn, queued, running, cancelling, cancelled, succeeded, failed.
templateVersionrequiredinteger
terminalAtstring (date-time)

ProvisionRequest

FieldTypeDescription
actionrequiredProvisionActionWhat a request asks for, and PROVISIONING_SPEC.md §4.2's action CHECK constraint exactly. create and destroy are the lifecycle (D5). There is no in-place update in v1: changing a resource is a destroy and a create, and each gets its own approval. A lifecycle with three verbs and one of them partial is worse than one with two that are complete. resolve is not a lifecycle verb. It is git ref resolution modelled as a request (§6.6) so that it inherits the claim, the fence, the replay rules, the heartbeat, the reap and the tenant scoping wholesale instead of growing a second copy of each. It is created only by publishing a git-sourced version, never by POST /provisioning/requests, and it is the one action that skips pending_approval: approval governs infrastructure, and resolving a ref creates none. adopt is Import's second step (MILESTONE_1 §9.3): a recorded import brought under its template version — imported into a new state with a plan that may import its declared addresses and nothing else, then stamped with a plan that may change their tags and nothing else. Submitted from a recorded import (importId); the approval names both steps. The agent protocol for it: AgentTask.step and imports, and the steps a status report records. stop is an action on an existing resource: one resource the inventory sees, named whole as target, acted on by a library template of the action's kind and approved as a create is. It is asked for at POST /provisioning/actions, never at POST /provisioning/requests; it leaves nothing behind — no provisioned resource, never partialApply — and a failed one is asked for again rather than retried. One of create, destroy, resolve, adopt, stop.
approvalProvisionApprovalA request is decided exactly once, ever — approvals is keyed (tenant_id, request_id). Actor, decision, comment and timestamp are permanent and outlive log retention.
attemptrequiredintegerThe fence (§6.3). A claim stamps this value onto the task it creates and leaves it alone; what increments it is every event that invalidates the current task — reap, cancel, retry — in the same transaction as the invalidation. Every agent write carrying an older value is refused task_superseded, which is what stops an abandoned task's process from resurrecting, from the instant it is abandoned rather than from the next claim.
expiresAtrequiredstring (date-time)The pending_approval deadline (D9). An unactioned request becomes expired — terminal — after which the pinned version can safely move on. A reminder fires at half the window.
failureClassProvisionRequestFailureClassWhy a request failed, as the platform records it: the six classes an agent reports (ProvisionFailureClass), and a seventh that only the platform writes (PROVISIONING_SPEC.md §7.4, amended 2026-09-12).
Class Meaning
restored The platform's database was restored from a backup while this request was queued, running or cancelling. What it did after the backup is unknown, and failureMessage names the backup and where the request's state is. It runs again only when an operator retries it.
An agent that reports restored, or anything outside ProvisionFailureClass, is refused with 400. One of retryable, permanent, configuration, infrastructure, timeout, cancelled, restored.
failureMessagestringScrubbed by the agent before it was ever sent.
idrequiredstring (uuid)
namerequiredstringThe requester's name for the thing. An action's is derived from it and its target, stop-i-0abc….
originProvisionRequestOriginAn action asked for from a finding: the finding as the findings read projected it when the request was made — its identity, what its source called it and its figure — recorded with the request and never changed, so the approver sees why it was asked and the request keeps saying why after the finding clears. Absent for an action asked for without one, and for every other request.
partialApplybooleanA failure left infrastructure behind. Surfaced loudly and never auto-destroyed: the operator gets the state backend location and both next actions, and makes the call themselves.
releaseFromStatebooleanPresent and true on an adopt that releases its import from state rather than taking it over (MILESTONE_1 §9.3.4): it removes the declared addresses from the state an earlier adopt's import wrote, and changes nothing in the cloud. Absent otherwise.
requestedAtrequiredstring (date-time)
requestedByrequiredActorRefWho did something, for display and for audit. Not User: that carries the tenant and the role of the session, and an audit record naming a role that has since changed would be a lie about the past.
resourceIdstring (uuid)The provisionedResources row this destroys. Present for destroy and absent for create, both enforced.
sourceIdentitystringThe pinned version's gitSha, inlineSha256 or librarySha256, denormalised onto the request because it is what the approver is actually approving and it must still read correctly after the template is archived.
staterequiredProvisionRequestStatePROVISIONING_SPEC.md §5, exactly. approved is deliberately not a state. Approving writes the approvals row and sets queued in one transaction; a separate approved state could only ever be observed as a limbo where a request is blessed but not runnable, and every reader would have to treat the two identically. cancelling is the only non-terminal state a human can ask for and not immediately get: the agent learns about it on its next heartbeat and moves the request to cancelled itself. One of pending_approval, rejected, expired, withdrawn, queued, running, cancelling, cancelled, succeeded, failed.
stateBackendstringWhere the agent put this request's Terraform state, as the agent reported it. The platform never reads it — state is customer-owned (D3) — and stores the location only so an operator can find it after a partial apply.
targetResourceActionTargetThe one inventory resource an action acts on. Present exactly for an action — stop — and absent for the lifecycle, both enforced.
targetAccountIdrequiredstring
teamIdstring (uuid)The owning team, which decides who may approve. Present for every action except resolve, which has no owning team because it creates no infrastructure and skips approval (§6.6).
teamNamestring
templateIdrequiredstring (uuid)
templateNamestring
templateSlugrequiredstring
templateVersionrequiredinteger
terminalAtstring (date-time)
variablesRedactedrequiredobjectThe submitted variables with every sensitive value masked. A destroy's are its create's, as the create's read back: a destroy runs with the variables its create was given. The unmasked values are never returned by any read endpoint — they leave the platform only to the agent that claims the request's task, a destroy's agent receiving its create's values, and that is a different response type. Keys are exactly the variable names the pinned version's schema declares; nothing else can reach this map, because the request — for a destroy, its create — was validated against that schema before it existed.

ProvisionRequestCreate

FieldTypeDescription
actionrequiredProvisionActioncreate, destroy or adopt. resolve is refused here with invalid_request: a resolution request is created by the transaction that publishes a git-sourced version (§6.6), never by a person, and one submitted by hand would pin a version to a SHA nobody asked for. So is an action on an existing resource — stop — which is asked for at POST /provisioning/actions, under the licence's remediation capability. An adopt (MILESTONE_1 §9.3) names a recorded import (importId), and the template version and account it was recorded under; its variables are validated as a create's (the pre-fill, GET /provisioning/imports/prefill, suggests them). Refused: an import that is not recorded (409 request_state_conflict); a version that declares no import of the resource's type, or whose identifiers the resource's inventory record cannot finish (400 invalid_request); a resource the inventory no longer sees (404); and a resource another state holds — a stamp of an apply, finished or in flight, or an owner row — or that an untagged apply in its account may hold (409 resource_already_managed, naming it). The server finishes every identifier at submit; the approval pins them. An adopt of a generated import names the import's own template and version, as the import reads them (its skeleton). The server finishes the identifiers of the resource and of its known companions from the companion table, the ones the inventory cannot confirm marked optional; the request is created queued for its generate step, needing an agent that advertises generate (agentVersionNeeded names the version), and nobody is asked to approve anything yet. The step plans the generated configuration and applies nothing; when it reports, the configuration is stored as a new version of the private template, the request pins it and moves to pending_approval, and the approval pins that version's sha like any version's. A type the companion table does not know, or an inventory record that cannot finish the skeleton or an identifier, is refused (400 invalid_request, naming it). When no agent of the account generates (none unrevoked advertised generate, adopt and plan-inspection under the version it runs), the platform renders the configuration itself from the inventory record — the resource and the companions the record confirms — and stores it as the private template's next version, marked not yet planned. The request pins it, queued, and cannot be approved until an agent that generates has re-planned it imports-only (generated.phase is unplanned). A type the platform has no generator for, or a record that lacks what the configuration needs, is refused (400 invalid_request) with the reason, naming the attribute; nothing is stored.
importIdstring (uuid)Required for adopt, rejected otherwise — the recorded import it makes fully managed.
namerequiredstringThe requester's name for the thing. For a create it also fills the version's string variable called name, when it declares one and variables leaves it out, made to fit that variable's rules (the operation's description).
resourceIdstring (uuid)Required for destroy, rejected for create.
targetAccountIdrequiredstringA 12-digit AWS account id, or a Google Cloud project id. Must exist in this tenant's accounts registry, be enabled, and be owned by a team the requester belongs to.
teamIdstring (uuid)The owning team, which decides who may approve. Optional when the requester belongs to exactly one team that owns the account; required when they belong to several, because guessing would decide the approver.
templateIdrequiredstring (uuid)
templateVersionrequiredintegerExplicit, never "latest". A request that pinned a moving target would be approved against one version and applied from another.
variablesobjectcreate only. Validated against the pinned version's schema before the request exists — both the value types, which this schema constrains, and the names, which it cannot: a key the version does not declare is rejected by the validator. Values marked sensitive are stored unmasked (the agent needs them) and masked everywhere they are read back. A destroy takes none. It runs with the variables its create was given, copied from the create when the destroy is submitted, sensitive values included, so the destroy plans exactly the configuration the create applied. A destroy that names any variable — even the create's own value — is refused, 400 invalid_request naming variables; an empty object names none and is accepted.

ProvisionRequestDetail

FieldTypeDescription
actionrequiredProvisionActionWhat a request asks for, and PROVISIONING_SPEC.md §4.2's action CHECK constraint exactly. create and destroy are the lifecycle (D5). There is no in-place update in v1: changing a resource is a destroy and a create, and each gets its own approval. A lifecycle with three verbs and one of them partial is worse than one with two that are complete. resolve is not a lifecycle verb. It is git ref resolution modelled as a request (§6.6) so that it inherits the claim, the fence, the replay rules, the heartbeat, the reap and the tenant scoping wholesale instead of growing a second copy of each. It is created only by publishing a git-sourced version, never by POST /provisioning/requests, and it is the one action that skips pending_approval: approval governs infrastructure, and resolving a ref creates none. adopt is Import's second step (MILESTONE_1 §9.3): a recorded import brought under its template version — imported into a new state with a plan that may import its declared addresses and nothing else, then stamped with a plan that may change their tags and nothing else. Submitted from a recorded import (importId); the approval names both steps. The agent protocol for it: AgentTask.step and imports, and the steps a status report records. stop is an action on an existing resource: one resource the inventory sees, named whole as target, acted on by a library template of the action's kind and approved as a create is. It is asked for at POST /provisioning/actions, never at POST /provisioning/requests; it leaves nothing behind — no provisioned resource, never partialApply — and a failed one is asked for again rather than retried. One of create, destroy, resolve, adopt, stop.
agentSettingsNeededarray of stringA queued request no agent of its account can run until it is set up for it: the agent settings that would let it, by their names. Today one: PANORAMA_RUNNER_AZURE_RESOURCE_GROUPS, for a template of the tenant's own (git or inline) in an Azure account, which runs only on an agent whose Azure runners are scoped to resource groups (capability azure-resource-groups). Not an upgrade: agentVersionNeeded says when an upgrade is needed too. The request waits, queued; absent while any agent of the account can run it.
agentVersionNeededstringA queued request no agent of its account can run (MILESTONE_1 §9.5): the agent version that can. The request waits, queued, for an agent of that version or later; the agent the account has is never offered it. Absent while any agent of the account can run it.
approvalProvisionApprovalA request is decided exactly once, ever — approvals is keyed (tenant_id, request_id). Actor, decision, comment and timestamp are permanent and outlive log retention.
attemptrequiredintegerThe fence (§6.3). A claim stamps this value onto the task it creates and leaves it alone; what increments it is every event that invalidates the current task — reap, cancel, retry — in the same transaction as the invalidation. Every agent write carrying an older value is refused task_superseded, which is what stops an abandoned task's process from resurrecting, from the instant it is abandoned rather than from the next claim.
bindingSha256stringWhile pending_approval: the digest of what an approval would pin now — the target, the template version and its source identity, the variables and the permitted effects. The approver sends it back with the decision (ProvisionDecision.bindingSha256), so a request that moved after it was read — a generated configuration regenerated, above all — is refused rather than approved unread.
destroyTargetProvisionedResourcedestroy only. What will be removed: the resource, its account, the number of tagged resources the create left behind (expectedCount), and the request that created it. "Destroy web-01" and "destroy 34 resources" are different decisions, and an approval screen that shows the template name alone cannot tell them apart.
expiresAtrequiredstring (date-time)The pending_approval deadline (D9). An unactioned request becomes expired — terminal — after which the pinned version can safely move on. A reminder fires at half the window.
failureClassProvisionRequestFailureClassWhy a request failed, as the platform records it: the six classes an agent reports (ProvisionFailureClass), and a seventh that only the platform writes (PROVISIONING_SPEC.md §7.4, amended 2026-09-12).
Class Meaning
restored The platform's database was restored from a backup while this request was queued, running or cancelling. What it did after the backup is unknown, and failureMessage names the backup and where the request's state is. It runs again only when an operator retries it.
An agent that reports restored, or anything outside ProvisionFailureClass, is refused with 400. One of retryable, permanent, configuration, infrastructure, timeout, cancelled, restored.
failureMessagestringScrubbed by the agent before it was ever sent.
generatedProvisionRequestGeneratedAn adopt of a generated import only: where its configuration stands. generating — the request pins its private template's skeleton and waits, queued or running, for an agent to generate the configuration; unplanned — no agent of the account generates, so the platform rendered the configuration from the inventory (slice 4); the request pins it and waits, queued or running, for an agent that generates to re-plan it imports-only, and it cannot be approved until one has (agentVersionNeeded names the agent version while none can); generated — the request pins the version a generate step reported or re-planned, whose HCL is that version's inline source (GET /provisioning/templates/{templateId}/versions/{version}), and asks to be approved on it. dropped are the optional companions the provider found no object for; gaps are what the companion table does not cover, and each companion the platform's configuration left out, in a reader's words; planInspection, once generated, is the generate step's (or the re-plan's) plan of it.
idrequiredstring (uuid)
importIdstring (uuid)adopt only (MILESTONE_1 §9.3): the recorded import this request makes fully managed — or, with releaseFromState, releases from state.
importsarray of AgentImportadopt only. Every address the request acts on and the identifier the server finished for it at submit (§9.4) — what the approval pins and the agent is handed, unchanged by any later change to the inventory record. For an adopt of a generated import, the addresses its generate step is handed until it reports — an optional companion marked optional — and the addresses it kept after.
mayApproveOwnbooleantrue when the reader asked for this pending_approval request and may approve it themselves, nobody else being able to; false otherwise. What the request page offers; the decision judges it again when it is made.
namerequiredstringThe requester's name for the thing. An action's is derived from it and its target, stop-i-0abc….
originProvisionRequestOriginAn action asked for from a finding: the finding as the findings read projected it when the request was made — its identity, what its source called it and its figure — recorded with the request and never changed, so the approver sees why it was asked and the request keeps saying why after the finding clears. Absent for an action asked for without one, and for every other request.
outputsarray of ProvisionRequestOutputA succeeded request whose version declares outputs (MILESTONE_1 §11.4): each declared output, in the version's order, as the attempt that succeeded reported it — never another attempt's. Absent for any other request.
outputsNeedAgentVersionstringPresent when the attempt that succeeded ran on an agent that was not asked for outputs, because it does not report them: the agent version that does. Every declared output is then missing.
partialApplybooleanA failure left infrastructure behind. Surfaced loudly and never auto-destroyed: the operator gets the state backend location and both next actions, and makes the call themselves.
previousApprovedVersionintegerThe last version of this template that was approved in this account, so the portal can show a diff against it. Absent when there is none, which is itself worth seeing.
recentOutcomesarray of ProvisionOutcomeSummaryNewest first.
releaseFromStatebooleanPresent and true on an adopt that releases its import from state rather than taking it over (MILESTONE_1 §9.3.4): it removes the declared addresses from the state an earlier adopt's import wrote, and changes nothing in the cloud. Absent otherwise.
requestedAtrequiredstring (date-time)
requestedByrequiredActorRefWho did something, for display and for audit. Not User: that carries the tenant and the role of the session, and an audit record naming a role that has since changed would be a lie about the past.
resourceIdstring (uuid)The provisionedResources row this destroys. Present for destroy and absent for create, both enforced.
sourceIdentitystringThe pinned version's gitSha, inlineSha256 or librarySha256, denormalised onto the request because it is what the approver is actually approving and it must still read correctly after the template is archived.
staterequiredProvisionRequestStatePROVISIONING_SPEC.md §5, exactly. approved is deliberately not a state. Approving writes the approvals row and sets queued in one transaction; a separate approved state could only ever be observed as a limbo where a request is blessed but not runnable, and every reader would have to treat the two identically. cancelling is the only non-terminal state a human can ask for and not immediately get: the agent learns about it on its next heartbeat and moves the request to cancelled itself. One of pending_approval, rejected, expired, withdrawn, queued, running, cancelling, cancelled, succeeded, failed.
stateBackendstringWhere the agent put this request's Terraform state, as the agent reported it. The platform never reads it — state is customer-owned (D3) — and stores the location only so an operator can find it after a partial apply.
targetResourceActionTargetThe one inventory resource an action acts on. Present exactly for an action — stop — and absent for the lifecycle, both enforced.
targetAccountrequiredAccountIncluding its environment tag. "Production" is the single most decision-changing fact on an approval screen.
targetAccountIdrequiredstring
teamIdstring (uuid)The owning team, which decides who may approve. Present for every action except resolve, which has no owning team because it creates no infrastructure and skips approval (§6.6).
teamNamestring
templateIdrequiredstring (uuid)
templateNamestring
templateSlugrequiredstring
templateVersionrequiredinteger
terminalAtstring (date-time)
variablesRedactedrequiredobjectThe submitted variables with every sensitive value masked. A destroy's are its create's, as the create's read back: a destroy runs with the variables its create was given. The unmasked values are never returned by any read endpoint — they leave the platform only to the agent that claims the request's task, a destroy's agent receiving its create's values, and that is a different response type. Keys are exactly the variable names the pinned version's schema declares; nothing else can reach this map, because the request — for a destroy, its create — was validated against that schema before it existed.
verificationProvisionActionVerificationA succeeded action only (the qualifying observation): whether the inventory has since seen the target in the state the action reaches (expected; stopped for a stop) — from a collection, never from the agent's report. A qualifying observation is one of the target made after actedAt by a collection run that finalized; a run that did not finish, or has not yet, proves nothing. Judged on each read from the inventory's own records until it freezes, then recorded once and never changed: at the first qualifying observation in expected (verified), or once windowEndsAt has passed without one (unverified, not_yet_observable). A recorded verdict carries recordedAt and stands after the snapshot, run or log it names has aged out under its own retention; awaiting is never recorded. verified — a qualifying observation saw the target in expected (the earliest, as observation); awaiting — not yet, and the target's collection is not yet late (windowEndsAt: actedAt plus the bucket's freshness window, the inventory's own "late"); unverified — the window has passed, and the target was observed after the action but never in expected; not_yet_observable — the window has passed with no complete collection of the target since the action. observation is the proving one when verified, and otherwise the newest qualifying one, if any.
versionrequiredTemplateVersionThe pinned version, including its source identity. inlineSource is present here: for an inline template this is the code being approved, and an approver who cannot read it is rubber-stamping. It is read in the same snapshot as bindingSha256, and only by a reader who may read the request — every one of whom may read the version itself. A read with source=false leaves it out and keeps inlineSha256. A library version's librarySource and libraryReadme are not repeated here: they are the single-version read's.

ProvisionRequestFailureClass

Why a request failed, as the platform records it: the six classes an agent reports (ProvisionFailureClass), and a seventh that only the platform writes (PROVISIONING_SPEC.md §7.4, amended 2026-09-12).

Class Meaning
restored The platform's database was restored from a backup while this request was queued, running or cancelling. What it did after the backup is unknown, and failureMessage names the backup and where the request's state is. It runs again only when an operator retries it.

An agent that reports restored, or anything outside ProvisionFailureClass, is refused with 400.

Type: string enum

ProvisionRequestGenerated

An adopt of a generated import only: where its configuration stands. generating — the request pins its private template's skeleton and waits, queued or running, for an agent to generate the configuration; unplanned — no agent of the account generates, so the platform rendered the configuration from the inventory (slice 4); the request pins it and waits, queued or running, for an agent that generates to re-plan it imports-only, and it cannot be approved until one has (agentVersionNeeded names the agent version while none can); generated — the request pins the version a generate step reported or re-planned, whose HCL is that version's inline source (GET /provisioning/templates/{templateId}/versions/{version}), and asks to be approved on it. dropped are the optional companions the provider found no object for; gaps are what the companion table does not cover, and each companion the platform's configuration left out, in a reader's words; planInspection, once generated, is the generate step's (or the re-plan's) plan of it.

FieldTypeDescription
droppedrequiredarray of string
gapsrequiredarray of string
phaserequiredstring enum One of generating, unplanned, generated.
planInspectionProvisionRequestGeneratedInspectiongenerated only (slice 3): the generate step's re-plan of the configuration, as its task stored the inspection — whether every change it held is an import, and how many changes it held. A configuration is stored only on an accepted inspection; a refused one fails the request and its message says why.
secretValuesProvisionRequestSecretValuesA generated configuration whose resource the provider reads a secret of back: the values the request asks for and whether they are sealed for its current attempt. Never a value, and never a sealed one. sealed holds when every needed variable is sealed, unexpired, for at least one agent that can claim the import; expiresAt is when the first of those expires. The import waits, queued, until they are.

ProvisionRequestGeneratedInspection

generated only (slice 3): the generate step's re-plan of the configuration, as its task stored the inspection — whether every change it held is an import, and how many changes it held. A configuration is stored only on an accepted inspection; a refused one fails the request and its message says why.

FieldTypeDescription
acceptedrequiredboolean
changesrequiredintegerHow many resource changes the plan held, imports included.

ProvisionRequestOrigin

An action asked for from a finding: the finding as the findings read projected it when the request was made — its identity, what its source called it and its figure — recorded with the request and never changed, so the approver sees why it was asked and the request keeps saying why after the finding clears. Absent for an action asked for without one, and for every other request.

FieldTypeDescription
costFindingCostThe finding's figure then, in its own currency — for Trusted Advisor, AWS's estimate.
labelstringWhat the source called it, as the finding's label read then — a Trusted Advisor check's name.
scoperequiredstring
sourcerequiredFindingSourceWhere a finding comes from, each read from its own table:
  • cost_anomaly — an open spend anomaly; scope its account or team.
  • unknown_status — a live resource whose status is unknown; scope the resource.
  • trusted_advisor — a recommendation Trusted Advisor flags (warning or error); sourceId is <account>/<check>/<resource>, scope the resource where the check names one the inventory maps, else the account; the figure is AWS's estimate and label the check's name.
  • cost_without_resource — spend in the last 14 days on a resource the inventory looks for and no longer has, or never had; scope the key.
  • drift_missing — a provisioned create the inventory sees fewer of than were stamped; sourceId the provisioned resource, scope its team, label its name.
  • drift_unprovisioned — a live resource of a kind provisioning produces that nobody provisioned; scope the resource.
  • stale_bucket — a bucket nothing has collected inside its window; sourceId <account>/<region>/<system>/<service>/<type>, scope the account, label the bucket.
  • tag_policy — a live resource with spend in the last 14 days whose tags the tenant's tag policy is not satisfied by, as the tag compliance read's violations list it; scope the resource, faults what to fix, the figure its spend. A change to the resource's tags is a new occurrence, which no earlier exception covers.
  • action_unverified — a provisioning action whose recorded verdict is unverified: its window passed and what a complete collection saw of its target was never the state the action reaches; sourceId the request, scope the team that asked (the request's own scope), label the request's name, accountId the target's account. Open until a complete collection registered after the action sees the target in that state, or a newer succeeded action of the same kind on the same target speaks for it; the recorded verdict itself never changes.
  • action_not_yet_observable — the same for a verdict of not_yet_observable: the window passed with no complete collection seeing the target at all.
One of cost_anomaly, unknown_status, trusted_advisor, cost_without_resource, drift_missing, drift_unprovisioned, stale_bucket, tag_policy, action_unverified, action_not_yet_observable.
sourceIdrequiredstring

ProvisionRequestOutput

One output of a request's version, and what became of it: reported with its value; withheld — declared sensitive, marked sensitive by OpenTofu, not of its declared type, or a value the agent's scrubber would change, so its value never reached the platform; missing — not reported at all.

FieldTypeDescription
descriptionrequiredstring
namerequiredstring
staterequiredstring enum One of reported, withheld, missing.
typerequiredstring enum One of string, number, boolean, list(string), map(string).
valueVariableValueOne variable value, in one of the five types PROVISIONING_SPEC.md §8.1 allows and in no other. That list is the entire vocabulary until a customer asks for more, and a value outside it is a template the validator will reject anyway. Written as a oneOf even though AgentTask deliberately avoids one for its work item: a union at a leaf costs a third-party generator nothing it does not already pay — a generator that cannot express it falls back to "any", which is precisely what an unconstrained schema gives it — while a oneOf at the top of a work item makes the whole response awkward to read. The value here is that the map carrying these is no longer free-form: an arbitrary nested blob does not validate, so there is nowhere for one to hide.

ProvisionRequestPage

FieldTypeDescription
itemsrequiredarray of ProvisionRequest
pagerequiredinteger
pageSizerequiredinteger
totalrequiredinteger (int64)
totalPagesrequiredinteger

ProvisionRequestSealing

What a browser seals a request's secret values to: the request's current attempt, the variables it needs, and every agent of its account that can claim the import — unrevoked, registered with generate and sealed-values under the version it runs now, and with a sealing key. Seal every variable to every agent: HPKE base mode, DHKEM(X25519, HKDF-SHA256), HKDF-SHA256, AES-128-GCM, info cloudpanorama secret channel v1, the associated data panorama-sealed-value/v1, the request id, the attempt, the agent id and the variable, each after a newline.

FieldTypeDescription
agentsrequiredarray of ProvisionSealingAgent
attemptrequiredinteger
neededrequiredarray of AgentValueNeeded

ProvisionRequestSecretValues

A generated configuration whose resource the provider reads a secret of back: the values the request asks for and whether they are sealed for its current attempt. Never a value, and never a sealed one. sealed holds when every needed variable is sealed, unexpired, for at least one agent that can claim the import; expiresAt is when the first of those expires. The import waits, queued, until they are.

FieldTypeDescription
attemptrequiredinteger
expiresAtstring (date-time)
neededrequiredarray of AgentValueNeeded
sealedrequiredboolean

ProvisionRequestState

PROVISIONING_SPEC.md §5, exactly.

approved is deliberately not a state. Approving writes the approvals row and sets queued in one transaction; a separate approved state could only ever be observed as a limbo where a request is blessed but not runnable, and every reader would have to treat the two identically.

cancelling is the only non-terminal state a human can ask for and not immediately get: the agent learns about it on its next heartbeat and moves the request to cancelled itself.

Type: string enum

ProvisionRequestTrail

A request's trail. request is the request as it was asked, with its approval; steppedUpAt, task and verification are the later hops when they exist; sources names every record the hops rest on, in the trail's order, with its retention. The finding and money hops are later slices.

FieldTypeDescription
blueprintRunIdstring (uuid)The Blueprint run whose step this request is. Its approval is the run's: the step's is copied from it, and the run's decision holds the step-up.
requestrequiredProvisionRequest
sourcesrequiredarray of ProvisionTrailSource
steppedUpAtstring (date-time)When the approver's session last confirmed its identity for the decision, as the decision's audit record kept it. Present only when the decision needed one — the requester approving their own request, or their own Blueprint run.
taskProvisionTaskOne attempt at one request, by one agent.
verificationProvisionActionVerificationA succeeded action only (the qualifying observation): whether the inventory has since seen the target in the state the action reaches (expected; stopped for a stop) — from a collection, never from the agent's report. A qualifying observation is one of the target made after actedAt by a collection run that finalized; a run that did not finish, or has not yet, proves nothing. Judged on each read from the inventory's own records until it freezes, then recorded once and never changed: at the first qualifying observation in expected (verified), or once windowEndsAt has passed without one (unverified, not_yet_observable). A recorded verdict carries recordedAt and stands after the snapshot, run or log it names has aged out under its own retention; awaiting is never recorded. verified — a qualifying observation saw the target in expected (the earliest, as observation); awaiting — not yet, and the target's collection is not yet late (windowEndsAt: actedAt plus the bucket's freshness window, the inventory's own "late"); unverified — the window has passed, and the target was observed after the action but never in expected; not_yet_observable — the window has passed with no complete collection of the target since the action. observation is the proving one when verified, and otherwise the newest qualifying one, if any.

ProvisionRequestValidationError

FieldTypeDescription
detailsarray of objectOne entry per variable that failed. A generated form that cannot say which field is wrong is worse than no form, so this is part of the contract rather than a courtesy.
errorrequiredobject

ProvisionSealedValue

FieldTypeDescription
agentIdrequiredstring (uuid)
sealedrequiredstring
variablerequiredstring

ProvisionSealedValuesBody

A request's secret values, sealed in the browser: every needed variable for every agent sealing listed, for attempt. Replaces what the attempt held.

FieldTypeDescription
attemptrequiredinteger
valuesrequiredarray of ProvisionSealedValue

ProvisionSealingAgent

FieldTypeDescription
agentIdrequiredstring (uuid)
namerequiredstring
publicKeyrequiredAgentSealingKeyThe public half of the agent's sealing key (agent-protocol.md §3.10): standard base64 of the 32 bytes of an X25519 public key. Sent at register beside sealed-values, which stores it, and on every claim, which hands only values sealed to it. The private half never leaves the agent.

ProvisionTask

One attempt at one request, by one agent.

FieldTypeDescription
agentIdrequiredstring (uuid)
attemptrequiredinteger
claimedAtrequiredstring (date-time)
finishedAtstring (date-time)
heartbeatAtrequiredstring (date-time)Falling behind heartbeatTimeoutSeconds is what makes the scheduler reap this task and return its request to queued.
idrequiredstring (uuid)
requestIdrequiredstring (uuid)
stagerequiredstringFree text from the agent — init, plan, apply and so on — not an enum, because a third-party agent may legitimately have stages this platform has never heard of, and refusing them would make the protocol brittle for no gain. Rendered as a label, never branched on.
staterequiredProvisionTaskStateOne attempt at one request. abandoned is what the reaper writes when a task's heartbeat goes stale — distinct from failed, because nobody reported a failure: the process stopped talking, and what it did before it stopped is unknown. One of claimed, running, succeeded, failed, cancelled, abandoned.

ProvisionTaskDetail

FieldTypeDescription
agentIdrequiredstring (uuid)
attemptrequiredinteger
claimedAtrequiredstring (date-time)
finishedAtstring (date-time)
heartbeatAtrequiredstring (date-time)Falling behind heartbeatTimeoutSeconds is what makes the scheduler reap this task and return its request to queued.
idrequiredstring (uuid)
requestIdrequiredstring (uuid)
stagerequiredstringFree text from the agent — init, plan, apply and so on — not an enum, because a third-party agent may legitimately have stages this platform has never heard of, and refusing them would make the protocol brittle for no gain. Rendered as a label, never branched on.
staterequiredProvisionTaskStateOne attempt at one request. abandoned is what the reaper writes when a task's heartbeat goes stale — distinct from failed, because nobody reported a failure: the process stopped talking, and what it did before it stopped is unknown. One of claimed, running, succeeded, failed, cancelled, abandoned.
summaryProvisionTaskSummaryWritten when the task becomes terminal.

ProvisionTaskLogLine

One stored log line, as the portal reads it.

Deliberately not AgentLogLine, for the same reason ResourceSnapshot is not SnapshotInput: the write shape carries the bounds the server enforces on the way in — a batch that violates them is rejected whole — and the read shape carries what was stored. Merging them would put a maxLength on a response, which is a promise about history that a change to the limit would break retroactively.

FieldTypeDescription
createdAtrequiredstring (date-time)
levelrequiredstring enum One of debug, info, warn, error.
messagerequiredstringAlready scrubbed by the agent when it was sent.
seqrequiredinteger (int64)Agent-assigned and monotonic per task. Gaps mean the agent's own buffer dropped lines under pressure; they are not an error.
stagerequiredstring

ProvisionTaskLogPage

Cursor-paginated, so deliberately no page, total or totalPages: a running apply appends continuously, and a page number has no stable meaning against a table that is growing at the tail.

FieldTypeDescription
completionrequiredstring enumWhether anything more will come after nextCursor. This — not the task's state, and not a short page — is the tailer's stop condition. The agent reports its terminal status before it drains the last log batches, so a task is terminal while its most useful lines are still in flight; a viewer that stopped at "terminal plus a short page" lost them until a reload.
  • pending: keep polling. The task is running, or it is terminal and the line at its watermark has not been paged out yet.
  • complete: every line the agent will ever send is at or before nextCursor. The task is terminal and a line at its finalLogSeq has been paged out — or the agent said it sent none.
  • incomplete: the task is terminal, the agent named a watermark, and a grace of 60 seconds after the finish passed without a line at it: the tail was lost, or is no longer retained. Nothing more will come; say so rather than spinning.
  • unverified: the task is terminal, the grace passed, and the agent never said where its log ends (an agent that sends neither finalLogSeq nor logLineCount). Nothing more is expected, but nobody vouched for it.
Decided by the server on every page, so a reader that reconnects or starts over from no cursor reaches the same answer once it has paged to the watermark. One of pending, complete, incomplete, unverified.
itemsrequiredarray of ProvisionTaskLogLine
nextCursorrequiredstringAlways present, and always positioned after the last line in items. Pass it as cursor on the next request and you get only what has been appended since. It is a watermark, not a "there is more" flag. An empty page returns the watermark it was given, so a tailer that polls with it makes progress and never re-reads; a tailer that polls with its own previous input cursor — which an earlier draft of this contract told it to do when nextCursor was absent — re-reads the page it just consumed, and on the first page, where there is no input cursor, re-reads the log from the beginning forever. "Is there more?" is answered by completion, not by this field and not by the task's state alone: a page can be empty because the apply is between log lines, or because the agent is still draining after its terminal report, and a client that stops on an empty page — or on a terminal state — stops before the tail.

ProvisionTaskPage

FieldTypeDescription
itemsrequiredarray of ProvisionTask
pagerequiredinteger
pageSizerequiredinteger
totalrequiredinteger (int64)
totalPagesrequiredinteger

ProvisionTaskState

One attempt at one request. abandoned is what the reaper writes when a task's heartbeat goes stale — distinct from failed, because nobody reported a failure: the process stopped talking, and what it did before it stopped is unknown.

Type: string enum

ProvisionTaskSummary

Permanent. A task's full logs are dropped after 30 days; this survives, because "what did it do, did it work, how long did it take, and which agent and OpenTofu version ran it" is the audit record. An audit that expires is not an audit.

FieldTypeDescription
agentVersionrequiredstring
createdAtrequiredstring (date-time)
createdCountinteger
destroyedCountinteger
failureClassProvisionFailureClassPROVISIONING_SPEC.md §7.4 — six classes, and PLATFORM.md §8 was amended on 2026-09-01 to match. These are what an agent reports. A request can also fail restored, which only the platform writes (ProvisionRequestFailureClass).
Class Meaning Retried by the agent
retryable Network, throttling, transient provider error Yes, bounded, with jitter
permanent The operation cannot succeed as specified No
configuration Bad template, bad variables, missing provider config No — the message points at the template
infrastructure The cloud refused: quota, outage, IAM denial No — the message points at the account
timeout A stage exceeded its limit No
cancelled A human asked it to stop No
The pair that earns its keep is configuration against infrastructure. One means the template author must fix something, the other means the cloud account must be; collapsing them into "permanent" routes every failure to the wrong person, which is what the four-class version of this list did. One of retryable, permanent, configuration, infrastructure, timeout, cancelled.
failureMessagestring
logLineCountinteger (int64)
outcomerequiredProvisionTaskStateOne attempt at one request. abandoned is what the reaper writes when a task's heartbeat goes stale — distinct from failed, because nobody reported a failure: the process stopped talking, and what it did before it stopped is unknown. One of claimed, running, succeeded, failed, cancelled, abandoned.
planDigeststringsha256 of the plan output. plan still runs even though it no longer gates approval (D1): it is a cheap failure detector before anything is created, and its digest is how two applies of the same version are compared after the logs are gone.
stageDurationsMsobjectStage name to elapsed milliseconds.
tofuVersionrequiredstring

ProvisionTrailSource

One record a hop of the trail rests on. id is the record's own: the request's id for request, approval and verdict; the audit record's for decision_audit; the run's for blueprint_run and collection_run; the task's for task_summary and task_log; the snapshot's, transition's and observing run's for snapshot, transition and resource_observation.

retention: permanent — kept for as long as the request is; limited — removed by the deployment's retention, at expiresAt at the earliest (the nightly pass removes it at or after then); until_next_observation — an unchanged observation, held only on the resource's row until the bucket's next collection replaces it. inspectable is whether the record can be read now; expiresAt is absent once it cannot, and for a limited source whose retention the server was not given.

FieldTypeDescription
expiresAtstring (date-time)
hoprequiredstring enum One of request, approval, task, verification.
idrequiredstring
inspectablerequiredboolean
kindrequiredstring enum One of request, approval, decision_audit, blueprint_run, task_summary, task_log, verdict, collection_run, snapshot, resource_observation, transition.
retentionrequiredstring enum One of permanent, limited, until_next_observation.

ProvisionedResource

What a create produced, and the anchor of the provenance badge on the inventory side. A create that stopped part-way has one too, partial, so what it made can be destroyed from the platform.

Or, a resource Import recorded (origin: imported, management: recorded): nothing was applied for it, so createRequestId, destroyRequestId, stateBackend, provenanceTagged, overriddenTags, expectedCount, driftScope, status and destroyedAt are absent, id is the import's, teamId is the team it names as owner, createdAt is when it was recorded, and import says which resource it is. Every other row has createRequestId, provenanceTagged, expectedCount and status.

FieldTypeDescription
accountIdrequiredstring
createRequestIdstring (uuid)
createdAtrequiredstring (date-time)
destroyRequestIdstring (uuid)
destroyedAtstring (date-time)
driftScopeDriftScopeWhat the drift diff compares of this resource. Present on the resource list and on a drift finding, whenever provenanceTagged is true; absent elsewhere, where it claims nothing.
expectedCountintegerTagged resources found in state immediately after the apply — everything the create stamped. The drift diff compares the part of it the inventory can see, driftScope.compared; without either, "provisioned but missing" has nothing to be missing from. Untaggable resource types simply do not contribute — they lower this number, they do not make it wrong.
idrequiredstring (uuid)
importResourceImportThe import that recorded it, when origin is imported.
managementrequiredProvisionedResourceManagementHow far the platform manages it: managed, a state the platform applied holds it, and destroy and drift are what they are for anything provisioned; recorded, it is recorded and nothing more — nothing was written to the cloud, its tags or any state. One of managed, recorded.
namerequiredstring
originrequiredProvisionedResourceOriginWhere a row of the Provisioned list came from: provisioned, a create request made it; imported, it was made elsewhere and recorded by Import. One of provisioned, imported.
overriddenTagsarray of stringThe stamp keys the template's own provider defaults also set literally, which the stamp then won. A tenant's tag policy may require keys the template's author also writes, and the stamp is merged last — so these are the keys whose template value the resource does not carry. Shown on the request so the author can read why, rather than discovering it from a plan. Absent means none were overridden — or that the agent which ran this create predates the field. The two are not distinguishable on the wire and the page treats them the same: it says nothing about overrides rather than claiming there were none.
provenanceTaggedbooleanWhether the panorama:request_id stamp verifiably landed — the agent read state after the apply and counted resources carrying it. False excludes this row from the drift diff and shows a "provenance unverified" badge instead. A stamp that silently failed to land is worse than no stamp at all, so it is never silently missing: the usual cause is a template declaring its own default_tags, which collides with the override.
stateBackendstringWhere its Terraform state lives, as the agent reported it.
statusProvisionedResourceStatusactive: a create succeeded, and the drift diff watches it. partial: a create or a destroy stopped part-way; what exists is in the state at stateBackend, and a destroy is how it goes. A destroy that fails cleanly leaves the status as it was. destroyed: a destroy succeeded; the row stays as the record. One of active, destroyed, partial.
teamIdrequiredstring (uuid)
templateIdrequiredstring (uuid)
templateSlugstring
templateVersionrequiredinteger

ProvisionedResourceManagement

How far the platform manages it: managed, a state the platform applied holds it, and destroy and drift are what they are for anything provisioned; recorded, it is recorded and nothing more — nothing was written to the cloud, its tags or any state.

Type: string enum

ProvisionedResourceOrigin

Where a row of the Provisioned list came from: provisioned, a create request made it; imported, it was made elsewhere and recorded by Import.

Type: string enum

ProvisionedResourcePage

FieldTypeDescription
itemsrequiredarray of ProvisionedResource
pagerequiredinteger
pageSizerequiredinteger
totalrequiredinteger (int64)
totalPagesrequiredinteger

ProvisionedResourceStatus

active: a create succeeded, and the drift diff watches it. partial: a create or a destroy stopped part-way; what exists is in the state at stateBackend, and a destroy is how it goes. A destroy that fails cleanly leaves the status as it was. destroyed: a destroy succeeded; the row stays as the record.

Type: string enum

ProvisioningAgent

FieldTypeDescription
accountIdrequiredstringThe one account this agent may ever claim work for. One agent per account (D4): an AWS account, or a Google Cloud project, whose id this is.
agentVersionstring
createdAtrequiredstring (date-time)
createdByrequiredActorRefWho did something, for display and for audit. Not User: that carries the tenant and the role of the session, and an audit record naming a role that has since changed would be a lie about the past.
idrequiredstring (uuid)
lastSeenAtstring (date-time)The last call this token made. A stale value on an account whose requests never start is the answer to why.
libraryReleaseintegerThe library release this agent last reported, on register or on a claim. Absent for an agent that runs no library templates.
namerequiredstring
revokedAtstring (date-time)
tofuVersionstring
versionStatusrequiredAgentVersionStatusReport and warn; refuse only below the declared minimum (D16). outdated still claims work — an operator upgrades a fleet over weeks, and a platform that stops the fleet the day it ships a release is a platform nobody upgrades. One of supported, outdated, unsupported.

ProvisioningAgentInstall

FieldTypeDescription
agentrequiredProvisioningAgent
expiresAtrequiredstring (date-time)When the token in the link lapses if no agent has registered with it: an hour after issue.
launchUrlrequiredstring (password)Returned once and never again. The AWS CloudFormation quick-create link for the account and region asked for: the stack cloudpanorama-agent with the platform address, the pinned image and the agent's token filled in. Open it signed in to that account.

ProvisioningAgentInstallCreate

FieldTypeDescription
accountIdrequiredstringThe AWS account the agent runs in: one of this tenant's accounts, enabled, or 409 account_not_managed. AWS only: the link is a CloudFormation stack.
dedicatedAccountrequiredbooleanThe admin's confirmation that the account holds nothing but what the agent creates (the owner, 2026-10-03). The one-step install is for a dedicated account only: in a shared account the agent could act through compute already there under a stronger identity. Must be true, or 400 invalid_request; recorded in the audit log. A shared account uses the manual install with a policy the customer scopes.
namerequiredstringThe agent's name, as for ProvisioningAgentTokenCreate.
regionrequiredstringThe AWS region the stack, and so the agent, runs in.

ProvisioningAgentPage

FieldTypeDescription
heartbeatTimeoutSecondsrequiredintegerThe server's agent_heartbeat_timeout: an agent whose lastSeenAt is older than this is stopped, and the portal says so using the same number the reaper uses, rather than one of its own.
itemsrequiredarray of ProvisioningAgent
pagerequiredinteger
pageSizerequiredinteger
totalrequiredinteger (int64)
totalPagesrequiredinteger

ProvisioningAgentTokenCreate

FieldTypeDescription
accountIdrequiredstringThe account the agent runs in, as the registry holds it: a 12-digit AWS account id, or a Google Cloud project id — the same two shapes AccountIdPath takes. It must be one of this tenant's accounts and enabled, or the answer is 409 account_not_managed.
namerequiredstringThe agent's name, as the Agents page lists it. Whitespace at either end is removed before it is stored, and what remains must not be empty: a name that is empty or only whitespace is 400 invalid_request, "name: must not be blank".

ProvisioningAgentTokenIssued

FieldTypeDescription
agentrequiredProvisioningAgent
tokenrequiredstring (password)Returned once and never again: only sha256(token) is stored. Install it in the agent's environment as its platform credential — and note that the agent must keep it out of OpenTofu's environment, which is what the §7.6 allowlist is for. A malicious template that could read this would have the tenant's provisioning API.

PublicEnvironment

One of PublicStatus's objects, and closed like it (see PublicStatus); named so the server builds it as a type rather than retyping it.

FieldTypeDescription
namerequiredstring
servicesDegradedrequiredinteger
servicesFailedrequiredinteger
servicesTotalrequiredinteger
servicesUnknownrequiredintegerServices with nothing confirmed lately: every resource in them is stale, or unknown. Neither healthy nor unhealthy, and not in servicesDegraded or servicesFailed.
statusrequiredPublicHealthAn environment's health as the public page says it: Status plus unknown, for an environment none of whose services has been confirmed inside its freshness window. Its own enum: a resource's unknown is health the inventory cannot see, which the public page counts as not confirmed, with the stale. One of failed, degraded, operational, unknown.
unhealthySincestring (date-time)When the environment stopped being healthy: the start of the oldest still-open incident among its unhealthy resources, each resource's incident beginning at its first non-operational transition after it was last operational (or deleted). Derived from the permanent per-resource history, so a neighbour recovering never moves it and it is not bounded by the timeline's window. Absent when the environment is healthy, and absent when no history establishes a start — the page then says the environment is unhealthy without saying since when.

PublicHealth

An environment's health as the public page says it: Status plus unknown, for an environment none of whose services has been confirmed inside its freshness window. Its own enum: a resource's unknown is health the inventory cannot see, which the public page counts as not confirmed, with the stale.

Type: string enum

PublicStatus

The public page's own type, not an internal type with fields omitted — a shared type is one careless serialisation tag away from leaking the whole inventory.

Never contains resource names, resource IDs, account IDs, regions, IP addresses, ARNs or additionalInfo.

Every object here is closed, including the nested ones. Without additionalProperties: false the allowlist is a comment: a response that added resourceId would still validate, and this is the one endpoint where that mistake is unauthenticated and public.

FieldTypeDescription
demorequiredbooleanTrue when this status page describes a fabricated estate (panorama admin seed-demo). The page says so on every load.
environmentsrequiredarray of PublicEnvironment
timelinerequiredarray of PublicTimelineEventAggregated. "3 instances became degraded", never a list of which.
timelineTruncatedrequiredbooleanTrue when the window held more events than timeline carries. This endpoint is unauthenticated and answers in one read, so the timeline is capped rather than paged; the page must say the list is the most recent part of the window rather than the whole of it. False when every event in the window is here.

PublicTenant

The slug the status page's own URL carries, and the demonstration mark it shows — and nothing else: the tenant's registered name is not public (review round 1).

FieldTypeDescription
demorequiredbooleanA demonstration tenant, as the status page marks it.
tenantSlugrequiredstring

PublicTimelineEvent

One of PublicStatus's objects, and closed like it (see PublicStatus); named so the server builds it as a type rather than retyping it.

FieldTypeDescription
descriptionrequiredstring
environmentrequiredstring
occurredAtrequiredstring (date-time)
servicerequiredstring
statusrequiredStatusThe health of something that currently exists. Derived once at ingest and stored, never recomputed at read time. deleted is absent, and that is the point: a deleted resource is tombstoned, not given a status, so a live resource carrying status: deleted is an impossible value. Keeping one enum for both made that impossible value contract-valid everywhere. unknown is a resource the inventory knows exists, or was recently active, whose health it cannot observe — found only from its metrics or its name. It is neither healthy nor a problem: never green, not on the Alerts page, and counted as not checked on the public page. One of failed, degraded, operational, unknown.

ReconciliationChargeType

FieldTypeDescription
chargeTyperequiredstring enum One of usage, credit, refund, tax, fee, commitment, other.
figuresrequiredarray of MoneyThe same money as the places, cut again; not a constituent of the total.

ReconciliationCoverage

FieldTypeDescription
coveragerequiredstring enum One of in_inventory, not_in_inventory, stale, no_collector, account_not_connected, exempt, unknown.
figuresrequiredarray of MoneyThe same money as the places, cut again; not a constituent of the total.

ReconciliationOwner

FieldTypeDescription
resourcesrequiredintegerThe row's billed resources this team owns.
teamrequiredTeamRefA team named where only its identity matters — an app's owner, or a team a picker offers. Team is the org model's full row, with the counts the settings page shows; this is the two fields everything else needs.

ReconciliationPlace

FieldTypeDescription
amountsrequiredarray of Money
placerequiredstring enum One of attributed, allocated, untagged.

ReconciliationRow

One (account, service, currency) in the window. Its amounts are decimal strings in the row's currency; the places, the coverage and the charge types each sum to total.

FieldTypeDescription
accountIdrequiredstring
apportionedrequiredbooleanThe breakdowns were scaled to the row's total because the source rows sum to something else.
chargeTypesrequiredobjectEach charge type's amount, keyed by ReconciliationChargeType.chargeType.
coveragerequiredobjectEach coverage value's amount, keyed by ReconciliationCoverage.coverage.
currencyrequiredstring
movesrequiredarray of string enumWhat would move the row's unhealthy values, each a kind the portal links to where it is done.
placesrequiredobjectEach place's amount, keyed by ReconciliationPlace.place.
servicerequiredstring
totalrequiredstring
untaggedOwnersrequiredarray of ReconciliationOwnerWhere the row holds untagged spend, the teams the ownership resolver infers for the row's billed resources the inventory holds, with how many each owns; empty where it infers none, and on a row with no untagged spend. Beside the money, never in it.

RegisteredAccount

A registration's answer: the account as the registry now holds it, and what to do in the customer's cloud before it can be enabled.

FieldTypeDescription
accountrequiredAccountexternalId is deliberately absent from every response. It is half of the confused-deputy defence on the cross-account role, and an API that hands it out has given away that half.
onboardingrequiredAccountOnboardingWhat an administrator has to do in the customer's cloud, and the values to do it with. externalId is present for an AWS account and is the one secret this contract ever returns. Every operation whose answer can carry it is marked x-step-up — the two registrations and the onboarding read — and the middleware's TestEveryAnswerCarryingTheExternalIdAsksForAStepUp holds that by the property rather than by a route's name, so a new operation answering with one inherits the rule.

RegisteredBucket

A registered bucket and the generation this run claimed for it.

Written out in full rather than composed from CollectionRunBucket with allOf. That composition was unsatisfiable and every successful registration violated it: additionalProperties is evaluated per subschema, so the closed base rejected the generation the sibling subschema added. Closing the request schema and extending it in a response are incompatible aims, and the request's closedness is the one that catches a real mistake — a collector sending a field that will be silently dropped.

The four identity fields are therefore duplicated on purpose. The response-validation test in internal/api/handlers is what keeps the copy honest.

FieldTypeDescription
generationrequiredinteger (int64)A run behind the current fence is superseded and may neither write nor finalize.
regionrequiredstring
resourceTyperequiredstring
servicerequiredstring
systemrequiredstring

ResolvedFinding

A finding an action resolved: the finding as the request recorded it when it was asked — its identity, label and cost — the request, and the request's recorded verdict, always verified and carrying recordedAt and the observation's evidence ids.

FieldTypeDescription
accountIdrequiredstring
costFindingCostThe finding's figure when the action was asked, in its own currency.
labelstringWhat the source called it when the action was asked.
requestrequiredResolvedFindingRequestThe request that resolved a finding, and the team that asked for it.
scoperequiredstring
sourcerequiredFindingSourceWhere a finding comes from, each read from its own table:
  • cost_anomaly — an open spend anomaly; scope its account or team.
  • unknown_status — a live resource whose status is unknown; scope the resource.
  • trusted_advisor — a recommendation Trusted Advisor flags (warning or error); sourceId is <account>/<check>/<resource>, scope the resource where the check names one the inventory maps, else the account; the figure is AWS's estimate and label the check's name.
  • cost_without_resource — spend in the last 14 days on a resource the inventory looks for and no longer has, or never had; scope the key.
  • drift_missing — a provisioned create the inventory sees fewer of than were stamped; sourceId the provisioned resource, scope its team, label its name.
  • drift_unprovisioned — a live resource of a kind provisioning produces that nobody provisioned; scope the resource.
  • stale_bucket — a bucket nothing has collected inside its window; sourceId <account>/<region>/<system>/<service>/<type>, scope the account, label the bucket.
  • tag_policy — a live resource with spend in the last 14 days whose tags the tenant's tag policy is not satisfied by, as the tag compliance read's violations list it; scope the resource, faults what to fix, the figure its spend. A change to the resource's tags is a new occurrence, which no earlier exception covers.
  • action_unverified — a provisioning action whose recorded verdict is unverified: its window passed and what a complete collection saw of its target was never the state the action reaches; sourceId the request, scope the team that asked (the request's own scope), label the request's name, accountId the target's account. Open until a complete collection registered after the action sees the target in that state, or a newer succeeded action of the same kind on the same target speaks for it; the recorded verdict itself never changes.
  • action_not_yet_observable — the same for a verdict of not_yet_observable: the window passed with no complete collection seeing the target at all.
One of cost_anomaly, unknown_status, trusted_advisor, cost_without_resource, drift_missing, drift_unprovisioned, stale_bucket, tag_policy, action_unverified, action_not_yet_observable.
sourceIdrequiredstring
stillReportedrequiredbooleanThe source still reports this occurrence of the finding — it is also open in GET /findings. A recurrence is a new occurrence, open again, and leaves this false.
verificationrequiredProvisionActionVerificationA succeeded action only (the qualifying observation): whether the inventory has since seen the target in the state the action reaches (expected; stopped for a stop) — from a collection, never from the agent's report. A qualifying observation is one of the target made after actedAt by a collection run that finalized; a run that did not finish, or has not yet, proves nothing. Judged on each read from the inventory's own records until it freezes, then recorded once and never changed: at the first qualifying observation in expected (verified), or once windowEndsAt has passed without one (unverified, not_yet_observable). A recorded verdict carries recordedAt and stands after the snapshot, run or log it names has aged out under its own retention; awaiting is never recorded. verified — a qualifying observation saw the target in expected (the earliest, as observation); awaiting — not yet, and the target's collection is not yet late (windowEndsAt: actedAt plus the bucket's freshness window, the inventory's own "late"); unverified — the window has passed, and the target was observed after the action but never in expected; not_yet_observable — the window has passed with no complete collection of the target since the action. observation is the proving one when verified, and otherwise the newest qualifying one, if any.

ResolvedFindingPage

FieldTypeDescription
itemsrequiredarray of ResolvedFinding
pagerequiredinteger
pageSizerequiredinteger
totalrequiredinteger (int64)
totalPagesrequiredinteger

ResolvedFindingRequest

The request that resolved a finding, and the team that asked for it.

FieldTypeDescription
actionrequiredResourceActionKindAn action on an existing resource — the closed set, one so far. stop stops a running EC2 instance (aws.ec2.instance): its template sets aws_ec2_instance_state to stopped, in a state of the request's own whose one resource only forgets the instance when it is deleted, so it never becomes a second owner of it. One of stop.
idrequiredstring (uuid)
namerequiredstring
teamrequiredTeamRefA team named where only its identity matters — an app's owner, or a team a picker offers. Team is the org model's full row, with the counts the settings page shows; this is the two fields everything else needs.

Resource

FieldTypeDescription
accountIdrequiredstring
additionalInfoobjectProvider-specific detail. How each key is displayed comes from /resource-types, not from the frontend.
cloudrequiredstringThe cloud the resource's account is in — aws, gcp or azure — and the value the system filter matches. The key's own system for every type but those collected under another word: a Kubernetes node or pod (k8s in its key) is in the cloud of the account its cluster runs in, aws for EKS. A type this server collects nothing of answers its key's system.
collectedAtstring (date-time)The bucket's last successful collection — when the resource was last confirmed. Absent when the bucket has never completed one.
firstSeenAtrequiredstring (date-time)
lastRunIdstring (uuid)The run that last observed this resource.
lastSeenAtrequiredstring (date-time)
ownershipOwnershipWho owns the resource, from the one resolver, derived when the row is read. state is the one-word answer; team, confidence, source and evidence are the winning tier's; reason says why there is no owner although a tier answered — a conflict between two matches of equal reach, or an archived team; tried lists the tiers that said nothing; candidates is every tier's answer, best first — the evidence one click away, and the discrepancy when a lower tier disagrees. Ownership explains and proposes: it never widens what a member sees, and never touches attribution. import (MILESTONE_1 §9.2) is a recorded import's owning team: explicit, as a declared owner is, and second only to a resource override — the correction a person makes stays the last word. Its evidence names the import and the template version.
regionrequiredstringglobal for global resources.
resourceIdrequiredstring
resourceNamerequiredstring
resourceTyperequiredstring
servicerequiredstring
stalerequiredbooleanTrue when the resource's bucket has no successful collection inside its freshness window — the module's cadence, plus the scheduler's maximum jitter, plus the configured headroom (FRESHNESS_HEADROOM). What is shown is what was last observed; the collector has not confirmed it since. Distinct from deletion, which tombstones the row and removes it from every list.
staterequiredstringThe raw provider state the status was derived from.
statusrequiredStatusThe health of something that currently exists. Derived once at ingest and stored, never recomputed at read time. deleted is absent, and that is the point: a deleted resource is tombstoned, not given a status, so a live resource carrying status: deleted is an impossible value. Keeping one enum for both made that impossible value contract-valid everywhere. unknown is a resource the inventory knows exists, or was recently active, whose health it cannot observe — found only from its metrics or its name. It is neither healthy nor a problem: never green, not on the Alerts page, and counted as not checked on the public page. One of failed, degraded, operational, unknown.
systemrequiredstringThe resource key's system (PLATFORM.md §3) — permanent, part of the resource's identity, and for most types the cloud. Not for every type: a Kubernetes node or pod is k8s in its key and runs in a cloud's account. What a person reads as the cloud is cloud.
tagsobjectThe provider's own tags, stored exactly as the provider returned them — keys included. AWS is case-sensitive about keys and customers are inconsistent, and inventory is a faithful record of what is actually there; case-folding belongs to whatever is matching against it (cost attribution, COST_SPEC.md §7.1), not here.

ResourceActionCreate

An action asked for: its kind, the action template's version it pins, and the one resource it acts on, by its key. No variables: the server fills the template's from the target — a stop's instance_id from resourceId and region from region — and no name: it is derived, stop-i-0abc…. The owning team is the account's. origin, optional, names the finding the action answers.

FieldTypeDescription
accountIdrequiredstring
actionrequiredResourceActionKindAn action on an existing resource — the closed set, one so far. stop stops a running EC2 instance (aws.ec2.instance): its template sets aws_ec2_instance_state to stopped, in a state of the request's own whose one resource only forgets the instance when it is deleted, so it never becomes a second owner of it. One of stop.
originResourceActionOriginThe finding an action answers, by its identity as GET /findings shows it — (source, sourceId, scope), all three. Only the identity is taken from the asker: the server finds the finding as the findings read projects it now, on the asker's own scope (their teams' accounts), and records what it read — never anything the asker says it showed. Refused unless such a finding exists and the asker sees it, its scope is the target's key — the finding is about the resource the action acts on — and its source is one the kind answers (stop: trusted_advisor).
regionrequiredstring
resourceIdrequiredstring
resourceTyperequiredstring
servicerequiredstring
systemrequiredstring
templateIdrequiredstring (uuid)A template enabled from the library whose version carries out this kind of action. A version that makes things, or carries out another kind, is refused.
templateVersionrequiredintegerExplicit, never "latest", as for a create.

ResourceActionKind

An action on an existing resource — the closed set, one so far. stop stops a running EC2 instance (aws.ec2.instance): its template sets aws_ec2_instance_state to stopped, in a state of the request's own whose one resource only forgets the instance when it is deleted, so it never becomes a second owner of it.

Type: string enum

ResourceActionOffer

One action a person may ask for on a resource now: the body of POST /provisioning/actions but for the key, which the asker already holds.

FieldTypeDescription
actionrequiredResourceActionKindAn action on an existing resource — the closed set, one so far. stop stops a running EC2 instance (aws.ec2.instance): its template sets aws_ec2_instance_state to stopped, in a state of the request's own whose one resource only forgets the instance when it is deleted, so it never becomes a second owner of it. One of stop.
templateIdrequiredstring (uuid)
templateNamerequiredstring
templateVersionrequiredinteger

ResourceActionOfferList

FieldTypeDescription
itemsrequiredarray of ResourceActionOffer

ResourceActionOrigin

The finding an action answers, by its identity as GET /findings shows it — (source, sourceId, scope), all three. Only the identity is taken from the asker: the server finds the finding as the findings read projects it now, on the asker's own scope (their teams' accounts), and records what it read — never anything the asker says it showed. Refused unless such a finding exists and the asker sees it, its scope is the target's key — the finding is about the resource the action acts on — and its source is one the kind answers (stop: trusted_advisor).

FieldTypeDescription
scoperequiredstring
sourcerequiredFindingSourceWhere a finding comes from, each read from its own table:
  • cost_anomaly — an open spend anomaly; scope its account or team.
  • unknown_status — a live resource whose status is unknown; scope the resource.
  • trusted_advisor — a recommendation Trusted Advisor flags (warning or error); sourceId is <account>/<check>/<resource>, scope the resource where the check names one the inventory maps, else the account; the figure is AWS's estimate and label the check's name.
  • cost_without_resource — spend in the last 14 days on a resource the inventory looks for and no longer has, or never had; scope the key.
  • drift_missing — a provisioned create the inventory sees fewer of than were stamped; sourceId the provisioned resource, scope its team, label its name.
  • drift_unprovisioned — a live resource of a kind provisioning produces that nobody provisioned; scope the resource.
  • stale_bucket — a bucket nothing has collected inside its window; sourceId <account>/<region>/<system>/<service>/<type>, scope the account, label the bucket.
  • tag_policy — a live resource with spend in the last 14 days whose tags the tenant's tag policy is not satisfied by, as the tag compliance read's violations list it; scope the resource, faults what to fix, the figure its spend. A change to the resource's tags is a new occurrence, which no earlier exception covers.
  • action_unverified — a provisioning action whose recorded verdict is unverified: its window passed and what a complete collection saw of its target was never the state the action reaches; sourceId the request, scope the team that asked (the request's own scope), label the request's name, accountId the target's account. Open until a complete collection registered after the action sees the target in that state, or a newer succeeded action of the same kind on the same target speaks for it; the recorded verdict itself never changes.
  • action_not_yet_observable — the same for a verdict of not_yet_observable: the window passed with no complete collection seeing the target at all.
One of cost_anomaly, unknown_status, trusted_advisor, cost_without_resource, drift_missing, drift_unprovisioned, stale_bucket, tag_policy, action_unverified, action_not_yet_observable.
sourceIdrequiredstring

ResourceActionTarget

The one inventory resource an action acts on: its key (PLATFORM.md §3), every part named and matched exactly — no wildcard can be written.

FieldTypeDescription
accountIdrequiredstring
regionrequiredstring
resourceIdrequiredstring
resourceTyperequiredstring
servicerequiredstring
systemrequiredstring

ResourceCategory

What kind of thing a resource type is (MILESTONE_1 §3). One taxonomy, declared once beside the collector that emits the type, and the thing the catalog, the filters, the Dashboard's blocks and the hottest lists all group by — so none of them carries a list of types of its own.

other is the last bucket: a legacy templates.category value outside the set lists under it. Nothing the platform emits is other — every resource type resolves to one of the seven real categories — so a client may show it only where template categories appear, and never when it is empty.

PerformanceSection is the same vocabulary, narrowed: a Performance page exists only for a category with measured types.

Type: string enum

ResourceDetail

FieldTypeDescription
historySnapshotHistoryHow much of the resource's past is retained, so a missing lastChange is explained rather than shown as an empty diff. sole_observation: one observation is retained. older_not_retained: the retained observations differ in nothing, and whatever changed last did so before retention.
lastChangeSnapshotDiffWhat changed from one observation to a later one: the one canonical diff, computed by the same function whether it was stored beside the snapshot at ingest or recomputed for two observations picked on request. from is always the older observation, whatever order a request named them in; selectionReversed says when the request had them the other way round.
resourcerequiredResource
snapshotsrequiredarray of ResourceSnapshotThe resource's most recently recorded states, newest first. A state is recorded when its status, state, details or tags change, and once a day while nothing does — not once per collection.
transitionsrequiredarray of TransitionStatus changes for this resource, newest first.

ResourceImport

A resource recorded by Import (MILESTONE_1 §9.2): which resource, under which template version, owned by which team, who recorded it and when — and, once released, who released it and when.

FieldTypeDescription
accountIdrequiredstring
adoptRequestIdstring (uuid)The live or last request making it fully managed.
idrequiredstring (uuid)
needsAttentionbooleanThe import is imported and no live request acts on it: its state holds the resource, and the retag that would stamp it failed or was cancelled (MILESTONE_1 §9.3.4). Forward is a retry of adoptRequestId; back is a release from state (POST /provisioning/imports/{importId}/release-from-state). It never returns to recorded on its own.
recordedAtrequiredstring (date-time)
recordedByrequiredActorRefWho did something, for display and for audit. Not User: that carries the tenant and the role of the session, and an audit record naming a role that has since changed would be a lie about the past.
regionrequiredstring
releasedAtstring (date-time)
releasedByActorRefWho did something, for display and for audit. Not User: that carries the tenant and the role of the session, and an audit record naming a role that has since changed would be a lie about the past.
resourceIdrequiredstring
resourceNamestringThe resource's name as the inventory last saw it, when it has one.
resourceTyperequiredstring
servicerequiredstring
staterequiredResourceImportStateAn import's lifecycle (MILESTONE_1 §9.3.4). recorded and released are step one's; adopting, imported, managed and destroyed are step two's, "make fully managed". released and destroyed are terminal, and free the resource to be imported again. One of recorded, adopting, imported, managed, released, destroyed.
systemrequiredstring
teamrequiredTeamRefA team named where only its identity matters — an app's owner, or a team a picker offers. Team is the org model's full row, with the counts the settings page shows; this is the two fields everything else needs.
templateIdrequiredstring (uuid)
templateModerequiredResourceImportTemplateModeHow an import is recorded: catalog, under a catalog template's version; generated, under a private template generated for it. Absent on a create means catalog. One of catalog, generated.
templateNamestring
templateSlugstring
templateVersionrequiredintegerThe version the import is recorded under — and, once an adopt of it succeeds, the version that adopt applied: for a generated import, the configuration its approver saw rather than the skeleton it was recorded under.

ResourceImportCreate

The resource by its key (PLATFORM.md §3), the template version it is recorded under, and the team that owns it. templateVersion left out pins the template's requestable version.

templateMode: generated names no template: the import is recorded under a private template made for it — one per import, never in the catalog — whose first version is the server's skeleton, the provider configuration alone. Making it fully managed generates the rest (ProvisionRequestCreate). Refused: a template named with it, or none named without it (400 invalid_request); and a resource type the companion table does not know, or an inventory record that cannot finish the skeleton or an identifier (400 invalid_request, naming it).

FieldTypeDescription
accountIdrequiredstring
regionrequiredstring
resourceIdrequiredstring
resourceTyperequiredstring
servicerequiredstring
systemrequiredstring
teamIdrequiredstring (uuid)
templateIdstring (uuid)Required unless templateMode is generated, and refused with it.
templateModeResourceImportTemplateModeHow an import is recorded: catalog, under a catalog template's version; generated, under a private template generated for it. Absent on a create means catalog. One of catalog, generated.
templateVersioninteger

ResourceImportPage

FieldTypeDescription
itemsrequiredarray of ResourceImport
pagerequiredinteger
pageSizerequiredinteger
totalrequiredinteger (int64)
totalPagesrequiredinteger

ResourceImportState

An import's lifecycle (MILESTONE_1 §9.3.4). recorded and released are step one's; adopting, imported, managed and destroyed are step two's, "make fully managed". released and destroyed are terminal, and free the resource to be imported again.

Type: string enum

ResourceImportTemplateMode

How an import is recorded: catalog, under a catalog template's version; generated, under a private template generated for it. Absent on a create means catalog.

Type: string enum

ResourceNeighbours

FieldTypeDescription
edgesrequiredarray of NeighbourEdge
elidedrequiredintegerNeighbours the viewer's team scope does not admit: counted, never rendered.
lookedForrequiredarray of NeighbourEdgeKindEvery kind the read tried, found or not.
nodesrequiredarray of NeighbourNode
rootrequiredNeighbourNodeOne thing in the graph. id is stable across reads: a resource's account and key, or a request's, template version's, app's or team's id.
truncatedrequiredbooleanA cap was hit.
truncatedCountsrequiredobjectHow many edges of each kind a cap left out.
unavailablerequiredarray of objectThe kinds that could not be answered for the time asked, each with its reason.

ResourcePage

FieldTypeDescription
itemsrequiredarray of Resource
pagerequiredinteger
pageSizerequiredinteger
statusCountsStatusCountsAbsent here. /resources/search returns ResourceSearchPage, where it is required.
totalrequiredinteger (int64)
totalPagesrequiredinteger

ResourceRef

One resource: its account and its key, region/system/service/type/id.

FieldTypeDescription
accountIdrequiredstring
keyrequiredstring

ResourceSearchPage

A separate schema so statusCounts is REQUIRED rather than optional. Search always returns it, and a generated client that has to null-check a field the server always sends will eventually stop checking — which is exactly when it will be absent.

FieldTypeDescription
nextCursorstringReads the next page as cursor. Present in the default order, by resource name, when there is a next page; absent otherwise. Whether there is one is read from the rows, one past the page, not from total: collection adds and removes resources while a list is paged, and the count moves with it. In that order totalPages follows the walk — at least page + 1 while nextCursor is present, and page on the page where it ends, an empty one included.
statusCountsrequiredStatusCountsComputed over the full filtered set, not the current page.
typeCountsrequiredarray of TypeCountHow many resources each type holds, over the full filtered set without the resourceType filter: the types a type filter can offer in this view, so choosing one does not take the others off the list. A type with no resource in the set is absent, never a zero. In key order.

ResourceSnapshot

One stored observation, as the API returns it. status and state are present because the server derived them; see SnapshotInput for what a collector actually sends.

FieldTypeDescription
accountIdrequiredstring
additionalInfoobject
idinteger (int64)The observation's handle for /resources/snapshots/diff. Present on every observation the server returns.
observedAtrequiredstring (date-time)
regionrequiredstring
resourceIdrequiredstring
resourceNamestring
resourceTyperequiredstring
schemaVersionintegerThe render descriptor version the observation was written under; 0 for a type no collector describes. Two observations under different versions diff as one schema change.
servicerequiredstring
staterequiredstringThe raw provider state the status was derived from.
statusrequiredStatusThe health of something that currently exists. Derived once at ingest and stored, never recomputed at read time. deleted is absent, and that is the point: a deleted resource is tombstoned, not given a status, so a live resource carrying status: deleted is an impossible value. Keeping one enum for both made that impossible value contract-valid everywhere. unknown is a resource the inventory knows exists, or was recently active, whose health it cannot observe — found only from its metrics or its name. It is neither healthy nor a problem: never green, not on the Alerts page, and counted as not checked on the public page. One of failed, degraded, operational, unknown.
systemrequiredstring
tagsobjectThe tags as they were at this observation. A tag that changed is a fact about the past, which is what this table is for.

ResourceTypeDescriptor

FieldTypeDescription
categoryrequiredResourceCategoryWhat kind of thing a resource type is (MILESTONE_1 §3). One taxonomy, declared once beside the collector that emits the type, and the thing the catalog, the filters, the Dashboard's blocks and the hottest lists all group by — so none of them carries a list of types of its own. other is the last bucket: a legacy templates.category value outside the set lists under it. Nothing the platform emits is other — every resource type resolves to one of the seven real categories — so a client may show it only where template categories appear, and never when it is empty. PerformanceSection is the same vocabulary, narrowed: a Performance page exists only for a category with measured types. One of compute, networking, databases, messaging, serverless, storage, security_and_identity, other.
groupsrequiredarray of FieldGroup
iconstring
importablebooleanWhether Import can adopt an existing resource of this type into a template's state. Absent means the deployment cannot say, for the same reason as provisionable.
labelrequiredstring
listColumnsrequiredarray of stringField keys to show as columns in the list view.
measuredrequiredbooleanWhether the Performance catalogue measures this type — whether a resource of it has series to show. False for a type nothing publishes metrics for, which is a fact about the catalogue and not about any one resource.
pluralLabelrequiredstring
provisionablebooleanWhether this deployment can create one through Provisioning: some enabled or library template's version declares it. It says the product offers the type, not that this user may request it — entitlement and approval decide that at the request. Absent means the deployment cannot say, which is not false. A template's declared effects are what answer this, and nothing records them until the library (§8.1) and the resource-level contract (§9.3) land; until then the field is omitted rather than answered false, because "no template makes this" and "we do not know what our templates make" are different facts and a client that showed the second as the first would be lying for us.
resourceTyperequiredstring
servicerequiredstring
systemrequiredstring

ResourceTypeList

FieldTypeDescription
resourceTypesrequiredarray of ResourceTypeDescriptor

SavingsAction

The succeeded action a claim came from: its request, its kind and its name.

FieldTypeDescription
actionrequiredstring
namerequiredstring
requestIdrequiredstring (uuid)

SavingsClaim

FieldTypeDescription
actionSavingsActionThe succeeded action a claim came from: its request, its kind and its name.
changedOnrequiredstring (date)
claimantActorRefWho made the claim (claimedBy), for display. Absent once the person is removed.
claimedByrequiredstring (uuid)
claimedCountrequiredinteger
claimedRuleSavingsTagRule
controlCountrequiredinteger
controlRuleSavingsTagRule
createdAtrequiredstring (date-time)
currentrequiredSavingsVerdictOne computed verdict. A day is covered when every account of the claim has per-resource evidence for it; the others are listed. Settled is through two days ago. adjustmentsSeparated is false when a covered day's evidence is Cost Explorer's, whose rows carry no charge type: the figures then have one component. word is the claim's across its currencies and never overstates one of them.
descriptionrequiredstring
idrequiredstring (uuid)
recommendationSavingsRecommendationThe Trusted Advisor recommendation a claim came from, as it stood when the claim was made. estimatedMonthlySavings is AWS's estimate, a month's, never a measurement: it is shown beside the verdict and is in no total. Absent where AWS gave no figure.
teamIdstring (uuid)Absent on a tenant-wide claim.
teamNamestringThe claim's team's name; absent on a tenant-wide claim.
windowDaysrequiredinteger

SavingsClaimCreate

A manual claim names changedOn, description and claimed. A claim over an action names actionRequestId instead: a succeeded request of one of the action kinds (stop) of the caller's teams. Its target is the claimed set, the day the request ended is the change, its team is the claim's team and, unless description is given, its name is the description; naming changedOn, claimed or another teamId beside it is invalid_request.

FieldTypeDescription
actionRequestIdstring (uuid)The succeeded action the claim came from.
changedOnstring (date)The day of the change; it is in neither window. Required unless the claim is over an action.
claimedSavingsSetSpecA list of resources, or a tag match — exactly one of them.
controlSavingsSetSpecA list of resources, or a tag match — exactly one of them.
descriptionstringWhat was done. Required unless the claim is over an action.
recommendationSavingsRecommendationRefThe Trusted Advisor recommendation the claim came from, by the three names GET /cost/recommendations gives it. It must be an open recommendation in the claim's accounts; its check name and AWS's estimate are copied onto the claim.
teamIdstring (uuid)The team the claim is for; omitted, a tenant-wide claim (administrators only), or the action's team.
windowDaysintegerEach window's length; 14 when omitted.

SavingsClaimDetail

A claim, the sets it froze — with the tag rule each was matched by, if any — and every version.

FieldTypeDescription
claimrequiredSavingsClaim
claimedrequiredarray of SavingsResourceRef
controlrequiredarray of SavingsResourceRef
versionsrequiredarray of SavingsVerdict

SavingsClaimList

FieldTypeDescription
itemsrequiredarray of SavingsClaim

SavingsComparison

What a verified amount was verified against. control_set: the claimed set's before-spend, moved by the control set's own ratio, is what the after-window would have cost; what that exceeds the actual by is the saving it supports, never more than the reduction observed. cost_per_unit (when no control set is usable): the Cost and Usage Report's own quantities — the saving supported is the rate part of the usage_and_rate components, what the after-window's work costs less per unit than before; less work at the same price supports nothing. Evidence for the claim, not proof of its cause.

Type: string enum

SavingsComponent

FieldTypeDescription
afterAmountrequiredstring
beforeAmountrequiredstring
componentrequiredstring enumadjustments are the report's Credit, Refund, Tax and Fee lines. usage_and_rate is one usage dimension (usage type, operation, pricing unit) with the report's quantity before the change: its reduction splits into usageReductionAmount (the change in quantity at the before rate) and rateReductionAmount (the change in price on the after quantity). Quantities in different units are never added. amount_moved is everything else — usage and rate together, where no quantity separates them (Cost Explorer days, reports without quantities, a dimension new after the change). One of amount_moved, usage_and_rate, adjustments.
currencyrequiredstring
operationstring
quantityAfterstring
quantityBeforestring
rateReductionAmountstring
reductionAmountrequiredstring
unitstringThe report's pricing/unit, for usage_and_rate.
usageReductionAmountstring
usageTypestring

SavingsCurrencyVerdict

One currency's answer, judged on the amortized amount moved: an adjustment is not engineering. verifiedAmount is present when the word is verified or partly_verified; controlBeforeAmount and controlAfterAmount whenever the claim has a control set.

FieldTypeDescription
comparisonrequiredSavingsComparisonWhat a verified amount was verified against. control_set: the claimed set's before-spend, moved by the control set's own ratio, is what the after-window would have cost; what that exceeds the actual by is the saving it supports, never more than the reduction observed. cost_per_unit (when no control set is usable): the Cost and Usage Report's own quantities — the saving supported is the rate part of the usage_and_rate components, what the after-window's work costs less per unit than before; less work at the same price supports nothing. Evidence for the claim, not proof of its cause. One of none, control_set, cost_per_unit.
controlAfterAmountstring
controlBeforeAmountstring
currencyrequiredstring
denominatorsarray of stringThe pricing units cost per unit was read in, when the comparison is cost_per_unit.
figuresrequiredarray of SavingsFigure
verifiedAmountstring
wordrequiredSavingsWord One of verified, partly_verified, reduced_cause_uncertain, not_yet_measurable, no_saving_measured.

SavingsFigure

One money figure of one currency for the claimed set. beforeAmount and afterAmount are sums over each window's covered days; reductionAmount is the before scaled to the after-window's covered days, less the after — so with every day covered it is their difference. It is the sum of the components' reductions, exactly.

FieldTypeDescription
afterAmountrequiredstring
beforeAmountrequiredstring
componentsrequiredarray of SavingsComponent
currencyrequiredstring
figurerequiredstring enum One of amortized, unblended.
reductionAmountrequiredstring

SavingsLedger

FieldTypeDescription
groupByrequiredstring enum One of team, app.
groupsrequiredarray of SavingsLedgerGroup
inNoTeamrequiredbooleanThe reader is not an administrator and belongs to no team, so the ledger is empty for that reason; as CostReadMeta.inNoTeam.
totalsrequiredarray of SavingsLedgerTotalOver every claim the caller may read, each counted claim once.

SavingsLedgerEntry

One claim in the ledger. counted says its figures are in the totals; a measured claim that is not counted overlaps one that is. inApp is set when the ledger is cut by app.

FieldTypeDescription
claimrequiredSavingsClaim
countedrequiredboolean
inAppstring enum One of whole, part.
overlapsElsewhererequiredintegerHow many overlapping claims the caller may not read.
overlapsWithrequiredarray of string (uuid)The overlapping claims the caller may read.

SavingsLedgerGroup

One team's or declared app's claims. Without key, the claims with no team (by team) or in no app (by app); that group comes last.

FieldTypeDescription
entriesrequiredarray of SavingsLedgerEntry
keystringThe team's or the app's id.
namestring
totalsrequiredarray of SavingsLedgerTotal

SavingsLedgerTotal

One currency's totals over counted claims: the amortized observed reduction (the measured difference, whatever caused it) and the verified amount, each a sum over the claims' own after-windows, and never added to each other. claims is how many claims they sum.

FieldTypeDescription
claimsrequiredinteger
currencyrequiredstring
observedReductionAmountrequiredstring
verifiedAmountrequiredstring

SavingsRecommendation

The Trusted Advisor recommendation a claim came from, as it stood when the claim was made. estimatedMonthlySavings is AWS's estimate, a month's, never a measurement: it is shown beside the verdict and is in no total. Absent where AWS gave no figure.

FieldTypeDescription
accountIdrequiredstring
checkIdrequiredstring
checkNamerequiredstring
estimatedMonthlySavingsMoneyOne amount in one currency. The only way money appears in this document. TestAllMoneyIsCurrencyQualified walks every response schema and fails on any amount-shaped property that is not inside this object or an array of it, because "a scalar total" is the one contract mistake that cannot be retrofitted once a client has summed two currencies (C7). amount is a decimal as a string, never a JSON number: numeric(20,10) does not survive a float, and a total that reconciles to the cent is the product.
resourceIdrequiredstring

SavingsRecommendationRef

The Trusted Advisor recommendation the claim came from, by the three names GET /cost/recommendations gives it. It must be an open recommendation in the claim's accounts; its check name and AWS's estimate are copied onto the claim.

FieldTypeDescription
accountIdrequiredstring
checkIdrequiredstring
resourceIdrequiredstring

SavingsResourceRef

FieldTypeDescription
accountIdrequiredstring
resourceKeyrequiredstringregion/system/service/type/id

SavingsSetSpec

A list of resources, or a tag match — exactly one of them.

FieldTypeDescription
resourcesarray of SavingsResourceRef
tagSavingsTagRule

SavingsTagRule

FieldTypeDescription
keyrequiredstring
valuerequiredstring

SavingsVerdict

One computed verdict. A day is covered when every account of the claim has per-resource evidence for it; the others are listed. Settled is through two days ago. adjustmentsSeparated is false when a covered day's evidence is Cost Explorer's, whose rows carry no charge type: the figures then have one component. word is the claim's across its currencies and never overstates one of them.

FieldTypeDescription
adjustmentsSeparatedrequiredboolean
afterWindowrequiredCostPeriod
beforeWindowrequiredCostPeriod
computedAtrequiredstring (date-time)
coveredDaysAfterrequiredinteger
coveredDaysBeforerequiredinteger
currenciesrequiredarray of SavingsCurrencyVerdict
methodrequiredstringThe method version the verdict was computed with.
missingDaysAfterrequiredarray of string (date)
missingDaysBeforerequiredarray of string (date)
reasonstring enumWhy it is not yet measurable. One of after_window_unsettled, no_evidence.
settledThroughrequiredstring (date)
supersededAtstring (date-time)
versionrequiredinteger
wordrequiredSavingsWord One of verified, partly_verified, reduced_cause_uncertain, not_yet_measurable, no_saving_measured.

SavingsWord

Type: string enum

SessionsEnded

FieldTypeDescription
endedrequiredinteger

SignOnIdentitiesReleased

FieldTypeDescription
releasedrequiredintegerThe accounts whose provider identity was cleared.

SingleSignOnOptions

FieldTypeDescription
enabledrequiredboolean
enforcedrequiredbooleanPasswords work only for break-glass administrators.

SnapshotBatchRequest

FieldTypeDescription
itemsrequiredarray of SnapshotInputNo duplicate resource keys within a batch: a duplicate makes the upsert order-dependent, which makes transitions non-deterministic.
runIdrequiredstring (uuid)The run these observations belong to. Every item must fall inside a bucket that run registered, or the batch is rejected.

SnapshotBatchResponse

FieldTypeDescription
replayedbooleanTrue when this key had already been processed, so nothing was written and these are the stored counts.
storedrequiredintegerSnapshots written.
transitionsrequiredintegerStatus changes detected and recorded.

SnapshotDiff

What changed from one observation to a later one: the one canonical diff, computed by the same function whether it was stored beside the snapshot at ingest or recomputed for two observations picked on request. from is always the older observation, whatever order a request named them in; selectionReversed says when the request had them the other way round.

FieldTypeDescription
attributionChangeAttributionWho may have made the change, on a stored change — the lastChange the ingest recorded and ?snapshotId= — and never on a comparison of two observations picked on request, which is not one change.
entriesrequiredarray of DiffEntry
fromrequiredSnapshotRefOne end of a diff — an observation by its id and time.
schemaChangedobjectThe two observations were written under different descriptor versions, so additionalInfo was not compared field by field.
selectionReversedboolean
torequiredSnapshotRefOne end of a diff — an observation by its id and time.

SnapshotHistory

How much of the resource's past is retained, so a missing lastChange is explained rather than shown as an empty diff. sole_observation: one observation is retained. older_not_retained: the retained observations differ in nothing, and whatever changed last did so before retention.

FieldTypeDescription
notestring enum One of sole_observation, older_not_retained.
oldestRetainedAtstring (date-time)
retainedrequiredintegerThe number of observations retained.

SnapshotInput

One observation, as a collector reports it.

Deliberately NOT the same schema as ResourceSnapshot. A collector reports what it saw; it does not report health. status and state are derived server-side at ingest by the deriver registered for the resource type, so that status is computed once, in one place, and stored — which is what makes a transition detectable at all. A collector that could submit its own status could also submit a different one each run, and no two modules would agree.

Identity

accountId, region, system, service, resourceType and resourceId together are the resource's primary key. Each must be non-empty and must carry no leading or trailing whitespace.

Violations are rejected with 400 invalid_batch, never trimmed, because i-123 and i-123 are two different resources for one real thing.

The duplicate row is not the lasting damage — the next successful finalize that does not observe it tombstones it, and retention purges that tombstone about thirty days later. The transitions are. Creating it writes a first observation, tombstoning it writes a Deleted, and transitions are kept forever: a moment of stray whitespace leaves permanent history for a resource that never appeared and never went away, next to a live one whose real history has a gap where those events should be. The Deleted also notifies, so somebody is told a resource vanished that never existed. While both rows exist the inventory and its status counts double-count the resource.

Trimming server-side would be worse than rejecting, not better: it would store a value the collector never sent, so the id in the inventory would no longer be the id the provider reports. Whitespace inside a value is untouched, because some providers use it.

maxLength on resourceId counts characters, as JSON Schema defines it, not bytes.

FieldTypeDescription
accountIdrequiredstring
additionalInfoobjectProvider-specific detail, at most 256 KB when marshalled. This is also the deriver's input: status and state are computed from it.
observedAtstring (date-time)Optional; the server uses its own clock when absent. When present it must be within ±24 hours of now — a clock-skewed collector writing far-future observations would suppress every later one.
regionrequiredstring
resourceIdrequiredstring
resourceNamestring
resourceTyperequiredstring
servicerequiredstring
systemrequiredstring
tagsobjectThe provider's own tags, at most 64 of them, keys at most 128 characters and values at most 256. Stored exactly as returned. Replaced wholesale on every observation, never merged. A tag removed in the provider has to disappear here, and merging would make removal unobservable — which breaks "resources tagged env=prod that should not be", and would leave a stale panorama:request_id on a resource re-created outside provisioning.

SnapshotPage

Cursor-paginated as TimelinePage is, newest first; nextCursor absent means the last page.

FieldTypeDescription
itemsrequiredarray of ResourceSnapshot
nextCursorstring

SnapshotRef

One end of a diff — an observation by its id and time.

FieldTypeDescription
idrequiredinteger (int64)
observedAtrequiredstring (date-time)

Status

The health of something that currently exists. Derived once at ingest and stored, never recomputed at read time.

deleted is absent, and that is the point: a deleted resource is tombstoned, not given a status, so a live resource carrying status: deleted is an impossible value. Keeping one enum for both made that impossible value contract-valid everywhere.

unknown is a resource the inventory knows exists, or was recently active, whose health it cannot observe — found only from its metrics or its name. It is neither healthy nor a problem: never green, not on the Alerts page, and counted as not checked on the public page.

Type: string enum

StatusCounts

Computed over the full filtered set, not the current page.

FieldTypeDescription
degradedrequiredinteger (int64)
failedrequiredinteger (int64)
operationalrequiredinteger (int64)
stalerequiredinteger (int64)How many of the counted resources are stale — in a bucket with no successful collection inside its freshness window. They are counted under their last known status as well; this is the part of each count the collector has not confirmed lately.
unknownrequiredinteger (int64)Resources whose health the inventory cannot observe; neither healthy nor a problem.

StepUpAtProvider

FieldTypeDescription
authorizationUrlrequiredstringThe provider's address the browser goes to.

StepUpAtProviderRequest

FieldTypeDescription
returnTostringA path on this site to come back to after the provider; anything else is the portal's root.

StepUpRequest

FieldTypeDescription
passwordrequiredstringThe current user's password.

SubscriptionRegistration

An Azure subscription an administrator is connecting.

FieldTypeDescription
clientIdrequiredstringThe application (client) id of the app registration this deployment signs in as — the one deploy/azure/reader.bicep outputs.
environmentrequiredstring enum One of production, staging, sdlc, shared, unknown.
namerequiredstring
subscriptionIdrequiredstringThe subscription's id, a GUID; held lower-case.
tenantIdrequiredstringThe directory (tenant) the subscription trusts, by its id.

TagComplianceBreakdown

The five classes with the money behind each, and the scope total they sum to.

FieldTypeDescription
classesrequiredarray of TagComplianceClass
discrepancyrequiredarray of MoneyA currency whose per-resource detail overshot the authoritative total — larger in magnitude, in the same direction. Empty is the ordinary case; a value means the classified classes were scaled to fit the total — their proportions survive, their absolutes bend to the authoritative figure — and this is the difference, signed. Detail that falls short of the total, or runs opposite to it, is not a discrepancy and is not scaled: unknown_coverage carries the difference, which is what that class is for. It is therefore signed too — a window whose authoritative figure is a net credit with nothing classified against it reports that credit as unknown coverage rather than as zero. The five classes always sum to totals, whatever the signs.
inNoTeamrequiredbooleanThe reader is not an administrator and belongs to no team, so every class is empty for that reason; as CostReadMeta.inNoTeam.
totalsrequiredarray of MoneyThe scope's authoritative net spend, which the five classes sum to. Signed: credits, refunds and negative adjustments are ordinary rows, so a window's figure can be a net credit.
windowrequiredCostPeriod

TagComplianceClass

One class's share of the scope's spend.

FieldTypeDescription
amountsrequiredarray of Money
classrequiredstring enum One of compliant, missing_required_key, value_not_allowed, exempt, unknown_coverage.
resourcesrequiredintegerHow many resources fell in this class. Zero for unknown_coverage, whose amount is a remainder rather than a count of anything.

TagComplianceGroup

One subject's share of the classes.

FieldTypeDescription
classesrequiredarray of TagComplianceClass
keyrequiredstringThe account id, team id or app id. Empty is the unowned subject of a team grouping.
labelrequiredstring

TagComplianceGroupList

FieldTypeDescription
groupByrequiredstring enum One of account, team, app.
groupsrequiredarray of TagComplianceGroup
windowrequiredCostPeriod

TagFault

One fault a resource carries, named so a reader can act on it.

FieldTypeDescription
keyrequiredstring
kindrequiredstring enum One of missing_required_key, value_not_allowed.
valuestringWhat the resource carries, when the fault is that the value is not allowed.

TagMatch

One tag match: a resource carrying key=value is in the app.

FieldTypeDescription
keyrequiredstring
valuerequiredstring

TagPolicy

The tenant's tag policy. declared false means nobody wrote one and this is the attribution keys, derived.

FieldTypeDescription
declaredrequiredbooleanThe tenant wrote this policy. False means it is derived from the attribution keys.
exemptResourceTypesrequiredarray of stringResource types the policy does not apply to. An exempt resource is never compliant and never in violation — it is exempt, whatever it carries.
requiredKeysrequiredarray of TagPolicyKey

TagPolicyKey

One key a resource must carry, and the values it admits.

FieldTypeDescription
allowedValuesrequiredarray of stringEmpty admits any non-empty value. The team key always carries the tenant's team_tag_values, whatever was stored, so this page and attribution cannot disagree about which value names a team.
keyrequiredstring

TagPolicyUpdate

A whole-document replace of the policy.

FieldTypeDescription
exemptResourceTypesrequiredarray of string
requiredKeysrequiredarray of TagPolicyKey

TagViolation

One resource the policy is not satisfied by.

FieldTypeDescription
accountIdrequiredstring
amountsrequiredarray of Money
classrequiredstring enum One of missing_required_key, value_not_allowed.
faultsrequiredarray of TagFault
ownStaterequiredstring enumThe ownership verdict, empty where the resolver has not answered. One of , owned, unowned, conflict.
proposedrequiredobjectThe value the product can justify for a key, where it can. Empty for every key nothing is inferable for, which is most of them. Today that is the attribution team key alone, and the value is one the tenant's own team_tag_values maps to the owning team — never the team's display name, which is what a page calls the team and not what a resource would be tagged. Where a team maps several admitted values the first in value order is proposed, so two reads of the same page propose the same thing.
resourceKeyrequiredstringregion/system/service/type/id

TagViolationList

FieldTypeDescription
itemsrequiredarray of TagViolation
windowrequiredCostPeriod

Team

FieldTypeDescription
accountCountrequiredinteger
archivedAtstring (date-time)Present when the team is closed. Its history is still readable.
createdAtrequiredstring (date-time)
directoryGroupstringThe provider's group the team follows, as the groups claim names it. Present when source is directory.
idrequiredstring (uuid)
lastSyncedAtstring (date-time)The last sign-in that considered this team's binding.
memberCountrequiredinteger
namerequiredstring
slugrequiredstring
sourcerequiredstring enumWho keeps the membership: administrators (manual), or the identity provider's group at each sign-in (directory). One of manual, directory.
tagValueCountrequiredinteger

TeamAccount

FieldTypeDescription
accountIdrequiredstring
attributedSpendrequiredarray of MoneyWhat the rollups attribute to this team through this account — the account-level fallback, attribution team_account — by currency, over all history. This is the number that moves to the untagged bucket if the account is unassigned.
namerequiredstring
usageFromstring (date)
usageTostring (date)

TeamAccountChange

What an assignment or an unassignment changes. On an unassignment spendMoving is what leaves this team for the untagged bucket; on an assignment it is what the account's untagged history brings in. Either way the rebuild is enqueued and rebuildEnqueued says so — false only when the enqueue itself failed, in which case the change stands and the next settings save will rebuild.

FieldTypeDescription
accountIdrequiredstring
assignedrequiredboolean
rebuildEnqueuedrequiredboolean
spendMovingrequiredarray of Money
teamIdrequiredstring (uuid)
usageFromstring (date)
usageTostring (date)

TeamCreate

FieldTypeDescription
namerequiredstring
slugstringDerived from the name when omitted.

TeamDetail

FieldTypeDescription
accountsrequiredarray of TeamAccount
membersrequiredarray of TeamMember
tagValuesrequiredarray of string
teamrequiredTeam

TeamDirectoryGroup

FieldTypeDescription
grouprequiredstringThe group exactly as the provider names it in the groups claim — an id or a name, whichever the provider emits.

TeamList

FieldTypeDescription
itemsrequiredarray of Team

TeamMember

FieldTypeDescription
emailrequiredstring
platformRolerequiredstring enum One of viewer, engineer, admin.
teamRolerequiredTeamRoleA user's role within one team. Deliberately disjoint from the platform roles so the two axes cannot be compared by accident. One of member, lead.
userIdrequiredstring (uuid)

TeamMemberSet

FieldTypeDescription
teamRolerequiredTeamRoleA user's role within one team. Deliberately disjoint from the platform roles so the two axes cannot be compared by accident. One of member, lead.

TeamMembership

A team seen from a user: which, and the user's role in it.

FieldTypeDescription
teamIdrequiredstring (uuid)
teamNamerequiredstring
teamRolerequiredTeamRoleA user's role within one team. Deliberately disjoint from the platform roles so the two axes cannot be compared by accident. One of member, lead.

TeamRef

A team named where only its identity matters — an app's owner, or a team a picker offers. Team is the org model's full row, with the counts the settings page shows; this is the two fields everything else needs.

FieldTypeDescription
idrequiredstring (uuid)
namerequiredstring

TeamRole

A user's role within one team. Deliberately disjoint from the platform roles so the two axes cannot be compared by accident.

Type: string enum

TeamTagValueChange

FieldTypeDescription
mappedrequiredboolean
rebuildEnqueuedrequiredboolean
tagValuerequiredstring
teamIdrequiredstring (uuid)

TeamTagValueSet

FieldTypeDescription
tagValuerequiredstring

TeamUpdate

FieldTypeDescription
archivedboolean
namestring

Template

FieldTypeDescription
archivedAtstring (date-time)Set means hidden from the catalog and closed to new versions. Archiving never deletes: resources provisioned from this template are still live and still need their provenance.
categoryrequiredResourceCategoryThe template's category. A row stored before the taxonomy reads back as other — including one stored with no category at all, which is the column's own default and the commonest of those rows. The value is never rewritten; category=other finds exactly them.
cloudTemplateCloudThe cloud the template makes its resources in (MILESTONE_1 §8.4): what the requester's catalog lists it under, and the cloud its next version is published for. Each version keeps the cloud it was published for (TemplateVersion.cloud), and a request is judged by the version it pins, so moving a template to another cloud moves no version already published. Absent for a template that has not said — one written before templates did — which runs wherever its code does.
costNotestringFor a library template: what running requestableVersion costs, in the library's words (MILESTONE_1 §8.2.4). Absent otherwise.
createdAtrequiredstring (date-time)
createdByrequiredActorRefWho did something, for display and for audit. Not User: that carries the tenant and the role of the session, and an audit record naming a role that has since changed would be a lie about the past.
descriptionrequiredstring
gitRefstringThe default ref new versions are published from.
gitUrlstringPresent for git sources. Validated on the way in: scheme allowlist, no embedded credentials, and the host must resolve outside private and reserved CIDR ranges. That last check is SSRF defence — the platform would otherwise dial arbitrary hosts on a caller's behalf.
idrequiredstring (uuid)
importablerequiredbooleanWhether requestableVersion declares how an existing resource is brought under it (MILESTONE_1 §9.4) — for a library template, an import in its meta.json. False with no requestable version, and for a template of the tenant's own until a version can declare it.
latestVersionintegerThe highest published version, resolved or not. Absent when the template has none, which also means it cannot be requested.
librarySlugstringPresent for library templates: the library template it was enabled from (MILESTONE_1 §8.1). Usually the template's own slug too, unless the admin enabled it under another.
namerequiredstring
requestableVersionintegerThe version a new request pins: the newest that is resolved and has a variables schema, of a template that is not archived — and, when the template names its cloud, a version published for that cloud. Absent when there is none — every version pending, failed, imported without a schema or published for another cloud, or the template archived — and the requester's catalog does not list the template. Always absent for an action template, which is asked for at POST /provisioning/actions, never created from.
requiredVariablesintegerWith requestableVersion: how many of its variables a requester must fill in — required, with no default. The catalog says it before the form asks.
resourceTypesrequiredarray of stringWhat the template makes, <cloud>.<service>.<type> as the inventory names each type — the requester's catalog lists the template under every one. Empty for a template that has not said, which lists under its category without a type. A library template's are its latest version's meta.json's.
retiredbooleanFor a library template: whether the library no longer maintains it (MILESTONE_1 §8.1). It stays enabled and requestable, and the catalog says it is no longer maintained. Absent for any other template, and when this platform's library no longer names it.
slugrequiredstringStable, human-facing and immutable. It is what a URL names and what an audit record refers to years later, so it cannot be renamed.
sourceTyperequiredTemplateSourceTypeWhere a template's code comes from, and therefore what its source identity is: a commit SHA for git, a content sha256 for inline, and for library the sha256 of a built-in library template's files (MILESTONE_1 §8.1), whose bytes the agent carries in its own image. Whichever it is, it is pinned at publish and never updated. One of git, inline, library.

TemplateCategory

What a template creates, in the one taxonomy (MILESTONE_1 §3). The same vocabulary as ResourceCategory without other: that bucket is where a value written before the set existed lists, not something a new template may declare.

A template stored with a category outside this set — written before the taxonomy, or by an older client — is read back as other; nothing rewrites the row, and category=other on the catalog finds exactly those.

Type: string enum

TemplateCloud

The cloud a template makes its resources in, as the account registry names it (Account.system): a library template's (MILESTONE_1 §8.1) or one of the tenant's own. azure is a library template's, one Import generated for an Azure resource, and — since then slice 6 — one of the tenant's own and a copy published to the catalog. A request for a template of the tenant's own in an Azure account waits for an agent whose Azure runners are scoped to resource groups (capability azure-resource-groups).

Type: string enum

TemplateCreate

FieldTypeDescription
categoryrequiredTemplateCategoryWhat a template creates, in the one taxonomy (MILESTONE_1 §3). The same vocabulary as ResourceCategory without other: that bucket is where a value written before the set existed lists, not something a new template may declare. A template stored with a category outside this set — written before the taxonomy, or by an older client — is read back as other; nothing rewrites the row, and category=other on the catalog finds exactly those. One of compute, networking, databases, messaging, serverless, storage, security_and_identity.
cloudTemplateCloudThe cloud a template makes its resources in, as the account registry names it (Account.system): a library template's (MILESTONE_1 §8.1) or one of the tenant's own. azure is a library template's, one Import generated for an Azure resource, and — since then slice 6 — one of the tenant's own and a copy published to the catalog. A request for a template of the tenant's own in an Azure account waits for an agent whose Azure runners are scoped to resource groups (capability azure-resource-groups). One of aws, gcp, azure.
descriptionstring
gitRefstringDefault ref for new versions. Only for git.
gitUrlstringRequired for git, rejected for inline.
namerequiredstring
resourceTypesarray of stringWhat the template makes, <cloud>.<service>.<type>, each of cloud, each once. Types need cloud.
slugrequiredstringA slug that names one of the Library's runners is refused (400): a git or inline template runs as the runner its slug names, so a Library template's slug, a slug beginning generated-, negative-control, and a slug on one of their Google service accounts are reserved.
sourceTyperequiredstring enumgit or inline. A library template is enabled from the library, never created here. One of git, inline.

TemplateOutput

One output a template version declares (MILESTONE_1 §11.4): the name of an output block in its source, the variables schema type its value has, and what it is. sensitive declares an output the platform must never receive: it is not asked for, never reported and never wired into another step.

FieldTypeDescription
descriptionrequiredstring
namerequiredstring
sensitivebooleanNever asked for, never reported, never wired. Absent is false.
typerequiredstring enum One of string, number, boolean, list(string), map(string).

TemplatePage

FieldTypeDescription
itemsrequiredarray of Template
pagerequiredinteger
pageSizerequiredinteger
totalrequiredinteger (int64)
totalPagesrequiredinteger

TemplateSourceType

Where a template's code comes from, and therefore what its source identity is: a commit SHA for git, a content sha256 for inline, and for library the sha256 of a built-in library template's files (MILESTONE_1 §8.1), whose bytes the agent carries in its own image. Whichever it is, it is pinned at publish and never updated.

Type: string enum

TemplateUpdate

Only the fields present are changed. slug and sourceType are absent on purpose — both are pinned by every request ever made against this template.

FieldTypeDescription
archivedbooleanTrue archives, false restores. Archiving hides the template and closes it to new versions; it never touches what was provisioned from it.
categoryTemplateCategoryWhat a template creates, in the one taxonomy (MILESTONE_1 §3). The same vocabulary as ResourceCategory without other: that bucket is where a value written before the set existed lists, not something a new template may declare. A template stored with a category outside this set — written before the taxonomy, or by an older client — is read back as other; nothing rewrites the row, and category=other on the catalog finds exactly those. One of compute, networking, databases, messaging, serverless, storage, security_and_identity.
cloudTemplateCloudMoves the template: its next version is published for this cloud, and the requester's catalog lists it here once one is. Every version already published keeps the cloud it was published for, and a request for it still runs only there. Refused for a library template.
descriptionstring
gitRefstring
namestring
resourceTypesarray of stringReplaces the template's types; empty clears them. Held with cloud, as the template stands after the change, to the rule at create. Refused for a library template.

TemplateVersion

Immutable once published. Its source identity — gitSha for git, inlineSha256 for inline — is written once and never updated, and a request pins (templateId, version). That pair is what makes "the approval approved this code" a statement of fact rather than a hope, and it is also the version's whole identity: there is no surrogate id, here or anywhere else, because template_versions has no column for one (PROVISIONING_SPEC.md §4.1).

FieldTypeDescription
agentVersionstringFor library versions, on the single-version read only: the first agent version that carries libraryRelease. Empty when this platform's library no longer names the release.
cloudTemplateCloudThe cloud this version makes its resources in (round 2): fixed when it was published — its template's cloud then, or a library version's meta.json's — and moved by nothing after, since the code it pins does not move. A create from this version is refused for an account in another cloud (targetAccountId, 400); a destroy never is. A version published before its template named a cloud takes the first one the template names. Absent when neither says: the version runs wherever its code does.
gitRefstringThe ref that was asked for. What ran is gitSha.
gitShastringThe pinned commit. Absent until an agent resolves it, which is the only reason a git version can sit pending.
gitUrlstring
importImportDeclarationWhat the version declares it can take over (MILESTONE_1 §9.4). Absent: the version is not importable. A library version's is its pinned hash's meta.json's.
inlineSha256stringsha256 of inlineSource, computed at publish.
inlineSourcestringThe exact bytes, for inline sources. Present on the single-version read, which is what the admin editor loads, and on a request's detail (ProvisionRequestDetail.version), where it is the code being approved, unless that read asks source=false; list responses omit it, because a catalog page does not need every template's source.
libraryReadmestringFor library versions, on the single-version read only: the library's README.md for this hash, Markdown — what the request form shows beside itself (MILESTONE_1 §8.4). Absent if this platform's library does not hold the hash.
libraryReleaseintegerFor library versions: the first library release that carried librarySha256. Every later release carries it too, so an agent reporting at least this release can run the version.
librarySha256stringFor library versions, the source identity: the sha256 of the library template's files in canonical order (MILESTONE_1 §8.1). The agent runs the bytes its own image holds under it; the platform stores and sends none.
librarySourcestringFor library versions, on the single-version read only: the library's main.tf for this hash, from the platform's own copy of the library, so the editor can show it read-only. Never stored on the version and never sent to an agent. Absent if this platform's library does not hold the hash.
needsSchemarequiredbooleanImported from the selfservice donor without a variables schema. Listed in the catalog, refused at submit with 409 version_needs_schema; an admin publishes the next version with a schema. Versions are immutable, so this one stays marked.
outputsarray of TemplateOutputWhat the version declares its apply produces (MILESTONE_1 §11.4): each an output block of its source, with the variables schema type its value has. The agent reports them after a successful create, and a later step can be wired from them. Immutable with the version, as the source is. Absent when it declares none — every version published in earlier releases. A library version's are its meta.json's.
publishedAtrequiredstring (date-time)
publishedByrequiredActorRefWho did something, for display and for audit. Not User: that carries the tenant and the role of the session, and an audit record naming a role that has since changed would be a lie about the past.
resolveAccountIdstringThe account whose agent resolves this version, chosen at enqueue among the enabled accounts with a registered agent — a live one first. Absent while pending and no account has an agent: the version is waiting, and the next registration enqueues it. Absent for inline versions, which resolve at publish.
resolveErrorstringThe agent's own message when resolution failed. Shown in the catalog because it is the only thing that makes the failure actionable — a ref that does not exist and a repository the agent cannot reach need different fixes.
resolveStaterequiredTemplateVersionResolveStateWhether a version has a source identity yet. inline versions are resolved the moment they are published; git versions are pending until an agent reports a commit SHA (D7), and a version that is not resolved cannot be requested. failed is retryable and visible. One of pending, resolved, failed.
resolvedAtstring (date-time)
runnableAccountsarray of stringFor library versions, on the single-version read only: the tenant's enabled accounts in the version's cloud whose agent last reported a library release holding librarySha256 — where a request for this version runs, said before it is made (MILESTONE_1 §8.1). An account of that cloud not listed has no such agent: a request for it waits, queued, until an agent of agentVersion or later claims it. An account of another cloud is never listed, and a create for one is refused.
sourceTyperequiredTemplateSourceTypeWhere a template's code comes from, and therefore what its source identity is: a commit SHA for git, a content sha256 for inline, and for library the sha256 of a built-in library template's files (MILESTONE_1 §8.1), whose bytes the agent carries in its own image. Whichever it is, it is pinned at publish and never updated. One of git, inline, library.
templateIdrequiredstring (uuid)
variablesSchemarequiredVariablesSchemaStored on the version and immutable with it. The portal generates the request form from this; the agent uses it to write generated.auto.tfvars as proper HCL through an encoder, never by string concatenation, because hand-rolled escaping is how tfvars injection happens.
versionrequiredinteger
warningsarray of stringPresent on the publish response only, and not stored: what the platform could tell about the source without running it. Today one warning exists — an inline source that declares the AWS provider's own default_tags, which will collide with the provenance stamp the agent writes (PROVISIONING_SPEC.md §9). The apply still runs; the post-apply verification is what catches the collision for real and marks the resource provenance-unverified. A git source cannot be inspected at publish, because its bytes arrive on the agent.

TemplateVersionPage

FieldTypeDescription
itemsrequiredarray of TemplateVersion
pagerequiredinteger
pageSizerequiredinteger
totalrequiredinteger (int64)
totalPagesrequiredinteger

TemplateVersionPublish

Exactly one of gitRef, inlineSource and fromLibrary applies, decided by the template's own sourceType rather than by a field here — a version cannot be a different kind of thing from its template. variablesSchema is required for git and inline and refused for library, whose schema is the library's; librarySha256 is required for library and refused for the other two.

FieldTypeDescription
fromLibrarybooleanFor library templates, and only true: publish the version the library now carries for the template's library slug (MILESTONE_1 §8.1). Requires librarySha256.
gitRefstringFor git templates. Defaults to the template's gitRef. This is what an agent resolves; it is never what runs.
importImportDeclarationFor inline templates: what this version declares it can take over (MILESTONE_1 §9.4), checked at publish against the source and the schema — every address a resource the source declares, every pre-filled variable one the schema declares with a value it accepts, and no identifier or pre-fill reading outside the resource key, resourceName and additionalInfo (400 invalid_request, naming the rule). Refused for git, whose source is not known at publish, and for library, whose declaration is the library's.
inlineSourcestringFor inline templates: the exact bytes, capped at 256 KiB, UTF-8, no NUL. Their sha256 is the version's source identity, so an inline version is resolved the instant it is published — "approved code equals executed code" holds without a clone.
librarySha256stringWith fromLibrary, and required with it: the hash of the library version whose change the administrator read — the library detail's sha256, 64 lower-case hexadecimal digits. The publish pins this hash or nothing: when the library no longer carries it for the slug, the answer is 409 library_changed and no version is created. Refused on a git or inline publish.
outputsarray of TemplateOutputFor git and inline templates: the outputs this version declares (MILESTONE_1 §11.4). Each name at most once, each of the variables schema's five types. Refused for library, whose outputs are the library's.
variablesSchemaVariablesSchemaStored on the version and immutable with it. The portal generates the request form from this; the agent uses it to write generated.auto.tfvars as proper HCL through an encoder, never by string concatenation, because hand-rolled escaping is how tfvars injection happens.

TemplateVersionResolveState

Whether a version has a source identity yet. inline versions are resolved the moment they are published; git versions are pending until an agent reports a commit SHA (D7), and a version that is not resolved cannot be requested. failed is retryable and visible.

Type: string enum

TimelinePage

Cursor-paginated, so there is deliberately no page, total or totalPages. A total would need a second full count of a forever-growing table on every request, and a page number has no stable meaning when rows arrive continuously at the head.

FieldTypeDescription
itemsrequiredarray of Transition
nextCursorstringPass as cursor for the next page. Absent means this is the last page — which is the only reliable end-of-results signal here, since a short page can also mean rows were filtered.

Transition

One status change, kept forever. Also the notification outbox — the same row the notifier drains.

FieldTypeDescription
accountIdrequiredstring
acknowledgementTransitionAcknowledgementThe latest acknowledgement of a transition, reversed or not: who acknowledged it, when and why, and who reversed it, when anyone did. An email is the user's as it is now, empty once the user is gone. Shown to an administrator, and to a member only on their teams' accounts (the owner, 2026-09-14).
fromStatestring
fromStatusTransitionStatusAbsent for the first ever observation of a resource. Those rows are written pre-suppressed: onboarding 300 accounts must not emit 40,000 notifications.
idrequiredinteger (int64)
occurredAtrequiredstring (date-time)
regionrequiredstring
resourceIdrequiredstring
resourceNamestring
resourceTyperequiredstring
runIdstring (uuid)
servicerequiredstring
systemrequiredstring
toStatestring
toStatusrequiredTransitionStatusA status a transition can move to or from. Extends Status with deleted, which only ever appears as a destination — the transition recording that a resource was tombstoned. One of failed, degraded, operational, unknown, deleted.

TransitionAcknowledgement

The latest acknowledgement of a transition, reversed or not: who acknowledged it, when and why, and who reversed it, when anyone did. An email is the user's as it is now, empty once the user is gone. Shown to an administrator, and to a member only on their teams' accounts (the owner, 2026-09-14).

FieldTypeDescription
acknowledgedAtrequiredstring (date-time)
acknowledgedByrequiredActorRefWho did something, for display and for audit. Not User: that carries the tenant and the role of the session, and an audit record naming a role that has since changed would be a lie about the past.
noterequiredstring
reversedAtstring (date-time)
reversedByActorRefWho did something, for display and for audit. Not User: that carries the tenant and the role of the session, and an audit record naming a role that has since changed would be a lie about the past.

TransitionStatus

A status a transition can move to or from. Extends Status with deleted, which only ever appears as a destination — the transition recording that a resource was tombstoned.

Type: string enum

TriageAgreement

FieldTypeDescription
agreedrequiredintegerOf those, the suggestions that matched what people did.
judgedrequiredintegerShadow decisions a day old or more that the model answered and no floor decided.
kindrequiredTriageKindAn event kind Triage decides (TRIAGE_SPEC §5). One of status_transition, cost_anomaly, budget, provisioning, usage_deviation, config_change, access_lost, provisioned_missing.
missedrequiredintegerOf those, the suggestions of none where people acted.

TriageAgreementList

FieldTypeDescription
itemsrequiredarray of TriageAgreement

TriageDecision

FieldTypeDescription
accountIdstring
appliedrequiredTriageOutcome One of reviewer, notify, none.
choiceTriageOutcome One of reviewer, notify, none.
confidencenumber (double)
decidedAtrequiredstring (date-time)
errorClassstring
eventRefrequiredstringThe event's outbox row, <table>:<id>.
idrequiredinteger (int64)
kindrequiredTriageKindAn event kind Triage decides (TRIAGE_SPEC §5). One of status_transition, cost_anomaly, budget, provisioning, usage_deviation, config_change, access_lost, provisioned_missing.
latencyMsrequiredinteger
moderequiredstring enum One of shadow, enforce.
modelrequiredstring
probabilitiesobject
providerrequiredstring
questionVersionrequiredstring
reasonrequiredstring enum One of floor, model_error, model_changed, threshold, reviewer_unavailable, shadow, model.
resourceKeystring
suggestedrequiredTriageOutcome One of reviewer, notify, none.

TriageDecisionPage

FieldTypeDescription
itemsrequiredarray of TriageDecision
nextCursorstring

TriageKind

An event kind Triage decides (TRIAGE_SPEC §5).

Type: string enum

TriageMode

Type: string enum

TriageOutcome

Type: string enum

TriageSetting

FieldTypeDescription
enforcedModelstringThe model the kind was switched to enforce against.
enforcedQuestionstringThe question version it was switched against.
isDefaultrequiredbooleanNo administrator has set this kind.
kindrequiredTriageKindAn event kind Triage decides (TRIAGE_SPEC §5). One of status_transition, cost_anomaly, budget, provisioning, usage_deviation, config_change, access_lost, provisioned_missing.
moderequiredTriageMode One of off, shadow, enforce.
noneThresholdrequirednumber (double)
reviewerThresholdrequirednumber (double)
updatedAtstring (date-time)
updatedBystring (uuid)

TriageSettingList

FieldTypeDescription
itemsrequiredarray of TriageSetting
providerConfiguredrequiredbooleanThe deployment configures a decision model (TRIAGE_PROVIDER).

TriageSettingUpdate

FieldTypeDescription
moderequiredTriageMode One of off, shadow, enforce.
noneThresholdnumber (double)
reviewerThresholdnumber (double)

TypeCount

How many resources of one type a filtered set holds.

FieldTypeDescription
countrequiredinteger (int64)
resourceTyperequiredstring
servicerequiredstring
systemrequiredstring

User

FieldTypeDescription
cloudsarray of string enumThe clouds this deployment reads (MILESTONE_1 §6), in the product's order: the ones the portal offers an administrator to connect an account in. Its source is the configuration the collectors already obey, never a switch of its own: the clouds the deployment's home identity reaches (GET /admin/cloud-identity). Under an AWS home — the default — aws is always listed: no setting gates the AWS reads, and a deployment without AWS credentials of its own fails them where the Collectors page shows it; under a Google or Azure home only when AWS_PLATFORM_ROLE_ARN lets the home reach AWS. gcp is listed exactly when a Google Cloud sign-in is configured (GCP_WORKLOAD_IDENTITY_PROVIDER, always under a Google home), without which the Google collectors refuse to read. azure is listed exactly when an Azure sign-in is configured: under an AWS home the deployment's own AWS identity through outbound web identity federation, or the certificate AZURE_CERTIFICATE_FILE names; under a Google home the platform service account's ID token (GCP_PLATFORM_SERVICE_ACCOUNT); under an Azure home always. A demonstration tenant is told every cloud this build carries: no collector reads it, and the demo fabricates its reads in every cloud. An account registered in a cloud missing here is still listed wherever accounts are; nothing it holds is hidden. Absent from an older server, which a portal reads as every cloud it carries — and which could not read Azure.
demorequiredbooleanTrue when the tenant's whole estate was fabricated by panorama admin seed-demo. The portal shows a persistent "Demo data" banner while it is set, so fabricated infrastructure is never mistaken for real.
demoGeneratedAtstring (date-time)When this demonstration tenant's data was generated: the moment the last panorama admin seed-demo that wrote its estate — the first seed, a --reset or a --refresh — dated its history from, as that seed recorded it with the data. The portal's Help says "The demonstration data was generated on" and the date from it. Sent only while demo is true, and never for any other tenant, whatever its audit log holds. Absent from a demonstration tenant no recent seed has written, and from a server that predates the field.
displayNamestringThe name the person gave themselves; absent when they gave none.
emailrequiredstringThe address as it was given when the account was created. Not format: email for the same reason as on the way in: nothing validates the syntax, so nothing should claim it.
idrequiredstring (uuid)
operatorContactstringHow this deployment's people reach whoever operates it, for help and for access (MILESTONE_1 §7.3 — the operator's contact the pilot gate requires): an email address, such as ops@example.com, or an https:// address, as the deployment's OPERATOR_CONTACT names it. The portal's Help menu shows it to everyone signed in, an address as a mail link. The same for every person and every tenant the deployment serves; absent when the deployment names none, and from an older server.
operatorManualbooleanTrue when this deployment serves the operator's manual (GET /manual) and the person is an administrator, the only role the route serves. Absent for everyone else, and wherever the deployment mounts no manual: the route is then 404, and the portal draws no link to it. It is answered by the same test the route applies at that moment — the file named by OPERATOR_MANUAL_PATH exists, is a regular file and is within the route's size bound — so the portal no longer probes HEAD /manual, whose 404 a browser logs as a console error on every visit. Absent from an older server, which a portal reads as no manual.
preferencesPreferencesThe person's portal preferences, applied by the portal; an absent field is the browser's own.
rolerequiredstring enumWhat this user may do. Separate from entitlements, which are what the tenant has bought. One of viewer, engineer, admin.
tenantNamerequiredstring
tenantSlugrequiredstring

VariableDefinition

One input a template declares. Deliberately small: a variables schema grows into a type system if allowed, and this is the entire vocabulary until a customer asks for more (PROVISIONING_SPEC.md §8.1).

One validator, two consumers. The Go validator runs at submit and is authoritative; the portal generates the form from this same schema served over the API and never reimplements a rule. A client-side check is a convenience; the server decides.

FieldTypeDescription
defaultVariableValueUsed when the value is omitted, and validated against this definition itself — a default that its own rules reject is a template bug that would otherwise surface as a failed apply.
descriptionstringForm rendering only.
enumarray of VariableValueClosed set, for string and number. Renders as a select. Typed as a variable value rather than left free-form; that the two scalar types are the only ones for which a closed set means anything is the validator's rule, as every other rule here is.
groupstringForm rendering only.
maxnumberInclusive, for number.
maxItemsintegerInclusive, for list(string).
maxLengthintegerInclusive, for string.
minnumberInclusive, for number.
minItemsintegerInclusive, for list(string).
minLengthintegerInclusive, for string.
namerequiredstring
patternstringRE2, for string. Anchored by the validator, not by the author: an unanchored pattern accepts anything that merely contains a match, which is the difference between a constraint and a suggestion.
patternRulestringThe template's own words for pattern, which it needs (PROVISIONING_SPEC.md §8.1). A value the pattern refuses is told this text as written — variable "<name>": <patternRule> — ahead of the words the platform derives from the pattern and of description; the portal says the same at the field. One line.
requiredbooleanAbsent at submit and no default → rejected.
sensitivebooleanMasked in the UI and in variablesRedacted, and exact-value redacted from stored logs by the agent. The exact-value rule is the strongest of the five scrubbing rules precisely because it does not guess: the agent knows the string it must not print.
titlestringThe field's label in the template's own words (PROVISIONING_SPEC.md §8.1). The generated forms show it in place of the key in words, with the key beside it as a hint. Plain text, one line; optional — without it the form labels the field with the key in words. Form rendering only: no rule reads it.
typerequiredstring enumNothing else. Anything richer is a type system. One of string, number, boolean, list(string), map(string).

VariableValue

One variable value, in one of the five types PROVISIONING_SPEC.md §8.1 allows and in no other. That list is the entire vocabulary until a customer asks for more, and a value outside it is a template the validator will reject anyway.

Written as a oneOf even though AgentTask deliberately avoids one for its work item: a union at a leaf costs a third-party generator nothing it does not already pay — a generator that cannot express it falls back to "any", which is precisely what an unconstrained schema gives it — while a oneOf at the top of a work item makes the whole response awkward to read. The value here is that the map carrying these is no longer free-form: an arbitrary nested blob does not validate, so there is nowhere for one to hide.

One of: string, number (double), boolean, array of string, object

VariablesSchema

Stored on the version and immutable with it. The portal generates the request form from this; the agent uses it to write generated.auto.tfvars as proper HCL through an encoder, never by string concatenation, because hand-rolled escaping is how tfvars injection happens.

FieldTypeDescription
variablesrequiredarray of VariableDefinition

Generated from observability/openapi/openapi.yaml (1a334f7db38d) by www/tools/docs; the value in brackets is a hash of the source's content, so a page changes only when its source does.