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,/readyzand/public/{tenantSlug}/statusneed no credentials. The public status endpoint has its own response type and exposes deliberately little; seePublicStatus. - Session — the web app, via an
HttpOnly; Secure; SameSite=Laxcookie. - 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; seeapiTokenbelow 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
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
| Status | Body | Meaning |
|---|---|---|
200 | Health | The process is alive. |
429 | Error | Rate limited. |
GET /metrics
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
| Status | Body | Meaning |
|---|---|---|
200 | string | Metrics in the Prometheus text exposition format. |
404 | — | METRICS_ |
429 | Error | Rate limited. |
GET /readyz
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
| Status | Body | Meaning |
|---|---|---|
200 | Health | Ready. |
429 | Error | Rate limited. |
503 | Not | Not 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
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
| Status | Body | Meaning |
|---|---|---|
200 | string | The documentation page. |
429 | Error | Rate limited. |
GET /openapi.json
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
| Status | Body | Meaning |
|---|---|---|
200 | object | The 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. |
429 | Error | Rate limited. |
public (2)
public
Unauthenticated. Deliberately minimal.
GET /public/tenant
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
| Status | Body | Meaning |
|---|---|---|
200 | Public | The tenant whose status page this host shows. |
404 | Error | not_found |
429 | Error | Rate limited. |
500 | Error | internal_. 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
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
| Name | In | Type | Description |
|---|---|---|---|
tenantSlugrequired | path | string | The only place a tenant is named in a request. Everywhere else the tenant comes from the credential; here there is none. |
Responses
| Status | Body | Meaning |
|---|---|---|
200 | Public | Current status summary and recent aggregated timeline. |
404 | Error | not_found |
429 | Error | Rate limited. |
500 | Error | internal_. 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
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
| Status | Body | Meaning |
|---|---|---|
200 | Identity | The registration, or null when none is registered. |
401 | Error | unauthorized — no credential, or one that is not valid. |
403 | Error | forbidden — authenticated, but the user's role does not permit this.
Distinct from module_, which is about the tenant's
subscription rather than the user. On an unsafe request, also origin_: the browser boundary
refused the request's origin before anything else ran. |
500 | Error | internal_. 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
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
| Field | Type | Description |
|---|---|---|
clientIdrequired | string | |
client | string | Required the first time; absent keeps the stored one. |
defaultRolerequired | string enum | One of viewer, engineer, admin. |
emailrequired | array of string | |
enforcedrequired | boolean | |
groupsClaim | string | The claim the groups are read from; empty or absent keeps groups. |
issuerrequired | string |
Responses
| Status | Body | Meaning |
|---|---|---|
200 | Identity | The registration. |
400 | Error | invalid_ — a malformed parameter or body. The batch ingest
endpoint uses invalid_ 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. |
401 | Error | unauthorized — no credential, or one that is not valid. |
403 | Error | forbidden — authenticated, but the user's role does not permit this.
Distinct from module_, which is about the tenant's
subscription rather than the user. On an unsafe request, also origin_: the browser boundary
refused the request's origin before anything else ran. |
409 | Error | request_ — accounts are bound to another
provider's issuer: POST / first. |
500 | Error | internal_. 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
Remove the tenant's identity provider
Passwords work again for everyone, and sign-ins in flight fail.
Responses
| Status | Body | Meaning |
|---|---|---|
204 | — | Removed. |
401 | Error | unauthorized — no credential, or one that is not valid. |
403 | Error | forbidden — authenticated, but the user's role does not permit this.
Distinct from module_, which is about the tenant's
subscription rather than the user. On an unsafe request, also origin_: the browser boundary
refused the request's origin before anything else ran. |
500 | Error | internal_. 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
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
| Status | Body | Meaning |
|---|---|---|
200 | Sign | How many accounts were released. |
401 | Error | unauthorized — no credential, or one that is not valid. |
403 | Error | forbidden — authenticated, but the user's role does not permit this.
Distinct from module_, which is about the tenant's
subscription rather than the user. On an unsafe request, also origin_: the browser boundary
refused the request's origin before anything else ran. |
500 | Error | internal_. 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
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
| Status | Body | Meaning |
|---|---|---|
200 | Own | The person's read tokens. |
401 | Error | unauthorized — no credential, or one that is not valid. |
500 | Error | internal_. 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
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
| Field | Type | Description |
|---|---|---|
expires | integer | Days until the token stops working, at most the organisation's longest
(/, 90 unless an administrator changed it).
When absent, 90, or that longest when it is shorter. |
namerequired | string | What the token is for; how a list tells two apart. Must not be blank. |
Responses
| Status | Body | Meaning |
|---|---|---|
201 | APIToken | The token and its record. Store the token now; it cannot be read back. |
400 | Error | invalid_ — a malformed parameter or body. The batch ingest
endpoint uses invalid_ 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. |
401 | Error | unauthorized — no credential, or one that is not valid. |
403 | Error | forbidden — authenticated, but the user's role does not permit this.
Distinct from module_, which is about the tenant's
subscription rather than the user. On an unsafe request, also origin_: the browser boundary
refused the request's origin before anything else ran. |
409 | Error | api_ — the person or the organisation holds as many live read tokens as it may. |
413 | Error | The 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 additional limit is roughly
160 MB — so reaching it means something is wrong with the sender. |
415 | Error | unsupported_ — the body of a browser-facing route is not
labelled application/ (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. |
500 | Error | internal_. 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}
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
| Status | Body | Meaning |
|---|---|---|
204 | — | Revoked, or already was. |
401 | Error | unauthorized — no credential, or one that is not valid. |
403 | Error | origin_ — 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_;
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. |
404 | Error | not_found |
500 | Error | internal_. 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
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
| Name | In | Type | Description |
|---|---|---|---|
before | query | integer (int64) | Only entries older than this id. |
limit | query | integer | At most this many entries; 100 when absent. |
Responses
| Status | Body | Meaning |
|---|---|---|
200 | InboxPage | A page of the person's inbox. |
400 | Error | invalid_ — a malformed parameter or body. The batch ingest
endpoint uses invalid_ 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. |
401 | Error | unauthorized — no credential, or one that is not valid. |
500 | Error | internal_. 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
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
| Field | Type | Description |
|---|---|---|
all | boolean | Every entry of the person's, whatever ids says. |
ids | array of integer (int64) |
Responses
| Status | Body | Meaning |
|---|---|---|
200 | InboxUnread | How many of the person's entries stay unread. |
400 | Error | invalid_ — a malformed parameter or body. The batch ingest
endpoint uses invalid_ 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. |
401 | Error | unauthorized — no credential, or one that is not valid. |
403 | Error | origin_ — 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_;
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. |
500 | Error | internal_. 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
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
| Field | Type | Description |
|---|---|---|
emailrequired | string | The 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. |
passwordrequired | string (password) | Bounded because verifying it is an argon2 derivation over whatever was sent, and this endpoint is unauthenticated. |
tenantSlug | string | Optional. 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.. Matched case-insensitively. |
Responses
| Status | Body | Meaning |
|---|---|---|
200 | User | Logged in. Sets an HttpOnly; Secure; Same session cookie. |
400 | Error | invalid_ — a malformed parameter or body. The batch ingest
endpoint uses invalid_ 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. |
401 | Error | Wrong credentials, unknown tenant, or a disabled user — deliberately indistinguishable, so this endpoint cannot be used to enumerate tenants or accounts. |
403 | Error | origin_ — 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_;
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. |
413 | Error | The 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 additional limit is roughly
160 MB — so reaching it means something is wrong with the sender. |
415 | Error | unsupported_ — the body of a browser-facing route is not
labelled application/ (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. |
429 | Error | Rate limited. |
500 | Error | internal_. 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
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
| Status | Body | Meaning |
|---|---|---|
204 | — | Logged out. Repeating this is not an error. |
401 | Error | unauthorized — no credential, or one that is not valid. |
403 | Error | origin_ — 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_;
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. |
429 | Error | Rate limited. |
500 | Error | internal_. 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
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
| Status | Body | Meaning |
|---|---|---|
200 | User | The authenticated user, their role and their tenant. |
401 | Error | unauthorized — no credential, or one that is not valid. |
429 | Error | Rate limited. |
500 | Error | internal_. 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
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
| Field | Type | Description |
|---|---|---|
displayName | string | |
firstRun | First | 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. |
preferences | Preferences | The person's portal preferences, applied by the portal; an absent field is the browser's own. |
Responses
| Status | Body | Meaning |
|---|---|---|
200 | User | The person, as GET /auth/me answers. |
400 | Error | invalid_ — a field breaks its rule; the message names the field and the rule. |
401 | Error | unauthorized — no credential, or one that is not valid. |
403 | Error | origin_ — 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_;
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. |
413 | Error | The 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 additional limit is roughly
160 MB — so reaching it means something is wrong with the sender. |
415 | Error | unsupported_ — the body of a browser-facing route is not
labelled application/ (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. |
500 | Error | internal_. 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
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
| Status | Body | Meaning |
|---|---|---|
200 | Notification | The person's choices. |
401 | Error | unauthorized — no credential, or one that is not valid. |
500 | Error | internal_. 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
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
| Field | Type | Description |
|---|---|---|
alertsrequired | boolean | Status 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. |
anomaliesrequired | boolean | Cost anomalies on the person's teams and their teams' accounts; every one, for an administrator. |
budgetsrequired | boolean | Budget thresholds of the person's teams; every team's, for an administrator. |
digestrequired | boolean | The 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. |
myRequestsrequired | boolean | Their own provisioning requests — approvals, successes and failures. |
Responses
| Status | Body | Meaning |
|---|---|---|
200 | Notification | The person's choices, as stored. |
400 | Error | invalid_ — a malformed parameter or body. The batch ingest
endpoint uses invalid_ 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. |
401 | Error | unauthorized — no credential, or one that is not valid. |
403 | Error | origin_ — 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_;
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. |
500 | Error | internal_. 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
Your teams
The teams the person belongs to and their role in each, by name. Read-only here: an administrator changes membership.
Responses
| Status | Body | Meaning |
|---|---|---|
200 | OwnTeamList | The person's teams. |
401 | Error | unauthorized — no credential, or one that is not valid. |
500 | Error | internal_. 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
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
| Field | Type | Description |
|---|---|---|
currentrequired | string | |
newPasswordrequired | string |
Responses
| Status | Body | Meaning |
|---|---|---|
204 | — | Changed. A fresh session cookie is set. |
400 | Error | invalid_ — 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. |
401 | Error | unauthorized — no credential, or one that is not valid. |
403 | Error | origin_ — 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_;
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. |
413 | Error | The 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 additional limit is roughly
160 MB — so reaching it means something is wrong with the sender. |
415 | Error | unsupported_ — the body of a browser-facing route is not
labelled application/ (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. |
429 | Error | Rate limited. |
500 | Error | internal_. 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
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
| Field | Type | Description |
|---|---|---|
newPasswordrequired | string | |
tokenrequired | string |
Responses
| Status | Body | Meaning |
|---|---|---|
204 | — | The password is set. Log in with it. |
400 | Error | invalid_ — a malformed parameter or body. The batch ingest
endpoint uses invalid_ 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. |
403 | Error | origin_ — 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_;
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. |
413 | Error | The 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 additional limit is roughly
160 MB — so reaching it means something is wrong with the sender. |
415 | Error | unsupported_ — the body of a browser-facing route is not
labelled application/ (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. |
429 | Error | Rate limited. |
500 | Error | internal_. 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
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
| Status | Body | Meaning |
|---|---|---|
200 | Own | The person's live sessions. |
401 | Error | unauthorized — no credential, or one that is not valid. |
500 | Error | internal_. 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
Sign out everywhere else
Ends every session of the person's but the one making this request.
Responses
| Status | Body | Meaning |
|---|---|---|
200 | Sessions | How many sessions ended. |
401 | Error | unauthorized — no credential, or one that is not valid. |
403 | Error | origin_ — 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_;
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. |
500 | Error | internal_. 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}
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
| Name | In | Type | Description |
|---|---|---|---|
sessionIdrequired | path | string | A session's id, as GET / gives it. |
Responses
| Status | Body | Meaning |
|---|---|---|
204 | — | The session ended. |
400 | Error | invalid_ — the session named is the one making the request. |
401 | Error | unauthorized — no credential, or one that is not valid. |
403 | Error | origin_ — 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_;
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. |
404 | Error | not_found |
500 | Error | internal_. 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
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
| Name | In | Type | Description |
|---|---|---|---|
state | query | string | |
code | query | string | |
error | query | string | The provider's error, when the person cancelled or was refused. |
Responses
| Status | Body | Meaning |
|---|---|---|
302 | — | To the return path with the session cookie, or to the login page. |
GET /auth/sso/options
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
| Name | In | Type | Description |
|---|---|---|---|
tenantSlug | query | string |
Responses
| Status | Body | Meaning |
|---|---|---|
200 | Single | The tenant's single sign-on options. |
500 | Error | internal_. 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
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
| Name | In | Type | Description |
|---|---|---|---|
tenantSlug | query | string | |
returnTo | query | string | A path on this site to return to; anything else is the portal's root. |
Responses
| Status | Body | Meaning |
|---|---|---|
302 | — | To the identity provider, or back to the login page. |
POST /auth/step-up
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
| Field | Type | Description |
|---|---|---|
passwordrequired | string | The current user's password. |
Responses
| Status | Body | Meaning |
|---|---|---|
204 | — | Confirmed. For 10 minutes this session may take destructive actions. |
400 | Error | invalid_ — the password is wrong, or longer than a
password can be. |
401 | Error | unauthorized — no credential, or one that is not valid. |
403 | Error | origin_ — 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_;
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. |
413 | Error | The 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 additional limit is roughly
160 MB — so reaching it means something is wrong with the sender. |
415 | Error | unsupported_ — the body of a browser-facing route is not
labelled application/ (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. |
429 | Error | Rate limited. |
500 | Error | internal_. 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
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
| Field | Type | Description |
|---|---|---|
returnTo | string | A path on this site to come back to after the provider; anything else is the portal's root. |
Responses
| Status | Body | Meaning |
|---|---|---|
200 | Step | Where the browser goes to confirm. |
401 | Error | unauthorized — no credential, or one that is not valid. |
403 | Error | origin_ — 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_;
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. |
404 | Error | not_found — your organisation has no identity provider. |
409 | Error | request_ — this account has never signed in through
the tenant's provider; it confirms with its password. |
413 | Error | The 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 additional limit is roughly
160 MB — so reaching it means something is wrong with the sender. |
415 | Error | unsupported_ — the body of a browser-facing route is not
labelled application/ (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. |
500 | Error | internal_. 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
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
| Status | Body | Meaning |
|---|---|---|
200 | Dashboard | The Dashboard. |
401 | Error | unauthorized — no credential, or one that is not valid. |
500 | Error | internal_. 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
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
| Status | Body | Meaning |
|---|---|---|
200 | FirstRun | The viewer's checklist. |
401 | Error | unauthorized — no credential, or one that is not valid. |
500 | Error | internal_. 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
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
| Status | Body | Meaning |
|---|---|---|
200 | Change | The alerts. |
401 | Error | unauthorized — no credential, or one that is not valid. |
403 | Error | forbidden — authenticated, but the user's role does not permit this.
Distinct from module_, which is about the tenant's
subscription rather than the user. On an unsafe request, also origin_: the browser boundary
refused the request's origin before anything else ran. |
500 | Error | internal_. 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
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
| Status | Body | Meaning |
|---|---|---|
200 | Change | The subscriptions. |
401 | Error | unauthorized — no credential, or one that is not valid. |
403 | Error | forbidden — authenticated, but the user's role does not permit this.
Distinct from module_, which is about the tenant's
subscription rather than the user. On an unsafe request, also origin_: the browser boundary
refused the request's origin before anything else ran. |
500 | Error | internal_. 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
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
| Field | Type | Description |
|---|---|---|
accountIds | array of string | |
appId | string (uuid) | One live declared app (GET /): 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. |
channelIds | array 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. |
hourlyLimit | integer | |
predicaterequired | Change | |
resource | array of Change | Absent or empty is every type; a details path needs at least one. |
teamId | string (uuid) | Absent is the whole tenant, an administrator's alone. |
Responses
| Status | Body | Meaning |
|---|---|---|
201 | Change | The subscription. |
400 | Error | invalid_ — a malformed parameter or body. The batch ingest
endpoint uses invalid_ 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. |
401 | Error | unauthorized — no credential, or one that is not valid. |
403 | Error | forbidden — authenticated, but the user's role does not permit this.
Distinct from module_, which is about the tenant's
subscription rather than the user. On an unsafe request, also origin_: the browser boundary
refused the request's origin before anything else ran. |
404 | Error | not_found |
500 | Error | internal_. 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
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
| Field | Type | Description |
|---|---|---|
accountIds | array of string | |
appId | string (uuid) | One live declared app (GET /): 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. |
channelIds | array 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. |
hourlyLimit | integer | |
predicaterequired | Change | |
resource | array of Change | Absent or empty is every type; a details path needs at least one. |
teamId | string (uuid) | Absent is the whole tenant, an administrator's alone. |
Responses
| Status | Body | Meaning |
|---|---|---|
200 | Change | What the draft would have alerted on. |
400 | Error | invalid_ — a malformed parameter or body. The batch ingest
endpoint uses invalid_ 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. |
401 | Error | unauthorized — no credential, or one that is not valid. |
403 | Error | forbidden — authenticated, but the user's role does not permit this.
Distinct from module_, which is about the tenant's
subscription rather than the user. On an unsafe request, also origin_: the browser boundary
refused the request's origin before anything else ran. |
404 | Error | not_found |
500 | Error | internal_. 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}
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
| Name | In | Type | Description |
|---|---|---|---|
subscriptionrequired | path | string (uuid) |
Responses
| Status | Body | Meaning |
|---|---|---|
204 | — | Ended. |
401 | Error | unauthorized — no credential, or one that is not valid. |
403 | Error | forbidden — authenticated, but the user's role does not permit this.
Distinct from module_, which is about the tenant's
subscription rather than the user. On an unsafe request, also origin_: the browser boundary
refused the request's origin before anything else ran. |
404 | Error | not_found |
500 | Error | internal_. 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
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
| Name | In | Type | Description |
|---|---|---|---|
accountIdrequired | query | string | |
regionrequired | query | string | |
systemrequired | query | string | |
servicerequired | query | string | |
resourcerequired | query | string | |
resourceIdrequired | query | string | |
at | query | string (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. |
window | query | string enum | Either side of the anchor, for status and performance. |
costDays | query | integer | Settled days either side of the anchor's day, for cost. |
depth | query | integer | |
kind | query | array of Neighbour | The neighbour kinds to walk; every kind when absent. |
Responses
| Status | Body | Meaning |
|---|---|---|
200 | Impact | The investigation. |
400 | Error | invalid_ — a malformed parameter or body. The batch ingest
endpoint uses invalid_ 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. |
401 | Error | unauthorized — no credential, or one that is not valid. |
403 | Error | forbidden — authenticated, but the user's role does not permit this.
Distinct from module_, which is about the tenant's
subscription rather than the user. On an unsafe request, also origin_: the browser boundary
refused the request's origin before anything else ran. |
404 | Error | not_found |
500 | Error | internal_. 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
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
| Name | In | Type | Description |
|---|---|---|---|
accountIdrequired | query | string | |
regionrequired | query | string | |
systemrequired | query | string | |
servicerequired | query | string | |
resourcerequired | query | string | |
resourceIdrequired | query | string | |
at | query | string (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. |
window | query | string enum | How far before the symptom the candidates are looked for. |
depth | query | integer |
Responses
| Status | Body | Meaning |
|---|---|---|
200 | Investigation | The investigation. |
400 | Error | invalid_ — a malformed parameter or body. The batch ingest
endpoint uses invalid_ 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. |
401 | Error | unauthorized — no credential, or one that is not valid. |
403 | Error | forbidden — authenticated, but the user's role does not permit this.
Distinct from module_, which is about the tenant's
subscription rather than the user. On an unsafe request, also origin_: the browser boundary
refused the request's origin before anything else ran. |
404 | Error | not_found |
500 | Error | internal_. 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
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
| Name | In | Type | Description |
|---|---|---|---|
If-None-Match | header | string | A previously returned ETag. |
Responses
| Status | Body | Meaning |
|---|---|---|
200 | Resource | Descriptors for every registered resource type. |
304 | — | The client's If-None-Match still matches. No body. |
401 | Error | unauthorized — no credential, or one that is not valid. |
403 | Error | forbidden — authenticated, but the user's role does not permit this.
Distinct from module_, which is about the tenant's
subscription rather than the user. On an unsafe request, also origin_: the browser boundary
refused the request's origin before anything else ran. |
429 | Error | Rate limited. |
500 | Error | internal_. 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
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
| Name | In | Type | Description |
|---|---|---|---|
accountId | query | array of string | Repeatable. Any of the given accounts. |
service | query | array of string | Repeatable. |
resource | query | array of string | Repeatable. |
category | query | array of Resource | Repeatable. Narrows to the resource types in those categories (MILESTONE_compute. A category this deployment collects no types for matches nothing,
which is the truthful answer rather than "everything". |
region | query | array of string | Repeatable. |
system | query | array of string enum | The 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. |
status | query | array of Transition | Repeatable. On /timeline this filters on the destination status, where
deleted is meaningful; on resource endpoints it filters live health,
where it is not. |
stale | query | boolean | true 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. |
ownership | query | string enum | Narrows 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. |
ownerTeamId | query | string (uuid) | Narrows to resources the resolver gives to this team. |
tag | query | array of string | Repeatable, 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:
is that key with the value my-stack, and panorama: 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.), so folding case here
would make the filter disagree with what the response shows. |
page | query | integer | 1-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. |
pageSize | query | integer | Normalised 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
| Status | Body | Meaning |
|---|---|---|
200 | Resource | A page of resources. |
400 | Error | invalid_ — a malformed parameter or body. The batch ingest
endpoint uses invalid_ 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. |
401 | Error | unauthorized — no credential, or one that is not valid. |
403 | Error | forbidden — authenticated, but the user's role does not permit this.
Distinct from module_, which is about the tenant's
subscription rather than the user. On an unsafe request, also origin_: the browser boundary
refused the request's origin before anything else ran. |
429 | Error | Rate limited. |
500 | Error | internal_. 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
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
| Name | In | Type | Description |
|---|---|---|---|
accountId | query | array of string | Repeatable. Any of the given accounts. |
service | query | array of string | Repeatable. |
status | query | array of Transition | Repeatable. On /timeline this filters on the destination status, where
deleted is meaningful; on resource endpoints it filters live health,
where it is not. |
Responses
| Status | Body | Meaning |
|---|---|---|
200 | Acknowledgement | The active acknowledgements. |
400 | Error | invalid_ — a malformed parameter or body. The batch ingest
endpoint uses invalid_ 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. |
401 | Error | unauthorized — no credential, or one that is not valid. |
403 | Error | forbidden — authenticated, but the user's role does not permit this.
Distinct from module_, which is about the tenant's
subscription rather than the user. On an unsafe request, also origin_: the browser boundary
refused the request's origin before anything else ran. |
500 | Error | internal_. 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
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
| Field | Type | Description |
|---|---|---|
accountIdrequired | string | |
noterequired | string | |
regionrequired | string | |
resourceIdrequired | string | |
resourcerequired | string | |
servicerequired | string | |
systemrequired | string |
Responses
| Status | Body | Meaning |
|---|---|---|
201 | Acknowledgement | The acknowledgement. |
400 | Error | invalid_ — a malformed parameter or body. The batch ingest
endpoint uses invalid_ 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. |
401 | Error | unauthorized — no credential, or one that is not valid. |
403 | Error | forbidden — authenticated, but the user's role does not permit this.
Distinct from module_, which is about the tenant's
subscription rather than the user. On an unsafe request, also origin_: the browser boundary
refused the request's origin before anything else ran. |
404 | Error | not_found |
409 | Error | The resource is not failed or degraded, or its alert is acknowledged already. |
500 | Error | internal_. 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
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
| Field | Type | Description |
|---|---|---|
accountIdrequired | string | |
regionrequired | string | |
resourceIdrequired | string | |
resourcerequired | string | |
servicerequired | string | |
systemrequired | string |
Responses
| Status | Body | Meaning |
|---|---|---|
204 | — | Reversed. |
400 | Error | invalid_ — a malformed parameter or body. The batch ingest
endpoint uses invalid_ 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. |
401 | Error | unauthorized — no credential, or one that is not valid. |
403 | Error | forbidden — authenticated, but the user's role does not permit this.
Distinct from module_, which is about the tenant's
subscription rather than the user. On an unsafe request, also origin_: the browser boundary
refused the request's origin before anything else ran. |
409 | Error | The alert is not acknowledged. |
500 | Error | internal_. 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
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
| Name | In | Type | Description |
|---|---|---|---|
accountIdrequired | query | string | |
regionrequired | query | string | |
systemrequired | query | string | |
servicerequired | query | string | |
resourcerequired | query | string | |
resourceIdrequired | query | string |
Responses
| Status | Body | Meaning |
|---|---|---|
200 | Resource | The resource, with its recent snapshots and transitions. |
400 | Error | invalid_ — a malformed parameter or body. The batch ingest
endpoint uses invalid_ 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. |
401 | Error | unauthorized — no credential, or one that is not valid. |
403 | Error | forbidden — authenticated, but the user's role does not permit this.
Distinct from module_, which is about the tenant's
subscription rather than the user. On an unsafe request, also origin_: the browser boundary
refused the request's origin before anything else ran. |
404 | Error | not_found |
429 | Error | Rate limited. |
500 | Error | internal_. 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
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
| Name | In | Type | Description |
|---|---|---|---|
accountIdrequired | query | string | |
regionrequired | query | string | |
systemrequired | query | string | |
servicerequired | query | string | |
resourcerequired | query | string | |
resourceIdrequired | query | string | |
depth | query | integer | One or two; anything else reads as one. |
kind | query | array of Neighbour | Repeatable. Only these kinds; omitted, every kind. |
at | query | string (date-time) | The graph as of this time; omitted, now. |
Responses
| Status | Body | Meaning |
|---|---|---|
200 | Resource | The graph around the resource. |
400 | Error | invalid_ — a malformed parameter or body. The batch ingest
endpoint uses invalid_ 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. |
401 | Error | unauthorized — no credential, or one that is not valid. |
403 | Error | forbidden — authenticated, but the user's role does not permit this.
Distinct from module_, which is about the tenant's
subscription rather than the user. On an unsafe request, also origin_: the browser boundary
refused the request's origin before anything else ran. |
404 | Error | not_found |
500 | Error | internal_. 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
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
| Name | In | Type | Description |
|---|---|---|---|
accountId | query | array of string | Repeatable. Any of the given accounts. |
service | query | array of string | Repeatable. |
resource | query | array of string | Repeatable. |
category | query | array of Resource | Repeatable. Narrows to the resource types in those categories (MILESTONE_compute. A category this deployment collects no types for matches nothing,
which is the truthful answer rather than "everything". |
region | query | array of string | Repeatable. |
system | query | array of string enum | The 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. |
status | query | array of Transition | Repeatable. On /timeline this filters on the destination status, where
deleted is meaningful; on resource endpoints it filters live health,
where it is not. |
stale | query | boolean | true 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. |
ownership | query | string enum | Narrows 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. |
ownerTeamId | query | string (uuid) | Narrows to resources the resolver gives to this team. |
acknowledged | query | boolean | false 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. |
tag | query | array of string | Repeatable, 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:
is that key with the value my-stack, and panorama: 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.), so folding case here
would make the filter disagree with what the response shows. |
query | query | string | Free-text match against resource name, resource ID or account ID, case-insensitively. |
sortBy | query | string enum | A closed set, not a column name. Anything interpolated into SQL goes through an allowlist. |
sort | query | string enum | |
page | query | integer | 1-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. |
pageSize | query | integer | Normalised 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. |
cursor | query | string | The 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
| Status | Body | Meaning |
|---|---|---|
200 | Resource | A page of matching resources, with counts over the whole match. |
400 | Error | invalid_ — a malformed parameter or body. The batch ingest
endpoint uses invalid_ 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. |
401 | Error | unauthorized — no credential, or one that is not valid. |
403 | Error | forbidden — authenticated, but the user's role does not permit this.
Distinct from module_, which is about the tenant's
subscription rather than the user. On an unsafe request, also origin_: the browser boundary
refused the request's origin before anything else ran. |
429 | Error | Rate limited. |
500 | Error | internal_. 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
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
| Name | In | Type | Description |
|---|---|---|---|
accountIdrequired | query | string | |
regionrequired | query | string | |
systemrequired | query | string | |
servicerequired | query | string | |
resourcerequired | query | string | |
resourceIdrequired | query | string | |
cursor | query | string | The previous page's nextCursor. |
pageSize | query | integer |
Responses
| Status | Body | Meaning |
|---|---|---|
200 | Snapshot | One page of observations, newest first. |
400 | Error | invalid_ — a malformed parameter or body. The batch ingest
endpoint uses invalid_ 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. |
401 | Error | unauthorized — no credential, or one that is not valid. |
403 | Error | forbidden — authenticated, but the user's role does not permit this.
Distinct from module_, which is about the tenant's
subscription rather than the user. On an unsafe request, also origin_: the browser boundary
refused the request's origin before anything else ran. |
404 | Error | not_found |
429 | Error | Rate limited. |
500 | Error | internal_. 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
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
| Name | In | Type | Description |
|---|---|---|---|
accountIdrequired | query | string | |
regionrequired | query | string | |
systemrequired | query | string | |
servicerequired | query | string | |
resourcerequired | query | string | |
resourceIdrequired | query | string | |
from | query | integer (int64) | |
to | query | integer (int64) | |
snapshotId | query | integer (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
| Status | Body | Meaning |
|---|---|---|
200 | Snapshot | The diff, older to newer. |
400 | Error | invalid_ — a malformed parameter or body. The batch ingest
endpoint uses invalid_ 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. |
401 | Error | unauthorized — no credential, or one that is not valid. |
403 | Error | forbidden — authenticated, but the user's role does not permit this.
Distinct from module_, which is about the tenant's
subscription rather than the user. On an unsafe request, also origin_: the browser boundary
refused the request's origin before anything else ran. |
404 | Error | not_found |
500 | Error | internal_. 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
Status transitions
Status is derived once at ingest and stored, so every change is materialised. This reads that history; it never recomputes status.
Parameters
| Name | In | Type | Description |
|---|---|---|---|
from | query | string (date-time) | Inclusive lower bound on occurredAt. |
to | query | string (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. |
accountId | query | array of string | Repeatable. Any of the given accounts. |
service | query | array of string | Repeatable. |
status | query | array of Transition | Repeatable. On /timeline this filters on the destination status, where
deleted is meaningful; on resource endpoints it filters live health,
where it is not. |
cursor | query | string | Opaque 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 (occurred, which is
stable under concurrent inserts. OFFSET 100000 also gets slower
forever. |
pageSize | query | integer | Normalised 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
| Status | Body | Meaning |
|---|---|---|
200 | Timeline | A page of transitions, newest first. |
400 | Error | invalid_ — a malformed parameter or body. The batch ingest
endpoint uses invalid_ 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. |
401 | Error | unauthorized — no credential, or one that is not valid. |
403 | Error | forbidden — authenticated, but the user's role does not permit this.
Distinct from module_, which is about the tenant's
subscription rather than the user. On an unsafe request, also origin_: the browser boundary
refused the request's origin before anything else ran. |
429 | Error | Rate limited. |
500 | Error | internal_. 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
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
| Name | In | Type | Description |
|---|---|---|---|
from | query | string (date-time) | Inclusive lower bound on at. |
to | query | string (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. |
kind | query | array of ChangeKind | Repeatable. Only these kinds; omitted, every kind. |
accountId | query | array of string | Repeatable. Any of the given accounts. |
resourceKey | query | string | One resource, with exactly one accountId. Sources without a resource drop out. |
resource | query | string | Events on resources of this type; sources without a resource drop out. |
service | query | array of string | Repeatable. Events on resources of these services — the resource key's third field; sources without a resource drop out. |
status | query | array of Transition | Repeatable. 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_, 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
(held, absent on a first observation) and the latest
acknowledgement, reversed or not, as a Transition
(acknowledgement, shown to the viewers / shows
it to). One resource's history carries the same. |
appId | query | string (uuid) | A declared app's resources (by its rules) and the markers naming it; sources without a resource drop out. |
teamId | query | string (uuid) | One team's accounts and scopes — narrowed within the viewer's own reach, never widened. |
cursor | query | string | Opaque cursor from the previous page's nextCursor; omit for the first page. |
pageSize | query | integer | Normalised 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
| Status | Body | Meaning |
|---|---|---|
200 | Change | A page of the feed, newest first. |
400 | Error | invalid_ — a malformed parameter or body. The batch ingest
endpoint uses invalid_ 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. |
401 | Error | unauthorized — no credential, or one that is not valid. |
403 | Error | forbidden — authenticated, but the user's role does not permit this.
Distinct from module_, which is about the tenant's
subscription rather than the user. On an unsafe request, also origin_: the browser boundary
refused the request's origin before anything else ran. |
429 | Error | Rate limited. |
500 | Error | internal_. 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
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
| Field | Type | Description |
|---|---|---|
accountId | string | The account the marker is about; omitted, the marker is the tenant's. |
appId | string (uuid) | The declared app the marker is about. |
atrequired | string (date-time) | When it happened, by the poster's clock. |
idempotencyrequired | string | |
labelrequired | string | |
resource | array of string | The resources it touched, as keys under accountId. |
url | string | A link back to the poster's own record. |
Responses
| Status | Body | Meaning |
|---|---|---|
200 | Change | The same marker again, as stored the first time. |
201 | Change | The marker, stored. |
400 | Error | invalid_ — a malformed parameter or body. The batch ingest
endpoint uses invalid_ 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. |
401 | Error | unauthorized — no credential, or one that is not valid. |
403 | Error | forbidden — authenticated, but the user's role does not permit this.
Distinct from module_, which is about the tenant's
subscription rather than the user. On an unsafe request, also origin_: the browser boundary
refused the request's origin before anything else ran. |
409 | Error | conflict — this idempotency key names a different marker. |
413 | Error | The 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 additional limit is roughly
160 MB — so reaching it means something is wrong with the sender. |
415 | Error | unsupported_ — the body of a browser-facing route is not
labelled application/ (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. |
500 | Error | internal_. 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
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
| Name | In | Type | Description |
|---|---|---|---|
sort | query | string enum |
Responses
| Status | Body | Meaning |
|---|---|---|
200 | FindingList | The findings. |
400 | Error | invalid_ — a malformed parameter or body. The batch ingest
endpoint uses invalid_ 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. |
401 | Error | unauthorized — no credential, or one that is not valid. |
403 | Error | forbidden — authenticated, but the user's role does not permit this.
Distinct from module_, which is about the tenant's
subscription rather than the user. On an unsafe request, also origin_: the browser boundary
refused the request's origin before anything else ran. |
500 | Error | internal_. 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
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
| Field | Type | Description |
|---|---|---|
expiresAtrequired | string (date-time) | |
reasonrequired | string | |
scoperequired | string | |
sourcerequired | Finding | Where a finding comes from, each read from its own table:
cost_, unknown_, trusted_, cost_, drift_, drift_, stale_, tag_policy, action_, action_. |
sourceIdrequired | string |
Responses
| Status | Body | Meaning |
|---|---|---|
201 | Finding | The exception. |
400 | Error | invalid_ — a malformed parameter or body. The batch ingest
endpoint uses invalid_ 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. |
401 | Error | unauthorized — no credential, or one that is not valid. |
403 | Error | forbidden — authenticated, but the user's role does not permit this.
Distinct from module_, which is about the tenant's
subscription rather than the user. On an unsafe request, also origin_: the browser boundary
refused the request's origin before anything else ran. |
404 | Error | not_found |
409 | Error | exception_ — the finding already has a live exception. |
413 | Error | The 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 additional limit is roughly
160 MB — so reaching it means something is wrong with the sender. |
415 | Error | unsupported_ — the body of a browser-facing route is not
labelled application/ (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. |
500 | Error | internal_. 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}
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
| Status | Body | Meaning |
|---|---|---|
204 | — | Revoked, or already was. |
401 | Error | unauthorized — no credential, or one that is not valid. |
403 | Error | forbidden — authenticated, but the user's role does not permit this.
Distinct from module_, which is about the tenant's
subscription rather than the user. On an unsafe request, also origin_: the browser boundary
refused the request's origin before anything else ran. |
404 | Error | not_found |
500 | Error | internal_. 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
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
| Name | In | Type | Description |
|---|---|---|---|
page | query | integer | 1-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. |
pageSize | query | integer | Normalised 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
| Status | Body | Meaning |
|---|---|---|
200 | Resolved | A page of resolved findings. |
401 | Error | unauthorized — no credential, or one that is not valid. |
403 | Error | forbidden — authenticated, but the user's role does not permit this.
Distinct from module_, which is about the tenant's
subscription rather than the user. On an unsafe request, also origin_: the browser boundary
refused the request's origin before anything else ran. |
500 | Error | internal_. 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
The tenant's findings settings
The maximum number of days an exception may run: 90 until an
administrator changes it (isDefault).
Responses
| Status | Body | Meaning |
|---|---|---|
200 | Finding | The settings. |
401 | Error | unauthorized — no credential, or one that is not valid. |
403 | Error | forbidden — authenticated, but the user's role does not permit this.
Distinct from module_, which is about the tenant's
subscription rather than the user. On an unsafe request, also origin_: the browser boundary
refused the request's origin before anything else ran. |
500 | Error | internal_. 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
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
| Field | Type | Description |
|---|---|---|
maxrequired | integer |
Responses
| Status | Body | Meaning |
|---|---|---|
200 | Finding | The settings as they now stand. |
400 | Error | invalid_ — a malformed parameter or body. The batch ingest
endpoint uses invalid_ 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. |
401 | Error | unauthorized — no credential, or one that is not valid. |
403 | Error | forbidden — authenticated, but the user's role does not permit this.
Distinct from module_, which is about the tenant's
subscription rather than the user. On an unsafe request, also origin_: the browser boundary
refused the request's origin before anything else ran. |
413 | Error | The 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 additional limit is roughly
160 MB — so reaching it means something is wrong with the sender. |
415 | Error | unsupported_ — the body of a browser-facing route is not
labelled application/ (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. |
500 | Error | internal_. 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
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
| Name | In | Type | Description |
|---|---|---|---|
window | query | Performance | The last hour, or the last 24 hours (the default), ending at the current step. |
category | query | Performance | One section's groups; all of them when absent. |
kind | query | Performance | One kind's groups; all three when absent. |
limit | query | integer | Leaders per group. |
Responses
| Status | Body | Meaning |
|---|---|---|
200 | Performance | The groups and their leaders. |
400 | Error | invalid_ — a malformed parameter or body. The batch ingest
endpoint uses invalid_ 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. |
401 | Error | unauthorized — no credential, or one that is not valid. |
403 | Error | module_ — the tenant has not bought Estate; or
capability_ — its licence does not carry
performance (x-capability). |
429 | Error | Rate limited. |
500 | Error | internal_. 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
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
| Name | In | Type | Description |
|---|---|---|---|
accountIdrequired | query | string | |
regionrequired | query | string | |
resourceIdrequired | query | string | The pod's inventory id, cluster:. |
windowrequired | query | Performance | 1h 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
| Status | Body | Meaning |
|---|---|---|
200 | Performance | The pod and its own series over the window. |
400 | Error | invalid_ — a malformed parameter or body. The batch ingest
endpoint uses invalid_ 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. |
401 | Error | unauthorized — no credential, or one that is not valid. |
403 | Error | module_ — the tenant has not bought Estate; or
capability_ — its licence does not carry
performance (x-capability). |
404 | Error | not_found |
429 | Error | Rate limited. |
500 | Error | internal_. The message is generic by design: internal error text
never reaches a response body. The real error is logged with the
request ID. |
502 | Error | upstream_ — CloudWatch did not answer for the pod: the
account's reader role refused the read, or the service failed. |
GET /performance/resource
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
| Name | In | Type | Description |
|---|---|---|---|
accountIdrequired | query | string | |
regionrequired | query | string | |
systemrequired | query | string | |
servicerequired | query | string | |
resourcerequired | query | string | |
resourceIdrequired | query | string | |
windowrequired | query | Performance | 1h 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. |
compare | query | string enum | previous: each series also carries the window before this one, at the same step. |
Responses
| Status | Body | Meaning |
|---|---|---|
200 | Performance | The resource and its series over the window. |
400 | Error | invalid_ — a malformed parameter or body. The batch ingest
endpoint uses invalid_ 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. |
401 | Error | unauthorized — no credential, or one that is not valid. |
403 | Error | module_ — the tenant has not bought Estate; or
capability_ — its licence does not carry
performance (x-capability). |
404 | Error | not_found |
429 | Error | Rate limited. |
500 | Error | internal_. 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}
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
| Name | In | Type | Description |
|---|---|---|---|
sectionrequired | path | Performance | |
windowrequired | query | Performance | 1h 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. |
limit | query | integer | Resources per page. |
compare | query | string enum | previous: each series also carries the window before this one, at the same step. |
cursor | query | string | The 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
| Status | Body | Meaning |
|---|---|---|
200 | Performance | A page of the section's resources and their series. |
400 | Error | invalid_ — a malformed parameter or body. The batch ingest
endpoint uses invalid_ 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. |
401 | Error | unauthorized — no credential, or one that is not valid. |
403 | Error | module_ — the tenant has not bought Estate; or
capability_ — its licence does not carry
performance (x-capability). |
500 | Error | internal_. 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
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
| Name | In | Type | Description |
|---|---|---|---|
keyrequired | query | array of string | A resource, as account
— its detail page's key. Repeatable, at most 100: a page of rows. |
kind | query | Performance | The kind to draw; when absent, each type's first of saturation, latency and errors that it declares a defining signal for. |
Responses
| Status | Body | Meaning |
|---|---|---|
200 | Performance | The rows' sparklines. |
400 | Error | invalid_ — a malformed parameter or body. The batch ingest
endpoint uses invalid_ 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. |
401 | Error | unauthorized — no credential, or one that is not valid. |
403 | Error | module_ — the tenant has not bought Estate; or
capability_ — its licence does not carry
performance (x-capability). |
500 | Error | internal_. 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
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
| Name | In | Type | Description |
|---|---|---|---|
accountIdrequired | query | string | |
regionrequired | query | string | |
systemrequired | query | string | |
servicerequired | query | string | |
resourcerequired | query | string | |
resourceIdrequired | query | string |
Responses
| Status | Body | Meaning |
|---|---|---|
200 | Performance | The resource and its parts. |
400 | Error | invalid_ — a malformed parameter or body. The batch ingest
endpoint uses invalid_ 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. |
401 | Error | unauthorized — no credential, or one that is not valid. |
403 | Error | module_ — the tenant has not bought Estate; or
capability_ — its licence does not carry
performance (x-capability). |
404 | Error | No live resource by this key, or its type has no topology. |
500 | Error | internal_. 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
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
| Status | Body | Meaning |
|---|---|---|
200 | AccountList | Every account registered for this tenant. |
401 | Error | unauthorized — no credential, or one that is not valid. |
403 | Error | forbidden — authenticated, but the user's role does not permit this.
Distinct from module_, which is about the tenant's
subscription rather than the user. On an unsafe request, also origin_: the browser boundary
refused the request's origin before anything else ran. |
429 | Error | Rate limited. |
500 | Error | internal_. 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
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
| Status | Body | Meaning |
|---|---|---|
200 | Account | Every account registered for this tenant, in registry order. |
401 | Error | unauthorized — no credential, or one that is not valid. |
403 | Error | forbidden — authenticated, but the user's role does not permit this.
Distinct from module_, which is about the tenant's
subscription rather than the user. On an unsafe request, also origin_: the browser boundary
refused the request's origin before anything else ran. |
429 | Error | Rate limited. |
500 | Error | internal_. 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
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
| Field | Type | Description |
|---|---|---|
accountIdrequired | string | The twelve-digit AWS account id. |
environmentrequired | string enum | One of production, staging, sdlc, shared, unknown. |
is | boolean | Whether this is the organisation's management account — the one discovery enumerates through. At most one per tenant. |
namerequired | string | |
regions | array of string | The regions to collect in. Empty means every region the account has. |
roleName | string | The 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
| Status | Body | Meaning |
|---|---|---|
201 | Registered | The account, disabled, and what to do next in the customer's account. |
400 | Error | invalid_ — a malformed parameter or body. The batch ingest
endpoint uses invalid_ 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. |
401 | Error | unauthorized — no credential, or one that is not valid. |
403 | Error | forbidden — authenticated, but the user's role does not permit this.
Distinct from module_, which is about the tenant's
subscription rather than the user. On an unsafe request, also origin_: the browser boundary
refused the request's origin before anything else ran. |
409 | Error | invalid_ — that account id is already registered, or this
registration claims the management account and another already is. |
413 | Error | The 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 additional limit is roughly
160 MB — so reaching it means something is wrong with the sender. |
415 | Error | unsupported_ — the body of a browser-facing route is not
labelled application/ (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. |
500 | Error | internal_. 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
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
| Status | Body | Meaning |
|---|---|---|
200 | Discovery | What discovery found, and what changed as a result. |
400 | Error | invalid_ — a malformed parameter or body. The batch ingest
endpoint uses invalid_ 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. |
401 | Error | unauthorized — no credential, or one that is not valid. |
403 | Error | forbidden — authenticated, but the user's role does not permit this.
Distinct from module_, which is about the tenant's
subscription rather than the user. On an unsafe request, also origin_: the browser boundary
refused the request's origin before anything else ran. |
500 | Error | internal_. The message is generic by design: internal error text
never reaches a response body. The real error is logged with the
request ID. |
502 | Error | AWS Organizations could not be reached or refused the call. |
POST /admin/accounts/projects
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
| Field | Type | Description |
|---|---|---|
environmentrequired | string enum | One of production, staging, sdlc, shared, unknown. |
namerequired | string | |
projectIdrequired | string | The project id, not its number and not its display name. |
service | string | The reader service account the deployment impersonates. Defaults to the convention the reader-role documentation uses. |
Responses
| Status | Body | Meaning |
|---|---|---|
201 | Registered | The project, disabled, and what to do next in Google Cloud. |
400 | Error | invalid_ — a malformed parameter or body. The batch ingest
endpoint uses invalid_ 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. |
401 | Error | unauthorized — no credential, or one that is not valid. |
403 | Error | forbidden — authenticated, but the user's role does not permit this.
Distinct from module_, which is about the tenant's
subscription rather than the user. On an unsafe request, also origin_: the browser boundary
refused the request's origin before anything else ran. |
409 | Error | invalid_ — that project id is already registered. |
413 | Error | The 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 additional limit is roughly
160 MB — so reaching it means something is wrong with the sender. |
415 | Error | unsupported_ — the body of a browser-facing route is not
labelled application/ (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. |
500 | Error | internal_. 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
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
| Field | Type | Description |
|---|---|---|
clientIdrequired | string | The application (client) id of the app registration this deployment
signs in as — the one deploy/ outputs. |
environmentrequired | string enum | One of production, staging, sdlc, shared, unknown. |
namerequired | string | |
subscriptionrequired | string | The subscription's id, a GUID; held lower-case. |
tenantIdrequired | string | The directory (tenant) the subscription trusts, by its id. |
Responses
| Status | Body | Meaning |
|---|---|---|
201 | Registered | The subscription, disabled, and what to do next in Azure. |
400 | Error | invalid_ — a malformed parameter or body. The batch ingest
endpoint uses invalid_ 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. |
401 | Error | unauthorized — no credential, or one that is not valid. |
403 | Error | forbidden — authenticated, but the user's role does not permit this.
Distinct from module_, which is about the tenant's
subscription rather than the user. On an unsafe request, also origin_: the browser boundary
refused the request's origin before anything else ran. |
409 | Error | invalid_ — that subscription is already registered. |
413 | Error | The 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 additional limit is roughly
160 MB — so reaching it means something is wrong with the sender. |
415 | Error | unsupported_ — the body of a browser-facing route is not
labelled application/ (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. |
500 | Error | internal_. 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}
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
| Status | Body | Meaning |
|---|---|---|
200 | Account | The account, without externalId. |
401 | Error | unauthorized — no credential, or one that is not valid. |
403 | Error | forbidden — authenticated, but the user's role does not permit this.
Distinct from module_, which is about the tenant's
subscription rather than the user. On an unsafe request, also origin_: the browser boundary
refused the request's origin before anything else ran. |
404 | Error | not_found |
500 | Error | internal_. 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}
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
| Field | Type | Description |
|---|---|---|
cloudtrail | boolean | Turn CloudTrail attribution on or off for this account.
On asks the platform to call cloudtrail: in the
account, which the reader role grants; a stack older
than that answers each lookup permission_. Turning it on
for a Google Cloud project or an Azure subscription is a 400. |
enabled | boolean | |
environment | string enum | One of production, staging, sdlc, shared, unknown. |
is | boolean | Mark (or unmark) this account as the organisation's management
account. Marking a second one is a 409: there is at most one. |
name | string | |
regions | array of string | |
roleArn | string | Correct the reader role's ARN when it differs from the convention
discovery assumed (role/). 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
| Status | Body | Meaning |
|---|---|---|
200 | Account | The updated account. |
400 | Error | invalid_ — a malformed parameter or body. The batch ingest
endpoint uses invalid_ 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. |
401 | Error | unauthorized — no credential, or one that is not valid. |
403 | Error | forbidden — authenticated, but the user's role does not permit this.
Distinct from module_, which is about the tenant's
subscription rather than the user. On an unsafe request, also origin_: the browser boundary
refused the request's origin before anything else ran. |
404 | Error | not_found |
409 | Error | account_ — 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_ with a 409 — marking this account as the management
account when another already is; unmark that one first. |
413 | Error | The 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 additional limit is roughly
160 MB — so reaching it means something is wrong with the sender. |
415 | Error | unsupported_ — the body of a browser-facing route is not
labelled application/ (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. |
500 | Error | internal_. 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}
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
| Status | Body | Meaning |
|---|---|---|
204 | — | The account is no longer registered. |
401 | Error | unauthorized — no credential, or one that is not valid. |
403 | Error | forbidden — authenticated, but the user's role does not permit this.
Distinct from module_, which is about the tenant's
subscription rather than the user. On an unsafe request, also origin_: the browser boundary
refused the request's origin before anything else ran. |
404 | Error | not_found |
409 | Error | The account stays in the registry, exactly as it was. account_ — 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 / with enabled: false):
nothing is collected from a disabled account, and its agent is
refused new work (403 account_). This is the answer
when a team holds the account too, since taking it off the team
would not let it go. account_ — a team holds this account, and the message
names the team. Unassign it there first
(DELETE /), then remove
it — or disable it instead. |
500 | Error | internal_. 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
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
| Status | Body | Meaning |
|---|---|---|
202 | Collection | The jobs are queued. |
401 | Error | unauthorized — no credential, or one that is not valid. |
403 | Error | forbidden — authenticated, but the user's role does not permit this.
Distinct from module_, which is about the tenant's
subscription rather than the user. On an unsafe request, also origin_: the browser boundary
refused the request's origin before anything else ran. |
404 | Error | not_found |
409 | Error | invalid_ — the account is disabled, or it names no regions
for its regional modules, so there is nothing to queue. |
500 | Error | internal_. 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
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
| Status | Body | Meaning |
|---|---|---|
200 | Account | The onboarding material for this account's cloud. |
401 | Error | unauthorized — no credential, or one that is not valid. |
403 | Error | forbidden — authenticated, but the user's role does not permit this.
Distinct from module_, which is about the tenant's
subscription rather than the user. On an unsafe request, also origin_: the browser boundary
refused the request's origin before anything else ran. |
404 | Error | not_found |
500 | Error | internal_. 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
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
| Name | In | Type | Description |
|---|---|---|---|
accountIdrequired | path | string | The 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
| Status | Body | Meaning |
|---|---|---|
200 | Account | The verification ran. ok reports the outcome — a failed check is
not an HTTP error, it is the answer to the question asked. |
401 | Error | unauthorized — no credential, or one that is not valid. |
403 | Error | forbidden — authenticated, but the user's role does not permit this.
Distinct from module_, which is about the tenant's
subscription rather than the user. On an unsafe request, also origin_: the browser boundary
refused the request's origin before anything else ran. |
404 | Error | not_found |
500 | Error | internal_. 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
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
| Name | In | Type | Description |
|---|---|---|---|
actor | query | string (uuid) | Only this account's actions. |
action | query | string | Only actions that begin with this, as auth. or auth.. |
subjectType | query | string | |
subjectId | query | string | |
from | query | string (date-time) | Entries at or after this instant. |
to | query | string (date-time) | Entries before this instant. |
limit | query | integer | |
cursor | query | string | The previous page's nextCursor; the page is the entries older than its last. |
Responses
| Status | Body | Meaning |
|---|---|---|
200 | Audit | A page of the log. |
400 | Error | invalid_ — a malformed parameter or body. The batch ingest
endpoint uses invalid_ 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. |
401 | Error | unauthorized — no credential, or one that is not valid. |
403 | Error | forbidden — authenticated, but the user's role does not permit this.
Distinct from module_, which is about the tenant's
subscription rather than the user. On an unsafe request, also origin_: the browser boundary
refused the request's origin before anything else ran. |
500 | Error | internal_. 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
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
| Status | Body | Meaning |
|---|---|---|
200 | Cloud | The home and one entry per cloud, in the product's order. |
401 | Error | unauthorized — no credential, or one that is not valid. |
403 | Error | forbidden — authenticated, but the user's role does not permit this.
Distinct from module_, which is about the tenant's
subscription rather than the user. On an unsafe request, also origin_: the browser boundary
refused the request's origin before anything else ran. |
500 | Error | internal_. 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
Recent runs for one account
Parameters
| Name | In | Type | Description |
|---|---|---|---|
accountIdrequired | query | string | |
limit | query | integer |
Responses
| Status | Body | Meaning |
|---|---|---|
200 | Collector | The most recent runs, newest first. |
400 | Error | invalid_ — a malformed parameter or body. The batch ingest
endpoint uses invalid_ 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. |
401 | Error | unauthorized — no credential, or one that is not valid. |
403 | Error | forbidden — authenticated, but the user's role does not permit this.
Distinct from module_, which is about the tenant's
subscription rather than the user. On an unsafe request, also origin_: the browser boundary
refused the request's origin before anything else ran. |
500 | Error | internal_. 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
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
| Status | Body | Meaning |
|---|---|---|
200 | Collector | One entry per account, region and module, plus a per-account roll-up in which the worst cell wins, and the columns those entries name. |
401 | Error | unauthorized — no credential, or one that is not valid. |
403 | Error | forbidden — authenticated, but the user's role does not permit this.
Distinct from module_, which is about the tenant's
subscription rather than the user. On an unsafe request, also origin_: the browser boundary
refused the request's origin before anything else ran. |
500 | Error | internal_. 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
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
| Status | Body | Meaning |
|---|---|---|
200 | Admin | The overview. |
401 | Error | unauthorized — no credential, or one that is not valid. |
403 | Error | forbidden — authenticated, but the user's role does not permit this.
Distinct from module_, which is about the tenant's
subscription rather than the user. On an unsafe request, also origin_: the browser boundary
refused the request's origin before anything else ran. |
500 | Error | internal_. 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
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
| Status | Body | Meaning |
|---|---|---|
200 | string | The manual. |
401 | Error | unauthorized — no credential, or one that is not valid. |
403 | Error | forbidden — authenticated, but the user's role does not permit this.
Distinct from module_, which is about the tenant's
subscription rather than the user. On an unsafe request, also origin_: the browser boundary
refused the request's origin before anything else ran. |
entitlements (1)
entitlements
What the tenant's subscription unlocks.
GET /entitlements
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
| Status | Body | Meaning |
|---|---|---|
200 | Entitlements | The tenant's modules and account cap. |
401 | Error | unauthorized — no credential, or one that is not valid. |
500 | Error | internal_. 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
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
| Name | In | Type | Description |
|---|---|---|---|
X-Collection-Lease | header | string (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
| Field | Type | Description |
|---|---|---|
accountIdrequired | string | |
bucketsrequired | array of Collection | Must 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_, 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. |
modulerequired | string | |
regionrequired | string | |
runIdrequired | string (uuid) | Generated by the collector before its first attempt, and reused on every retry. This is the idempotency key for registration. |
Responses
| Status | Body | Meaning |
|---|---|---|
200 | Collection | Registered, 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. |
400 | Error | invalid_ — a malformed parameter or body. The batch ingest
endpoint uses invalid_ 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. |
401 | Error | unauthorized — no credential, or one that is not valid. |
403 | Error | module_ — 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). |
409 | Error | run_ — this runId is already registered
with a different account, region, module or bucket set. |
413 | Error | The 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 additional limit is roughly
160 MB — so reaching it means something is wrong with the sender. |
500 | Error | internal_. 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
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
| Name | In | Type | Description |
|---|---|---|---|
X-Collection-Lease | header | string (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. |
runIdrequired | path | string (uuid) |
Request body application/json
| Field | Type | Description |
|---|---|---|
reasonrequired | string | Why 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. |
resourcesrequired | integer | Resources 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_ 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_. |
Responses
| Status | Body | Meaning |
|---|---|---|
204 | — | Recorded. |
400 | Error | invalid_ — a malformed parameter or body. The batch ingest
endpoint uses invalid_ 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. |
401 | Error | unauthorized — no credential, or one that is not valid. |
403 | Error | module_ — 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). |
404 | Error | run_ |
413 | Error | The 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 additional limit is roughly
160 MB — so reaching it means something is wrong with the sender. |
500 | Error | internal_. 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
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
| Name | In | Type | Description |
|---|---|---|---|
X-Collection-Lease | header | string (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. |
runIdrequired | path | string (uuid) |
Request body application/json
| Field | Type | Description |
|---|---|---|
resourcesrequired | integer | Resources 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_. |
Responses
| Status | Body | Meaning |
|---|---|---|
200 | Finalize | Reconciled, or already finalized and reporting the stored result. |
400 | Error | invalid_ — a malformed parameter or body. The batch ingest
endpoint uses invalid_ 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. |
401 | Error | unauthorized — no credential, or one that is not valid. |
403 | Error | module_ — 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). |
404 | Error | run_ |
409 | Error | run_ (the run failed) or run_ (a newer run
has claimed one of these buckets, so this one may no longer decide
what is missing). |
413 | Error | The 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 additional limit is roughly
160 MB — so reaching it means something is wrong with the sender. |
500 | Error | internal_. 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
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
| Name | In | Type | Description |
|---|---|---|---|
X-Collection-Lease | header | string (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-Keyrequired | header | string | Stable 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_. 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 body application/json
| Field | Type | Description |
|---|---|---|
itemsrequired | array of Snapshot | No duplicate resource keys within a batch: a duplicate makes the upsert order-dependent, which makes transitions non-deterministic. |
runIdrequired | string (uuid) | The run these observations belong to. Every item must fall inside a bucket that run registered, or the batch is rejected. |
Responses
| Status | Body | Meaning |
|---|---|---|
200 | Snapshot | Stored, or replayed from the stored result. Identical either way. |
400 | Error | invalid_, idempotency_, or invalid_
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. |
401 | Error | unauthorized — no credential, or one that is not valid. |
403 | Error | module_ — this tenant's subscription does not include
the module producing these snapshots. |
404 | Error | run_ — no run with this ID in this tenant. Matches
POST / and /fail, and tasks 23
and 25, which require 404 here. |
409 | Error | idempotency_, run_, run_,
run_ or run_ — the last two
meaning the batch is for a bucket this run never registered. run_ 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. |
413 | Error | The 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 additional limit is roughly
160 MB — so reaching it means something is wrong with the sender. |
500 | Error | internal_. 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
The longest life of a new read token
The organisation's longest read-token life, in days: 90 until an administrator sets it.
Responses
| Status | Body | Meaning |
|---|---|---|
200 | APIToken | The setting. |
401 | Error | unauthorized — no credential, or one that is not valid. |
403 | Error | forbidden — authenticated, but the user's role does not permit this.
Distinct from module_, which is about the tenant's
subscription rather than the user. On an unsafe request, also origin_: the browser boundary
refused the request's origin before anything else ran. |
500 | Error | internal_. 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
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
| Field | Type | Description |
|---|---|---|
maxrequired | integer | The longest life, in days, of a read token issued from now on. |
Responses
| Status | Body | Meaning |
|---|---|---|
200 | APIToken | The setting as it now stands. |
400 | Error | invalid_ — a malformed parameter or body. The batch ingest
endpoint uses invalid_ 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. |
401 | Error | unauthorized — no credential, or one that is not valid. |
403 | Error | forbidden — authenticated, but the user's role does not permit this.
Distinct from module_, which is about the tenant's
subscription rather than the user. On an unsafe request, also origin_: the browser boundary
refused the request's origin before anything else ran. |
413 | Error | The 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 additional limit is roughly
160 MB — so reaching it means something is wrong with the sender. |
415 | Error | unsupported_ — the body of a browser-facing route is not
labelled application/ (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. |
500 | Error | internal_. 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
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
| Status | Body | Meaning |
|---|---|---|
200 | Marker | This tenant's marker credentials, newest first. |
401 | Error | unauthorized — no credential, or one that is not valid. |
403 | Error | forbidden — authenticated, but the user's role does not permit this.
Distinct from module_, which is about the tenant's
subscription rather than the user. On an unsafe request, also origin_: the browser boundary
refused the request's origin before anything else ran. |
500 | Error | internal_. 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
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
| Field | Type | Description |
|---|---|---|
accountIds | array of string | |
appIds | array of string (uuid) | |
namerequired | string | The 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_, "name: must not be blank". |
Responses
| Status | Body | Meaning |
|---|---|---|
201 | Marker | The credential and its token. Store the token now; it cannot be read back. |
400 | Error | invalid_ — a malformed parameter or body. The batch ingest
endpoint uses invalid_ 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. |
401 | Error | unauthorized — no credential, or one that is not valid. |
403 | Error | forbidden — authenticated, but the user's role does not permit this.
Distinct from module_, which is about the tenant's
subscription rather than the user. On an unsafe request, also origin_: the browser boundary
refused the request's origin before anything else ran. |
413 | Error | The 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 additional limit is roughly
160 MB — so reaching it means something is wrong with the sender. |
415 | Error | unsupported_ — the body of a browser-facing route is not
labelled application/ (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. |
500 | Error | internal_. 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}
Revoke a marker credential
Idempotent. The row stays as the record; the token stops being honoured at once.
Responses
| Status | Body | Meaning |
|---|---|---|
204 | — | Revoked, or already was. |
401 | Error | unauthorized — no credential, or one that is not valid. |
403 | Error | forbidden — authenticated, but the user's role does not permit this.
Distinct from module_, which is about the tenant's
subscription rather than the user. On an unsafe request, also origin_: the browser boundary
refused the request's origin before anything else ran. |
404 | Error | not_found |
500 | Error | internal_. 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
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
| Status | Body | Meaning |
|---|---|---|
200 | Notification | The channels, by name. |
401 | Error | unauthorized — no credential, or one that is not valid. |
500 | Error | internal_. 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
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
| Field | Type | Description |
|---|---|---|
enabled | boolean | |
kindrequired | Notification | 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. One of slack, webhook, ai-reviewer, teams, pagerduty, email. |
namerequired | string | |
targetrequired | string | An https URL this deployment will post to. Never read back. |
teamId | string (uuid) | Scope the channel to a team; omit for the tenant default. |
Responses
| Status | Body | Meaning |
|---|---|---|
201 | Notification | The channel, redacted. |
400 | Error | invalid_ — a malformed parameter or body. The batch ingest
endpoint uses invalid_ 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. |
401 | Error | unauthorized — no credential, or one that is not valid. |
403 | Error | forbidden — authenticated, but the user's role does not permit this.
Distinct from module_, which is about the tenant's
subscription rather than the user. On an unsafe request, also origin_: the browser boundary
refused the request's origin before anything else ran. |
404 | Error | not_found |
409 | Error | channel_ — the same target is already a channel of that kind for that team (or the tenant default). |
413 | Error | The 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 additional limit is roughly
160 MB — so reaching it means something is wrong with the sender. |
415 | Error | unsupported_ — the body of a browser-facing route is not
labelled application/ (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. |
500 | Error | internal_. 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}
One channel, redacted
Responses
| Status | Body | Meaning |
|---|---|---|
200 | Notification | The channel. |
401 | Error | unauthorized — no credential, or one that is not valid. |
404 | Error | not_found |
500 | Error | internal_. 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}
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
| Field | Type | Description |
|---|---|---|
enabled | boolean | |
name | string | |
target | string | A new target — the rotation. Checked like a new one; the last test is cleared. |
teamId | string (uuid) | Move the channel to a team. |
tenant | boolean | true takes the channel out of its team and makes it a tenant default. |
Responses
| Status | Body | Meaning |
|---|---|---|
200 | Notification | The channel as it now stands. |
400 | Error | invalid_ — a malformed parameter or body. The batch ingest
endpoint uses invalid_ 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. |
401 | Error | unauthorized — no credential, or one that is not valid. |
403 | Error | forbidden — authenticated, but the user's role does not permit this.
Distinct from module_, which is about the tenant's
subscription rather than the user. On an unsafe request, also origin_: the browser boundary
refused the request's origin before anything else ran. |
404 | Error | not_found |
409 | Error | channel_ — the same target is already a channel of that kind for that team (or the tenant default). |
413 | Error | The 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 additional limit is roughly
160 MB — so reaching it means something is wrong with the sender. |
415 | Error | unsupported_ — the body of a browser-facing route is not
labelled application/ (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. |
500 | Error | internal_. 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}
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
| Status | Body | Meaning |
|---|---|---|
204 | — | Removed. |
401 | Error | unauthorized — no credential, or one that is not valid. |
403 | Error | forbidden — authenticated, but the user's role does not permit this.
Distinct from module_, which is about the tenant's
subscription rather than the user. On an unsafe request, also origin_: the browser boundary
refused the request's origin before anything else ran. |
404 | Error | not_found |
500 | Error | internal_. 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
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
| Status | Body | Meaning |
|---|---|---|
200 | Notification | What the test delivery did. |
401 | Error | unauthorized — no credential, or one that is not valid. |
403 | Error | forbidden — authenticated, but the user's role does not permit this.
Distinct from module_, which is about the tenant's
subscription rather than the user. On an unsafe request, also origin_: the browser boundary
refused the request's origin before anything else ran. |
404 | Error | not_found |
429 | Error | Rate limited. |
500 | Error | internal_. 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
The tenant's ownership overrides
Live first, then revoked, newest first within each; a team narrows the list.
Parameters
| Name | In | Type | Description |
|---|---|---|---|
teamId | query | string (uuid) | |
include | query | boolean |
Responses
| Status | Body | Meaning |
|---|---|---|
200 | Ownership | The overrides. |
400 | Error | invalid_ — a malformed parameter or body. The batch ingest
endpoint uses invalid_ 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. |
401 | Error | unauthorized — no credential, or one that is not valid. |
500 | Error | internal_. 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
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
| Field | Type | Description |
|---|---|---|
accountId | string | |
expiresAt | string (date-time) | |
kindrequired | string enum | One of resource, tag, account. |
reasonrequired | string | |
resourceKey | string | |
tagKey | string | |
tagValue | string | |
teamIdrequired | string (uuid) |
Responses
| Status | Body | Meaning |
|---|---|---|
200 | Ownership | The override. |
400 | Error | invalid_ — a malformed parameter or body. The batch ingest
endpoint uses invalid_ 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. |
401 | Error | unauthorized — no credential, or one that is not valid. |
403 | Error | forbidden — authenticated, but the user's role does not permit this.
Distinct from module_, which is about the tenant's
subscription rather than the user. On an unsafe request, also origin_: the browser boundary
refused the request's origin before anything else ran. |
404 | Error | not_found |
409 | Error | The match already has a live override, or the team is archived. |
500 | Error | internal_. 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}
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
| Name | In | Type | Description |
|---|---|---|---|
overrideIdrequired | path | string (uuid) |
Responses
| Status | Body | Meaning |
|---|---|---|
200 | Ownership | The override, revoked. |
401 | Error | unauthorized — no credential, or one that is not valid. |
403 | Error | forbidden — authenticated, but the user's role does not permit this.
Distinct from module_, which is about the tenant's
subscription rather than the user. On an unsafe request, also origin_: the browser boundary
refused the request's origin before anything else ran. |
404 | Error | not_found |
500 | Error | internal_. 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
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
| Name | In | Type | Description |
|---|---|---|---|
include | query | boolean |
Responses
| Status | Body | Meaning |
|---|---|---|
200 | TeamList | The teams. |
401 | Error | unauthorized — no credential, or one that is not valid. |
500 | Error | internal_. 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
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
| Field | Type | Description |
|---|---|---|
namerequired | string | |
slug | string | Derived from the name when omitted. |
Responses
| Status | Body | Meaning |
|---|---|---|
201 | Team | Created. |
400 | Error | invalid_ — a malformed parameter or body. The batch ingest
endpoint uses invalid_ 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. |
401 | Error | unauthorized — no credential, or one that is not valid. |
403 | Error | forbidden — authenticated, but the user's role does not permit this.
Distinct from module_, which is about the tenant's
subscription rather than the user. On an unsafe request, also origin_: the browser boundary
refused the request's origin before anything else ran. |
409 | Error | team_exists — the name or the slug is already a team's. |
413 | Error | The 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 additional limit is roughly
160 MB — so reaching it means something is wrong with the sender. |
415 | Error | unsupported_ — the body of a browser-facing route is not
labelled application/ (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. |
500 | Error | internal_. 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}
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
| Status | Body | Meaning |
|---|---|---|
200 | TeamDetail | The team. |
401 | Error | unauthorized — no credential, or one that is not valid. |
404 | Error | not_found |
500 | Error | internal_. 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}
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
| Field | Type | Description |
|---|---|---|
archived | boolean | |
name | string |
Responses
| Status | Body | Meaning |
|---|---|---|
200 | Team | The team as it now stands. |
400 | Error | invalid_ — a malformed parameter or body. The batch ingest
endpoint uses invalid_ 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. |
401 | Error | unauthorized — no credential, or one that is not valid. |
403 | Error | forbidden — authenticated, but the user's role does not permit this.
Distinct from module_, which is about the tenant's
subscription rather than the user. On an unsafe request, also origin_: the browser boundary
refused the request's origin before anything else ran. |
404 | Error | not_found |
409 | Error | team_exists — the name or the slug is already a team's. |
413 | Error | The 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 additional limit is roughly
160 MB — so reaching it means something is wrong with the sender. |
415 | Error | unsupported_ — the body of a browser-facing route is not
labelled application/ (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. |
500 | Error | internal_. 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}
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
| Status | Body | Meaning |
|---|---|---|
200 | Team | The assignment, and what it changes. |
401 | Error | unauthorized — no credential, or one that is not valid. |
403 | Error | forbidden — authenticated, but the user's role does not permit this.
Distinct from module_, which is about the tenant's
subscription rather than the user. On an unsafe request, also origin_: the browser boundary
refused the request's origin before anything else ran. |
404 | Error | not_found |
409 | Error | account_ — another team owns the account, named in the
message. team_ — the team is closed. |
500 | Error | internal_. 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}
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
| Status | Body | Meaning |
|---|---|---|
200 | Team | What moved. |
401 | Error | unauthorized — no credential, or one that is not valid. |
403 | Error | forbidden — authenticated, but the user's role does not permit this.
Distinct from module_, which is about the tenant's
subscription rather than the user. On an unsafe request, also origin_: the browser boundary
refused the request's origin before anything else ran. |
404 | Error | not_found |
500 | Error | internal_. 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
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
| Field | Type | Description |
|---|---|---|
grouprequired | string | The group exactly as the provider names it in the groups claim — an id or a name, whichever the provider emits. |
Responses
| Status | Body | Meaning |
|---|---|---|
200 | Team | The team, bound. |
400 | Error | invalid_ — a malformed parameter or body. The batch ingest
endpoint uses invalid_ 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. |
401 | Error | unauthorized — no credential, or one that is not valid. |
403 | Error | forbidden — authenticated, but the user's role does not permit this.
Distinct from module_, which is about the tenant's
subscription rather than the user. On an unsafe request, also origin_: the browser boundary
refused the request's origin before anything else ran. |
404 | Error | not_found |
409 | Error | That group is bound to another live team of the tenant. |
500 | Error | internal_. 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
Unbind the team from its group
The memberships stay as they are and the team is hand-kept again. Idempotent.
Responses
| Status | Body | Meaning |
|---|---|---|
200 | Team | The team, hand-kept. |
401 | Error | unauthorized — no credential, or one that is not valid. |
403 | Error | forbidden — authenticated, but the user's role does not permit this.
Distinct from module_, which is about the tenant's
subscription rather than the user. On an unsafe request, also origin_: the browser boundary
refused the request's origin before anything else ran. |
404 | Error | not_found |
500 | Error | internal_. 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}
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
| Field | Type | Description |
|---|---|---|
teamRolerequired | TeamRole | A 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
| Status | Body | Meaning |
|---|---|---|
200 | TeamMember | The membership. |
400 | Error | invalid_ — a malformed parameter or body. The batch ingest
endpoint uses invalid_ 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. |
401 | Error | unauthorized — no credential, or one that is not valid. |
403 | Error | forbidden — authenticated, but the user's role does not permit this.
Distinct from module_, which is about the tenant's
subscription rather than the user. On an unsafe request, also origin_: the browser boundary
refused the request's origin before anything else ran. |
404 | Error | not_found |
409 | Error | team_ — the team is closed and takes no new assignment. |
413 | Error | The 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 additional limit is roughly
160 MB — so reaching it means something is wrong with the sender. |
415 | Error | unsupported_ — the body of a browser-facing route is not
labelled application/ (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. |
500 | Error | internal_. 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}
Remove a user from the team
Responses
| Status | Body | Meaning |
|---|---|---|
204 | — | Removed. |
401 | Error | unauthorized — no credential, or one that is not valid. |
403 | Error | forbidden — authenticated, but the user's role does not permit this.
Distinct from module_, which is about the tenant's
subscription rather than the user. On an unsafe request, also origin_: the browser boundary
refused the request's origin before anything else ran. |
404 | Error | not_found |
500 | Error | internal_. 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
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
| Field | Type | Description |
|---|---|---|
tagValuerequired | string |
Responses
| Status | Body | Meaning |
|---|---|---|
200 | Team | The mapping, and whether a rebuild was enqueued. |
400 | Error | invalid_ — a malformed parameter or body. The batch ingest
endpoint uses invalid_ 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. |
401 | Error | unauthorized — no credential, or one that is not valid. |
403 | Error | forbidden — authenticated, but the user's role does not permit this.
Distinct from module_, which is about the tenant's
subscription rather than the user. On an unsafe request, also origin_: the browser boundary
refused the request's origin before anything else ran. |
404 | Error | not_found |
409 | Error | tag_ — another team holds the value, named in the
message. team_ — the team is closed. |
413 | Error | The 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 additional limit is roughly
160 MB — so reaching it means something is wrong with the sender. |
415 | Error | unsupported_ — the body of a browser-facing route is not
labelled application/ (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. |
500 | Error | internal_. 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}
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
| Status | Body | Meaning |
|---|---|---|
200 | Team | The mapping that was removed, and whether a rebuild was enqueued. |
401 | Error | unauthorized — no credential, or one that is not valid. |
403 | Error | forbidden — authenticated, but the user's role does not permit this.
Distinct from module_, which is about the tenant's
subscription rather than the user. On an unsafe request, also origin_: the browser boundary
refused the request's origin before anything else ran. |
404 | Error | not_found |
500 | Error | internal_. 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
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
| Status | Body | Meaning |
|---|---|---|
200 | Triage | The counts, one per kind. |
401 | Error | unauthorized — no credential, or one that is not valid. |
403 | Error | forbidden — authenticated, but the user's role does not permit this.
Distinct from module_, which is about the tenant's
subscription rather than the user. On an unsafe request, also origin_: the browser boundary
refused the request's origin before anything else ran. |
500 | Error | internal_. 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
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
| Name | In | Type | Description |
|---|---|---|---|
kind | query | TriageKind | |
accountId | query | string | |
resourceKey | query | string | The resource's key within accountId, which it needs. |
cursor | query | string | The nextCursor of the previous page. |
pageSize | query | integer | Normalised 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
| Status | Body | Meaning |
|---|---|---|
200 | Triage | A page of decisions. |
400 | Error | invalid_ — a malformed parameter or body. The batch ingest
endpoint uses invalid_ 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. |
401 | Error | unauthorized — no credential, or one that is not valid. |
500 | Error | internal_. 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
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
| Status | Body | Meaning |
|---|---|---|
200 | Triage | The settings, one per kind. |
401 | Error | unauthorized — no credential, or one that is not valid. |
403 | Error | forbidden — authenticated, but the user's role does not permit this.
Distinct from module_, which is about the tenant's
subscription rather than the user. On an unsafe request, also origin_: the browser boundary
refused the request's origin before anything else ran. |
500 | Error | internal_. 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}
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
| Field | Type | Description |
|---|---|---|
moderequired | TriageMode | One of off, shadow, enforce. |
none | number (double) | |
reviewer | number (double) |
Responses
| Status | Body | Meaning |
|---|---|---|
200 | Triage | The kind's settings as they now stand. |
400 | Error | invalid_ — a malformed parameter or body. The batch ingest
endpoint uses invalid_ 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. |
401 | Error | unauthorized — no credential, or one that is not valid. |
403 | Error | forbidden — authenticated, but the user's role does not permit this.
Distinct from module_, which is about the tenant's
subscription rather than the user. On an unsafe request, also origin_: the browser boundary
refused the request's origin before anything else ran. |
413 | Error | The 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 additional limit is roughly
160 MB — so reaching it means something is wrong with the sender. |
415 | Error | unsupported_ — the body of a browser-facing route is not
labelled application/ (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. |
500 | Error | internal_. 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
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
| Name | In | Type | Description |
|---|---|---|---|
include | query | boolean | Also list users whose access has been deactivated. |
Responses
| Status | Body | Meaning |
|---|---|---|
200 | OrgUserList | The users, by email. |
401 | Error | unauthorized — no credential, or one that is not valid. |
403 | Error | forbidden — authenticated, but the user's role does not permit this.
Distinct from module_, which is about the tenant's
subscription rather than the user. On an unsafe request, also origin_: the browser boundary
refused the request's origin before anything else ran. |
500 | Error | internal_. 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
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
| Field | Type | Description |
|---|---|---|
emailrequired | string | |
rolerequired | string enum | One of viewer, engineer, admin. |
Responses
| Status | Body | Meaning |
|---|---|---|
201 | Org | The user, and their temporary password, shown once. |
400 | Error | invalid_ — a malformed parameter or body. The batch ingest
endpoint uses invalid_ 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. |
401 | Error | unauthorized — no credential, or one that is not valid. |
403 | Error | forbidden — authenticated, but the user's role does not permit this.
Distinct from module_, which is about the tenant's
subscription rather than the user. On an unsafe request, also origin_: the browser boundary
refused the request's origin before anything else ran. |
409 | Error | user_exists — a user with that email is already in the tenant. |
413 | Error | The 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 additional limit is roughly
160 MB — so reaching it means something is wrong with the sender. |
415 | Error | unsupported_ — the body of a browser-facing route is not
labelled application/ (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. |
500 | Error | internal_. 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}
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
| Field | Type | Description |
|---|---|---|
breakGlass | boolean | An administrator who keeps a password when the tenant enforces single sign-on. Only an administrator can be one. |
disabled | boolean | true deactivates the account; false reactivates it. |
role | string enum | One of viewer, engineer, admin. |
Responses
| Status | Body | Meaning |
|---|---|---|
200 | OrgUser | The user as they now stand. |
400 | Error | invalid_ — a malformed parameter or body. The batch ingest
endpoint uses invalid_ 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. |
401 | Error | unauthorized — no credential, or one that is not valid. |
403 | Error | forbidden — authenticated, but the user's role does not permit this.
Distinct from module_, which is about the tenant's
subscription rather than the user. On an unsafe request, also origin_: the browser boundary
refused the request's origin before anything else ran. |
404 | Error | not_found |
409 | Error | last_admin — the change would leave the tenant with no enabled
administrator, and nothing inside the product could repair that. |
413 | Error | The 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 additional limit is roughly
160 MB — so reaching it means something is wrong with the sender. |
415 | Error | unsupported_ — the body of a browser-facing route is not
labelled application/ (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. |
500 | Error | internal_. 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
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
| Status | Body | Meaning |
|---|---|---|
200 | APIToken | The person's read tokens. |
401 | Error | unauthorized — no credential, or one that is not valid. |
403 | Error | forbidden — authenticated, but the user's role does not permit this.
Distinct from module_, which is about the tenant's
subscription rather than the user. On an unsafe request, also origin_: the browser boundary
refused the request's origin before anything else ran. |
404 | Error | not_found |
500 | Error | internal_. 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}
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
| Status | Body | Meaning |
|---|---|---|
204 | — | Revoked, or already was. |
401 | Error | unauthorized — no credential, or one that is not valid. |
403 | Error | forbidden — authenticated, but the user's role does not permit this.
Distinct from module_, which is about the tenant's
subscription rather than the user. On an unsafe request, also origin_: the browser boundary
refused the request's origin before anything else ran. |
404 | Error | not_found |
500 | Error | internal_. 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
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
| Status | Body | Meaning |
|---|---|---|
201 | Password | The reset token, shown once, and when it expires. |
401 | Error | unauthorized — no credential, or one that is not valid. |
403 | Error | forbidden — authenticated, but the user's role does not permit this.
Distinct from module_, which is about the tenant's
subscription rather than the user. On an unsafe request, also origin_: the browser boundary
refused the request's origin before anything else ran. |
404 | Error | not_found |
500 | Error | internal_. 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
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
| Status | Body | Meaning |
|---|---|---|
204 | — | The sessions are gone. |
401 | Error | unauthorized — no credential, or one that is not valid. |
403 | Error | forbidden — authenticated, but the user's role does not permit this.
Distinct from module_, which is about the tenant's
subscription rather than the user. On an unsafe request, also origin_: the browser boundary
refused the request's origin before anything else ran. |
404 | Error | not_found |
500 | Error | internal_. 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
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
| Status | Body | Meaning |
|---|---|---|
200 | AppGroups | The groups, the keys they came from, and the period spend covers. |
401 | Error | unauthorized — no credential, or one that is not valid. |
403 | Error | forbidden — authenticated, but the user's role does not permit this.
Distinct from module_, which is about the tenant's
subscription rather than the user. On an unsafe request, also origin_: the browser boundary
refused the request's origin before anything else ran. |
429 | Error | Rate limited. |
500 | Error | internal_. 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
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
| Status | Body | Meaning |
|---|---|---|
200 | Declared | The declared apps, by name, and the period spend covers. |
401 | Error | unauthorized — no credential, or one that is not valid. |
403 | Error | forbidden — authenticated, but the user's role does not permit this.
Distinct from module_, which is about the tenant's
subscription rather than the user. On an unsafe request, also origin_: the browser boundary
refused the request's origin before anything else ran. |
429 | Error | Rate limited. |
500 | Error | internal_. 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
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
| Field | Type | Description |
|---|---|---|
description | string | |
exclude | array of ResourceRef | Resources kept out although a match hits them. |
include | array of ResourceRef | Resources added by hand. |
matchesrequired | array of TagMatch | |
namerequired | string | |
teamIdrequired | string (uuid) |
Responses
| Status | Body | Meaning |
|---|---|---|
201 | DeclaredApp | The app, with the list's columns. |
400 | Error | invalid_ — a malformed parameter or body. The batch ingest
endpoint uses invalid_ 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. |
401 | Error | unauthorized — no credential, or one that is not valid. |
403 | Error | forbidden — authenticated, but the user's role does not permit this.
Distinct from module_, which is about the tenant's
subscription rather than the user. On an unsafe request, also origin_: the browser boundary
refused the request's origin before anything else ran. |
404 | Error | not_found |
409 | Error | app_exists: a live app already has this name; team_: the owning team is closed. |
413 | Error | The 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 additional limit is roughly
160 MB — so reaching it means something is wrong with the sender. |
415 | Error | unsupported_ — the body of a browser-facing route is not
labelled application/ (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. |
500 | Error | internal_. 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}
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
| Status | Body | Meaning |
|---|---|---|
200 | Declared | The app, its members and its trend. |
401 | Error | unauthorized — no credential, or one that is not valid. |
403 | Error | forbidden — authenticated, but the user's role does not permit this.
Distinct from module_, which is about the tenant's
subscription rather than the user. On an unsafe request, also origin_: the browser boundary
refused the request's origin before anything else ran. |
404 | Error | not_found |
500 | Error | internal_. 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}
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
| Field | Type | Description |
|---|---|---|
description | string | |
exclude | array of ResourceRef | Resources kept out although a match hits them. |
include | array of ResourceRef | Resources added by hand. |
matchesrequired | array of TagMatch | |
namerequired | string | |
teamIdrequired | string (uuid) |
Responses
| Status | Body | Meaning |
|---|---|---|
200 | DeclaredApp | The app as it now is. |
400 | Error | invalid_ — a malformed parameter or body. The batch ingest
endpoint uses invalid_ 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. |
401 | Error | unauthorized — no credential, or one that is not valid. |
403 | Error | forbidden — authenticated, but the user's role does not permit this.
Distinct from module_, which is about the tenant's
subscription rather than the user. On an unsafe request, also origin_: the browser boundary
refused the request's origin before anything else ran. |
404 | Error | not_found |
409 | Error | app_exists, app_ or team_. |
413 | Error | The 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 additional limit is roughly
160 MB — so reaching it means something is wrong with the sender. |
415 | Error | unsupported_ — the body of a browser-facing route is not
labelled application/ (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. |
500 | Error | internal_. 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
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
| Status | Body | Meaning |
|---|---|---|
200 | DeclaredApp | The app, archived. |
401 | Error | unauthorized — no credential, or one that is not valid. |
403 | Error | forbidden — authenticated, but the user's role does not permit this.
Distinct from module_, which is about the tenant's
subscription rather than the user. On an unsafe request, also origin_: the browser boundary
refused the request's origin before anything else ran. |
404 | Error | not_found |
409 | Error | app_: already archived. |
500 | Error | internal_. 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
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
| Name | In | Type | Description |
|---|---|---|---|
page | query | integer | 1-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. |
pageSize | query | integer | Normalised 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
| Status | Body | Meaning |
|---|---|---|
200 | Library | A page of the library, by slug. |
400 | Error | invalid_ — a malformed parameter or body. The batch ingest
endpoint uses invalid_ 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. |
401 | Error | unauthorized — no credential, or one that is not valid. |
403 | Error | module_ — Require 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_: the browser boundary
refused the request's origin before anything else ran. |
500 | Error | internal_. 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}
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
| Status | Body | Meaning |
|---|---|---|
200 | Library | The template. |
401 | Error | unauthorized — no credential, or one that is not valid. |
403 | Error | module_ — Require 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_: the browser boundary
refused the request's origin before anything else ran. |
404 | Error | not_found |
500 | Error | internal_. 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
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
| Field | Type | Description |
|---|---|---|
name | string | Its name, when not the library's title. |
slugrequired | string | The 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
| Status | Body | Meaning |
|---|---|---|
201 | Template | The new inline template. Its latest is 1. |
400 | Error | invalid_ — a malformed parameter or body. The batch ingest
endpoint uses invalid_ 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. |
401 | Error | unauthorized — no credential, or one that is not valid. |
403 | Error | module_ — Require 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_: the browser boundary
refused the request's origin before anything else ran. |
404 | Error | not_found |
409 | Error | request_ — the slug is taken, or the library
template is an action template. |
413 | Error | The 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 additional limit is roughly
160 MB — so reaching it means something is wrong with the sender. |
415 | Error | unsupported_ — the body of a browser-facing route is not
labelled application/ (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. |
500 | Error | internal_. 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
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
| Field | Type | Description |
|---|---|---|
slug | string | The 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
| Status | Body | Meaning |
|---|---|---|
201 | Template | The enabled template. Its latest is 1. |
400 | Error | invalid_ — a malformed parameter or body. The batch ingest
endpoint uses invalid_ 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. |
401 | Error | unauthorized — no credential, or one that is not valid. |
403 | Error | module_ — Require 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_: the browser boundary
refused the request's origin before anything else ran. |
404 | Error | not_found |
409 | Error | request_ — the tenant has a template enabled from
this library slug already, the template slug is taken, or the
library retired the template. |
413 | Error | The 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 additional limit is roughly
160 MB — so reaching it means something is wrong with the sender. |
415 | Error | unsupported_ — the body of a browser-facing route is not
labelled application/ (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. |
500 | Error | internal_. 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
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
| Name | In | Type | Description |
|---|---|---|---|
category | query | array of string | Repeatable. Any of the given categories. |
resource | query | array of string | Repeatable. Templates whose resource 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. |
cloud | query | Template | Templates for this cloud. A template that names none matches no cloud. |
q | query | string | Case-insensitive text within a template's name, description, slug or one of its resource types — the catalog's search box. |
requestable | query | boolean | Only templates a request may be made from now: not archived, with a
requestable. Default false. |
include | query | boolean | Include templates with archivedAt set. Default false. |
page | query | integer | 1-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. |
pageSize | query | integer | Normalised 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
| Status | Body | Meaning |
|---|---|---|
200 | Template | A page of templates. |
400 | Error | invalid_ — a malformed parameter or body. The batch ingest
endpoint uses invalid_ 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. |
401 | Error | unauthorized — no credential, or one that is not valid. |
403 | Error | module_ — Require 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_: the browser boundary
refused the request's origin before anything else ran. |
500 | Error | internal_. 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
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
| Field | Type | Description |
|---|---|---|
categoryrequired | Template | What a template creates, in the one taxonomy (MILESTONE_Resource 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_. |
cloud | Template | The cloud a template makes its resources in, as the account registry
names it (Account.): a library template's (MILESTONE_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. |
description | string | |
gitRef | string | Default ref for new versions. Only for git. |
gitUrl | string | Required for git, rejected for inline. |
namerequired | string | |
resource | array of string | What the template makes, <cloud>.<service>.<type>, each of
cloud, each once. Types need cloud. |
slugrequired | string | 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. |
sourceTyperequired | string enum | git or inline. A library template is enabled from the
library, never created here. One of git, inline. |
Responses
| Status | Body | Meaning |
|---|---|---|
201 | Template | The registered template. |
400 | Error | invalid_ — a malformed parameter or body. The batch ingest
endpoint uses invalid_ 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. |
401 | Error | unauthorized — no credential, or one that is not valid. |
403 | Error | module_ — Require 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_: the browser boundary
refused the request's origin before anything else ran. |
409 | Error | invalid_ 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. |
413 | Error | The 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 additional limit is roughly
160 MB — so reaching it means something is wrong with the sender. |
415 | Error | unsupported_ — the body of a browser-facing route is not
labelled application/ (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. |
500 | Error | internal_. 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}
One template
Responses
| Status | Body | Meaning |
|---|---|---|
200 | Template | The template. |
401 | Error | unauthorized — no credential, or one that is not valid. |
403 | Error | module_ — Require 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_: the browser boundary
refused the request's origin before anything else ran. |
404 | Error | not_found |
500 | Error | internal_. 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}
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
| Field | Type | Description |
|---|---|---|
archived | boolean | True archives, false restores. Archiving hides the template and closes it to new versions; it never touches what was provisioned from it. |
category | Template | What a template creates, in the one taxonomy (MILESTONE_Resource 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_. |
cloud | Template | Moves 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. |
description | string | |
gitRef | string | |
name | string | |
resource | array of string | Replaces 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
| Status | Body | Meaning |
|---|---|---|
200 | Template | The updated template. |
400 | Error | invalid_ — a malformed parameter or body. The batch ingest
endpoint uses invalid_ 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. |
401 | Error | unauthorized — no credential, or one that is not valid. |
403 | Error | module_ — Require 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_: the browser boundary
refused the request's origin before anything else ran. |
404 | Error | not_found |
413 | Error | The 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 additional limit is roughly
160 MB — so reaching it means something is wrong with the sender. |
415 | Error | unsupported_ — the body of a browser-facing route is not
labelled application/ (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. |
500 | Error | internal_. 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
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
| Name | In | Type | Description |
|---|---|---|---|
page | query | integer | 1-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. |
pageSize | query | integer | Normalised 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
| Status | Body | Meaning |
|---|---|---|
200 | Template | A page of versions. |
400 | Error | invalid_ — a malformed parameter or body. The batch ingest
endpoint uses invalid_ 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. |
401 | Error | unauthorized — no credential, or one that is not valid. |
403 | Error | module_ — Require 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_: the browser boundary
refused the request's origin before anything else ran. |
404 | Error | not_found |
500 | Error | internal_. 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
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
| Field | Type | Description |
|---|---|---|
fromLibrary | boolean | For library templates, and only true: publish the version the
library now carries for the template's library slug (MILESTONE_library. |
gitRef | string | For git templates. Defaults to the template's gitRef. This is
what an agent resolves; it is never what runs. |
import | Import | For inline templates: what this version declares it can take
over (MILESTONE_resource and additional (400 invalid_, naming the rule). Refused for git, whose source
is not known at publish, and for library, whose declaration is
the library's. |
inline | string | For 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. |
library | string | With 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_ and
no version is created. Refused on a git or inline publish. |
outputs | array of Template | For git and inline templates: the outputs this version
declares (MILESTONE_library, whose outputs are the library's. |
variables | Variables | Stored on the version and immutable with it. The portal generates the
request form from this; the agent uses it to write
generated. as proper HCL through an encoder, never by
string concatenation, because hand-rolled escaping is how tfvars
injection happens. |
Responses
| Status | Body | Meaning |
|---|---|---|
201 | Template | The new version. resolved for inline, pending for git. |
400 | Error | invalid_ — a malformed parameter or body. The batch ingest
endpoint uses invalid_ 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. |
401 | Error | unauthorized — no credential, or one that is not valid. |
403 | Error | module_ — Require 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_: the browser boundary
refused the request's origin before anything else ran. |
404 | Error | not_found |
409 | Error | request_ — the template is archived, so it takes no
new versions; or, with fromLibrary, its latest version already
pins the hash the library carries. library_ — with fromLibrary, the library carries another
version of the template's slug than library names: it
changed since the change was read. Nothing was published. Read the
library detail again, and publish only once its change is shown. |
413 | Error | The 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 additional limit is roughly
160 MB — so reaching it means something is wrong with the sender. |
415 | Error | unsupported_ — the body of a browser-facing route is not
labelled application/ (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. |
500 | Error | internal_. 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}
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
| Status | Body | Meaning |
|---|---|---|
200 | Template | The version. |
401 | Error | unauthorized — no credential, or one that is not valid. |
403 | Error | module_ — Require 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_: the browser boundary
refused the request's origin before anything else ran. |
404 | Error | not_found |
500 | Error | internal_. 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
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
| Field | Type | Description |
|---|---|---|
categoryrequired | Template | What a template creates, in the one taxonomy (MILESTONE_Resource 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_. |
description | string | |
namerequired | string | |
slugrequired | string | The 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
| Status | Body | Meaning |
|---|---|---|
201 | Template | The new catalog template. |
400 | Error | invalid_ — a malformed parameter or body. The batch ingest
endpoint uses invalid_ 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. |
401 | Error | unauthorized — no credential, or one that is not valid. |
403 | Error | module_ — Require 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_: the browser boundary
refused the request's origin before anything else ran. |
404 | Error | not_found |
409 | Error | request_ — the version is not a generated
configuration, or the slug is taken. |
500 | Error | internal_. 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
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
| Status | Body | Meaning |
|---|---|---|
202 | Template | Requeued. resolve is back to pending; resolve
names the account whose agent will claim it, or is absent when
none is eligible yet. |
401 | Error | unauthorized — no credential, or one that is not valid. |
403 | Error | module_ — Require 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_: the browser boundary
refused the request's origin before anything else ran. |
404 | Error | not_found |
409 | Error | resolution_ — the version is resolved (its source
identity is written once and never updated), or an agent is
running its resolution now. |
500 | Error | internal_. 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
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
| Field | Type | Description |
|---|---|---|
accountIdrequired | string | |
actionrequired | Resource | An action on an existing resource — the closed set, one so
far. stop stops a running EC2 instance (aws.): its
template sets aws_ 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. |
origin | Resource | 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_). |
regionrequired | string | |
resourceIdrequired | string | |
resourcerequired | string | |
servicerequired | string | |
systemrequired | string | |
templateIdrequired | string (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. |
templaterequired | integer | Explicit, never "latest", as for a create. |
Responses
| Status | Body | Meaning |
|---|---|---|
201 | Provision | Created in pending_; the approvers are told. |
400 | Error | invalid_, 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 (resource), 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. |
401 | Error | unauthorized — no credential, or one that is not valid. |
403 | Error | capability_ — the licence does not carry
remediation; module_ — 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. |
404 | Error | not_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. |
409 | Error | resource_ — a provisioning state holds the
resource, the owner named; request_ — 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_; account_. |
413 | Error | The 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 additional limit is roughly
160 MB — so reaching it means something is wrong with the sender. |
415 | Error | unsupported_ — the body of a browser-facing route is not
labelled application/ (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. |
500 | Error | internal_. 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
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
| Name | In | Type | Description |
|---|---|---|---|
accountIdrequired | query | string | |
regionrequired | query | string | |
systemrequired | query | string | |
servicerequired | query | string | |
resourcerequired | query | string | |
resourceIdrequired | query | string | |
origin | query | Finding | With origin 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. |
origin | query | string | |
originScope | query | string |
Responses
| Status | Body | Meaning |
|---|---|---|
200 | Resource | The actions offered, by kind and then template name; empty when none is. |
400 | Error | invalid_ — the key is not named whole, or the origin is named in part. |
401 | Error | unauthorized — no credential, or one that is not valid. |
403 | Error | capability_ — the licence does not carry
remediation; module_; or forbidden — a viewer. |
500 | Error | internal_. 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
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
| Name | In | Type | Description |
|---|---|---|---|
state | query | array of Provision | Repeatable. Any of the given request states. |
accountId | query | array of string | Repeatable. Any of the given accounts. |
templateId | query | string (uuid) | |
awaiting | query | boolean | Only requests in pending_ 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. |
from | query | string (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. |
expires | query | string (date-time) | Only requests whose expiresAt — the pending_ deadline
— is before this instant. With
awaiting, 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. |
page | query | integer | 1-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. |
pageSize | query | integer | Normalised 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
| Status | Body | Meaning |
|---|---|---|
200 | Provision | A page of requests. |
400 | Error | invalid_ — 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. |
401 | Error | unauthorized — no credential, or one that is not valid. |
403 | Error | module_ — Require 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_: the browser boundary
refused the request's origin before anything else ran. |
500 | Error | internal_. 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
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
| Field | Type | Description |
|---|---|---|
actionrequired | Provision | create, destroy or adopt. resolve is refused here with
invalid_: 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 /, under
the licence's remediation capability. An adopt (MILESTONE_importId), and the template version and account it was recorded
under; its variables are validated as a create's (the pre-fill,
GET /, suggests them). Refused: an
import that is not recorded (409 request_); a
version that declares no import of the resource's type, or whose
identifiers the resource's inventory record cannot finish (400 invalid_); 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_, 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
(agent 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_, 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_, 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. is unplanned).
A type the platform has no generator for, or a record that lacks
what the configuration needs, is refused (400 invalid_)
with the reason, naming the attribute; nothing is stored. |
importId | string (uuid) | Required for adopt, rejected otherwise — the recorded import it makes fully managed. |
namerequired | string | The 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). |
resourceId | string (uuid) | Required for destroy, rejected for create. |
targetrequired | string | A 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. |
teamId | string (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. |
templateIdrequired | string (uuid) | |
templaterequired | integer | Explicit, never "latest". A request that pinned a moving target would be approved against one version and applied from another. |
variables | object | create 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_ naming variables; an empty object names none
and is accepted. |
Responses
| Status | Body | Meaning |
|---|---|---|
201 | Provision | Created in pending_, with expiresAt already set. The
approvers for the target account's owning team are notified through
the platform outbox. |
400 | Provision | invalid_. 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". |
401 | Error | unauthorized — no credential, or one that is not valid. |
403 | Error | module_ — Require 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_: the browser boundary
refused the request's origin before anything else ran. |
404 | Error | not_found |
409 | Error | version_ — 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_, 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_
covers a destroy its resource cannot take now: already destroyed,
or another request on it in flight, its create's retry included. |
413 | Error | The 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 additional limit is roughly
160 MB — so reaching it means something is wrong with the sender. |
415 | Error | unsupported_ — the body of a browser-facing route is not
labelled application/ (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. |
500 | Error | internal_. 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}
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
| Name | In | Type | Description |
|---|---|---|---|
source | query | boolean | false leaves out version., 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. and
binding 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. of the answer it takes binding
from. When a poll's inline 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
| Status | Body | Meaning |
|---|---|---|
200 | Provision | The request. |
401 | Error | unauthorized — no credential, or one that is not valid. |
403 | Error | module_ — Require 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_: the browser boundary
refused the request's origin before anything else ran. |
404 | Error | not_found |
500 | Error | internal_. 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
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
| Status | Body | Meaning |
|---|---|---|
200 | Provision | cancelled if nothing had been claimed, cancelling if an agent is
running it. |
401 | Error | unauthorized — no credential, or one that is not valid. |
403 | Error | module_ — Require 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_: the browser boundary
refused the request's origin before anything else ran. |
404 | Error | not_found |
409 | Error | request_ — the request is terminal, or still
awaiting approval (withdraw it instead). |
500 | Error | internal_. 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
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
| Field | Type | Description |
|---|---|---|
binding | string | The request detail's binding 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_ 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. |
comment | string | Recorded permanently and shown to the requester. Optional on approve; a rejection without one is technically allowed and practically unkind. |
decisionrequired | string enum | One of approved, rejected. |
Responses
| Status | Body | Meaning |
|---|---|---|
200 | Provision | Decided. 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. |
400 | Error | invalid_ — a malformed parameter or body. The batch ingest
endpoint uses invalid_ 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. |
401 | Error | unauthorized — no credential, or one that is not valid. |
403 | Error | self_ — 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_ — 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 / and send the decision again. Otherwise forbidden (not an approver for this account's team) or
module_. |
404 | Error | not_found |
409 | Error | request_ — the request is not in pending_.
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_ — an approval whose binding 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. |
413 | Error | The 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 additional limit is roughly
160 MB — so reaching it means something is wrong with the sender. |
415 | Error | unsupported_ — the body of a browser-facing route is not
labelled application/ (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. |
500 | Error | internal_. 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
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
| Status | Body | Meaning |
|---|---|---|
200 | Provision | Queued for its generate step. |
400 | Error | invalid_ — a malformed parameter or body. The batch ingest
endpoint uses invalid_ 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. |
401 | Error | unauthorized — no credential, or one that is not valid. |
403 | Error | module_ — Require 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_: the browser boundary
refused the request's origin before anything else ran. |
404 | Error | not_found |
409 | Error | request_ — not in pending_ (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. |
500 | Error | internal_. 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
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
| Status | Body | Meaning |
|---|---|---|
200 | Provision | Queued again, with attempt incremented. |
401 | Error | unauthorized — no credential, or one that is not valid. |
403 | Error | module_ — Require 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_: the browser boundary
refused the request's origin before anything else ran. |
404 | Error | not_found |
409 | Error | request_ — 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_, if the pinned version's
resolution has since been invalidated. |
500 | Error | internal_. 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
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
| Field | Type | Description |
|---|---|---|
attemptrequired | integer | |
valuesrequired | array of Provision |
Responses
| Status | Body | Meaning |
|---|---|---|
204 | — | Sealed for the attempt; the import may be claimed. |
400 | Error | invalid_ — a malformed parameter or body. The batch ingest
endpoint uses invalid_ 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. |
401 | Error | unauthorized — no credential, or one that is not valid. |
403 | Error | module_ — Require 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_: the browser boundary
refused the request's origin before anything else ran. |
404 | Error | not_found |
409 | Error | request_ — not an adopt of a generated import that
needs values, or not pending_ 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. |
500 | Error | internal_. 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
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
| Status | Body | Meaning |
|---|---|---|
200 | Provision | The keys to seal to. |
401 | Error | unauthorized — no credential, or one that is not valid. |
403 | Error | module_ — Require 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_: the browser boundary
refused the request's origin before anything else ran. |
404 | Error | not_found |
409 | Error | request_ — not an adopt of a generated import that
needs values, or not pending_ or queued; or no agent of
the account can open them, or more than 16 can. |
500 | Error | internal_. 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
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
| Status | Body | Meaning |
|---|---|---|
200 | Provision | The trail. |
401 | Error | unauthorized — no credential, or one that is not valid. |
403 | Error | module_ — Require 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_: the browser boundary
refused the request's origin before anything else ran. |
404 | Error | not_found |
500 | Error | internal_. 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
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
| Status | Body | Meaning |
|---|---|---|
200 | Provision | Withdrawn. |
401 | Error | unauthorized — no credential, or one that is not valid. |
403 | Error | module_ — Require 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_: the browser boundary
refused the request's origin before anything else ran. |
404 | Error | not_found |
409 | Error | request_ — not in pending_. |
500 | Error | internal_. 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
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
| Name | In | Type | Description |
|---|---|---|---|
page | query | integer | 1-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. |
pageSize | query | integer | Normalised 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
| Status | Body | Meaning |
|---|---|---|
200 | Provision | A page of tasks, newest first. |
400 | Error | invalid_ — a malformed parameter or body. The batch ingest
endpoint uses invalid_ 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. |
401 | Error | unauthorized — no credential, or one that is not valid. |
403 | Error | module_ — Require 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_: the browser boundary
refused the request's origin before anything else ran. |
404 | Error | not_found |
500 | Error | internal_. 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}
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
| Status | Body | Meaning |
|---|---|---|
200 | Provision | The task, with its summary once it is terminal. |
401 | Error | unauthorized — no credential, or one that is not valid. |
403 | Error | module_ — Require 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_: the browser boundary
refused the request's origin before anything else ran. |
404 | Error | not_found |
500 | Error | internal_. 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
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
| Name | In | Type | Description |
|---|---|---|---|
cursor | query | string | Opaque keyset cursor from the previous page's nextCursor. Omit for
the first page, which starts at the lowest retained seq. |
pageSize | query | integer | Normalised 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
| Status | Body | Meaning |
|---|---|---|
200 | Provision | A page of log lines, oldest first. |
400 | Error | invalid_ — a malformed parameter or body. The batch ingest
endpoint uses invalid_ 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. |
401 | Error | unauthorized — no credential, or one that is not valid. |
403 | Error | module_ — Require 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_: the browser boundary
refused the request's origin before anything else ran. |
404 | Error | not_found |
500 | Error | internal_. 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
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
| Name | In | Type | Description |
|---|---|---|---|
accountId | query | array of string | Repeatable. Any of the given accounts. |
page | query | integer | 1-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. |
pageSize | query | integer | Normalised 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
| Status | Body | Meaning |
|---|---|---|
200 | Drift | A page of provisioned-but-missing findings. |
400 | Error | invalid_ — a malformed parameter or body. The batch ingest
endpoint uses invalid_ 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. |
401 | Error | unauthorized — no credential, or one that is not valid. |
403 | Error | module_ — Require 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_: the browser boundary
refused the request's origin before anything else ran. |
429 | Error | Rate limited. |
500 | Error | internal_. 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
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
| Name | In | Type | Description |
|---|---|---|---|
accountId | query | array of string | Repeatable. Any of the given accounts. |
service | query | array of string | Repeatable. |
imported | query | boolean | true: only the resources a live import has recorded, each with
importId. false, the default: only the ones nobody has. |
page | query | integer | 1-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. |
pageSize | query | integer | Normalised 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
| Status | Body | Meaning |
|---|---|---|
200 | Drift | A page of existing-but-unprovisioned findings. |
400 | Error | invalid_ — a malformed parameter or body. The batch ingest
endpoint uses invalid_ 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. |
401 | Error | unauthorized — no credential, or one that is not valid. |
403 | Error | module_ — Require 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_: the browser boundary
refused the request's origin before anything else ran. |
429 | Error | Rate limited. |
500 | Error | internal_. 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
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
| Name | In | Type | Description |
|---|---|---|---|
accountId | query | array of string | Repeatable. Any of the given accounts. |
templateId | query | string (uuid) | |
origin | query | Provisioned | Only rows of this origin: provisioned here, or
imported. |
create | query | string (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). |
status | query | array of Provisioned | Repeatable. |
page | query | integer | 1-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. |
pageSize | query | integer | Normalised 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
| Status | Body | Meaning |
|---|---|---|
200 | Provisioned | A page of provisioned resources. |
400 | Error | invalid_ — a malformed parameter or body. The batch ingest
endpoint uses invalid_ 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. |
401 | Error | unauthorized — no credential, or one that is not valid. |
403 | Error | module_ — Require 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_: the browser boundary
refused the request's origin before anything else ran. |
429 | Error | Rate limited. |
500 | Error | internal_. 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
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
| Name | In | Type | Description |
|---|---|---|---|
accountId | query | array of string | Repeatable. Any of the given accounts. |
region | query | string | |
system | query | string | |
service | query | string | |
resource | query | string | |
resourceId | query | string | |
state | query | array of Resource | Repeatable. |
page | query | integer | 1-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. |
pageSize | query | integer | Normalised 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
| Status | Body | Meaning |
|---|---|---|
200 | Resource | A page of imports. |
400 | Error | invalid_ — a malformed parameter or body. The batch ingest
endpoint uses invalid_ 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. |
401 | Error | unauthorized — no credential, or one that is not valid. |
403 | Error | module_ — Require 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_: the browser boundary
refused the request's origin before anything else ran. |
500 | Error | internal_. 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
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
| Field | Type | Description |
|---|---|---|
accountIdrequired | string | |
regionrequired | string | |
resourceIdrequired | string | |
resourcerequired | string | |
servicerequired | string | |
systemrequired | string | |
teamIdrequired | string (uuid) | |
templateId | string (uuid) | Required unless template is generated, and refused with it. |
template | Resource | 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. One of catalog, generated. |
template | integer |
Responses
| Status | Body | Meaning |
|---|---|---|
201 | Resource | Recorded. |
400 | Error | invalid_ — a malformed parameter or body. The batch ingest
endpoint uses invalid_ 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. |
401 | Error | unauthorized — no credential, or one that is not valid. |
403 | Error | module_ — Require 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_: the browser boundary
refused the request's origin before anything else ran. |
404 | Error | not_found |
409 | Error | already_, resource_,
account_, version_ or
version_. |
500 | Error | internal_. 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
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
| Name | In | Type | Description |
|---|---|---|---|
accountIdrequired | query | string | |
regionrequired | query | string | |
systemrequired | query | string | |
servicerequired | query | string | |
resourcerequired | query | string | |
resourceIdrequired | query | string |
Responses
| Status | Body | Meaning |
|---|---|---|
200 | Import | The preview. |
400 | Error | invalid_ — a malformed parameter or body. The batch ingest
endpoint uses invalid_ 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. |
401 | Error | unauthorized — no credential, or one that is not valid. |
403 | Error | module_ — Require 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_: the browser boundary
refused the request's origin before anything else ran. |
404 | Error | not_found |
409 | Error | account_ — no team holds the resource's account. |
500 | Error | internal_. 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
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
| Name | In | Type | Description |
|---|---|---|---|
accountIdrequired | query | string | |
regionrequired | query | string | |
systemrequired | query | string | |
servicerequired | query | string | |
resourcerequired | query | string | |
resourceIdrequired | query | string | |
templateIdrequired | query | string (uuid) | |
versionrequired | query | integer |
Responses
| Status | Body | Meaning |
|---|---|---|
200 | Import | The pre-fill. |
400 | Error | invalid_ — a malformed parameter or body. The batch ingest
endpoint uses invalid_ 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. |
401 | Error | unauthorized — no credential, or one that is not valid. |
403 | Error | module_ — Require 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_: the browser boundary
refused the request's origin before anything else ran. |
404 | Error | not_found |
409 | Error | account_ — no team holds the resource's account. |
500 | Error | internal_. 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
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
| Name | In | Type | Description |
|---|---|---|---|
importIdrequired | path | string (uuid) |
Responses
| Status | Body | Meaning |
|---|---|---|
200 | Resource | Released. |
400 | Error | invalid_ — a malformed parameter or body. The batch ingest
endpoint uses invalid_ 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. |
401 | Error | unauthorized — no credential, or one that is not valid. |
403 | Error | module_ — Require 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_: the browser boundary
refused the request's origin before anything else ran. |
404 | Error | not_found |
409 | Error | import_ — the import is no longer only recorded. |
500 | Error | internal_. 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
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
| Name | In | Type | Description |
|---|---|---|---|
importIdrequired | path | string (uuid) |
Responses
| Status | Body | Meaning |
|---|---|---|
201 | Provision | Created in pending_. |
400 | Error | invalid_ — a malformed parameter or body. The batch ingest
endpoint uses invalid_ 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. |
401 | Error | unauthorized — no credential, or one that is not valid. |
403 | Error | module_ — Require 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_: the browser boundary
refused the request's origin before anything else ran. |
404 | Error | not_found |
409 | Error | request_ — the import is not imported, or a
request still acts on it; account_ — no team holds
the resource's account. |
500 | Error | internal_. 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
List Blueprint runs
Newest first. An administrator reads every run of the organisation; anyone else the runs of their teams.
Parameters
| Name | In | Type | Description |
|---|---|---|---|
state | query | array of Blueprint | Repeatable. |
awaiting | query | boolean | Only runs in pending_ this user may decide — their own
exactly when they may approve it themselves because nobody else
could — and none for a viewer. |
page | query | integer | 1-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. |
pageSize | query | integer | Normalised 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
| Status | Body | Meaning |
|---|---|---|
200 | Blueprint | A page of runs. |
400 | Error | invalid_ — a malformed parameter or body. The batch ingest
endpoint uses invalid_ 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. |
401 | Error | unauthorized — no credential, or one that is not valid. |
403 | Error | module_ — Require 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_: the browser boundary
refused the request's origin before anything else ran. |
500 | Error | internal_. 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
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
| Field | Type | Description |
|---|---|---|
blueprintIdrequired | string (uuid) | |
namerequired | string | The run's name; each step's request is named after it. |
slotrequired | object | Every slot of the version to its accounts. |
teamId | string (uuid) | Optional; when given, it must be the team that owns every slot account. |
variables | object | |
versionrequired | integer |
Responses
| Status | Body | Meaning |
|---|---|---|
201 | Blueprint | Created in pending_. |
400 | Provision | invalid_ — the name, the slot mapping's shape, or a run
variable (details names every variable that failed and why). |
401 | Error | unauthorized — no credential, or one that is not valid. |
403 | Error | forbidden — the requester does not belong to the team that owns the accounts. |
404 | Error | not_found |
409 | Error | account_ — 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_ — a
slot account has no live agent that can run its steps, and the
message says why; tag_; or
request_ — the Blueprint or a step's template is
archived. |
413 | Error | The 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 additional limit is roughly
160 MB — so reaching it means something is wrong with the sender. |
415 | Error | unsupported_ — the body of a browser-facing route is not
labelled application/ (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. |
500 | Error | internal_. 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}
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
| Status | Body | Meaning |
|---|---|---|
200 | Blueprint | The run. |
401 | Error | unauthorized — no credential, or one that is not valid. |
403 | Error | module_ — Require 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_: the browser boundary
refused the request's origin before anything else ran. |
404 | Error | not_found |
500 | Error | internal_. 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
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
| Status | Body | Meaning |
|---|---|---|
200 | Blueprint | Cancelled, or cancelling while a step stops. |
401 | Error | unauthorized — no credential, or one that is not valid. |
403 | Error | forbidden — not of the run's team, and not an administrator. |
404 | Error | not_found |
409 | Error | request_ — the run is not running or failed, the message naming its state. |
500 | Error | internal_. 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
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
| Field | Type | Description |
|---|---|---|
binding | string | Required to approve: the digest of the binding the approver read.
A binding that moved since is refused (409 binding_). |
comment | string | |
decisionrequired | string enum | One of approved, rejected. |
Responses
| Status | Body | Meaning |
|---|---|---|
200 | Blueprint | Decided. |
400 | Error | invalid_ — a malformed parameter or body. The batch ingest
endpoint uses invalid_ 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. |
401 | Error | unauthorized — no credential, or one that is not valid. |
403 | Error | self_ — the actor is the requester, whatever
their role, and somebody else could decide the run, or the
decision is a rejection; step_ — 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. |
404 | Error | not_found |
409 | Error | request_ — the run is not in pending_,
the message naming its state; a decision at or after expiresAt
expires it then and is refused the same way. binding_ —
the binding named is not the run's binding now; read it again. |
413 | Error | The 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 additional limit is roughly
160 MB — so reaching it means something is wrong with the sender. |
415 | Error | unsupported_ — the body of a browser-facing route is not
labelled application/ (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. |
500 | Error | internal_. 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
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
| Status | Body | Meaning |
|---|---|---|
201 | Blueprint | The destroy run, in pending_. |
401 | Error | unauthorized — no credential, or one that is not valid. |
403 | Error | step_ — 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. |
404 | Error | not_found |
409 | Error | request_, 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_ or slot_ — a slot
account no longer meets the run's rules, the message naming it. |
500 | Error | internal_. 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
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
| Status | Body | Meaning |
|---|---|---|
200 | Blueprint | Running again. |
401 | Error | unauthorized — no credential, or one that is not valid. |
403 | Error | forbidden — not of the run's team, and not an administrator. |
404 | Error | not_found |
409 | Error | request_, the message naming why: the run is not
failed; it stopped at a wired value (output_,
output_), 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. |
500 | Error | internal_. 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
Withdraw a run awaiting approval
The requester's own, and only from pending_approval.
Responses
| Status | Body | Meaning |
|---|---|---|
200 | Blueprint | Withdrawn. |
401 | Error | unauthorized — no credential, or one that is not valid. |
403 | Error | module_ — Require 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_: the browser boundary
refused the request's origin before anything else ran. |
404 | Error | not_found |
409 | Error | request_ — not in pending_. |
500 | Error | internal_. 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
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
| Name | In | Type | Description |
|---|---|---|---|
include | query | boolean | |
page | query | integer | 1-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. |
pageSize | query | integer | Normalised 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
| Status | Body | Meaning |
|---|---|---|
200 | Blueprint | A page of Blueprints. |
400 | Error | invalid_ — a malformed parameter or body. The batch ingest
endpoint uses invalid_ 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. |
401 | Error | unauthorized — no credential, or one that is not valid. |
403 | Error | module_ — Require 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_: the browser boundary
refused the request's origin before anything else ran. |
500 | Error | internal_. 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
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
| Field | Type | Description |
|---|---|---|
description | string | |
namerequired | string | |
slugrequired | string |
Responses
| Status | Body | Meaning |
|---|---|---|
201 | Blueprint | Created, with no version yet. |
400 | Error | invalid_ — a malformed parameter or body. The batch ingest
endpoint uses invalid_ 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. |
401 | Error | unauthorized — no credential, or one that is not valid. |
403 | Error | module_ — Require 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_: the browser boundary
refused the request's origin before anything else ran. |
409 | Error | request_ — a Blueprint with this slug exists. |
413 | Error | The 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 additional limit is roughly
160 MB — so reaching it means something is wrong with the sender. |
415 | Error | unsupported_ — the body of a browser-facing route is not
labelled application/ (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. |
500 | Error | internal_. 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}
One Blueprint
Responses
| Status | Body | Meaning |
|---|---|---|
200 | Blueprint | The Blueprint. |
401 | Error | unauthorized — no credential, or one that is not valid. |
403 | Error | module_ — Require 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_: the browser boundary
refused the request's origin before anything else ran. |
404 | Error | not_found |
500 | Error | internal_. 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
A Blueprint's published versions
Newest first.
Parameters
| Name | In | Type | Description |
|---|---|---|---|
page | query | integer | 1-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. |
pageSize | query | integer | Normalised 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
| Status | Body | Meaning |
|---|---|---|
200 | Blueprint | A page of versions. |
400 | Error | invalid_ — a malformed parameter or body. The batch ingest
endpoint uses invalid_ 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. |
401 | Error | unauthorized — no credential, or one that is not valid. |
403 | Error | module_ — Require 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_: the browser boundary
refused the request's origin before anything else ran. |
404 | Error | not_found |
500 | Error | internal_. 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
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
| Field | Type | Description |
|---|---|---|
non | array of string | Run 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. |
slotsrequired | array of Blueprint | |
stepsrequired | array of Blueprint | |
variables | Variables | Stored on the version and immutable with it. The portal generates the
request form from this; the agent uses it to write
generated. as proper HCL through an encoder, never by
string concatenation, because hand-rolled escaping is how tfvars
injection happens. |
Responses
| Status | Body | Meaning |
|---|---|---|
201 | Blueprint | Published. |
400 | Error | invalid_ — a malformed parameter or body. The batch ingest
endpoint uses invalid_ 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. |
401 | Error | unauthorized — no credential, or one that is not valid. |
403 | Error | module_ — Require 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_: the browser boundary
refused the request's origin before anything else ran. |
404 | Error | not_found |
409 | Error | request_ — the Blueprint is archived. |
413 | Error | The 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 additional limit is roughly
160 MB — so reaching it means something is wrong with the sender. |
415 | Error | unsupported_ — the body of a browser-facing route is not
labelled application/ (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. |
500 | Error | internal_. 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}
One version, with its graph
Responses
| Status | Body | Meaning |
|---|---|---|
200 | Blueprint | The version. |
401 | Error | unauthorized — no credential, or one that is not valid. |
403 | Error | module_ — Require 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_: the browser boundary
refused the request's origin before anything else ran. |
404 | Error | not_found |
500 | Error | internal_. 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
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
| Status | Body | Meaning |
|---|---|---|
200 | Blueprint | The version's slots, each with the reader's accounts judged. |
401 | Error | unauthorized — no credential, or one that is not valid. |
403 | Error | module_ — Require 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_: the browser boundary
refused the request's origin before anything else ran. |
404 | Error | not_found |
500 | Error | internal_. 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
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
| Name | In | Type | Description |
|---|---|---|---|
page | query | integer | 1-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. |
pageSize | query | integer | Normalised 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
| Status | Body | Meaning |
|---|---|---|
200 | Library | A page of the library's Blueprints, by slug. |
400 | Error | invalid_ — a malformed parameter or body. The batch ingest
endpoint uses invalid_ 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. |
401 | Error | unauthorized — no credential, or one that is not valid. |
403 | Error | module_ — Require 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_: the browser boundary
refused the request's origin before anything else ran. |
500 | Error | internal_. 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
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
| Status | Body | Meaning |
|---|---|---|
201 | Blueprint | The enabled Blueprint. Its latest is 1. |
400 | Error | invalid_ — a malformed parameter or body. The batch ingest
endpoint uses invalid_ 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. |
401 | Error | unauthorized — no credential, or one that is not valid. |
403 | Error | module_ — Require 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_: the browser boundary
refused the request's origin before anything else ran. |
404 | Error | not_found |
409 | Error | request_ — 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. |
500 | Error | internal_. 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
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
| Status | Body | Meaning |
|---|---|---|
201 | Blueprint | The Blueprint's new version. |
400 | Error | invalid_ — a malformed parameter or body. The batch ingest
endpoint uses invalid_ 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. |
401 | Error | unauthorized — no credential, or one that is not valid. |
403 | Error | module_ — Require 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_: the browser boundary
refused the request's origin before anything else ran. |
404 | Error | not_found |
409 | Error | request_ — 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. |
500 | Error | internal_. 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
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
| Name | In | Type | Description |
|---|---|---|---|
include | query | boolean | |
page | query | integer | 1-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. |
pageSize | query | integer | Normalised 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
| Status | Body | Meaning |
|---|---|---|
200 | Provisioning | A page of the agent tokens registered for this tenant. |
400 | Error | invalid_ — a malformed parameter or body. The batch ingest
endpoint uses invalid_ 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. |
401 | Error | unauthorized — no credential, or one that is not valid. |
403 | Error | module_ — Require 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_: the browser boundary
refused the request's origin before anything else ran. |
500 | Error | internal_. 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
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
| Field | Type | Description |
|---|---|---|
accountIdrequired | string | The 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 Account takes. It must be one of this
tenant's accounts and enabled, or the answer is 409 account_. |
namerequired | string | The 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_, "name: must not be blank". |
Responses
| Status | Body | Meaning |
|---|---|---|
201 | Provisioning | The agent and its token. Store the token now; it cannot be read back. |
400 | Error | invalid_ — a malformed parameter or body. The batch ingest
endpoint uses invalid_ 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. |
401 | Error | unauthorized — no credential, or one that is not valid. |
403 | Error | module_ — Require 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_: the browser boundary
refused the request's origin before anything else ran. |
409 | Error | account_ — 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. |
413 | Error | The 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 additional limit is roughly
160 MB — so reaching it means something is wrong with the sender. |
415 | Error | unsupported_ — the body of a browser-facing route is not
labelled application/ (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. |
500 | Error | internal_. 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
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
| Field | Type | Description |
|---|---|---|
accountIdrequired | string | The AWS account the agent runs in: one of this tenant's accounts,
enabled, or 409 account_. AWS only: the link is a
Cloud |
dedicatedrequired | boolean | The 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_; recorded in the audit log.
A shared account uses the manual install with a policy the
customer scopes. |
namerequired | string | The agent's name, as for Provisioning. |
regionrequired | string | The AWS region the stack, and so the agent, runs in. |
Responses
| Status | Body | Meaning |
|---|---|---|
201 | Provisioning | The agent, and the link that installs it. |
400 | Error | invalid_ — a malformed parameter or body. The batch ingest
endpoint uses invalid_ 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. |
401 | Error | unauthorized — no credential, or one that is not valid. |
403 | Error | module_ — Require 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_: the browser boundary
refused the request's origin before anything else ran. |
409 | Error | account_ — the account is not this tenant's or is
disabled, as for issue.
agent_ — this deployment does not offer the
install link; issue a token and install by hand. |
413 | Error | The 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 additional limit is roughly
160 MB — so reaching it means something is wrong with the sender. |
415 | Error | unsupported_ — the body of a browser-facing route is not
labelled application/ (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. |
500 | Error | internal_. 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}
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
| Status | Body | Meaning |
|---|---|---|
204 | — | Revoked. Repeating this is not an error. |
401 | Error | unauthorized — no credential, or one that is not valid. |
403 | Error | module_ — Require 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_: the browser boundary
refused the request's origin before anything else ran. |
404 | Error | not_found |
500 | Error | internal_. 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
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
| Field | Type | Description |
|---|---|---|
agentrequired | string | |
claimIdrequired | string (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. |
library | integer | The newest release of the built-in library this agent's image
carries (MILESTONE_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. |
sealingKey | Agent | The public half of the agent's sealing key (agent-protocol. §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. |
tofuVersionrequired | string | |
waitSeconds | integer | How long the agent is willing to hold the poll open. Clamped
server-side to claim; it is a hint, not a
demand. |
Responses
| Status | Body | Meaning |
|---|---|---|
200 | AgentTask | Work 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. |
400 | Error | invalid_ — a malformed parameter or body. The batch ingest
endpoint uses invalid_ 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. |
401 | Error | unauthorized — 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. |
403 | Error | account_ — the token is valid, but the account it is bound
to is disabled in the tenant's accounts registry — or
module_, 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_). 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. |
409 | Error | claim_ — 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. |
413 | Error | The 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 additional limit is roughly
160 MB — so reaching it means something is wrong with the sender. |
426 | Error | agent_ — the agent is below
min 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. |
429 | Error | Rate limited. |
500 | Error | internal_. 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
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
| Field | Type | Description | ||||||||||||||||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
agentrequired | string | |||||||||||||||||||||||||
capabilities | array of string | Optional 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 agent
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:
| ||||||||||||||||||||||||
library | integer | The newest release of the built-in library this agent's image
carries (MILESTONE_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. | ||||||||||||||||||||||||
sealingKey | Agent | The public half of the agent's sealing key (agent-protocol. §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. | ||||||||||||||||||||||||
tofuVersionrequired | string | The 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
| Status | Body | Meaning |
|---|---|---|
200 | Agent | Registered. 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. |
400 | Error | invalid_ — a malformed parameter or body. The batch ingest
endpoint uses invalid_ 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. |
401 | Error | unauthorized — 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. |
403 | Error | account_ — the token is valid, but the account it is bound
to is disabled in the tenant's accounts registry — or
module_, 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_). 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. |
413 | Error | The 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 additional limit is roughly
160 MB — so reaching it means something is wrong with the sender. |
429 | Error | Rate limited. |
500 | Error | internal_. 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
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
| Field | Type | Description |
|---|---|---|
attemptrequired | integer | The fence. |
stage | string |
Responses
| Status | Body | Meaning |
|---|---|---|
200 | Agent | Liveness recorded. Act on cancel. |
400 | Error | invalid_ — a malformed parameter or body. The batch ingest
endpoint uses invalid_ 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. |
401 | Error | unauthorized — 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. |
403 | Error | account_ — the token is valid, but the account it is bound
to is disabled in the tenant's accounts registry — or
module_, 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_). 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. |
404 | Error | not_found |
409 | Error | task_ — 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. |
413 | Error | The 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 additional limit is roughly
160 MB — so reaching it means something is wrong with the sender. |
429 | Error | Rate limited. |
500 | Error | internal_. 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
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
| Name | In | Type | Description |
|---|---|---|---|
Idempotency-Keyrequired | header | string | Stable 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_. Over-length is
400 invalid_, 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
| Field | Type | Description |
|---|---|---|
attemptrequired | integer | The fence. |
linesrequired | array of Agent | At most max from the policy; 500 is the ceiling this
contract allows regardless. |
Responses
| Status | Body | Meaning |
|---|---|---|
200 | Agent | Stored, or the stored acknowledgement replayed unchanged. |
400 | Error | invalid_ — the body is not a batch, idempotency_
when the header is missing, one line's message exceeds
max, 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. |
401 | Error | unauthorized — 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. |
403 | Error | account_ — the token is valid, but the account it is bound
to is disabled in the tenant's accounts registry — or
module_, 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_). 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. |
404 | Error | not_found |
409 | Error | task_ (the fence, or a task that finished more than
fifteen minutes ago: its log is closed) or
idempotency_ (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. |
413 | Error | invalid_ — more than max 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. |
429 | Error | Rate limited. |
500 | Error | internal_. 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
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
| Field | Type | Description |
|---|---|---|
agentrequired | string | |
attemptrequired | integer | The fence, from the claimed task. A value that is not the request's
current attempt is task_, and that is the answer, not an
error to retry past. |
created | integer | |
destroyed | integer | |
failure | Provision | Required 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. |
failure | string | Already scrubbed when it is sent. The platform stores it verbatim and shows it to humans; it has no second chance to redact. |
finalLogSeq | integer (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
(get) 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 log when
log 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". |
generated | Agent | The terminal succeeded of an adopt's generate step only, with step generated and the re-plan's accepted
plan. The platform stores it as a private template
version and the request moves to pending_ on it
(Agent); from any other report it is refused. |
gitSha | string | resolve 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_ and the stored value does not move — it would
otherwise silently change what a pending approval is about to
approve. |
log | integer (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. |
outputs | object | The terminal succeeded of a create handed outputs
(MILESTONE_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 withheld. 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_.
A request's outputs are those of the attempt that succeeded, and
they are what a later step consumes. |
overridden | array of string | The 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. |
partial | boolean | The 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. |
planDigest | string | sha256 of the plan output. Kept in the permanent summary. |
plan | Agent | Terminal reports of a task handed permitted.
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_ — 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
failure, 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. |
provenance | object | Beside provenance, 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_ — 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 provenance; anything else is 400 invalid_. 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 (drift false), since
nothing says which of its resources the inventory could see. |
provenance | integer | Resources carrying panorama: that the agent counted in state
after a successful apply, by reading tofu show -json locally — no
extra cloud API call. It becomes expected, the baseline the
drift diff measures against. Zero on a create that produced resources means the stamp did not
land: the row is stored provenance and excluded from
drift with a visible badge, never silently omitted. |
stage | string | |
stage | object | |
staterequired | Provision | 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. One of claimed, running, succeeded, failed, cancelled, abandoned. |
state | string | Where 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). |
step | Agent | adopt only (MILESTONE_imported on a running report, with the import step's
accepted plan and its state; 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_. generated on the
terminal succeeded of a generate step, with generated. |
tofuVersionrequired | string | |
withheld | array of string | The 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
| Status | Body | Meaning |
|---|---|---|
200 | Agent | Recorded, or the stored response replayed unchanged. There is nothing in the body that distinguishes the two, deliberately. |
400 | Error | invalid_ — a malformed parameter or body. The batch ingest
endpoint uses invalid_ 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. |
401 | Error | unauthorized — 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. |
403 | Error | account_ — the token is valid, but the account it is bound
to is disabled in the tenant's accounts registry — or
module_, 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_). 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. |
404 | Error | not_found |
409 | Error | task_ (the fence rejected this attempt),
task_ (a different terminal state was already
recorded), resolution_ (a different gitSha is already
recorded for the version this resolve task pins) or
outputs_ (a replayed terminal report whose outputs or
withheld 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. |
413 | Error | The 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 additional limit is roughly
160 MB — so reaching it means something is wrong with the sender. |
429 | Error | Rate limited. |
500 | Error | internal_. 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
The change feed in a window, for the reviewer
As GET /changes, tenant-wide.
Parameters
| Name | In | Type | Description |
|---|---|---|---|
from | query | string (date-time) | |
to | query | string (date-time) | |
kind | query | array of ChangeKind | |
accountId | query | array of string | Repeatable. Any of the given accounts. |
resourceKey | query | string | |
resource | query | string | |
cursor | query | string | |
pageSize | query | integer | Normalised 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
| Status | Body | Meaning |
|---|---|---|
200 | Change | The page. |
400 | Error | invalid_ — a malformed parameter or body. The batch ingest
endpoint uses invalid_ 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. |
401 | Error | unauthorized — no credential, or one that is not valid. |
403 | Error | forbidden — authenticated, but the user's role does not permit this.
Distinct from module_, which is about the tenant's
subscription rather than the user. On an unsafe request, also origin_: the browser boundary
refused the request's origin before anything else ran. |
500 | Error | internal_. 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
Cost anomalies, for the reviewer
As GET /cost/anomalies, tenant-wide.
Parameters
| Name | In | Type | Description |
|---|---|---|---|
status | query | array of Cost | |
accountId | query | array of string | Repeatable. Any of the given accounts. |
page | query | integer | 1-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. |
pageSize | query | integer | Normalised 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
| Status | Body | Meaning |
|---|---|---|
200 | Cost | The page. |
400 | Error | invalid_ — a malformed parameter or body. The batch ingest
endpoint uses invalid_ 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. |
401 | Error | unauthorized — no credential, or one that is not valid. |
403 | Error | forbidden — authenticated, but the user's role does not permit this.
Distinct from module_, which is about the tenant's
subscription rather than the user. On an unsafe request, also origin_: the browser boundary
refused the request's origin before anything else ran. |
500 | Error | internal_. 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
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
| Status | Body | Meaning |
|---|---|---|
200 | CostAnomaly | The anomaly. |
400 | Error | This review is not of a cost anomaly. error.code is invalid_. |
401 | Error | unauthorized — no credential, or one that is not valid. |
403 | Error | forbidden — authenticated, but the user's role does not permit this.
Distinct from module_, which is about the tenant's
subscription rather than the user. On an unsafe request, also origin_: the browser boundary
refused the request's origin before anything else ran. |
404 | Error | not_found |
500 | Error | internal_. 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
The cost forecast, for the reviewer
As GET /cost/forecast, for the tenant or one account.
Parameters
| Name | In | Type | Description |
|---|---|---|---|
accountId | query | string | |
month | query | string |
Responses
| Status | Body | Meaning |
|---|---|---|
200 | Cost | The forecast. |
400 | Error | invalid_ — a malformed parameter or body. The batch ingest
endpoint uses invalid_ 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. |
401 | Error | unauthorized — no credential, or one that is not valid. |
403 | Error | forbidden — authenticated, but the user's role does not permit this.
Distinct from module_, which is about the tenant's
subscription rather than the user. On an unsafe request, also origin_: the browser boundary
refused the request's origin before anything else ran. |
500 | Error | internal_. 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
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
| Status | Body | Meaning |
|---|---|---|
200 | Impact | The investigation. |
400 | Error | This review is not of an investigation. error.code is invalid_. |
401 | Error | unauthorized — no credential, or one that is not valid. |
403 | Error | forbidden — authenticated, but the user's role does not permit this.
Distinct from module_, which is about the tenant's
subscription rather than the user. On an unsafe request, also origin_: the browser boundary
refused the request's origin before anything else ran. |
404 | Error | not_found |
500 | Error | internal_. 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
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
| Name | In | Type | Description |
|---|---|---|---|
accountIdrequired | query | string | |
regionrequired | query | string | |
systemrequired | query | string | |
servicerequired | query | string | |
resourcerequired | query | string | |
resourceIdrequired | query | string |
Responses
| Status | Body | Meaning |
|---|---|---|
200 | Resource | The resource. |
401 | Error | unauthorized — no credential, or one that is not valid. |
403 | Error | forbidden — authenticated, but the user's role does not permit this.
Distinct from module_, which is about the tenant's
subscription rather than the user. On an unsafe request, also origin_: the browser boundary
refused the request's origin before anything else ran. |
404 | Error | not_found |
500 | Error | internal_. 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
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
| Status | Body | Meaning |
|---|---|---|
200 | AIReview | The review. |
401 | Error | unauthorized — no credential, or one that is not valid. |
403 | Error | forbidden — authenticated, but the user's role does not permit this.
Distinct from module_, which is about the tenant's
subscription rather than the user. On an unsafe request, also origin_: the browser boundary
refused the request's origin before anything else ran. |
500 | Error | internal_. 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
A resource's observations, for the reviewer
As GET /resources/snapshots.
Parameters
| Name | In | Type | Description |
|---|---|---|---|
accountIdrequired | query | string | |
regionrequired | query | string | |
systemrequired | query | string | |
servicerequired | query | string | |
resourcerequired | query | string | |
resourceIdrequired | query | string | |
cursor | query | string | |
pageSize | query | integer |
Responses
| Status | Body | Meaning |
|---|---|---|
200 | Snapshot | The page. |
400 | Error | invalid_ — a malformed parameter or body. The batch ingest
endpoint uses invalid_ 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. |
401 | Error | unauthorized — no credential, or one that is not valid. |
403 | Error | forbidden — authenticated, but the user's role does not permit this.
Distinct from module_, which is about the tenant's
subscription rather than the user. On an unsafe request, also origin_: the browser boundary
refused the request's origin before anything else ran. |
404 | Error | not_found |
500 | Error | internal_. 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
The difference between two observations, for the reviewer
As GET /resources/snapshots/diff, sensitive fields suppressed the same way.
Parameters
| Name | In | Type | Description |
|---|---|---|---|
accountIdrequired | query | string | |
regionrequired | query | string | |
systemrequired | query | string | |
servicerequired | query | string | |
resourcerequired | query | string | |
resourceIdrequired | query | string | |
fromrequired | query | integer (int64) | |
torequired | query | integer (int64) |
Responses
| Status | Body | Meaning |
|---|---|---|
200 | Snapshot | The difference. |
400 | Error | invalid_ — a malformed parameter or body. The batch ingest
endpoint uses invalid_ 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. |
401 | Error | unauthorized — no credential, or one that is not valid. |
403 | Error | forbidden — authenticated, but the user's role does not permit this.
Distinct from module_, which is about the tenant's
subscription rather than the user. On an unsafe request, also origin_: the browser boundary
refused the request's origin before anything else ran. |
404 | Error | not_found |
500 | Error | internal_. 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
Status transitions in a window, for the reviewer
As GET /timeline, tenant-wide, acknowledgements shown.
Parameters
| Name | In | Type | Description |
|---|---|---|---|
from | query | string (date-time) | |
to | query | string (date-time) | |
accountId | query | array of string | Repeatable. Any of the given accounts. |
service | query | array of string | Repeatable. |
status | query | array of Transition | Repeatable. On /timeline this filters on the destination status, where
deleted is meaningful; on resource endpoints it filters live health,
where it is not. |
cursor | query | string | |
pageSize | query | integer | Normalised 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
| Status | Body | Meaning |
|---|---|---|
200 | Timeline | The page. |
400 | Error | invalid_ — a malformed parameter or body. The batch ingest
endpoint uses invalid_ 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. |
401 | Error | unauthorized — no credential, or one that is not valid. |
403 | Error | forbidden — authenticated, but the user's role does not permit this.
Distinct from module_, which is about the tenant's
subscription rather than the user. On an unsafe request, also origin_: the browser boundary
refused the request's origin before anything else ran. |
500 | Error | internal_. 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
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
| Name | In | Type | Description |
|---|---|---|---|
accountId | query | string | |
resourceKey | query | string | region/ |
subjectKind | query | string enum | |
subjectId | query | string | Needs subjectKind. |
limit | query | integer |
Responses
| Status | Body | Meaning |
|---|---|---|
200 | AIReview | The reviews. |
400 | Error | invalid_ — a malformed parameter or body. The batch ingest
endpoint uses invalid_ 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. |
401 | Error | unauthorized — no credential, or one that is not valid. |
500 | Error | internal_. 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
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
| Field | Type | Description |
|---|---|---|
itemsrequired | array of AIReview |
Responses
| Status | Body | Meaning |
|---|---|---|
201 | AIReview | The reviews opened. |
400 | Error | invalid_ — a malformed parameter or body. The batch ingest
endpoint uses invalid_ 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. |
401 | Error | unauthorized — no credential, or one that is not valid. |
403 | Error | forbidden — authenticated, but the user's role does not permit this.
Distinct from module_, which is about the tenant's
subscription rather than the user. On an unsafe request, also origin_: the browser boundary
refused the request's origin before anything else ran. |
404 | Error | not_found |
409 | Error | This deployment has no AI reviewer queue, so there is nothing to
ask. error.code is reviewer_. |
500 | Error | internal_. 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
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
| Name | In | Type | Description |
|---|---|---|---|
reviewIdrequired | path | string (uuid) |
Request body application/json
| Field | Type | Description |
|---|---|---|
confidence | string enum | The model's own estimate, not a measured one; the portal labels it so. One of low, medium, high. |
evidence | array of AIReview | The rows the answer as a whole rests on: the review's subject as
the reviewer read it. A nothing_ 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. |
findings | array of AIReview | |
modelrequired | string | The model that reviewed, as the reviewer names it. |
summaryrequired | string | |
verdictrequired | string enum | One of nothing_, findings, could_. |
Responses
| Status | Body | Meaning |
|---|---|---|
200 | AIReview | The review, answered. |
400 | Error | invalid_ — a malformed parameter or body. The batch ingest
endpoint uses invalid_ 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. |
401 | Error | unauthorized — no credential, or one that is not valid. |
403 | Error | forbidden — authenticated, but the user's role does not permit this.
Distinct from module_, which is about the tenant's
subscription rather than the user. On an unsafe request, also origin_: the browser boundary
refused the request's origin before anything else ran. |
404 | Error | not_found |
409 | Error | The review was already answered differently. |
500 | Error | internal_. 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
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
| Name | In | Type | Description |
|---|---|---|---|
status | query | array of Cost | Repeatable. Defaults to open. |
accountId | query | array of string | Repeatable. Any of the given accounts. |
teamId | query | array 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. |
page | query | integer | 1-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. |
pageSize | query | integer | Normalised 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. |
format | query | string enum | csv 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
| Status | Body | Meaning |
|---|---|---|
200 | Cost | A 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, window (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. |
400 | Error | invalid_ — a malformed parameter or body. The batch ingest
endpoint uses invalid_ 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. |
401 | Error | unauthorized — no credential, or one that is not valid. |
403 | Error | module_ — Require 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_: the browser boundary
refused the request's origin before anything else ran. |
429 | Error | Rate limited. |
500 | Error | internal_. 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}
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
| Field | Type | Description |
|---|---|---|
statusrequired | string enum | One of acknowledged, dismissed. |
Responses
| Status | Body | Meaning |
|---|---|---|
200 | CostAnomaly | The anomaly as it now stands. |
400 | Error | invalid_ — a malformed parameter or body. The batch ingest
endpoint uses invalid_ 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. |
401 | Error | unauthorized — no credential, or one that is not valid. |
403 | Error | module_ — Require 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_: the browser boundary
refused the request's origin before anything else ran. |
404 | Error | not_found |
413 | Error | The 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 additional limit is roughly
160 MB — so reaching it means something is wrong with the sender. |
415 | Error | unsupported_ — the body of a browser-facing route is not
labelled application/ (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. |
500 | Error | internal_. 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
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
| Name | In | Type | Description |
|---|---|---|---|
from | query | string (date) | First day of the range, inclusive. Defaults to the first of the
current month. from after to is invalid_. |
to | query | string (date) | Last day of the range, inclusive. Defaults to today. |
figure | query | CostFigure | Which 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. |
groupByrequired | query | string enum | |
accountId | query | array of string | Repeatable. Any of the given accounts. |
teamId | query | array 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. |
service | query | array of string | Repeatable. |
limit | query | integer | Rows to return before the rest is summed into an other row.
Normalised: < 1 becomes 20, > 200 becomes 200. |
format | query | string enum | csv 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
| Status | Body | Meaning |
|---|---|---|
200 | Cost | The breakdown. As format=csv, the same rows with the columns
from, to, figure, groupBy, key, label, currency, amount, allocated — one line per row and currency, the other
row included, no total line. |
400 | Error | invalid_ — a malformed parameter or body. The batch ingest
endpoint uses invalid_ 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. |
401 | Error | unauthorized — no credential, or one that is not valid. |
403 | Error | module_ — Require 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_: the browser boundary
refused the request's origin before anything else ran. |
429 | Error | Rate limited. |
500 | Error | internal_. 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
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
| Name | In | Type | Description |
|---|---|---|---|
anomalyId | query | string (uuid) | Explain this anomaly. Not with from/to. |
from | query | string (date) | The current window's first day. |
to | query | string (date) | The current window's last day, at most 366 days after from. |
previous | query | string (date) | The previous window's first day; with previousTo. |
previousTo | query | string (date) | The previous window's last day; with previous. |
teamId | query | string (uuid) | Narrow to one team, within the reader's scope. |
accountId | query | string | Narrow to one account. |
version | query | string | The version an earlier answer carried; that answer is served, and superseded says whether it is still current. |
figure | query | CostFigure | Which 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
| Status | Body | Meaning |
|---|---|---|
200 | Cost | The explanation. |
400 | Error | invalid_ — a malformed parameter or body. The batch ingest
endpoint uses invalid_ 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. |
401 | Error | unauthorized — no credential, or one that is not valid. |
403 | Error | module_ — Require 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_: the browser boundary
refused the request's origin before anything else ran. |
404 | Error | not_found |
500 | Error | internal_. 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
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
| Name | In | Type | Description |
|---|---|---|---|
scoperequired | query | Cost | |
teamId | query | string (uuid) | The team, with scope=team. |
accountId | query | string | The account, with scope=account. |
month | query | string | The month, YYYY-MM; the current month when absent. |
currency | query | string | One currency; every currency the scope spends in when absent. |
Responses
| Status | Body | Meaning |
|---|---|---|
200 | Cost | The scope's forecasts for the month. |
400 | Error | invalid_ — a malformed parameter or body. The batch ingest
endpoint uses invalid_ 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. |
401 | Error | unauthorized — no credential, or one that is not valid. |
403 | Error | module_ — Require 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_: the browser boundary
refused the request's origin before anything else ran. |
429 | Error | Rate limited. |
500 | Error | internal_. 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
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
| Name | In | Type | Description |
|---|---|---|---|
from | query | string (date) | First day of the range, inclusive. Defaults to the first of the
current month. from after to is invalid_. |
to | query | string (date) | Last day of the range, inclusive. Defaults to today. |
figure | query | CostFigure | Which 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. |
accountId | query | array of string | Repeatable. Any of the given accounts. |
cluster | query | string | A cluster name; with region, one cluster. |
region | query | string | The cluster's region, as the inventory holds it. |
Responses
| Status | Body | Meaning |
|---|---|---|
200 | Cost | The attribution. |
400 | Error | invalid_ — a malformed parameter or body. The batch ingest
endpoint uses invalid_ 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. |
401 | Error | unauthorized — no credential, or one that is not valid. |
403 | Error | module_ — Require 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_: the browser boundary
refused the request's origin before anything else ran. |
429 | Error | Rate limited. |
500 | Error | internal_. 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
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
| Name | In | Type | Description |
|---|---|---|---|
from | query | string (date) | The window's first day. Defaults with to to the month to date. |
to | query | string (date) | The window's last day, inclusive. |
Responses
| Status | Body | Meaning |
|---|---|---|
200 | Cost | The reconciliation. |
400 | Error | invalid_ — a malformed parameter or body. The batch ingest
endpoint uses invalid_ 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. |
401 | Error | unauthorized — no credential, or one that is not valid. |
403 | Error | module_ — Require 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_: the browser boundary
refused the request's origin before anything else ran. |
500 | Error | internal_. 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
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
| Status | Body | Meaning |
|---|---|---|
200 | Cost | The schedules and the teams the caller may write. |
401 | Error | unauthorized — no credential, or one that is not valid. |
403 | Error | module_ — Require 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_: the browser boundary
refused the request's origin before anything else ran. |
500 | Error | internal_. 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
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
| Field | Type | Description |
|---|---|---|
channelIdrequired | string (uuid) | |
enabled | boolean | |
teamIdrequired | string (uuid) |
Responses
| Status | Body | Meaning |
|---|---|---|
201 | Cost | The schedule. |
400 | Error | invalid_ — a malformed parameter or body. The batch ingest
endpoint uses invalid_ 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. |
401 | Error | unauthorized — no credential, or one that is not valid. |
403 | Error | module_ — Require 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_: the browser boundary
refused the request's origin before anything else ran. |
409 | Error | report_ — the team already sends this report to that channel. |
413 | Error | The 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 additional limit is roughly
160 MB — so reaching it means something is wrong with the sender. |
415 | Error | unsupported_ — the body of a browser-facing route is not
labelled application/ (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. |
500 | Error | internal_. 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}
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
| Field | Type | Description |
|---|---|---|
channelId | string (uuid) | |
enabled | boolean |
Responses
| Status | Body | Meaning |
|---|---|---|
200 | Cost | The schedule as it now stands. |
400 | Error | invalid_ — a malformed parameter or body. The batch ingest
endpoint uses invalid_ 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. |
401 | Error | unauthorized — no credential, or one that is not valid. |
403 | Error | module_ — Require 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_: the browser boundary
refused the request's origin before anything else ran. |
404 | Error | not_found |
409 | Error | report_ — the team already sends this report to that channel. |
413 | Error | The 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 additional limit is roughly
160 MB — so reaching it means something is wrong with the sender. |
415 | Error | unsupported_ — the body of a browser-facing route is not
labelled application/ (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. |
500 | Error | internal_. 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}
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
| Status | Body | Meaning |
|---|---|---|
204 | — | Deleted. |
401 | Error | unauthorized — no credential, or one that is not valid. |
403 | Error | module_ — Require 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_: the browser boundary
refused the request's origin before anything else ran. |
404 | Error | not_found |
500 | Error | internal_. 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
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
| Status | Body | Meaning |
|---|---|---|
202 | Cost | Whether this request queued the report. |
400 | Error | invalid_ — a malformed parameter or body. The batch ingest
endpoint uses invalid_ 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. |
401 | Error | unauthorized — no credential, or one that is not valid. |
403 | Error | module_ — Require 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_: the browser boundary
refused the request's origin before anything else ran. |
404 | Error | not_found |
500 | Error | internal_. 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
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
| Name | In | Type | Description |
|---|---|---|---|
figure | query | CostFigure | Which 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. |
filter | query | string enum | |
accountId | query | array of string | Repeatable. Any of the given accounts. |
service | query | array of string | Repeatable. |
query | query | string | Substring match on the resource id and name. |
page | query | integer | 1-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. |
pageSize | query | integer | Normalised 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. |
format | query | string enum | csv 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
| Status | Body | Meaning |
|---|---|---|
200 | Cost | A 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, resource: 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. |
400 | Error | invalid_ — a malformed parameter or body. The batch ingest
endpoint uses invalid_ 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. |
401 | Error | unauthorized — no credential, or one that is not valid. |
403 | Error | module_ — Require 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_: the browser boundary
refused the request's origin before anything else ran. |
429 | Error | Rate limited. |
500 | Error | internal_. 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
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
| Name | In | Type | Description |
|---|---|---|---|
teamId | query | array 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
| Status | Body | Meaning |
|---|---|---|
200 | Savings | The claims. |
401 | Error | unauthorized — no credential, or one that is not valid. |
403 | Error | module_ — Require 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_: the browser boundary
refused the request's origin before anything else ran. |
500 | Error | internal_. 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
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
| Field | Type | Description |
|---|---|---|
action | string (uuid) | The succeeded action the claim came from. |
changedOn | string (date) | The day of the change; it is in neither window. Required unless the claim is over an action. |
claimed | Savings | A list of resources, or a tag match — exactly one of them. |
control | Savings | A list of resources, or a tag match — exactly one of them. |
description | string | What was done. Required unless the claim is over an action. |
recommendation | Savings | The Trusted Advisor recommendation the claim came from, by the three
names GET / 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. |
teamId | string (uuid) | The team the claim is for; omitted, a tenant-wide claim (administrators only), or the action's team. |
windowDays | integer | Each window's length; 14 when omitted. |
Responses
| Status | Body | Meaning |
|---|---|---|
201 | Savings | The claim, its frozen sets and its first verdict. |
400 | Error | invalid_ — a malformed parameter or body. The batch ingest
endpoint uses invalid_ 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. |
401 | Error | unauthorized — no credential, or one that is not valid. |
403 | Error | module_ — Require 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_: the browser boundary
refused the request's origin before anything else ran. |
404 | Error | not_found |
413 | Error | The 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 additional limit is roughly
160 MB — so reaching it means something is wrong with the sender. |
415 | Error | unsupported_ — the body of a browser-facing route is not
labelled application/ (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. |
500 | Error | internal_. 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}
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
| Status | Body | Meaning |
|---|---|---|
200 | Savings | The claim. |
401 | Error | unauthorized — no credential, or one that is not valid. |
403 | Error | module_ — Require 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_: the browser boundary
refused the request's origin before anything else ran. |
404 | Error | not_found |
500 | Error | internal_. 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
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
| Name | In | Type | Description |
|---|---|---|---|
groupBy | query | string enum |
Responses
| Status | Body | Meaning |
|---|---|---|
200 | Savings | The ledger. |
400 | Error | invalid_ — a malformed parameter or body. The batch ingest
endpoint uses invalid_ 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. |
401 | Error | unauthorized — no credential, or one that is not valid. |
403 | Error | module_ — Require 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_: the browser boundary
refused the request's origin before anything else ran. |
500 | Error | internal_. 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
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
| Name | In | Type | Description |
|---|---|---|---|
from | query | string (date) | First day of the range, inclusive. Defaults to the first of the
current month. from after to is invalid_. |
to | query | string (date) | Last day of the range, inclusive. Defaults to today. |
figure | query | CostFigure | Which 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. |
accountId | query | array of string | Repeatable. Any of the given accounts. |
teamId | query | array 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
| Status | Body | Meaning |
|---|---|---|
200 | CostSummary | The period's totals. |
400 | Error | invalid_ — a malformed parameter or body. The batch ingest
endpoint uses invalid_ 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. |
401 | Error | unauthorized — no credential, or one that is not valid. |
403 | Error | module_ — Require 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_: the browser boundary
refused the request's origin before anything else ran. |
429 | Error | Rate limited. |
500 | Error | internal_. 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
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
| Name | In | Type | Description |
|---|---|---|---|
from | query | string (date) | First day of the range, inclusive. Defaults to the first of the
current month. from after to is invalid_. |
to | query | string (date) | Last day of the range, inclusive. Defaults to today. |
figure | query | CostFigure | Which 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. |
groupByrequired | query | string enum | |
granularity | query | string enum | |
accountId | query | array of string | Repeatable. Any of the given accounts. |
teamId | query | array 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
| Status | Body | Meaning |
|---|---|---|
200 | CostTrend | The series. |
400 | Error | invalid_ — a malformed parameter or body. The batch ingest
endpoint uses invalid_ 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. |
401 | Error | unauthorized — no credential, or one that is not valid. |
403 | Error | module_ — Require 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_: the browser boundary
refused the request's origin before anything else ran. |
429 | Error | Rate limited. |
500 | Error | internal_. 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
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
| Status | Body | Meaning |
|---|---|---|
200 | Cost | The exports, by subscription. |
401 | Error | unauthorized — no credential, or one that is not valid. |
403 | Error | module_ — Require 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_: the browser boundary
refused the request's origin before anything else ran. |
500 | Error | internal_. 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
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
| Field | Type | Description |
|---|---|---|
containerrequired | string | |
directoryrequired | string | |
max | integer (int64) | |
pair | string | |
storagerequired | string | |
subscriptionrequired | string (uuid) |
Responses
| Status | Body | Meaning |
|---|---|---|
201 | Cost | Configured. |
400 | Error | invalid_ — a malformed parameter or body. The batch ingest
endpoint uses invalid_ 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. |
401 | Error | unauthorized — no credential, or one that is not valid. |
403 | Error | module_ — Require 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_: the browser boundary
refused the request's origin before anything else ran. |
409 | Error | invalid_ — that export is already configured. |
413 | Error | The 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 additional limit is roughly
160 MB — so reaching it means something is wrong with the sender. |
415 | Error | unsupported_ — the body of a browser-facing route is not
labelled application/ (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. |
500 | Error | internal_. 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}
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
| Field | Type | Description |
|---|---|---|
container | string | |
directory | string | |
max | integer (int64) | |
pair | string | |
storage | string |
Responses
| Status | Body | Meaning |
|---|---|---|
200 | Cost | The export as it now stands. |
400 | Error | invalid_ — a malformed parameter or body. The batch ingest
endpoint uses invalid_ 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. |
401 | Error | unauthorized — no credential, or one that is not valid. |
403 | Error | module_ — Require 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_: the browser boundary
refused the request's origin before anything else ran. |
404 | Error | not_found |
409 | Error | invalid_ — another export is already configured there. |
413 | Error | The 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 additional limit is roughly
160 MB — so reaching it means something is wrong with the sender. |
415 | Error | unsupported_ — the body of a browser-facing route is not
labelled application/ (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. |
500 | Error | internal_. 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}
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
| Status | Body | Meaning |
|---|---|---|
204 | — | Removed. |
401 | Error | unauthorized — no credential, or one that is not valid. |
403 | Error | module_ — Require 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_: the browser boundary
refused the request's origin before anything else ran. |
404 | Error | not_found |
500 | Error | internal_. 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
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
| Name | In | Type | Description |
|---|---|---|---|
teamId | query | array 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
| Status | Body | Meaning |
|---|---|---|
200 | Cost | The budgets. |
401 | Error | unauthorized — no credential, or one that is not valid. |
403 | Error | module_ — Require 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_: the browser boundary
refused the request's origin before anything else ran. |
429 | Error | Rate limited. |
500 | Error | internal_. 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
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
| Field | Type | Description |
|---|---|---|
amountrequired | Money | One amount in one currency. The only way money appears in this
document. Test 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. |
enabled | boolean | |
period | string enum | One of monthly. |
teamIdrequired | string (uuid) | |
thresholds | array of integer |
Responses
| Status | Body | Meaning |
|---|---|---|
201 | CostBudget | Created. |
400 | Error | invalid_ — a malformed parameter or body. The batch ingest
endpoint uses invalid_ 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. |
401 | Error | unauthorized — no credential, or one that is not valid. |
403 | Error | module_ — Require 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_: the browser boundary
refused the request's origin before anything else ran. |
404 | Error | not_found |
409 | Error | budget_ — this team already has a monthly budget; edit that one instead. |
413 | Error | The 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 additional limit is roughly
160 MB — so reaching it means something is wrong with the sender. |
415 | Error | unsupported_ — the body of a browser-facing route is not
labelled application/ (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. |
500 | Error | internal_. 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}
One budget with its progress
Responses
| Status | Body | Meaning |
|---|---|---|
200 | CostBudget | The budget. |
401 | Error | unauthorized — no credential, or one that is not valid. |
403 | Error | module_ — Require 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_: the browser boundary
refused the request's origin before anything else ran. |
404 | Error | not_found |
500 | Error | internal_. 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}
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
| Field | Type | Description |
|---|---|---|
amount | Money | One amount in one currency. The only way money appears in this
document. Test 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. |
enabled | boolean | |
thresholds | array of integer |
Responses
| Status | Body | Meaning |
|---|---|---|
200 | CostBudget | The budget as it now stands. |
400 | Error | invalid_ — a malformed parameter or body. The batch ingest
endpoint uses invalid_ 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. |
401 | Error | unauthorized — no credential, or one that is not valid. |
403 | Error | module_ — Require 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_: the browser boundary
refused the request's origin before anything else ran. |
404 | Error | not_found |
409 | Error | currency_ — see the description. |
413 | Error | The 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 additional limit is roughly
160 MB — so reaching it means something is wrong with the sender. |
415 | Error | unsupported_ — the body of a browser-facing route is not
labelled application/ (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. |
500 | Error | internal_. 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}
Remove a budget
Its notification history goes with it.
Responses
| Status | Body | Meaning |
|---|---|---|
204 | — | Removed. |
401 | Error | unauthorized — no credential, or one that is not valid. |
403 | Error | module_ — Require 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_: the browser boundary
refused the request's origin before anything else ran. |
404 | Error | not_found |
500 | Error | internal_. 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
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
| Status | Body | Meaning |
|---|---|---|
200 | Cost | The exports, by billing account. |
401 | Error | unauthorized — no credential, or one that is not valid. |
403 | Error | module_ — Require 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_: the browser boundary
refused the request's origin before anything else ran. |
500 | Error | internal_. 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
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
| Field | Type | Description |
|---|---|---|
billingrequired | string | |
max | integer (int64) | |
tableNamerequired | string |
Responses
| Status | Body | Meaning |
|---|---|---|
201 | Cost | Configured. |
400 | Error | invalid_ — a malformed parameter or body. The batch ingest
endpoint uses invalid_ 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. |
401 | Error | unauthorized — no credential, or one that is not valid. |
403 | Error | module_ — Require 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_: the browser boundary
refused the request's origin before anything else ran. |
409 | Error | invalid_ — the billing account already has an export. |
413 | Error | The 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 additional limit is roughly
160 MB — so reaching it means something is wrong with the sender. |
415 | Error | unsupported_ — the body of a browser-facing route is not
labelled application/ (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. |
500 | Error | internal_. 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}
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
| Field | Type | Description |
|---|---|---|
max | integer (int64) | |
tableName | string |
Responses
| Status | Body | Meaning |
|---|---|---|
200 | Cost | The export as it now stands. |
400 | Error | invalid_ — a malformed parameter or body. The batch ingest
endpoint uses invalid_ 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. |
401 | Error | unauthorized — no credential, or one that is not valid. |
403 | Error | module_ — Require 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_: the browser boundary
refused the request's origin before anything else ran. |
404 | Error | not_found |
413 | Error | The 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 additional limit is roughly
160 MB — so reaching it means something is wrong with the sender. |
415 | Error | unsupported_ — the body of a browser-facing route is not
labelled application/ (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. |
500 | Error | internal_. 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}
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
| Status | Body | Meaning |
|---|---|---|
204 | — | Removed. |
401 | Error | unauthorized — no credential, or one that is not valid. |
403 | Error | module_ — Require 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_: the browser boundary
refused the request's origin before anything else ran. |
404 | Error | not_found |
500 | Error | internal_. 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
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
| Name | In | Type | Description |
|---|---|---|---|
accountId | query | array of string | Repeatable. Any of the given accounts. |
page | query | integer | 1-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. |
pageSize | query | integer | Normalised 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. |
format | query | string enum | csv 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
| Status | Body | Meaning |
|---|---|---|
200 | Cost | The 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, resource (metadata the check's fields as
key=value, sorted by key and joined by ; ): one line per
recommendation, AWS's estimate in its currency.
unavailable 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. |
400 | Error | invalid_ — a malformed parameter or body. The batch ingest
endpoint uses invalid_ 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. |
401 | Error | unauthorized — no credential, or one that is not valid. |
403 | Error | module_ — Require 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_: the browser boundary
refused the request's origin before anything else ran. |
429 | Error | Rate limited. |
500 | Error | internal_. 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
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
| Status | Body | Meaning |
|---|---|---|
200 | Cost | The settings. |
401 | Error | unauthorized — no credential, or one that is not valid. |
403 | Error | module_ — Require 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_: the browser boundary
refused the request's origin before anything else ran. |
500 | Error | internal_. 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
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
| Field | Type | Description |
|---|---|---|
allocation | array of Cost | The 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. |
anomalyrequired | Cost | |
attributionrequired | array of Cost | |
curReports | array of Cost | The 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. |
defaultrequired | CostFigure | One of amortized, unblended. |
teamrequired | array of Cost |
Responses
| Status | Body | Meaning |
|---|---|---|
200 | Cost | The settings as they now stand. |
400 | Error | invalid_ — a malformed parameter or body. The batch ingest
endpoint uses invalid_ 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. |
401 | Error | unauthorized — no credential, or one that is not valid. |
403 | Error | module_ — Require 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_: the browser boundary
refused the request's origin before anything else ran. |
409 | Error | attribution_ — a rebuild from the previous change is still running. |
413 | Error | The 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 additional limit is roughly
160 MB — so reaching it means something is wrong with the sender. |
415 | Error | unsupported_ — the body of a browser-facing route is not
labelled application/ (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. |
500 | Error | internal_. 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
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
| Name | In | Type | Description |
|---|---|---|---|
from | query | string (date) | The window's first day. Defaults with to to the month to date. |
to | query | string (date) | The window's last day, inclusive. |
accountId | query | array of string |
Responses
| Status | Body | Meaning |
|---|---|---|
200 | Tag | The breakdown. |
400 | Error | invalid_ — a malformed parameter or body. The batch ingest
endpoint uses invalid_ 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. |
401 | Error | unauthorized — no credential, or one that is not valid. |
403 | Error | module_ — Require 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_: the browser boundary
refused the request's origin before anything else ran. |
429 | Error | Rate limited. |
500 | Error | internal_. 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
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
| Name | In | Type | Description |
|---|---|---|---|
from | query | string (date) | |
to | query | string (date) | |
accountId | query | array of string | |
groupByrequired | query | string enum |
Responses
| Status | Body | Meaning |
|---|---|---|
200 | Tag | The subjects, sorted by label. |
400 | Error | invalid_ — a malformed parameter or body. The batch ingest
endpoint uses invalid_ 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. |
401 | Error | unauthorized — no credential, or one that is not valid. |
403 | Error | module_ — Require 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_: the browser boundary
refused the request's origin before anything else ran. |
500 | Error | internal_. 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
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
| Status | Body | Meaning |
|---|---|---|
200 | TagPolicy | The policy. |
401 | Error | unauthorized — no credential, or one that is not valid. |
403 | Error | module_ — Require 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_: the browser boundary
refused the request's origin before anything else ran. |
500 | Error | internal_. 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
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
| Field | Type | Description |
|---|---|---|
exemptrequired | array of string | |
requiredrequired | array of Tag |
Responses
| Status | Body | Meaning |
|---|---|---|
200 | TagPolicy | The policy as it now stands. |
400 | Error | invalid_ — a malformed parameter or body. The batch ingest
endpoint uses invalid_ 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. |
401 | Error | unauthorized — no credential, or one that is not valid. |
403 | Error | module_ — Require 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_: the browser boundary
refused the request's origin before anything else ran. |
413 | Error | The 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 additional limit is roughly
160 MB — so reaching it means something is wrong with the sender. |
415 | Error | unsupported_ — the body of a browser-facing route is not
labelled application/ (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. |
500 | Error | internal_. 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
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
| Name | In | Type | Description |
|---|---|---|---|
from | query | string (date) | |
to | query | string (date) | |
accountId | query | array of string | |
unowned | query | boolean | Only the resources the resolver could not give an owner. |
limit | query | integer | |
format | query | string enum | csv 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
| Status | Body | Meaning |
|---|---|---|
200 | Tag | The 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, resource: 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. |
400 | Error | invalid_ — a malformed parameter or body. The batch ingest
endpoint uses invalid_ 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. |
401 | Error | unauthorized — no credential, or one that is not valid. |
403 | Error | module_ — Require 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_: the browser boundary
refused the request's origin before anything else ran. |
429 | Error | Rate limited. |
500 | Error | internal_. 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
| Field | Type | Description |
|---|---|---|
accountId | string | |
anchor | AIReview | 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. |
answer | string | |
answeredAt | string (date-time) | |
confidence | string enum | One of low, medium, high. |
expiresAtrequired | string (date-time) | When the request's token stops answering. |
findingsrequired | array of AIReview | |
idrequired | string (uuid) | |
model | string | |
requestedAtrequired | string (date-time) | |
resourceKey | string | |
subjectIdrequired | string | |
subjectKindrequired | string enum | One of alert, anomaly, test, impact. |
summaryrequired | string | The one line the reviewer was handed. |
verdict | string enum | One of nothing_, findings, could_. |
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.
| Field | Type | Description |
|---|---|---|
atrequired | string (date-time) | |
depthrequired | integer | |
windowrequired | string enum | One of 1h, 2h, 6h, 24h. |
AIReviewAnswer
| Field | Type | Description |
|---|---|---|
confidence | string enum | The model's own estimate, not a measured one; the portal labels it so. One of low, medium, high. |
evidence | array of AIReview | The rows the answer as a whole rests on: the review's subject as
the reviewer read it. A nothing_ 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. |
findings | array of AIReview | |
modelrequired | string | The model that reviewed, as the reviewer names it. |
summaryrequired | string | |
verdictrequired | string enum | One of nothing_, findings, could_. |
AIReviewEvidence
A row a finding rests on; it must be one this tenant can see.
| Field | Type | Description |
|---|---|---|
idrequired | string | A resource's account/; a transition's or an anomaly's id. |
kindrequired | string enum | One of resource, transition, anomaly. |
AIReviewFinding
| Field | Type | Description |
|---|---|---|
detail | string | |
evidence | array of AIReview | |
titlerequired | string |
AIReviewList
| Field | Type | Description |
|---|---|---|
itemsrequired | array 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.
| Field | Type | Description |
|---|---|---|
itemsrequired | array of AIReview |
AIReviewRequestItem
| Field | Type | Description |
|---|---|---|
accountIdrequired | string | |
at | string (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. |
depth | integer | How far an impact item's neighbours go; 1 when omitted. |
kindrequired | string enum | One of alert, impact. |
resourceKeyrequired | string | region/ |
window | string enum | An 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.
| Field | Type | Description |
|---|---|---|
createdAtrequired | string (date-time) | |
expiresAtrequired | string (date-time) | |
idrequired | string (uuid) | |
lastUsedAt | string (date-time) | |
namerequired | string | |
revokedAt | string (date-time) |
APITokenCreate
| Field | Type | Description |
|---|---|---|
expires | integer | Days until the token stops working, at most the organisation's longest
(/, 90 unless an administrator changed it).
When absent, 90, or that longest when it is shorter. |
namerequired | string | What the token is for; how a list tells two apart. Must not be blank. |
APITokenIssued
| Field | Type | Description |
|---|---|---|
apiTokenrequired | APIToken | A read token, everything but the token. |
tokenrequired | string | The read token, panorama_ and 43 characters. Shown here once. |
APITokenList
| Field | Type | Description |
|---|---|---|
itemsrequired | array of APIToken |
APITokenSettings
| Field | Type | Description |
|---|---|---|
maxrequired | integer | The 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.
| Field | Type | Description |
|---|---|---|
accountIdrequired | string | |
azure | string | The 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. |
azure | string | An Azure subscription's directory (tenant) id, where the app the platform signs in as is registered; absent for the other clouds. |
cloudtrail | boolean | Whether an administrator turned CloudTrail attribution on for this
account: the platform then reads the account's CloudTrail
event history (cloudtrail:, 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. |
createdAtrequired | string (date-time) | |
enabledrequired | boolean | New accounts default to false. Discovery registers everything it finds; collecting from an unapproved account is both surprising and expensive. |
environmentrequired | string enum | One of production, staging, sdlc, shared, unknown. |
google | string | A Google Cloud project's reader service account, which the platform impersonates; absent for an AWS account. |
isrequired | boolean | Whether 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. |
namerequired | string | |
regionsrequired | array of string | |
roleArnrequired | string | |
systemrequired | string enum | The 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.
| Field | Type | Description |
|---|---|---|
last | string (date-time) | The most recent successful collection of any module, absent where none has succeeded. |
modulesrequired | array of Account | Every module scheduled for this account, worst cell per module, in the scheduler's order. |
stalest | Account | |
statusrequired | string enum | The worst cell of this account's account × region × module grid. One of ok, error, slow, no-data. |
worstModule | Account |
AccountList
| Field | Type | Description |
|---|---|---|
itemsrequired | array of Account |
AccountModuleState
| Field | Type | Description |
|---|---|---|
last | string (date-time) | |
modulerequired | string | |
region | string | The region of the cell this state came from; global for a per-account module. |
statusrequired | string 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.
| Field | Type | Description |
|---|---|---|
certificate | string | Azure only, for a deployment off AWS: the SHA-256 thumbprint of the certificate the app registration must hold, base64url, as Entra ID shows it. |
externalId | string | The external id the customer's trust policy must demand. AWS only. |
federation | string | Azure only: the audience the federated credential trusts,
api://. 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. |
federation | string | Azure 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. |
federation | string | Azure only: the subject the federated credential trusts, this deployment's AWS role's ARN as its token names it. |
readerRole | string | The custom role to bind to that service account (Google Cloud), or to assign the app at the subscription (Azure). |
roleArn | string | The role ARN this deployment will assume. AWS only. |
roleName | string | The role the customer creates. AWS only. |
service | string | The reader service account to grant impersonation on. Google Cloud only. |
stepsrequired | array of Onboarding | The 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. |
systemrequired | string enum | The account's cloud. One of aws, gcp, azure. |
template | string | The Cloud |
workload | string | The 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.
| Field | Type | Description |
|---|---|---|
accountrequired | 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. |
collection | Account | 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. |
region | Account | A Google Cloud project's region discovery (MILESTONE_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. |
resourcesrequired | Account | What this account holds, counted from the live inventory. |
AccountOverviewList
| Field | Type | Description |
|---|---|---|
itemsrequired | array of Account |
AccountRegionCount
| Field | Type | Description |
|---|---|---|
countrequired | integer | |
regionrequired | string |
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.
| Field | Type | Description |
|---|---|---|
lastRunAt | string (date-time) | When the latest discovery finished; absent while none has. |
message | string | What 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. |
regions | integer | How many regions the latest finished discovery found in use. |
statusrequired | string enum | The 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.
| Field | Type | Description |
|---|---|---|
accountIdrequired | string | The twelve-digit AWS account id. |
environmentrequired | string enum | One of production, staging, sdlc, shared, unknown. |
is | boolean | Whether this is the organisation's management account — the one discovery enumerates through. At most one per tenant. |
namerequired | string | |
regions | array of string | The regions to collect in. Empty means every region the account has. |
roleName | string | The 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.
| Field | Type | Description |
|---|---|---|
byRegionrequired | array of Account | The regions this account's resources are actually in, most first. |
byStatusrequired | array of Account | |
resourcerequired | integer |
AccountStatusCount
| Field | Type | Description |
|---|---|---|
countrequired | integer | |
statusrequired | 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. One of failed, degraded, operational, unknown. |
AccountUpdate
Only the fields present are changed.
| Field | Type | Description |
|---|---|---|
cloudtrail | boolean | Turn CloudTrail attribution on or off for this account.
On asks the platform to call cloudtrail: in the
account, which the reader role grants; a stack older
than that answers each lookup permission_. Turning it on
for a Google Cloud project or an Azure subscription is a 400. |
enabled | boolean | |
environment | string enum | One of production, staging, sdlc, shared, unknown. |
is | boolean | Mark (or unmark) this account as the organisation's management
account. Marking a second one is a 409: there is at most one. |
name | string | |
regions | array of string | |
roleArn | string | Correct the reader role's ARN when it differs from the convention
discovery assumed (role/). 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
| Field | Type | Description |
|---|---|---|
assumedRole | boolean | Whether the role assumption itself succeeded. |
checksrequired | array of Account | |
okrequired | boolean | True only if every check that ran passed. A check in skipped
did not run, and ok says nothing about it. |
regions | array of string | A 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. |
skipped | array of Account | The 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
| Field | Type | Description |
|---|---|---|
detail | string | What to grant, when the check failed. "Access denied" without saying what is missing turns onboarding into guesswork. |
okrequired | boolean | |
permissionrequired | string |
AccountVerificationSkip
| Field | Type | Description |
|---|---|---|
checkrequired | string | |
reasonrequired | string |
AcknowledgeRequest
A resource's alert to acknowledge, and why. The note is required.
| Field | Type | Description |
|---|---|---|
accountIdrequired | string | |
noterequired | string | |
regionrequired | string | |
resourceIdrequired | string | |
resourcerequired | string | |
servicerequired | string | |
systemrequired | string |
Acknowledgement
An active acknowledgement, with the resource it covers.
| Field | Type | Description |
|---|---|---|
accountIdrequired | string | |
acknowledgedrequired | string (date-time) | |
acknowledgedrequired | 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. |
noterequired | string | |
regionrequired | string | |
resourceIdrequired | string | |
resourcerequired | string | |
resourcerequired | string | |
servicerequired | string | |
statusrequired | 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. One of failed, degraded, operational, unknown. |
systemrequired | string | |
transitionrequired | integer (int64) |
AcknowledgementList
| Field | Type | Description |
|---|---|---|
itemsrequired | array of Acknowledgement |
AcknowledgementTarget
A resource, by its natural key.
| Field | Type | Description |
|---|---|---|
accountIdrequired | string | |
regionrequired | string | |
resourceIdrequired | string | |
resourcerequired | string | |
servicerequired | string | |
systemrequired | string |
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.
| Field | Type | Description |
|---|---|---|
emailrequired | string | |
idrequired | string (uuid) | |
name | string | The name the person gave themselves, when they gave one. |
AdminOverview
| Field | Type | Description |
|---|---|---|
accountsrequired | object | |
collectionrequired | object | The enabled accounts by their collectors' worst state, as the collector matrix rolls it up. |
licencerequired | Entitlements | |
recentrequired | array of AuditEntry | The latest audit entries, newest first. |
ssorequired | object | |
usersrequired | object |
AgentAdoptStep
A step of an adopt task (MILESTONE_1 §9.3.3, §9.5):
import—importblocks 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 reportedimportedbefore anything more happens. It is followed, in the same task, byretag.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. Reportedtaggedon the terminalsucceeded.release— the declared addresses removed from state (state rm) and nothing else, leaving the resources standing. Reportedreleasedon the terminalsucceeded.generate— the task'sinlineSourceis the server's skeleton (the provider configuration and nothing else);importblocks for the declared addresses, thentofu plan -generate-config-out. Anoptionalimport 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 theimportrules: nothing but the declared imports. Nothing is applied and no state is written. Reportedgenerated, withgenerated, on the terminalsucceeded. 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 asinlineSource, every import required: its source already declares the resources, so it generates nothing — it plans the configuration as handed under theimportrules and reports those bytes asgenerated.
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
| Field | Type | Description |
|---|---|---|
agentrequired | string | |
claimIdrequired | string (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. |
library | integer | The newest release of the built-in library this agent's image
carries (MILESTONE_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. |
sealingKey | Agent | The public half of the agent's sealing key (agent-protocol. §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. |
tofuVersionrequired | string | |
waitSeconds | integer | How long the agent is willing to hold the poll open. Clamped
server-side to claim; 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).
| Field | Type | Description |
|---|---|---|
addressesrequired | array of string | The imports hcl holds, one resource block each, in the task's order. |
droppedrequired | array of string | The optional imports dropped because the provider found no object for them. |
hclrequired | string | |
placeholders | object | What 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. |
sensitiverequired | array of string | The 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. |
sha256required | string | The hex SHA-256 of hcl's bytes. |
values | array of Agent | The 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
| Field | Type | Description |
|---|---|---|
attemptrequired | integer | The fence. |
stage | string |
AgentHeartbeatResult
| Field | Type | Description |
|---|---|---|
cancelrequired | boolean | A 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. |
heartbeatrequired | integer | Repeated 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.
| Field | Type | Description |
|---|---|---|
addressrequired | string | |
idrequired | string | |
optional | boolean | A 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.. Never sent on another
step, which acts on every address it names; an agent refuses a
task that does. |
AgentLogBatch
| Field | Type | Description |
|---|---|---|
attemptrequired | integer | The fence. |
linesrequired | array of Agent | At most max 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.
| Field | Type | Description |
|---|---|---|
duplicatesrequired | integer | Lines 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. |
highestSeqrequired | integer (int64) | The highest seq stored for this task as of this batch. |
storedrequired | integer | Lines this batch wrote. A stored value, so a replay reports the original number. |
AgentLogLine
| Field | Type | Description |
|---|---|---|
createdAtrequired | string (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. |
levelrequired | string enum | One of debug, info, warn, error. |
messagerequired | string | Already 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. |
seqrequired | integer (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. |
stagerequired | string |
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.
| Field | Type | Description |
|---|---|---|
namerequired | string | |
typerequired | string 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).
| Field | Type | Description |
|---|---|---|
addresses | array of string | |
operationrequired | string enum | One of create, update, replace, delete, import, forget, move. |
paths | array 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.
| Field | Type | Description |
|---|---|---|
digestrequired | string | The sha256 of the rules. Echo it, verbatim, as
plan: it is how a stored inspection says
which effects it was checked against. |
rulesrequired | array of Agent |
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.
| Field | Type | Description |
|---|---|---|
acceptedrequired | boolean | Every change the plan holds is permitted, and the apply may run. |
changesrequired | integer | How many resource changes the plan held, no-ops included. |
effectsrequired | string | The digest of the permitted inspected against, echoed verbatim. |
violations | array of Agent | What 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
| Field | Type | Description |
|---|---|---|
addressrequired | string | The resource address the change is to, as OpenTofu writes it. |
operationrequired | string | One of Agent'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). |
paths | array of string | For an update, the changed property paths no rule permits. |
reasonrequired | string | Why 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.
| Field | Type | Description |
|---|---|---|
claimrequired | integer | The 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. |
heartbeatrequired | integer | How often to heartbeat each held task. Default 30. |
heartbeatrequired | integer | After 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. |
maxAttemptsrequired | integer | After this many attempts a request fails rather than returning to
queued. Default 3. |
maxrequired | integer | |
maxrequired | integer | |
minrequired | string | Below 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.
| Field | Type | Description | ||||||||||||||||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
agentrequired | string | |||||||||||||||||||||||||
capabilities | array of string | Optional 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 agent
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:
| ||||||||||||||||||||||||
library | integer | The newest release of the built-in library this agent's image
carries (MILESTONE_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. | ||||||||||||||||||||||||
sealingKey | Agent | The public half of the agent's sealing key (agent-protocol. §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. | ||||||||||||||||||||||||
tofuVersionrequired | string | The 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
| Field | Type | Description |
|---|---|---|
accountIdrequired | string | Echoed 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. |
agentIdrequired | string (uuid) | |
policyrequired | 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. |
versionrequired | Agent | 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. One of supported, outdated, unsupported. |
AgentSealedValue
One secret value, sealed to the claiming agent's key.
| Field | Type | Description |
|---|---|---|
sealedrequired | string | Standard base64 of HPKE's encapsulated key (32 bytes) and the ciphertext: the value, of at most 64 KiB, and a 16-byte tag. |
variablerequired | string |
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.
| Field | Type | Description |
|---|---|---|
accountIdrequired | string | The 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. |
actionrequired | Provision | What a request asks for, and PROVISIONING_ §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 /, and it is
the one action that skips pending_: approval governs
infrastructure, and resolving a ref creates none. adopt is Import's second step (MILESTONE_importId); the approval names both
steps. The agent protocol for it: Agent 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 /, never at
POST /; it leaves nothing behind — no
provisioned resource, never partial — and a failed one is
asked for again rather than retried. One of create, destroy, resolve, adopt, stop. |
attemptrequired | integer | The 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_ the moment the task is reaped, cancelled or
retried, which is the moment it stops being current rather than the
moment somebody else claims. |
claimIdrequired | string (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. |
claimedAtrequired | string (date-time) | |
create | string (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:
stamp on the live resources, and provenance 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. |
deadline | integer | The budget for the whole task. Exceeding it is
failure, reported by the agent — the platform's own
reaper is the backstop for an agent that cannot report at all. |
gitRef | string | resolve 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. |
gitSha | string | create 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. |
gitUrl | string | For git. create/destroy: clone --depth 1 and check out
gitSha. resolve: git ls-remote this, and nothing more. |
imports | array of AgentImport | adopt only, and required there. The addresses the template
version declares importable and the identifier each is imported
by, finished on the server (MILESTONE_import writes one import block per
entry into panorama_; 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. |
inline | string | |
inline | string | For inline: the exact bytes to write into the workspace. Verify
inline over them before running anything. |
library | string | For 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_configuration, and is never fetched from anywhere. |
librarySlug | string | For library: the library template's slug, for the log and for
the agent's own sanity check. What runs is library. |
namerequired | string | |
outputs | array of Agent | create only, and only for an agent that registered the
outputs capability under the version it claims with (MILESTONE_tofu output -json), and report on the terminal succeeded the
ones found in outputs — never one OpenTofu marks sensitive,
which is named in withheld instead. Absent, report none. |
permitted | Agent | create 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 plan 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. |
placeholders | object | An 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 sealed gives. |
provenance | object | create, destroy and adopt. The tags to stamp onto everything the
apply creates, written into a generated ccc_
before init — an override merged into each provider "aws"
configuration the template declares, its own default_ kept
and these merged last. Always includes panorama:. 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. |
requestIdrequired | string (uuid) | |
resourceId | string (uuid) | destroy only. The provisioned row being torn down. |
runnerSlug | string | The runner the task runs as, when the agent
runs each template as its own runner role or runner account
(PANORAMA_,
PANORAMA_): the runner's name, and
the segment of a Google state prefix that runner may reach. It is
template 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
template. |
sealed | array of Agent | An 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.
§3.10). One per variable generated. 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/ and the four fields, each after a
newline — and write the values into a 0600 variables file of the
attempt, never into generated.; 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. |
sourceTyperequired | Template | 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_git, inline, library. |
state | string | destroy, 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. |
step | Agent | adopt only, and required there (MILESTONE_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. |
taskIdrequired | string (uuid) | |
templateIdrequired | string (uuid) | |
templaterequired | string | |
templaterequired | integer | With templateId, the whole identity of the version:
template_ is keyed (tenant_id, template_ and
has no surrogate id. An earlier draft of this contract invented one
for a resolve endpoint that no longer exists. |
variables | object | create, 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. 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 variables 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: property 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_ names its variable
password, and a name rule strong enough to refuse
aws_ 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
variables, 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. |
variables | Variables | create, 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.
| Field | Type | Description |
|---|---|---|
requestrequired | Provision | Where the request landed. Worth reading: a terminal task on a
request that went back to queued means this attempt was already
superseded. |
taskIdrequired | string (uuid) | |
taskStaterequired | Provision | 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. 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.
| Field | Type | Description |
|---|---|---|
agentrequired | string | |
attemptrequired | integer | The fence, from the claimed task. A value that is not the request's
current attempt is task_, and that is the answer, not an
error to retry past. |
created | integer | |
destroyed | integer | |
failure | Provision | Required 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. |
failure | string | Already scrubbed when it is sent. The platform stores it verbatim and shows it to humans; it has no second chance to redact. |
finalLogSeq | integer (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
(get) 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 log when
log 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". |
generated | Agent | The terminal succeeded of an adopt's generate step only, with step generated and the re-plan's accepted
plan. The platform stores it as a private template
version and the request moves to pending_ on it
(Agent); from any other report it is refused. |
gitSha | string | resolve 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_ and the stored value does not move — it would
otherwise silently change what a pending approval is about to
approve. |
log | integer (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. |
outputs | object | The terminal succeeded of a create handed outputs
(MILESTONE_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 withheld. 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_.
A request's outputs are those of the attempt that succeeded, and
they are what a later step consumes. |
overridden | array of string | The 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. |
partial | boolean | The 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. |
planDigest | string | sha256 of the plan output. Kept in the permanent summary. |
plan | Agent | Terminal reports of a task handed permitted.
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_ — 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
failure, 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. |
provenance | object | Beside provenance, 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_ — 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 provenance; anything else is 400 invalid_. 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 (drift false), since
nothing says which of its resources the inventory could see. |
provenance | integer | Resources carrying panorama: that the agent counted in state
after a successful apply, by reading tofu show -json locally — no
extra cloud API call. It becomes expected, the baseline the
drift diff measures against. Zero on a create that produced resources means the stamp did not
land: the row is stored provenance and excluded from
drift with a visible badge, never silently omitted. |
stage | string | |
stage | object | |
staterequired | Provision | 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. One of claimed, running, succeeded, failed, cancelled, abandoned. |
state | string | Where 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). |
step | Agent | adopt only (MILESTONE_imported on a running report, with the import step's
accepted plan and its state; 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_. generated on the
terminal succeeded of a generate step, with generated. |
tofuVersionrequired | string | |
withheld | array of string | The 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.
| Field | Type | Description |
|---|---|---|
addressrequired | string | |
attributerequired | string | |
variablerequired | string |
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.
| Field | Type | Description |
|---|---|---|
healthrequired | Status | Computed over the full filtered set, not the current page. |
provisioned | integer | How many of the group's live resources Provisioning created. Absent when the tenant does not hold Provisioning. |
spend | array of Money | Month-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. |
tagKeyrequired | string | |
tagValuerequired | string |
AppGroups
| Field | Type | Description |
|---|---|---|
groupsrequired | array of AppGroup | |
periodrequired | object | The month-to-date range spend covers, inclusive. |
tagKeysrequired | array of string | The 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.
| Field | Type | Description |
|---|---|---|
count | integer | How many; absent where the item is not a count. |
kindrequired | string enum | One of alerts., alerts., estate., collection., collection., cost., cost.budget, provisioning., provisioning., provisioning., provisioning., agents.. |
linkrequired | string | The portal address that shows exactly these rows. |
severityrequired | string enum | One of destructive, warning, neutral. |
since | string (date-time) | The oldest of what it counts. |
titlerequired | string | One sentence, in the product's words. |
AuditActor
The account that acted; absent for the platform's own changes and the CLI's.
| Field | Type | Description |
|---|---|---|
displayName | string | The name the person gave themselves, while the account exists and they gave one. |
email | string | The account's email, while the account exists. |
idrequired | string (uuid) |
AuditEntry
| Field | Type | Description |
|---|---|---|
actionrequired | string | Dotted, as auth. or notify.. |
actor | AuditActor | The account that acted; absent for the platform's own changes and the CLI's. |
after | object | The subject after the change, as the audit recorded it. |
atrequired | string (date-time) | |
before | object | The subject before the change, as the audit recorded it. |
idrequired | integer (int64) | |
subjectIdrequired | string | |
subjectTyperequired | string |
AuditLogPage
| Field | Type | Description |
|---|---|---|
entriesrequired | array of AuditEntry | |
nextCursor | string | Present 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.
| Field | Type | Description |
|---|---|---|
archivedAt | string (date-time) | |
createdAtrequired | string (date-time) | |
createdByrequired | 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. |
descriptionrequired | string | |
idrequired | string (uuid) | |
latest | integer | Absent until a version is published. |
librarySlug | string | The library Blueprint it was enabled from, when it was one. |
namerequired | string | |
slugrequired | string |
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.
| Field | Type | Description |
|---|---|---|
blueprintrequired | object | |
createRunId | string (uuid) | A destroy run's binding only — the create run it takes down. |
kindrequired | string enum | One of create, destroy. |
stepsrequired | array of Blueprint | |
teamIdrequired | string (uuid) | |
variablesrequired | string | |
versionrequired | integer | 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.
| Field | Type | Description |
|---|---|---|
constraints | Blueprint | 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. |
fromrequired | string enum | One of literal, variable, output. |
index | integer | The 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. |
producer | object | |
runVariable | string | |
value | Variable | One variable value, in one of the five types PROVISIONING_ §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. |
variablerequired | string |
BlueprintBoundStep
| Field | Type | Description |
|---|---|---|
accountIdrequired | string | |
inputsrequired | array of Blueprint | Empty in a destroy run, whose steps run with what their creates ran with. |
needs | array of string | The 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. |
resource | object | A 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. |
slotIndexrequired | integer | |
steprequired | string | |
templaterequired | object |
BlueprintCreate
| Field | Type | Description |
|---|---|---|
description | string | |
namerequired | string | |
slugrequired | string |
BlueprintDecision
| Field | Type | Description |
|---|---|---|
binding | string | Required to approve: the digest of the binding the approver read.
A binding that moved since is refused (409 binding_). |
comment | string | |
decisionrequired | string 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.
| Field | Type | Description |
|---|---|---|
non | array of string | Run 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. |
slotsrequired | array of Blueprint | |
stepsrequired | array of Blueprint | |
variables | Variables | Stored on the version and immutable with it. The portal generates the
request form from this; the agent uses it to write
generated. 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.
| Field | Type | Description |
|---|---|---|
literal | Variable | One variable value, in one of the five types PROVISIONING_ §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. |
output | string | |
var | string |
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.
| Field | Type | Description |
|---|---|---|
enum | array of Variable | |
max | number | |
maxItems | integer | |
maxLength | integer | |
min | number | |
minItems | integer | |
minLength | integer | |
pattern | string | |
required | boolean | |
sensitive | boolean | |
typerequired | string enum | One of string, number, boolean, list(string), map(string). |
BlueprintPage
| Field | Type | Description |
|---|---|---|
itemsrequired | array of Blueprint | |
pagerequired | integer | |
pageSizerequired | integer | |
totalrequired | integer (int64) | |
totalPagesrequired | integer |
BlueprintRun
| Field | Type | Description |
|---|---|---|
binding | Blueprint | What a run's one approval binds (MILESTONE_ |
binding | string | The binding's digest, which an approval names. |
blueprintIdrequired | string (uuid) | |
commentrequired | string | |
createRunId | string (uuid) | A destroy run's create run. |
decidedAt | string (date-time) | |
decidedBy | 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. |
decision | string enum | One of approved, rejected. |
destroy | array of string (uuid) | A create run's destroy runs, newest first; absent when it has none. |
expiresAtrequired | string (date-time) | The approval deadline. |
failure | string enum | Why 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_, output_. |
failure | string | Every step that stopped the run, and why. |
idrequired | string (uuid) | |
kindrequired | string enum | One of create, destroy. |
may | boolean | On a single run's read: true when the reader asked for this
pending_ run and may approve it themselves, nobody else
being able to. Absent from the list. |
namerequired | string | |
requestedAtrequired | string (date-time) | |
requestedByrequired | 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. |
self | boolean | Present once the run is decided: whether its requester approved it themselves, because nobody else could. |
slotrequired | object | |
staterequired | Blueprint | MILESTONE_1 §11.2's machine, enforced by a trigger:
pending_ → rejected | expired | withdrawn | running;
running → succeeded | failed | cancelling; cancelling →
cancelled; failed → running (resume) | cancelling (cancel). One of pending_, rejected, expired, withdrawn, running, succeeded, failed, cancelling, cancelled. |
stepsrequired | array of Blueprint | |
teamIdrequired | string (uuid) | |
terminalAt | string (date-time) | |
variablesrequired | object | The run's variables, a sensitive value masked. |
versionrequired | integer |
BlueprintRunCreate
| Field | Type | Description |
|---|---|---|
blueprintIdrequired | string (uuid) | |
namerequired | string | The run's name; each step's request is named after it. |
slotrequired | object | Every slot of the version to its accounts. |
teamId | string (uuid) | Optional; when given, it must be the team that owns every slot account. |
variables | object | |
versionrequired | integer |
BlueprintRunPage
| Field | Type | Description |
|---|---|---|
itemsrequired | array of Blueprint | |
pagerequired | integer | |
pageSizerequired | integer | |
totalrequired | integer (int64) | |
totalPagesrequired | integer |
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
| Field | Type | Description |
|---|---|---|
accountIdrequired | string | |
failure | string | A failed step's request's failure class. |
failure | string | Why the step stopped: its request's failure message, or — for
output_ and output_ — the input, the expression
and the rule, never the value; for skipped, why there was
nothing to destroy. |
requestId | string (uuid) | The step's request, once it exists. |
request | Provision | PROVISIONING_ §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_, rejected, expired, withdrawn, queued, running, cancelling, cancelled, succeeded, failed. |
resolvedAt | string (date-time) | |
resolved | object | The 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. |
slotIndexrequired | integer | The account's position in its slot; 0 for a slot that takes one. |
staterequired | Blueprint | waiting — not started; started — its request exists, and
request says where it has got to; succeeded and failed — its
request ended so (a cancelled request is a failed step);
output_ and output_ — 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_, output_, skipped. |
stepIdrequired | string |
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.
| Field | Type | Description |
|---|---|---|
cloudrequired | Template | The cloud a template makes its resources in, as the account registry
names it (Account.): a library template's (MILESTONE_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. |
many | boolean | |
namerequired | string |
BlueprintSlotAccount
| Field | Type | Description |
|---|---|---|
accountIdrequired | string | |
detail | string | The sentence a submission mapping the account to this slot would be refused with. |
namerequired | string | The registry's name for the account. |
qualifiesrequired | boolean | |
reason | string enum | The first rule the account fails for this slot, absent when it
qualifies: account_; wrong_cloud (the slot is another
cloud's); no_agent; agent_ (its agent has not called in
within the heartbeat timeout); library_ (its agent
does not hold the library release the slot's steps pin);
capability_ (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_, wrong_cloud, no_agent, agent_, library_, capability_. |
teamIdrequired | string (uuid) | The team that owns the account. |
BlueprintSlotChoice
| Field | Type | Description |
|---|---|---|
accountsrequired | array of Blueprint | The reader's teams' accounts, by account id, each judged for this slot. |
slotrequired | Blueprint | A place a run maps to accounts: exactly one, or — many — one or more,
each running its own instance of the slot's steps. |
BlueprintSlotChoices
| Field | Type | Description |
|---|---|---|
slotsrequired | array of Blueprint | The 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.
| Field | Type | Description |
|---|---|---|
forEachSlot | boolean | |
idrequired | string | |
inputs | object | |
needs | array of string | The steps whose success this step waits for. Acyclic. |
slotrequired | string | |
templaterequired | Blueprint | 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. |
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.
| Field | Type | Description |
|---|---|---|
idrequired | string (uuid) | |
identity | string | |
versionrequired | integer |
BlueprintVersion
One published, immutable version.
| Field | Type | Description |
|---|---|---|
blueprintIdrequired | string (uuid) | |
graphrequired | Blueprint | A version's document (MILESTONE_ |
graphSha256required | string | The sha256 of the version's canonical document, which a run's binding names. |
publishedAtrequired | string (date-time) | |
publishedByrequired | 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. |
versionrequired | integer |
BlueprintVersionPage
| Field | Type | Description |
|---|---|---|
itemsrequired | array of Blueprint | |
pagerequired | integer | |
pageSizerequired | integer | |
totalrequired | integer (int64) | |
totalPagesrequired | integer |
ChangeAlert
| Field | Type | Description |
|---|---|---|
accountIdrequired | string | |
appId | string (uuid) | The declared app the subscription is narrowed to; absent is none. |
appName | string | That app's name as it is now, archived or not; present with appId. |
entriesrequired | array of DiffEntry | |
hourlyLimitrequired | integer | |
idrequired | integer (int64) | |
occurredAtrequired | string (date-time) | |
predicaterequired | Change | |
regionrequired | string | |
resourceIdrequired | string | |
resource | string | |
resourcerequired | string | |
servicerequired | string | |
subscriptionrequired | string (uuid) | |
systemrequired | string | |
teamId | string (uuid) | The subscription's team; absent is the whole tenant's. |
throttledrequired | boolean | The alert that said the subscription reached its hourly limit; the rest of that hour was not sent. |
ChangeAlertList
| Field | Type | Description |
|---|---|---|
itemsrequired | array of ChangeAlert | Newest first, at most 100. |
sincerequired | string (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.
| Field | Type | Description |
|---|---|---|
candidatesrequired | array of Change | |
lookedUpAt | string (date-time) | |
staterequired | string enum | One of pending, matched, no_match, truncated, permission_, unavailable, beyond_, not_, not_. |
summaryrequired | string |
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.
| Field | Type | Description |
|---|---|---|
atrequired | string (date-time) | |
basisrequired | object | |
eventIdrequired | string | |
eventNamerequired | string | |
principalrequired | string | The caller's ARN, else the user name CloudTrail gives. |
principal | string | Cloud |
ChangeEvent
One row of the change feed. id is <kind>:<source id> — stable
across reads and pages. at is the event's own time.
| Field | Type | Description |
|---|---|---|
accountId | string | The account, where the source has one. |
actor | 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. |
appId | string (uuid) | The declared app a marker names. |
atrequired | string (date-time) | |
attribution | Change | Who may have made a config_ of an AWS resource; absent on every other row. |
detailrequired | object | The source's own fields, by kind. |
evidencerequired | Change | Where the row's source can be read in full. |
idrequired | string | |
kindrequired | ChangeKind | One source's kind of event in the change feed. One of config_, status_, acknowledgement, provisioning_, provisioning_, provisioning_, cost_, budget_, external_, ownership_, ai_review, provisioning_, instance_. |
resourceKey | string | The resource key (region/ |
summaryrequired | string | What happened, in one line. Never why. |
teamId | string (uuid) | The team, for a team-scoped event. |
ChangeEvidence
Where the row's source can be read in full.
| Field | Type | Description |
|---|---|---|
apirequired | string | The API read behind the row. |
hrefrequired | string | A portal path, or a marker's own URL. |
ChangeFeedPage
| Field | Type | Description |
|---|---|---|
itemsrequired | array of ChangeEvent | |
nextCursor | string | Present when another page follows; opaque. |
truncatedrequired | array of ChangeKind | The 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
| Field | Type | Description |
|---|---|---|
accountId | string | |
appId | string (uuid) | |
atrequired | string (date-time) | |
createdAtrequired | string (date-time) | |
idrequired | string (uuid) | |
idempotencyrequired | string | |
labelrequired | string | |
resourcerequired | array of string | |
urlrequired | string |
ChangeMarkerCreate
| Field | Type | Description |
|---|---|---|
accountId | string | The account the marker is about; omitted, the marker is the tenant's. |
appId | string (uuid) | The declared app the marker is about. |
atrequired | string (date-time) | When it happened, by the poster's clock. |
idempotencyrequired | string | |
labelrequired | string | |
resource | array of string | The resources it touched, as keys under accountId. |
url | string | A link back to the poster's own record. |
ChangeSubscription
| Field | Type | Description |
|---|---|---|
accountIdsrequired | array of string | |
appId | string (uuid) | The declared app the scope is narrowed to; absent is none. |
channelIdsrequired | array of string (uuid) | The chosen channels; empty is the team's routing. |
createdAtrequired | string (date-time) | |
createdByrequired | string (uuid) | |
hourlyLimitrequired | integer | |
idrequired | string (uuid) | |
predicaterequired | Change | |
resourcerequired | array of Change | |
teamId | string (uuid) |
ChangeSubscriptionInput
| Field | Type | Description |
|---|---|---|
accountIds | array of string | |
appId | string (uuid) | One live declared app (GET /): 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. |
channelIds | array 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. |
hourlyLimit | integer | |
predicaterequired | Change | |
resource | array of Change | Absent or empty is every type; a details path needs at least one. |
teamId | string (uuid) | Absent is the whole tenant, an administrator's alone. |
ChangeSubscriptionList
| Field | Type | Description |
|---|---|---|
itemsrequired | array of Change |
ChangeSubscriptionPredicate
| Field | Type | Description |
|---|---|---|
oprequired | string enum | One of changed, added, removed, equals. |
pathrequired | string | status, state, tags.<key> or additional — a field of every named type's descriptor. |
value | string | The new value, for equals only. |
ChangeSubscriptionPreview
| Field | Type | Description |
|---|---|---|
matchedrequired | integer | Changes the draft matched. |
matchesrequired | array of Change | The newest 50. |
partialrequired | boolean | The day held more changes than one preview reads. |
sincerequired | string (date-time) | |
throttledrequired | integer | Of them, those past the hourly limit: not sent. |
ChangeSubscriptionPreviewMatch
| Field | Type | Description |
|---|---|---|
accountIdrequired | string | |
entriesrequired | array of DiffEntry | |
observedAtrequired | string (date-time) | |
regionrequired | string | |
resourceIdrequired | string | |
resourcerequired | string | |
servicerequired | string | |
systemrequired | string | |
throttledrequired | boolean |
ChangeSubscriptionType
| Field | Type | Description |
|---|---|---|
resourcerequired | string | |
servicerequired | string | |
systemrequired | string |
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.
| Field | Type | Description |
|---|---|---|
cloudsrequired | array of CloudSignIn | Every cloud this build carries, in the product's order (AWS, Google Cloud, Azure). |
homerequired | string enum | One of aws, gcp, azure. |
CloudSignIn
| Field | Type | Description |
|---|---|---|
cloudrequired | string enum | One of aws, gcp, azure. |
from | string enum | The cloud whose identity a federated sign-in starts from; absent otherwise. One of aws, gcp, azure. |
signInrequired | string enum | home, 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.
| Field | Type | Description |
|---|---|---|
jobsrequired | integer | How many jobs were queued — one per module per region. |
modulesrequired | array of string | The 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.
| Field | Type | Description |
|---|---|---|
regionrequired | string | |
resourcerequired | string | |
servicerequired | string | |
systemrequired | string |
CollectionRunRegistered
| Field | Type | Description |
|---|---|---|
bucketsrequired | array of Registered | Each registered bucket with the generation this run claimed. |
runIdrequired | string (uuid) |
CollectionRunRegistration
| Field | Type | Description |
|---|---|---|
accountIdrequired | string | |
bucketsrequired | array of Collection | Must 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_, 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. |
modulerequired | string | |
regionrequired | string | |
runIdrequired | string (uuid) | Generated by the collector before its first attempt, and reused on every retry. This is the idempotency key for registration. |
CollectorMatrix
| Field | Type | Description |
|---|---|---|
accountsrequired | array of Collector | Per-account roll-up, worst status wins. |
columnsrequired | array of Collector | Every 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_module names its
column here. The grid's columns differ per cloud, and a module's name cannot
say which: k8s. 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. |
entriesrequired | array of MatrixEntry |
CollectorMatrixAccount
| Field | Type | Description |
|---|---|---|
accountIdrequired | string | |
overallrequired | string enum | One of ok, error, slow, no-data. |
CollectorMatrixColumn
| Field | Type | Description |
|---|---|---|
category | Resource | What kind of thing this collector reads: the Resource
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. |
modulerequired | string | |
systemrequired | string | The cloud of the accounts this collector runs for — one of
Account.'s values. The scheduler pairs a module only with
accounts of its own cloud. |
CollectorRun
| Field | Type | Description |
|---|---|---|
accountIdrequired | string | |
durationMs | integer (int64) | |
error | string | |
finishedAt | string (date-time) | |
modulerequired | string | |
regionrequired | string | |
resources | integer (int64) | |
resources | integer (int64) | |
runIdrequired | string (uuid) | |
startedAtrequired | string (date-time) | |
staterequired | string enum | One of running, finalized, failed. |
tombstoned | integer (int64) |
CollectorRunList
| Field | Type | Description |
|---|---|---|
itemsrequired | array of Collector |
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.
| Field | Type | Description |
|---|---|---|
enabledrequired | boolean | |
fixedShares | object | For fixed: team id → share. Any positive numbers; they are normalised. |
id | string (uuid) | |
methodrequired | string enum | proportional — 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. |
namerequired | string | |
servicesrequired | array of string | Cost Explorer SERVICE names, as the breakdown spells them. |
CostAnomaly
| Field | Type | Description |
|---|---|---|
baselinerequired | Money | One amount in one currency. The only way money appears in this
document. Test 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. |
detectedAtrequired | string (date-time) | |
deviationrequired | number | |
idrequired | string (uuid) | |
observedrequired | Money | One amount in one currency. The only way money appears in this
document. Test 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. |
resolvedAt | string (date-time) | |
resolvedBy | 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. |
scopeIdrequired | string | An account id, or a team id. |
scopeLabel | string | The account's or team's name, for display. |
scopeTyperequired | string enum | One of account, team. |
servicerequired | string | Empty for a scope-wide anomaly. |
severityrequired | string enum | One of low, medium, high. |
statusrequired | Cost | One of open, acknowledged, dismissed. |
windowEndrequired | string (date) | |
windowStartrequired | string (date) |
CostAnomalyPage
| Field | Type | Description |
|---|---|---|
inNoTeamrequired | boolean | The reader is not an administrator and belongs to no team, so
the page is empty for that reason; as
Cost. On the reviewer's route the reader
is the review's audience. |
itemsrequired | array of CostAnomaly | |
pagerequired | integer | |
pageSizerequired | integer | |
totalrequired | integer (int64) | |
totalPagesrequired | integer |
CostAnomalySettings
| Field | Type | Description |
|---|---|---|
minDeltarequired | Money | One amount in one currency. The only way money appears in this
document. Test 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. |
sensitivityrequired | number | The relative deviation from baseline that counts. |
suppressionrequired | integer |
CostAnomalyStatus
Type: string enum
CostAnomalyUpdate
| Field | Type | Description |
|---|---|---|
statusrequired | string enum | One of acknowledged, dismissed. |
CostAttributionAmounts
| Field | Type | Description |
|---|---|---|
amountsrequired | array of Money | |
attributionrequired | Cost | 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. One of tag, team_, allocated, untagged. |
CostAttributionRule
| Field | Type | Description |
|---|---|---|
priorityrequired | integer | |
tagKeyrequired | string |
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.
| Field | Type | Description |
|---|---|---|
containerrequired | string | |
directoryrequired | string | The export's directory in the container: FOCUS, or the ActualCost export of a pair. |
idrequired | string (uuid) | |
lastPull | Cost | The latest run record of the job cost. 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. |
maxrequired | integer (int64) | The most bytes one blob may hold; over it, the pull is refused and says so. |
pair | string | The Amortized |
storagerequired | string | |
subscriptionrequired | string (uuid) | |
updatedAtrequired | string (date-time) |
CostAzureBillingExportCreate
| Field | Type | Description |
|---|---|---|
containerrequired | string | |
directoryrequired | string | |
max | integer (int64) | |
pair | string | |
storagerequired | string | |
subscriptionrequired | string (uuid) |
CostAzureBillingExportList
| Field | Type | Description |
|---|---|---|
itemsrequired | array of Cost |
CostAzureBillingExportUpdate
| Field | Type | Description |
|---|---|---|
container | string | |
directory | string | |
max | integer (int64) | |
pair | string | |
storage | string |
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.
| Field | Type | Description |
|---|---|---|
endedAt | string (date-time) | |
message | string | |
rowsWritten | integer | Rows the run wrote, over every export of the subscription. |
startedAtrequired | string (date-time) | |
staterequired | string enum | One of running, succeeded, refused, failed. |
CostBreakdown
| Field | Type | Description |
|---|---|---|
figurerequired | CostFigure | One of amortized, unblended. |
groupByrequired | string enum | One of team, service, account. |
inNoTeamrequired | boolean | True 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. |
periodrequired | CostPeriod | |
rebuildrequired | boolean | |
rebuiltAt | string (date-time) | When the rollups these figures come from were last built. |
rowsrequired | array of Cost | |
totalsrequired | array of Money | The sum of every row including other, per currency. |
CostBreakdownRow
| Field | Type | Description |
|---|---|---|
allocated | array of Money | The 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. |
amountsrequired | array of Money | The row's whole figure, per currency, allocation included. |
keyrequired | string | Pass it back as the matching filter to drill down. other and untagged are not drillable. |
labelrequired | string | |
sharePct | number | This 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
| Field | Type | Description |
|---|---|---|
amountrequired | Money | One amount in one currency. The only way money appears in this
document. Test 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. |
createdAtrequired | string (date-time) | |
enabledrequired | boolean | |
idrequired | string (uuid) | |
periodrequired | string enum | One of monthly. |
progress | Cost | The current period against the budget, in the budget's currency. |
teamIdrequired | string (uuid) | |
teamNamerequired | string | |
thresholdsrequired | array of integer | Percentages of amount, each firing once per period. |
updatedAtrequired | string (date-time) |
CostBudgetCreate
| Field | Type | Description |
|---|---|---|
amountrequired | Money | One amount in one currency. The only way money appears in this
document. Test 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. |
enabled | boolean | |
period | string enum | One of monthly. |
teamIdrequired | string (uuid) | |
thresholds | array of integer |
CostBudgetList
| Field | Type | Description |
|---|---|---|
inNoTeamrequired | boolean | The reader is not an administrator and belongs to no team, so the list is empty for that reason; as Cost. |
itemsrequired | array of CostBudget |
CostBudgetProgress
The current period against the budget, in the budget's currency.
| Field | Type | Description |
|---|---|---|
other | array of Money | The team's spend this period in currencies other than the budget's — not converted (C7), listed so the mismatch is seen. |
pctrequired | number | spent / amount × 100. |
periodStartrequired | string (date) | |
spentrequired | Money | One amount in one currency. The only way money appears in this
document. Test 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. |
thresholdsrequired | array of integer | The thresholds that have already fired this period. |
CostBudgetUpdate
| Field | Type | Description |
|---|---|---|
amount | Money | One amount in one currency. The only way money appears in this
document. Test 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. |
enabled | boolean | |
thresholds | array of integer |
CostCloudAmounts
| Field | Type | Description |
|---|---|---|
amountsrequired | array of Money | |
systemrequired | string enum | The 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.
| Field | Type | Description |
|---|---|---|
accountIdrequired | string | |
bucketrequired | string | |
bucketrequired | string | S3 does not follow a request to the wrong region; the client is built for this one. |
curOnly | boolean | Skip 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. |
enabledrequired | boolean | |
last | string | |
last | string (date) | |
prefixrequired | string | |
reportNamerequired | string | |
report | string | The 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 /
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.
| Field | Type | Description |
|---|---|---|
anomalyId | string (uuid) | |
changesFrom | string (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. |
comparablerequired | Cost | |
coveragerequired | string enum | One of complete, incomplete. |
currenciesrequired | array of Cost | |
currentrequired | Cost | Both days inclusive, after the cut at the settled day. |
dataVersion | string (date-time) | The newest rebuild among the rows read; absent when none was. |
figurerequired | CostFigure | One of amortized, unblended. |
grainrequired | string enum | What a mover is; one grain per explanation. One of resource, service. |
inNoTeamrequired | boolean | As Cost. |
methodrequired | string | The method the figures were computed with. |
missingDaysrequired | array of string (date) | Days of either window with no row. |
previousrequired | Cost | Both days inclusive, after the cut at the settled day. |
rebuildrequired | boolean | An attribution rebuild is queued or running (§7.3), so the movers and a team's share may move until it finishes; as Cost. |
rebuiltAt | string (date-time) | When the last attribution rebuild finished; absent if none has; as Cost. |
settledrequired | string (date) | |
supersededrequired | boolean | The request's version is not the current answer; false when none was given. |
unsettledrequired | array of string (date) | Days of either window after the settled day, cut from it. |
version | string | This 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
| Field | Type | Description |
|---|---|---|
atrequired | string (date-time) | |
kindrequired | string | |
resourcerequired | string | |
summaryrequired | string |
CostExplanationComparability
| Field | Type | Description |
|---|---|---|
currentDaysrequired | integer | |
equalLengthrequired | boolean | |
previousrequired | integer | |
weekdaysrequired | boolean | The windows hold a different mix of weekdays. |
CostExplanationCurrency
One currency's change; its movers sum to delta exactly.
| Field | Type | Description |
|---|---|---|
currencyrequired | string | |
currentrequired | string | |
currentrequired | string | |
deltarequired | string | |
deltaPerDayrequired | string | |
moversrequired | array of Cost | |
previousrequired | string | |
previousrequired | string |
CostExplanationMover
| Field | Type | Description |
|---|---|---|
accountIdrequired | string | |
changesrequired | array of Cost | What changed on the resource during the current window — shown beside the money, never as its cause. |
classrequired | string enum | One of new, gone, amount_, adjustment. |
currencyrequired | string | |
currentrequired | string | |
deltarequired | string | |
moreChanges | boolean | changes 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. |
owner | 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_ |
previousrequired | string | |
resource | string | At the resource grain only: the bill's own identifier of the resource. |
resourceKey | string | The inventory key (region/resource maps to, when it maps to one; the account is accountId. |
servicerequired | string | |
without | boolean | At 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.
| Field | Type | Description |
|---|---|---|
fromrequired | string (date) | |
torequired | string (date) |
CostFigure
Type: string enum
CostForecast
| Field | Type | Description |
|---|---|---|
actualrequired | Money | One amount in one currency. The only way money appears in this
document. Test 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. |
asOfrequired | string (date) | |
backtestrequired | Cost | The scope's own backtest, at the checkpoint nearest the forecast's day. |
computedAtrequired | string (date-time) | |
daysrequired | integer | |
high | Money | One amount in one currency. The only way money appears in this
document. Test 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. |
low | Money | One amount in one currency. The only way money appears in this
document. Test 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. |
lowrequired | boolean | |
methodrequired | string | |
projection | Money | One amount in one currency. The only way money appears in this
document. Test 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. |
settledrequired | string (date) | The last day counted; the two days after it are still arriving. |
staterequired | string enum | ready with a projection; not_ below 14 settled days, with none. One of ready, not_. |
CostForecastBacktest
The scope's own backtest, at the checkpoint nearest the forecast's day.
| Field | Type | Description |
|---|---|---|
checkpointrequired | integer | The day of the month the backtest was read at, 7, 14 or 21. |
p90ApePct | number (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. |
scoredrequired | integer |
CostForecastList
| Field | Type | Description |
|---|---|---|
forecastsrequired | array of Cost | One per currency, from the newest day computed; empty when none has been computed. |
monthrequired | string | |
scoperequired | Cost | What a forecast is for. One of tenant, team, account. |
scopeIdrequired | string | The 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.
| Field | Type | Description |
|---|---|---|
billingrequired | string | |
lastPull | Cost | The latest run record of the job cost. 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. |
maxrequired | integer (int64) | The most bytes one query may scan; over it, the pull is refused and says so. |
tableNamerequired | string | project., the detailed usage cost export. |
updatedAtrequired | string (date-time) |
CostGcpBillingExportCreate
| Field | Type | Description |
|---|---|---|
billingrequired | string | |
max | integer (int64) | |
tableNamerequired | string |
CostGcpBillingExportList
| Field | Type | Description |
|---|---|---|
itemsrequired | array of Cost |
CostGcpBillingExportUpdate
| Field | Type | Description |
|---|---|---|
max | integer (int64) | |
tableName | string |
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.
| Field | Type | Description |
|---|---|---|
endedAt | string (date-time) | |
message | string | |
rowsWritten | integer | Rows the run wrote, over every export of the project. |
startedAtrequired | string (date-time) | |
staterequired | string enum | One of running, succeeded, refused, failed. |
CostIngestionState
| Field | Type | Description |
|---|---|---|
accountIdrequired | string | |
backfill | boolean | |
backfill | string (date) | |
backfill | string (date) | |
last | string (date) | |
lastError | string | |
lastRunAt | string (date-time) | |
pass | Money | One amount in one currency. The only way money appears in this
document. Test 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. |
resource | boolean | Absent until the first resource pull has asked. |
sourcerequired | string enum | One of ce_, ce_tag, ce_resource, cur, trusted_. |
support | boolean | For trusted_: 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
| Field | Type | Description |
|---|---|---|
clustersrequired | array of Cost | |
figurerequired | CostFigure | One of amortized, unblended. |
inNoTeamrequired | boolean | True 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. |
periodrequired | CostPeriod | |
rebuildrequired | boolean | |
rebuiltAt | string (date-time) | When the rollups these figures come from were last built. |
rowsrequired | array of Cost | Largest first, across clusters; filter by cluster for one. |
CostKubernetesCluster
| Field | Type | Description |
|---|---|---|
accountIdrequired | string | |
clusterrequired | string | |
idleSpendrequired | array of Money | The part no pod requested, per currency. |
nodeCountrequired | integer | Nodes in the inventory on the last day computed. |
nodeSpendrequired | array of Money | The cluster's node line items over the window, per currency: what the rows sum to. |
regionrequired | string | The 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
| Field | Type | Description |
|---|---|---|
accountIdrequired | string | |
clusterrequired | string | |
namespacerequired | string | Empty for the idle row. |
regionrequired | string | The cluster's region; with accountId and cluster, which cluster this row belongs to. |
sharePct | number | This row's share of the cluster's node spend, in the row's leading currency, when the cluster has one currency. |
spendrequired | array of Money | |
workloadrequired | string | Deployment, Stateful |
workloadrequired | string |
CostPeriod
| Field | Type | Description |
|---|---|---|
fromrequired | string (date) | |
torequired | string (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.
| Field | Type | Description |
|---|---|---|
figurerequired | CostFigure | One of amortized, unblended. |
inNoTeamrequired | boolean | True 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. |
rebuildrequired | boolean | |
rebuiltAt | string (date-time) | When the rollups these figures come from were last built. |
CostRecommendation
| Field | Type | Description |
|---|---|---|
accountIdrequired | string | |
checkIdrequired | string | |
checkNamerequired | string | |
estimated | Money | One amount in one currency. The only way money appears in this
document. Test 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. |
firstSeenAt | string (date-time) | |
metadatarequired | object | The check's columns, by the names Trusted Advisor gives them. |
observedAtrequired | string (date-time) | |
region | string | |
resourceIdrequired | string | Trusted Advisor's identifier for the flagged resource. |
resourceKey | string | The inventory key, when the check names a resource the derivation can map. |
statusrequired | string enum | One of ok, warning, error. |
CostRecommendationPage
| Field | Type | Description |
|---|---|---|
inNoTeamrequired | boolean | The reader is not an administrator and belongs to no team, so the page is empty for that reason; as Cost. |
itemsrequired | array of Cost | |
pagerequired | integer | |
pageSizerequired | integer | |
totalrequired | integer (int64) | |
totalPagesrequired | integer | |
unavailablerequired | array of Cost | Accounts with no recommendations and why: no Trusted Advisor on the support plan, or not pulled yet. |
CostReconciliation
The three places, the two breakdowns and the rows. Every list names every value, zero included, so nothing is hidden by absence.
| Field | Type | Description |
|---|---|---|
chargeTypesrequired | array of Reconciliation | |
coveragerequired | array of Reconciliation | |
inNoTeamrequired | boolean | The reader is not an administrator and belongs to no team, so everything is empty for that reason; as Cost. |
placesrequired | array of Reconciliation | |
rebuildrequired | boolean | An attribution rebuild is queued or running (§7.3), so the places and a team's share may move until it finishes; as Cost. |
rebuiltAt | string (date-time) | When the last attribution rebuild finished; absent if none has; as Cost. |
rowsrequired | array of Reconciliation | |
totalsrequired | array of Money | The scope's authoritative net spend, signed; the places, the coverage and the charge types each sum to it. |
windowrequired | CostPeriod |
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.
| Field | Type | Description |
|---|---|---|
attemptsrequired | integer | |
createdAtrequired | string (date-time) | |
dropped | string enum | One of schedule_, destination_, destination_. |
periodrequired | string | |
sentAt | string (date-time) |
CostReportDestination
| Field | Type | Description |
|---|---|---|
idrequired | string (uuid) | |
kindrequired | string enum | One of email, slack, teams, webhook. |
namerequired | string |
CostReportSchedule
| Field | Type | Description |
|---|---|---|
cadencerequired | string enum | One of monthly. |
canEditrequired | boolean | Whether the caller may change it — a lead of its team, or an administrator. |
channelIdrequired | string (uuid) | |
channelKindrequired | string enum | One of email, slack, teams, webhook. |
channelNamerequired | string | |
createdAtrequired | string (date-time) | |
enabledrequired | boolean | |
idrequired | string (uuid) | |
last | Cost | A schedule's newest delivery. period is 2026-09 for a month's
report and on-demand/ for one asked for. No sentAt and
no dropped is still owed; attempts counts the tries.
dropped says why nothing was sent: the schedule or its
channel was switched off, or the channel was no longer the team's. |
teamIdrequired | string (uuid) | |
teamNamerequired | string | |
updatedAtrequired | string (date-time) |
CostReportScheduleCreate
| Field | Type | Description |
|---|---|---|
channelIdrequired | string (uuid) | |
enabled | boolean | |
teamIdrequired | string (uuid) |
CostReportScheduleList
| Field | Type | Description |
|---|---|---|
itemsrequired | array of Cost | |
teamsrequired | array of Cost |
CostReportScheduleUpdate
| Field | Type | Description |
|---|---|---|
channelId | string (uuid) | |
enabled | boolean |
CostReportSend
| Field | Type | Description |
|---|---|---|
periodrequired | string | |
queuedrequired | boolean | False when today's report was already asked for. |
CostReportTeam
| Field | Type | Description |
|---|---|---|
channelsrequired | array of Cost | |
idrequired | string (uuid) | |
namerequired | string |
CostResource
| Field | Type | Description |
|---|---|---|
accountIdrequired | string | |
amountsrequired | array of Money | Spend in the window, per currency. |
inventoryrequired | Cost | 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). One of live, tombstoned, missing. |
lastSeenAt | string (date-time) | The inventory's last sighting, when it has a row. |
mapped | boolean | Whether the identifier the source reported mapped to a platform key (§5.4). Unmapped rows are attribution gaps in disguise and are counted in settings. |
region | string | |
resourceId | string | |
resourceKeyrequired | string | The platform resource key (PLATFORM. |
resource | string | From the inventory. Absent when the inventory has no row. |
resource | string | |
servicerequired | string | |
status | string | The inventory's status word, when it has a row. |
system | string |
CostResourcePage
| Field | Type | Description |
|---|---|---|
figurerequired | CostFigure | One of amortized, unblended. |
filterrequired | string enum | One of all, orphaned, unattributed. |
inNoTeamrequired | boolean | True 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. |
itemsrequired | array of Cost | |
pagerequired | integer | |
pageSizerequired | integer | |
rebuildrequired | boolean | |
rebuiltAt | string (date-time) | When the rollups these figures come from were last built. |
totalrequired | integer (int64) | |
totalPagesrequired | integer | |
unavailablerequired | array of Cost | Accounts with no usable per-resource evidence and why.
resource_: 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_: no source has been pulled yet. Documented
states, never errors. |
windowrequired | CostPeriod | |
windowDaysrequired | integer | Always 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. |
CostSeries
| Field | Type | Description |
|---|---|---|
keyrequired | string | The group's identity: a team id, a service name, an account id —
or untagged for the unattributed remainder under group. |
labelrequired | string | |
pointsrequired | array of Cost |
CostSettings
| Field | Type | Description |
|---|---|---|
allocationrequired | array of Cost | Shared-cost allocation rules in the order they are tried. |
anomalyrequired | Cost | |
attributionrequired | array of Cost | |
curReportsrequired | array of Cost | Per account: where its Cost & Usage Report is, when one is configured. |
defaultrequired | CostFigure | One of amortized, unblended. |
ingestionrequired | array of Cost | |
rebuild | boolean | On the response to a PUT that changed the rules or the tag values. |
rebuildrequired | boolean | |
teamrequired | array of Cost | |
unmappedrequired | integer | Per-resource rows whose identifier the key derivation could not map (§5.4), in the current window. |
CostSettingsUpdate
| Field | Type | Description |
|---|---|---|
allocation | array of Cost | The 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. |
anomalyrequired | Cost | |
attributionrequired | array of Cost | |
curReports | array of Cost | The 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. |
defaultrequired | CostFigure | One of amortized, unblended. |
teamrequired | array of Cost |
CostSummary
| Field | Type | Description |
|---|---|---|
byrequired | array of Cost | The three tiers, each per currency. Their sum is totals, to
the cent — the reconciliation invariant (§7.2). |
byCloudrequired | array of Cost | The 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. |
deltarequired | array of MoneyDelta | |
figurerequired | CostFigure | One of amortized, unblended. |
inNoTeamrequired | boolean | True 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. |
periodrequired | CostPeriod | |
previousrequired | CostPeriod | |
previousrequired | array of Money | |
rebuildrequired | boolean | |
rebuiltAt | string (date-time) | When the rollups these figures come from were last built. |
totalsrequired | array of Money | Per currency. A single-currency tenant sees one element. |
CostTeamTagValue
| Field | Type | Description |
|---|---|---|
tagValuerequired | string | Matched case-insensitively. |
teamIdrequired | string (uuid) |
CostTrend
| Field | Type | Description |
|---|---|---|
figurerequired | CostFigure | One of amortized, unblended. |
granularityrequired | string enum | One of daily, monthly. |
groupByrequired | string enum | One of team, service, account. |
inNoTeamrequired | boolean | True 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. |
periodrequired | CostPeriod | |
rebuildrequired | boolean | |
rebuiltAt | string (date-time) | When the rollups these figures come from were last built. |
seriesrequired | array of CostSeries |
CostTrendPoint
| Field | Type | Description |
|---|---|---|
amountsrequired | array of Money | Per currency; empty on a day with no spend. |
daterequired | string (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.
| Field | Type | Description |
|---|---|---|
attentionrequired | array of Attention | What needs someone now, ranked; empty is a sentence, not a panel. |
changes | Dashboard | The change feed's newest entries, in the feed's own schema. |
collection | Dashboard | How collection is going, in the words / uses for one account, counted across them. |
estate | Dashboard | What the estate is, by cloud and by category (the taxonomy), from one grouped read so the panels cannot disagree. |
firstRun | Dashboard | The first-run checklist on the start page (MILESTONE_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. |
generatedAtrequired | string (date-time) | |
hottest | Dashboard | The hottest resources (MILESTONE_GET /'s groups. No order
across groups. Absent unless the viewer's licence carries Estate and
the performance capability; empty groups when nothing is measured. |
provisioning | Dashboard | What is waiting on this person, what is moving, and what has gone wrong lately. |
spend | Dashboard | 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_/ is. |
DashboardCategoryCount
| Field | Type | Description |
|---|---|---|
categoryrequired | Resource | What kind of thing a resource type is (MILESTONE_other is the last bucket: a legacy templates. 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. Performance 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_, other. |
degradedrequired | integer | |
failedrequired | integer | |
operationalrequired | integer | |
resourcerequired | integer | |
unknownrequired | integer |
DashboardChanges
The change feed's newest entries, in the feed's own schema.
| Field | Type | Description |
|---|---|---|
items | array of ChangeEvent | |
unavailable | Dashboard | 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. |
DashboardCloudCount
| Field | Type | Description |
|---|---|---|
accountrequired | integer | How 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. |
degradedrequired | integer | |
failedrequired | integer | |
operationalrequired | integer | |
resourcerequired | integer | |
systemrequired | string | |
unknownrequired | integer |
DashboardCollection
How collection is going, in the words /accounts/overview uses for one account, counted across them.
| Field | Type | Description |
|---|---|---|
error | integer | |
noData | integer | |
ok | integer | |
slow | integer | |
unavailable | Dashboard | 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. |
worst | array of Dashboard | The 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.
| Field | Type | Description |
|---|---|---|
byCategory | array of Dashboard | |
byCloud | array of Dashboard | |
collectedAt | string (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. |
counts | Dashboard | How many resources are at each status. A count, not an amount. |
unavailable | Dashboard | 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. |
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.
| Field | Type | Description |
|---|---|---|
steps | array of First | |
unavailable | Dashboard | 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. |
DashboardForecast
Month-end, as the forecast reads it; absent where there is not enough settled history to say.
| Field | Type | Description |
|---|---|---|
highrequired | string | |
lowrequired | string | |
midrequired | string |
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.
| Field | Type | Description |
|---|---|---|
from | string (date-time) | |
groups | array of Performance | |
to | string (date-time) | |
unavailable | Dashboard | 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. |
window | Performance | 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. One of 1h, 24h. |
DashboardProvisioning
What is waiting on this person, what is moving, and what has gone wrong lately.
| Field | Type | Description |
|---|---|---|
awaitingYou | integer | Requests in pending_ this viewer may decide — the approvals queue's total, their own included exactly when nobody else could approve it. |
drift | integer | |
drift | integer | |
failed | integer | |
inFlight | integer | |
links | Dashboard | Where 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 /
unchanged. "Awaiting you" is the approvals queue, which asks the
list awaiting and, from the band's expiring item,
its expires bound (MILESTONE_ |
unavailable | Dashboard | 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. |
DashboardProvisioningLinks
Where 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).
| Field | Type | Description |
|---|---|---|
awaitingYourequired | string | |
failedrequired | string | |
inFlightrequired | string |
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.
| Field | Type | Description |
|---|---|---|
currencies | array of Dashboard | |
unavailable | Dashboard | 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. |
DashboardSpendCurrency
| Field | Type | Description |
|---|---|---|
budgetsrequired | integer | Budgets over 100 % or forecast to exceed. |
currencyrequired | string | |
forecast | Dashboard | Month-end, as the forecast reads it; absent where there is not enough settled history to say. |
monthToDaterequired | string | A decimal string; money is never a float. |
previousrequired | string | The 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.
| Field | Type | Description |
|---|---|---|
degradedrequired | integer | |
failedrequired | integer | |
operationalrequired | integer | |
unknownrequired | integer |
DashboardWorstAccount
| Field | Type | Description |
|---|---|---|
accountIdrequired | string | |
last | string (date-time) | |
name | string | |
statusrequired | string enum | One of ok, slow, error, no-data. |
DeclaredApp
| Field | Type | Description |
|---|---|---|
archivedAt | string (date-time) | |
canEditrequired | boolean | Whether 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. |
createdAtrequired | string (date-time) | |
description | string | |
excluderequired | array of ResourceRef | |
healthrequired | Status | Computed over the full filtered set, not the current page. |
idrequired | string (uuid) | |
includerequired | array of ResourceRef | |
matchesrequired | array of TagMatch | |
money | Declared | An app's money for the period, at one grain, named
(platform/ §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 overlap says how
many members hit more than one match (their money may be counted
twice) while explicit 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. |
namerequired | string | |
provisioned | integer | How many of the app's live members Provisioning created, over the viewer's accounts. Absent without the module. |
slugrequired | string | |
teamrequired | 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. |
updatedAtrequired | string (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.
| Field | Type | Description |
|---|---|---|
description | string | |
exclude | array of ResourceRef | Resources kept out although a match hits them. |
include | array of ResourceRef | Resources added by hand. |
matchesrequired | array of TagMatch | |
namerequired | string | |
teamIdrequired | string (uuid) |
DeclaredAppDetail
| Field | Type | Description |
|---|---|---|
apprequired | DeclaredApp | |
membersrequired | array of Declared | |
ownablerequired | array of TeamRef | The 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. |
trend | array of Declared | Six calendar months ending with the current one, each at its own grain. Absent without Spend. |
DeclaredAppList
| Field | Type | Description |
|---|---|---|
appsrequired | array of DeclaredApp | |
canCreaterequired | boolean | Whether 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.ownable being non-empty: a lead whose only team has
been closed may not declare either, because every team the write
would accept is gone. |
ownablerequired | array of TeamRef | The 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_), and not a reader offered any. Empty exactly when canCreate is false. |
periodrequired | object | The month-to-date range spend covers, inclusive. |
DeclaredAppMember
| Field | Type | Description |
|---|---|---|
accountIdrequired | string | |
lastSeenAtrequired | string (date-time) | |
namerequired | string | |
regionrequired | string | |
resourceIdrequired | string | |
resourcerequired | string | |
servicerequired | string | |
statusrequired | 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. One of failed, degraded, operational, unknown. |
systemrequired | string | |
viarequired | string enum | Why 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.
| Field | Type | Description |
|---|---|---|
amountsrequired | array of Money | |
explicitrequired | integer | |
grainrequired | string enum | One of resource, tag. |
overlaprequired | integer | |
sourcerequired | string enum | One of cur, ce, legacy, mixed, none. |
DeclaredAppTrendMonth
| Field | Type | Description |
|---|---|---|
moneyrequired | Declared | An app's money for the period, at one grain, named
(platform/ §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 overlap says how
many members hit more than one match (their money may be counted
twice) while explicit 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. |
monthrequired | string (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.
| Field | Type | Description |
|---|---|---|
kindrequired | string enum | One of added, removed, changed. |
new | object | |
old | object | |
pathrequired | string | |
redacted | boolean |
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.
| Field | Type | Description |
|---|---|---|
accounts | array of Account | |
createdrequired | integer | Newly registered, all with enabled = false. |
foundrequired | integer | Accounts returned by Organizations. |
unchangedrequired | integer | Already known and identical. |
updatedrequired | integer | Already 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).
| Field | Type | Description |
|---|---|---|
expectedrequired | integer | The stamped resources drift compares, resource.
— not resource., which is everything the create
stamped. |
gaprequired | integer | expected, always positive here. |
last | string (date-time) | |
observedrequired | integer | How many of those the inventory still sees: live rows in that
account, of the compared kinds, whose tags carry
panorama: 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. |
resourcerequired | Provisioned | 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
create, destroy, state,
provenance, overridden, expected, 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
create, provenance, expected and status. |
DriftMissingPage
| Field | Type | Description |
|---|---|---|
itemsrequired | array of Drift | |
pagerequired | integer | |
pageSizerequired | integer | |
totalrequired | integer (int64) | |
totalPagesrequired | integer |
DriftNotCompared
| Field | Type | Description |
|---|---|---|
countrequired | integer | |
reasonrequired | string enum | not_ — no registered collector emits the type.
tags_ — 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_, tags_. |
typerequired | string | The OpenTofu resource type, as the agent reported it (aws_). |
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.
| Field | Type | Description |
|---|---|---|
compared | integer | Judged 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. |
judgedrequired | boolean | False 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. |
notCompared | array of Drift | Judged 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.
| Field | Type | Description |
|---|---|---|
accountIdrequired | string | |
firstSeenAtrequired | string (date-time) | |
importId | string (uuid) | The live import that recorded it — only on a list asked with imported=true. |
lastSeenAtrequired | string (date-time) | |
regionrequired | string | |
resourceIdrequired | string | |
resource | string | |
resourcerequired | string | |
servicerequired | string | |
systemrequired | string |
DriftUnprovisionedPage
| Field | Type | Description |
|---|---|---|
itemsrequired | array of Drift | |
pagerequired | integer | |
pageSizerequired | integer | |
totalrequired | integer (int64) | |
totalPagesrequired | integer |
Entitlements
| Field | Type | Description |
|---|---|---|
capabilitiesrequired | array of string | Features the licence carries beside its modules: sso
(Enterprise) and performance (Professional and Enterprise);
later scim. Locked with the modules at expiry. |
enabledrequired | integer | |
licensee | string | Who the licence was issued to, when the row came from a licence. |
maxAccounts | integer | Absent means unlimited. |
modulesrequired | array of string | Unlocked modules, now. A tenant with no row has none — unless the
deployment runs with ENTITLEMENTS_state says unrestricted. |
staterequired | string enum | The 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. |
tier | string | The 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. |
validUntil | string (date-time) | Checked at read time, not by a scheduled job: an expired row behaves as unentitled the moment it is read. |
ErrorResponse
| Field | Type | Description |
|---|---|---|
errorrequired | object |
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.
| Field | Type | Description |
|---|---|---|
reasonrequired | string | Why 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. |
resourcesrequired | integer | Resources 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_ 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_. |
FieldDescriptor
| Field | Type | Description |
|---|---|---|
badgeMap | object | Raw value to badge tone, for format: badge. |
columns | array of Field | Column descriptors, for format: table. |
formatrequired | 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. One of text, number, bytes, datetime, relative, boolean, badge, list, table, json, link. |
keyrequired | string | A path into the resource, including additional prefixes. |
labelrequired | string | |
unit | string | |
volatile | boolean | Present 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
| Field | Type | Description |
|---|---|---|
fieldsrequired | array of Field | |
titlerequired | string |
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.
| Field | Type | Description |
|---|---|---|
resourcesrequired | integer | Resources 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_. |
FinalizeResponse
| Field | Type | Description |
|---|---|---|
already | boolean | True when this call replayed an earlier finalize. |
runIdrequired | string (uuid) | |
tombstonedrequired | integer (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.
| Field | Type | Description |
|---|---|---|
accountId | string | |
apps | array of FindingApp | The 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. |
changedrequired | boolean | A live exception exists, but the finding changed materially since it was taken. |
exception | Finding | |
faults | array of TagFault | A 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. |
firstSeenrequired | string (date-time) | |
label | string | What the source calls it when the scope alone does not say — a check's name, a create's name, a bucket. |
owner | 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. |
priorityrequired | Finding | |
scoperequired | string | |
sourcerequired | Finding | Where a finding comes from, each read from its own table:
cost_, unknown_, trusted_, cost_, drift_, drift_, stale_, tag_policy, action_, action_. |
sourceIdrequired | string |
FindingApp
A declared app a finding's resource is in.
| Field | Type | Description |
|---|---|---|
idrequired | string (uuid) | |
namerequired | string |
FindingCost
The source's figure, in its own currency.
| Field | Type | Description |
|---|---|---|
amountrequired | string | A decimal string, never a float. |
currencyrequired | string |
FindingException
| Field | Type | Description |
|---|---|---|
createdAtrequired | string (date-time) | |
createdByrequired | string (uuid) | |
expiresAtrequired | string (date-time) | |
idrequired | string (uuid) | |
reasonrequired | string | |
revokedAt | string (date-time) | |
revokedBy | string (uuid) | |
scoperequired | string | |
sourcerequired | Finding | Where a finding comes from, each read from its own table:
cost_, unknown_, trusted_, cost_, drift_, drift_, stale_, tag_policy, action_, action_. |
sourceIdrequired | string |
FindingExceptionCreate
| Field | Type | Description |
|---|---|---|
expiresAtrequired | string (date-time) | |
reasonrequired | string | |
scoperequired | string | |
sourcerequired | Finding | Where a finding comes from, each read from its own table:
cost_, unknown_, trusted_, cost_, drift_, drift_, stale_, tag_policy, action_, action_. |
sourceIdrequired | string |
FindingGroup
FindingList
| Field | Type | Description |
|---|---|---|
exceptedrequired | array of Finding | Findings hidden by a live exception, until it expires or is revoked. |
groupsrequired | array of Finding | By owning team, teams by name; the group without owner (unowned) last. |
FindingPriority
| Field | Type | Description |
|---|---|---|
cost | FindingCost | The source's figure, in its own currency. |
exposurerequired | string enum | From the collectors' posture fields; unknown when they say nothing. One of public, not_public, unknown. |
importancerequired | string enum | in_path when a live load balancer targets the resource; else carries_ 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_, unknown. |
FindingSettings
| Field | Type | Description |
|---|---|---|
isDefaultrequired | boolean | |
maxrequired | integer | |
updatedAt | string (date-time) | |
updatedBy | string (uuid) |
FindingSettingsUpdate
| Field | Type | Description |
|---|---|---|
maxrequired | integer |
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);sourceIdis<account>/<check>/<resource>, scope the resource where the check names one the inventory maps, else the account; the figure is AWS's estimate andlabelthe 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;sourceIdthe provisioned resource, scope its team,labelits 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,labelthe 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,faultswhat 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 isunverified: its window passed and what a complete collection saw of its target was never the state the action reaches;sourceIdthe request, scope the team that asked (the request's own scope),labelthe request's name,accountIdthe 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 ofnot_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.
| Field | Type | Description |
|---|---|---|
completerequired | boolean | Every step here is done. The Dashboard stops carrying the checklist then. |
stepsrequired | array of First |
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.
| Field | Type | Description |
|---|---|---|
checklist | boolean | true dismisses the checklist: the server records its own time,
and a dismissal already recorded keeps its time. false brings the
checklist back. |
tour | integer | The 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.
| Field | Type | Description | ||||||||||||||||||||||||||||||||||||||||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
donerequired | boolean | |||||||||||||||||||||||||||||||||||||||||||||||||
doneAt | string (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_) — 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. | ||||||||||||||||||||||||||||||||||||||||||||||||
idrequired | string enum | The 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.
connect_, first_read, invite_, create_team, alert_, set_budget, install_, enable_, first_, join_team, confirm_. | ||||||||||||||||||||||||||||||||||||||||||||||||
linkrequired | string | The 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.
| Field | Type | Description |
|---|---|---|
categoryrequired | Template | What a template creates, in the one taxonomy (MILESTONE_Resource 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_. |
description | string | |
namerequired | string | |
slugrequired | string | The 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
| Field | Type | Description |
|---|---|---|
statusrequired | string |
IdentityProvider
| Field | Type | Description |
|---|---|---|
boundrequired | integer | Accounts bound to a provider's subject. While any is bound to another issuer, saving this one is refused. |
clientIdrequired | string | |
clientrequired | boolean | The secret is stored and never returned. |
defaultRolerequired | string enum | One of viewer, engineer, admin. |
emailrequired | array of string | |
enforcedrequired | boolean | |
groupsClaimrequired | string | The ID-token claim the person's groups are read from at sign-in; groups unless named. |
issuerrequired | string | |
redirectUrirequired | string | The callback address to register at the provider, for this host. |
updatedAtrequired | string (date-time) |
IdentityProviderInput
| Field | Type | Description |
|---|---|---|
clientIdrequired | string | |
client | string | Required the first time; absent keeps the stored one. |
defaultRolerequired | string enum | One of viewer, engineer, admin. |
emailrequired | array of string | |
enforcedrequired | boolean | |
groupsClaim | string | The claim the groups are read from; empty or absent keeps groups. |
issuerrequired | string |
Impact
| Field | Type | Description |
|---|---|---|
anchorrequired | object | |
limitsrequired | object | Retention and resolution, per panel. |
methodrequired | string | The page's one sentence of method; correlation in time is not a proven cause. |
nodesrequired | array of ImpactNode | The anchor first (depth 0), then the affected neighbours, then the rest. |
topologyrequired | object | |
windowrequired | object |
ImpactNode
| Field | Type | Description |
|---|---|---|
affectedrequired | boolean | A transition or a deviation inside the window — changed in the window, nothing more. |
costDaysrequired | object | |
depthrequired | integer | |
performancerequired | object | |
reasonsrequired | array of string | |
relationsrequired | array of object | |
resourcerequired | Impact | A resource as the impact page names it, the same fields the neighbours read gives. |
statusrequired | object |
ImpactResource
A resource as the impact page names it, the same fields the neighbours read gives.
| Field | Type | Description |
|---|---|---|
accountIdrequired | string | |
namerequired | string | |
regionrequired | string | |
resourceIdrequired | string | |
resourcerequired | string | |
servicerequired | string | |
statusrequired | string | |
systemrequired | string |
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.
| Field | Type | Description |
|---|---|---|
addressesrequired | array of object | |
prefill | array of object | |
resourcerequired | string |
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.
| Field | Type | Description |
|---|---|---|
agentReadyrequired | boolean | An 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. |
agentrequired | string | The first agent version that generates a template. |
gapsrequired | array of string | What the companion table does not cover for this type, in a reader's words. |
importsrequired | array of AgentImport | The 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.
| Field | Type | Description |
|---|---|---|
importsrequired | array of AgentImport | |
variablesrequired | object |
InboxEntry
| Field | Type | Description |
|---|---|---|
createdAtrequired | string (date-time) | |
headerrequired | string | |
idrequired | integer (int64) | |
kindrequired | string | The notification's kind, as a webhook receives it — estate., cost., provisioning.. |
linesrequired | array of string | |
readAt | string (date-time) | When the person marked it read; absent while unread. |
InboxPage
| Field | Type | Description |
|---|---|---|
itemsrequired | array of InboxEntry | |
morerequired | boolean | Whether older entries follow this page. |
unreadrequired | integer | How many of the person's entries are unread, across every page. |
InboxRead
| Field | Type | Description |
|---|---|---|
all | boolean | Every entry of the person's, whatever ids says. |
ids | array of integer (int64) |
InboxUnread
| Field | Type | Description |
|---|---|---|
unreadrequired | integer |
Investigation
The investigator's answer for one symptom.
| Field | Type | Description |
|---|---|---|
candidatesrequired | array of Investigation | |
methodrequired | string | The page's one sentence of method; a candidate is correlated in time, not a proven cause. |
missingrequired | array of object | What the investigation lacks, each a row. |
symptomrequired | object | |
topologyrequired | object | How the neighbours were read, as the impact page says it. |
windowrequired | object |
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.
| Field | Type | Description |
|---|---|---|
actor | 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. |
atrequired | string (date-time) | |
attributedrequired | boolean | A person is recorded behind the change. |
confidencerequired | string | In words: how the candidate is tied to the symptom's resource. |
evidencerequired | array of object | Every 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 Change. |
idrequired | string | |
kindrequired | ChangeKind | One source's kind of event in the change feed. One of config_, status_, acknowledgement, provisioning_, provisioning_, provisioning_, cost_, budget_, external_, ownership_, ai_review, provisioning_, instance_. |
leadrequired | integer | |
rankrequired | integer | |
scorerequired | number | |
summaryrequired | string | The feed row's line. Never why. |
whenrequired | string | In words: "4 minutes before". |
whererequired | object | Where the change happened relative to the symptom's resource. |
wouldRaiserequired | array 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.
| Field | Type | Description |
|---|---|---|
enabled | Blueprint | A catalog object whose versions are graphs of template steps
(MILESTONE_ |
graphrequired | object | |
readmerequired | string | The Blueprint's README, Markdown. |
sha256required | string | Its identity, over its three files. |
slugrequired | string | |
summaryrequired | string | |
templatesrequired | array of Library | The library templates its steps pin, each once, in step order. |
titlerequired | string | |
update | boolean | Present with enabled: the enabled Blueprint is not archived and
its latest version was not published from this library
Blueprint's sha256 — update
publishes it as the next version. |
LibraryBlueprintPage
| Field | Type | Description |
|---|---|---|
itemsrequired | array of Library | |
pagerequired | integer | |
pageSizerequired | integer | |
totalrequired | integer (int64) | |
totalPagesrequired | integer |
LibraryBlueprintPin
| Field | Type | Description |
|---|---|---|
sha256required | string | |
slugrequired | string |
LibraryClone
| Field | Type | Description |
|---|---|---|
name | string | Its name, when not the library's title. |
slugrequired | string | The 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
| Field | Type | Description |
|---|---|---|
slug | string | The 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.
| Field | Type | Description |
|---|---|---|
agentrequired | string | The first agent version carrying release, or empty when this
platform's library no longer names the release. |
archivedAt | string (date-time) | |
releaserequired | integer | |
runnablerequired | array of string | The 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. |
sha256required | string | |
templateIdrequired | string (uuid) | |
templaterequired | string | |
versionrequired | integer |
LibraryFileDiff
| Field | Type | Description |
|---|---|---|
pathrequired | string | |
unifiedrequired | string | A 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.
| Field | Type | Description |
|---|---|---|
agentrequired | string | The first agent version that carries release: what an account's
agent needs to run this version. |
categoryrequired | Template | What a template creates, in the one taxonomy (MILESTONE_Resource 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_. |
cloudrequired | Template | The cloud a template makes its resources in, as the account registry
names it (Account.): a library template's (MILESTONE_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. |
costNoterequired | string | |
enabled | Library | The tenant's template enabled from this library slug, and its latest version — which may pin an older hash than the library's. |
iamrequired | array of string | The least-privilege statements the agent's role needs (§8.2.7). |
importablerequired | boolean | Whether the template declares how an existing resource is brought
under it — an import in its meta.json (MILESTONE_ |
provenancerequired | Library | How a library template's resources carry the provenance stamp: the
provider's default tags on AWS, labels on Google (MILESTONE_tags, labels, none. |
providersrequired | object | Provider name to version constraint. |
releaserequired | integer | The first library release that carried sha256. |
resourcerequired | array of string | What it creates, <cloud>.<service>.<type>. |
retiredrequired | boolean | The library no longer maintains it. An enabled one stays enabled and runnable; it is not enabled anew. |
retiredIn | integer | The release that retired it. |
runnablerequired | array of string | The 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. |
sha256required | string | The version the library now carries — its identity. An update
names it as the publish's library. |
slugrequired | string | |
summaryrequired | string | |
titlerequired | string | |
update | boolean | An 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
| Field | Type | Description |
|---|---|---|
agentrequired | string | The first agent version that carries release: what an account's
agent needs to run this version. |
categoryrequired | Template | What a template creates, in the one taxonomy (MILESTONE_Resource 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_. |
cloudrequired | Template | The cloud a template makes its resources in, as the account registry
names it (Account.): a library template's (MILESTONE_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. |
costNoterequired | string | |
diff | array of Library | For an administrator, when update: 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). |
enabled | Library | The tenant's template enabled from this library slug, and its latest version — which may pin an older hash than the library's. |
iamrequired | array of string | The least-privilege statements the agent's role needs (§8.2.7). |
importablerequired | boolean | Whether the template declares how an existing resource is brought
under it — an import in its meta.json (MILESTONE_ |
provenancerequired | Library | How a library template's resources carry the provenance stamp: the
provider's default tags on AWS, labels on Google (MILESTONE_tags, labels, none. |
providersrequired | object | Provider name to version constraint. |
readmerequired | string | The template's README, Markdown. |
releaserequired | integer | The first library release that carried sha256. |
resourcerequired | array of string | What it creates, <cloud>.<service>.<type>. |
retiredrequired | boolean | The library no longer maintains it. An enabled one stays enabled and runnable; it is not enabled anew. |
retiredIn | integer | The release that retired it. |
runnablerequired | array of string | The 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. |
sha256required | string | The version the library now carries — its identity. An update
names it as the publish's library. |
slugrequired | string | |
sourcerequired | string | The library's main.tf for sha256. |
summaryrequired | string | |
titlerequired | string | |
update | boolean | An 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. |
variablesrequired | Variables | Stored on the version and immutable with it. The portal generates the
request form from this; the agent uses it to write
generated. as proper HCL through an encoder, never by
string concatenation, because hand-rolled escaping is how tfvars
injection happens. |
LibraryTemplatePage
| Field | Type | Description |
|---|---|---|
itemsrequired | array of Library | |
pagerequired | integer | |
pageSizerequired | integer | |
releaserequired | integer | The newest library release this platform carries; 0 before the first. |
totalrequired | integer (int64) | |
totalPagesrequired | integer |
LoginRequest
| Field | Type | Description |
|---|---|---|
emailrequired | string | The 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. |
passwordrequired | string (password) | Bounded because verifying it is an argon2 derivation over whatever was sent, and this endpoint is unauthenticated. |
tenantSlug | string | Optional. 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.. Matched case-insensitively. |
MarkerCredential
A marker credential, everything but the token.
| Field | Type | Description |
|---|---|---|
accountIdsrequired | array of string | The accounts it may mark; empty is any. |
appIdsrequired | array of string (uuid) | The declared apps it may mark; empty is any. |
createdAtrequired | string (date-time) | |
idrequired | string (uuid) | |
lastUsedAt | string (date-time) | |
namerequired | string | |
revokedAt | string (date-time) |
MarkerCredentialCreate
| Field | Type | Description |
|---|---|---|
accountIds | array of string | |
appIds | array of string (uuid) | |
namerequired | string | The 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_, "name: must not be blank". |
MarkerCredentialIssued
| Field | Type | Description |
|---|---|---|
credentialrequired | Marker | A marker credential, everything but the token. |
tokenrequired | string (password) | Returned once and never again: only sha256(token) is stored. |
MarkerCredentialList
| Field | Type | Description |
|---|---|---|
itemsrequired | array of Marker |
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.
| Field | Type | Description |
|---|---|---|
accountIdrequired | string | |
durationMs | integer (int64) | |
error | string | |
lastRunAt | string (date-time) | |
modulerequired | string | |
regionrequired | string | |
resources | integer (int64) | |
resources | integer (int64) | |
statusrequired | string enum | slow 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.
| Field | Type | Description |
|---|---|---|
amountrequired | string | |
currencyrequired | string |
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.
| Field | Type | Description |
|---|---|---|
amountrequired | string | Current minus previous. |
currencyrequired | string | |
pct | number | Percentage change, when the previous period is non-zero. |
NeighbourEdge
| Field | Type | Description |
|---|---|---|
categoryrequired | string enum | Impact reads only the operational edges. One of structural, operational. |
confidencerequired | string enum | One of observed, declared, inferred. |
evidencerequired | object | What the edge rests on: the collector field, the tag, the request, the app's rule, the resolver's candidate. |
fromrequired | string | |
kindrequired | Neighbour | One kind of relationship. One of contains, runs_on, fronts, guards, provisioned_, belongs_to, owned_by. |
torequired | string |
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.
| Field | Type | Description |
|---|---|---|
depthrequired | integer | Edges from the root: 0 the root, 1 a neighbour, 2 a neighbour's neighbour. |
detailrequired | object | The node's own fields: a request's state, a template version's source identity, an app's or team's slug. |
idrequired | string | |
kindrequired | string enum | One of resource, request, template_, app, team. |
labelrequired | string | |
resource | object | The resource behind a resource node. |
NotReadyStatus
| Field | Type | Description |
|---|---|---|
reasonrequired | string | Names the cause without naming a credential, host or database role. |
statusrequired | string |
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.
| Field | Type | Description |
|---|---|---|
createdAtrequired | string (date-time) | |
enabledrequired | boolean | |
idrequired | string (uuid) | |
kindrequired | Notification | 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. One of slack, webhook, ai-reviewer, teams, pagerduty, email. |
lastTest | Notification | The most recent test delivery, if any since the last rotation. |
namerequired | string | |
targetrequired | string | sha256 of the target, hex. Two channels with the same fingerprint point at the same place. |
targetHintrequired | string | The last few characters of the destination's path, for recognition; empty for a row that predates the surface. |
targetHostrequired | string | The destination's host. |
teamId | string (uuid) | The team whose events this channel receives; absent for the tenant default. |
teamName | string | |
updatedAtrequired | string (date-time) |
NotificationChannelCreate
| Field | Type | Description |
|---|---|---|
enabled | boolean | |
kindrequired | Notification | 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. One of slack, webhook, ai-reviewer, teams, pagerduty, email. |
namerequired | string | |
targetrequired | string | An https URL this deployment will post to. Never read back. |
teamId | string (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
| Field | Type | Description |
|---|---|---|
itemsrequired | array of Notification |
NotificationChannelTest
| Field | Type | Description |
|---|---|---|
atrequired | string (date-time) | |
errorClass | string enum | Why it did not deliver, without the destination's words or its URL. One of dns, tls, timeout, network, redirect, private_, invalid_, refused. |
httpStatus | integer | The destination's status code, when it answered. |
outcomerequired | string enum | One of delivered, refused, unreachable, blocked. |
NotificationChannelUpdate
| Field | Type | Description |
|---|---|---|
enabled | boolean | |
name | string | |
target | string | A new target — the rotation. Checked like a new one; the last test is cleared. |
teamId | string (uuid) | Move the channel to a team. |
tenant | boolean | true takes the channel out of its team and makes it a tenant default. |
NotificationChoices
What reaches the person's own inbox.
| Field | Type | Description |
|---|---|---|
alertsrequired | boolean | Status 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. |
anomaliesrequired | boolean | Cost anomalies on the person's teams and their teams' accounts; every one, for an administrator. |
budgetsrequired | boolean | Budget thresholds of the person's teams; every team's, for an administrator. |
digestrequired | boolean | The 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. |
myRequestsrequired | boolean | Their own provisioning requests — approvals, successes and failures. |
OnboardingStep
| Field | Type | Description |
|---|---|---|
command | string | The exact command, where the step has one. |
detailrequired | string | |
titlerequired | string |
OrgUser
| Field | Type | Description |
|---|---|---|
breakGlass | boolean | An administrator who keeps a password under enforced single sign-on. |
createdAtrequired | string (date-time) | |
disabledAt | string (date-time) | When the account was deactivated; absent while it is active. A deactivated user cannot log in and holds no session. |
displayName | string | The name the person gave themselves; absent while they gave none. |
emailrequired | string | |
idrequired | string (uuid) | |
rolerequired | string enum | The platform role. Says nothing about teams. One of viewer, engineer, admin. |
teamsrequired | array of Team |
OrgUserCreate
| Field | Type | Description |
|---|---|---|
emailrequired | string | |
rolerequired | string enum | One of viewer, engineer, admin. |
OrgUserCreated
| Field | Type | Description |
|---|---|---|
temporaryrequired | string | Shown once. Not stored in plaintext, not logged, not shown again: a lost one is replaced with a password reset. |
userrequired | OrgUser |
OrgUserList
| Field | Type | Description |
|---|---|---|
itemsrequired | array of OrgUser |
OrgUserUpdate
| Field | Type | Description |
|---|---|---|
breakGlass | boolean | An administrator who keeps a password when the tenant enforces single sign-on. Only an administrator can be one. |
disabled | boolean | true deactivates the account; false reactivates it. |
role | string enum | One of viewer, engineer, admin. |
OwnAPITokenList
| Field | Type | Description |
|---|---|---|
itemsrequired | array of APIToken | |
maxrequired | integer | The longest life, in days, the organisation allows a new read token. |
OwnSession
| Field | Type | Description |
|---|---|---|
address | string | The address the session began from, when it was recorded. |
createdAtrequired | string (date-time) | |
currentrequired | boolean | Whether this is the session making the request. |
expiresAtrequired | string (date-time) | |
idrequired | string | |
lastSeenAtrequired | string (date-time) | |
userAgent | string | The browser the session began in, when it was recorded. |
OwnSessionList
| Field | Type | Description |
|---|---|---|
itemsrequired | array of OwnSession |
OwnTeam
| Field | Type | Description |
|---|---|---|
idrequired | string (uuid) | |
namerequired | string | |
rolerequired | string enum | The person's role in the team. One of member, lead. |
slugrequired | string |
OwnTeamList
| Field | Type | Description |
|---|---|---|
itemsrequired | array 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.
| Field | Type | Description |
|---|---|---|
candidatesrequired | array of Ownership | |
confidencerequired | string enum | One of explicit, derived, inferred, unknown. |
evidence | object | |
reason | string | |
sourcerequired | string enum | One of override, import, provenance, tag, account, relationship, none. |
staterequired | string enum | One of owned, unowned, conflict. |
team | 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. |
triedrequired | array of string enum |
OwnershipCandidate
| Field | Type | Description |
|---|---|---|
archived | boolean | |
confidencerequired | string enum | One of explicit, derived, inferred. |
evidence | object | |
rankrequired | integer | |
sourcerequired | string enum | One of override, import, provenance, tag, account, relationship. |
teamrequired | 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. |
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.
| Field | Type | Description |
|---|---|---|
accountId | string | |
createdAtrequired | string (date-time) | |
createdByrequired | string (uuid) | |
expired | boolean | Past its expiry: listed, but no longer applying. |
expiresAt | string (date-time) | |
idrequired | string (uuid) | |
kindrequired | string enum | One of resource, tag, account. |
reasonrequired | string | |
resourceKey | string | region/, for a resource override. |
revokedAt | string (date-time) | |
revokedBy | string (uuid) | |
tagKey | string | |
tagValue | string | |
teamrequired | 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. |
OwnershipOverrideInput
| Field | Type | Description |
|---|---|---|
accountId | string | |
expiresAt | string (date-time) | |
kindrequired | string enum | One of resource, tag, account. |
reasonrequired | string | |
resourceKey | string | |
tagKey | string | |
tagValue | string | |
teamIdrequired | string (uuid) |
OwnershipOverrideList
| Field | Type | Description |
|---|---|---|
itemsrequired | array of Ownership |
PageInfo
| Field | Type | Description |
|---|---|---|
pagerequired | integer | |
pageSizerequired | integer | |
totalrequired | integer (int64) | |
totalPagesrequired | integer |
PasswordChange
| Field | Type | Description |
|---|---|---|
currentrequired | string | |
newPasswordrequired | string |
PasswordResetIssued
| Field | Type | Description |
|---|---|---|
expiresAtrequired | string (date-time) | |
tokenrequired | string | Shown once; only its hash is stored. |
PasswordResetRedeem
| Field | Type | Description |
|---|---|---|
newPasswordrequired | string | |
tokenrequired | string |
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.
| Field | Type | Description |
|---|---|---|
errors | string | |
latency | string | |
saturation | string |
PerformanceHotResource
| Field | Type | Description |
|---|---|---|
accountIdrequired | string | |
latestAtrequired | string (date-time) | Its newest stored point in the window. |
pressure | number (double) | value over the group's capacity, 0 to 1 (a maximum can touch
1); present only where the signal declares a capacity. |
regionrequired | string | |
resourceIdrequired | string | |
resourcerequired | string | |
resourcerequired | string | |
servicerequired | string | |
systemrequired | string | |
typeLabel | string | Its type's plural label, as Performance. |
valuerequired | number (double) | The raw measurement, in the group's unit and statistic. |
PerformanceHottest
| Field | Type | Description |
|---|---|---|
fromrequired | string (date-time) | |
groupsrequired | array of Performance | One per category and defining signal with a leader, in category, kind and signal order. No order across them. |
torequired | string (date-time) | |
windowrequired | Performance | 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. 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.
| Field | Type | Description |
|---|---|---|
capacity | number (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. |
categoryrequired | Performance | Estate's Performance sections; the catalogue places each resource type in one. One of compute, networking, databases, messaging, serverless, storage. |
familyrequired | string | The signal's family, as Performance. |
kindrequired | Performance | What a series stands for. One of latency, errors, saturation. |
leadersrequired | array of Performance | |
metricrequired | string | The cloud's name for the metric, as a chart labels it. |
signalrequired | string | The defining signal's catalogue name (ec2.cpu), as a series carries it. |
statisticrequired | Performance | What a group's values are (MILESTONE_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. |
typeLabelsrequired | array of string | The plural labels of the types among the leaders, from their render descriptors (the apart where the catalogue gives one). |
unitrequired | string | The 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.
| Field | Type | Description |
|---|---|---|
clusterrequired | string | The cluster's name; empty for an instance only a Karpenter node pool tag places in Kubernetes. |
instanceId | string | A node's EC2 instance. |
namespace | string | A pod's namespace. |
node | string | A pod's node. |
nodeGroup | string | The managed node group, when one is known. |
nodePool | string | The Karpenter node pool, when one is known. |
rolerequired | string enum | What 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. |
rule | string enum | An EC2 instance's: which part of the rule placed it — its Auto
Scaling group is a node group's, or it carries eks:,
kubernetes. or karpenter.. One of node, eks, kubernetes, karpenter. |
workload | string | A 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.
| Field | Type | Description |
|---|---|---|
atrequired | string (date-time) | |
metricrequired | string | |
signalrequired | string | |
unitrequired | string | |
valuerequired | number (double) |
PerformancePodWindow
A pod's own series over a window, read from CloudWatch on demand.
| Field | Type | Description |
|---|---|---|
containersrequired | integer | The pod's containers past the first eight, whose series the read left out. |
fromrequired | string (date-time) | |
readAtrequired | string (date-time) | When CloudWatch was read for these series — now, or within the step before, whose answer is kept. |
resourcerequired | Performance | |
stepSecondsrequired | integer | |
torequired | string (date-time) | |
windowrequired | Performance | 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. One of 1h, 24h, 7d, 30d. |
PerformancePoint
| Field | Type | Description |
|---|---|---|
atrequired | string (date-time) | The start of the step the value covers. |
max | number (double) | The highest stored five-minute value in the step, where the step combines more than one. |
min | number (double) | The lowest stored five-minute value in the step, where the step combines more than one. |
valuerequired | number (double) |
PerformanceResourceSeries
| Field | Type | Description |
|---|---|---|
absentKindsrequired | array of Performance | The kinds this resource's type publishes no metric for; the view says so rather than showing another metric. |
absent | Performance | 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. |
accountIdrequired | string | |
hasTopology | boolean | Set when / draws this resource's parts: an EKS cluster's, a load balancer's. The portal links to
the drawing by it, naming no service. |
kubernetes | Performance | 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. |
regionrequired | string | |
resourceIdrequired | string | |
resourcerequired | string | |
resourcerequired | string | |
section | Performance | Estate's Performance sections; the catalogue places each resource type in one. One of compute, networking, databases, messaging, serverless, storage. |
seriesrequired | array of Performance | |
servicerequired | string | |
systemrequired | string | |
typeLabel | string | The type's plural label, from its render descriptor (the same as
/ 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
| Field | Type | Description |
|---|---|---|
fromrequired | string (date-time) | |
resourcerequired | Performance | |
stepSecondsrequired | integer | |
torequired | string (date-time) | |
windowrequired | Performance | 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. One of 1h, 24h, 7d, 30d. |
PerformanceSection
Estate's Performance sections; the catalogue places each resource type in one.
Type: string enum
PerformanceSectionPage
| Field | Type | Description |
|---|---|---|
fromrequired | string (date-time) | |
nextCursor | string | Present when more resources follow; pass it back as cursor. |
resourcesrequired | array of Performance | |
sectionrequired | Performance | Estate's Performance sections; the catalogue places each resource type in one. One of compute, networking, databases, messaging, serverless, storage. |
stepSecondsrequired | integer | |
torequired | string (date-time) | |
windowrequired | Performance | 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. One of 1h, 24h, 7d, 30d. |
PerformanceSeries
| Field | Type | Description |
|---|---|---|
detail | string | Which 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. |
familyrequired | string | The series this one belongs with (MILESTONE_signal for an average, and the average's for the peak
beside it — ec2.cpu.max and elb. are
in the families ec2.cpu and elb.. 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. |
kindrequired | Performance | What a series stands for. One of latency, errors, saturation. |
latestAt | string (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. |
metricrequired | string | |
namespacerequired | string | |
periodrequired | integer | How often the cloud publishes this metric, and the period the
collector asks for (MILESTONE_ |
pointsrequired | array of Performance | One per step that has a stored point, oldest first. A step with none is left out, not zero. |
previous | array of Performance | With 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. |
signalrequired | string | The catalogue's name for the series, stable across releases (cpu, read_). |
statisticrequired | string | |
summary | Performance | 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. |
unitrequired | string | CloudPercent, 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.
| Field | Type | Description |
|---|---|---|
countrequired | integer | The five-minute values the summary is of. |
maxrequired | number (double) | |
meanrequired | number (double) | |
minrequired | number (double) | |
p95 | number (double) | Absent from the 30d window's summary, which the hourly rollup cannot give one. |
PerformanceSparkPoint
| Field | Type | Description |
|---|---|---|
atrequired | string (date-time) | |
valuerequired | number (double) |
PerformanceSparkline
| Field | Type | Description |
|---|---|---|
keyrequired | string | The row's key, as it was asked for. |
kindrequired | Performance | What a series stands for. One of latency, errors, saturation. |
metricrequired | string | |
pointsrequired | array of Performance | One per hour with a stored point, oldest first; an hour with none is left out, not zero. |
signalrequired | string | |
statisticrequired | string | The signal's own statistic, which each hour's point combines by. |
unitrequired | string |
PerformanceSparklines
| Field | Type | Description |
|---|---|---|
fromrequired | string (date-time) | |
itemsrequired | array of Performance | |
stepSecondsrequired | integer | |
torequired | string (date-time) |
PerformanceTopology
A resource's parts, for a drawing.
| Field | Type | Description |
|---|---|---|
loadSincerequired | string (date-time) | The oldest a stored point may be and still be a part's load; a part with none since has no load. |
rootrequired | Performance |
PerformanceTopologyPart
| Field | Type | Description |
|---|---|---|
childrenrequired | array of Performance | |
cutrequired | integer | How many of its children were left out past the caps. |
kindrequired | string enum | What 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, load, targetGroup, target. |
labelrequired | string | Its name — a resource's, a group's, or a target's id and port; empty for ungrouped and unscheduled. |
load | Performance | A part's newest stored value of its first saturation series. |
pods | integer | A 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. |
reason | string | Why a target is in its state, as the balancer gave it; or what could not be read of a balancer or a group. |
resource | Performance | The inventory resource a part is, for a link to its detail or its Performance. |
state | string | A target's health state, as the balancer gave it (healthy, unhealthy, draining, …); its status follows it. |
status | 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. One of failed, degraded, operational, unknown. |
PerformanceTopologyResource
The inventory resource a part is, for a link to its detail or its Performance.
| Field | Type | Description |
|---|---|---|
accountIdrequired | string | |
regionrequired | string | |
resourceIdrequired | string | |
resourcerequired | string | |
servicerequired | string | |
systemrequired | string |
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.
| Field | Type | Description |
|---|---|---|
dateFormat | string enum | iso 2026-09-15, dmy 15.09.2026, mdy 09/15/2026. One of iso, dmy, mdy. |
firstRun | Preferences | What the person has done with the first-run checklist and the tour
(MILESTONE_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
Profile (First). |
number | string enum | commaDot 1,234.5, dotComma 1.234,5, spaceComma 1 234,5, plain 1234.5. One of commaDot, dotComma, spaceComma, plain. |
preferences | string (date-time) | When the person first saved their preferences (MILESTONE_confirm_ 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. |
theme | string enum | One of system, light, dark. |
timeZone | string | An IANA time zone, such as Europe/. |
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).
| Field | Type | Description |
|---|---|---|
checklist | string (date-time) | When the person dismissed the checklist; the Dashboard no longer
carries it for them. Recorded with the server's time by
first, and kept through a dismissal
sent again; false brings the checklist back. |
tour | integer | The 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 first above
it moves it. |
ProfileUpdate
| Field | Type | Description |
|---|---|---|
displayName | string | |
firstRun | First | 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. |
preferences | Preferences | The 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.
| Field | Type | Description |
|---|---|---|
environmentrequired | string enum | One of production, staging, sdlc, shared, unknown. |
namerequired | string | |
projectIdrequired | string | The project id, not its number and not its display name. |
service | string | The 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.
| Field | Type | Description |
|---|---|---|
observedAtrequired | string (date-time) | |
presentrequired | boolean | |
runId | string (uuid) | The collection run that observed it, which finalized. |
snapshotId | integer (int64) | |
state | string | The inventory state observed; absent for a tombstone. |
transition | integer (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.
| Field | Type | Description |
|---|---|---|
actedAtrequired | string (date-time) | When the platform recorded the action's terminal report — the request's terminalAt. |
expectedrequired | string | The inventory state the action reaches. |
methodrequired | string | The rule that judged it, versioned. |
observation | Provision | 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. transition names a tombstone's deletion
transition, which is kept for ever. |
recordedAt | string (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. |
statusrequired | string enum | One of awaiting, verified, unverified, not_. |
windowrequired | string (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.
| Field | Type | Description |
|---|---|---|
actorrequired | 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. |
comment | string | |
decidedAtrequired | string (date-time) | |
decisionrequired | string enum | One of approved, rejected. |
selfrequired | boolean | The requester approved their own request, because nobody else
could. Always false on a rejection. |
ProvisionDecision
| Field | Type | Description |
|---|---|---|
binding | string | The request detail's binding 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_ 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. |
comment | string | Recorded permanently and shown to the requester. Optional on approve; a rejection without one is technically allowed and practically unkind. |
decisionrequired | string 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.
| Field | Type | Description | ||||
|---|---|---|---|---|---|---|
failure | Provision | Why a request failed, as the platform records it: the six classes an
agent reports (Provision), and a seventh that only the
platform writes (PROVISIONING_ §7.4, amended 2026-09-12).
restored, or anything outside
Provision, is refused with 400. One of retryable, permanent, configuration, infrastructure, timeout, cancelled, restored. | ||||
requestIdrequired | string (uuid) | |||||
requestedAtrequired | string (date-time) | |||||
requestedByrequired | 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. | ||||
staterequired | Provision | PROVISIONING_ §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_, rejected, expired, withdrawn, queued, running, cancelling, cancelled, succeeded, failed. | ||||
templaterequired | integer | |||||
terminalAt | string (date-time) |
ProvisionRequest
| Field | Type | Description | ||||
|---|---|---|---|---|---|---|
actionrequired | Provision | What a request asks for, and PROVISIONING_ §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 /, and it is
the one action that skips pending_: approval governs
infrastructure, and resolving a ref creates none. adopt is Import's second step (MILESTONE_importId); the approval names both
steps. The agent protocol for it: Agent 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 /, never at
POST /; it leaves nothing behind — no
provisioned resource, never partial — and a failed one is
asked for again rather than retried. One of create, destroy, resolve, adopt, stop. | ||||
approval | Provision | 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. | ||||
attemptrequired | integer | The 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_, which is what stops an
abandoned task's process from resurrecting, from the instant it is
abandoned rather than from the next claim. | ||||
expiresAtrequired | string (date-time) | The pending_ deadline (D9). An unactioned request becomes
expired — terminal — after which the pinned version can safely
move on. A reminder fires at half the window. | ||||
failure | Provision | Why a request failed, as the platform records it: the six classes an
agent reports (Provision), and a seventh that only the
platform writes (PROVISIONING_ §7.4, amended 2026-09-12).
restored, or anything outside
Provision, is refused with 400. One of retryable, permanent, configuration, infrastructure, timeout, cancelled, restored. | ||||
failure | string | Scrubbed by the agent before it was ever sent. | ||||
idrequired | string (uuid) | |||||
namerequired | string | The requester's name for the thing. An action's is derived from
it and its target, stop-i-0abc…. | ||||
origin | Provision | 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. | ||||
partial | boolean | A 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. | ||||
release | boolean | Present and true on an adopt that releases its import from state
rather than taking it over (MILESTONE_ | ||||
requestedAtrequired | string (date-time) | |||||
requestedByrequired | 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. | ||||
resourceId | string (uuid) | The provisioned row this destroys. Present for
destroy and absent for create, both enforced. | ||||
source | string | The pinned version's gitSha, inline or library,
denormalised onto the request because it is what the approver is
actually approving and it must still read correctly after the
template is archived. | ||||
staterequired | Provision | PROVISIONING_ §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_, rejected, expired, withdrawn, queued, running, cancelling, cancelled, succeeded, failed. | ||||
state | string | Where 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. | ||||
target | Resource | The one inventory resource an action acts on. Present
exactly for an action — stop — and absent for the lifecycle,
both enforced. | ||||
targetrequired | string | |||||
teamId | string (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). | ||||
teamName | string | |||||
templateIdrequired | string (uuid) | |||||
template | string | |||||
templaterequired | string | |||||
templaterequired | integer | |||||
terminalAt | string (date-time) | |||||
variablesrequired | object | The 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
| Field | Type | Description |
|---|---|---|
actionrequired | Provision | create, destroy or adopt. resolve is refused here with
invalid_: 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 /, under
the licence's remediation capability. An adopt (MILESTONE_importId), and the template version and account it was recorded
under; its variables are validated as a create's (the pre-fill,
GET /, suggests them). Refused: an
import that is not recorded (409 request_); a
version that declares no import of the resource's type, or whose
identifiers the resource's inventory record cannot finish (400 invalid_); 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_, 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
(agent 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_, 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_, 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. is unplanned).
A type the platform has no generator for, or a record that lacks
what the configuration needs, is refused (400 invalid_)
with the reason, naming the attribute; nothing is stored. |
importId | string (uuid) | Required for adopt, rejected otherwise — the recorded import it makes fully managed. |
namerequired | string | The 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). |
resourceId | string (uuid) | Required for destroy, rejected for create. |
targetrequired | string | A 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. |
teamId | string (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. |
templateIdrequired | string (uuid) | |
templaterequired | integer | Explicit, never "latest". A request that pinned a moving target would be approved against one version and applied from another. |
variables | object | create 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_ naming variables; an empty object names none
and is accepted. |
ProvisionRequestDetail
| Field | Type | Description | ||||
|---|---|---|---|---|---|---|
actionrequired | Provision | What a request asks for, and PROVISIONING_ §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 /, and it is
the one action that skips pending_: approval governs
infrastructure, and resolving a ref creates none. adopt is Import's second step (MILESTONE_importId); the approval names both
steps. The agent protocol for it: Agent 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 /, never at
POST /; it leaves nothing behind — no
provisioned resource, never partial — and a failed one is
asked for again rather than retried. One of create, destroy, resolve, adopt, stop. | ||||
agent | array of string | A 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_, 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:
agent says when an upgrade is needed too. The
request waits, queued; absent while any agent of the account
can run it. | ||||
agent | string | A queued request no agent of its account can run (MILESTONE_ | ||||
approval | Provision | 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. | ||||
attemptrequired | integer | The 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_, which is what stops an
abandoned task's process from resurrecting, from the instant it is
abandoned rather than from the next claim. | ||||
binding | string | While pending_: 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 (Provision), so a request
that moved after it was read — a generated configuration
regenerated, above all — is refused rather than approved
unread. | ||||
destroy | Provisioned | destroy only. What will be removed: the resource, its
account, the number of tagged resources the create left behind
(expected), 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. | ||||
expiresAtrequired | string (date-time) | The pending_ deadline (D9). An unactioned request becomes
expired — terminal — after which the pinned version can safely
move on. A reminder fires at half the window. | ||||
failure | Provision | Why a request failed, as the platform records it: the six classes an
agent reports (Provision), and a seventh that only the
platform writes (PROVISIONING_ §7.4, amended 2026-09-12).
restored, or anything outside
Provision, is refused with 400. One of retryable, permanent, configuration, infrastructure, timeout, cancelled, restored. | ||||
failure | string | Scrubbed by the agent before it was ever sent. | ||||
generated | Provision | 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 (agent 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 /), 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; plan, once generated, is the
generate step's (or the re-plan's) plan of it. | ||||
idrequired | string (uuid) | |||||
importId | string (uuid) | adopt only (MILESTONE_release, releases from state. | ||||
imports | array of AgentImport | adopt 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. | ||||
may | boolean | true when the reader asked for this pending_
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. | ||||
namerequired | string | The requester's name for the thing. An action's is derived from
it and its target, stop-i-0abc…. | ||||
origin | Provision | 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. | ||||
outputs | array of Provision | A succeeded request whose version declares outputs
(MILESTONE_ | ||||
outputs | string | Present 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. | ||||
partial | boolean | A 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. | ||||
previous | integer | The 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. | ||||
recent | array of Provision | Newest first. | ||||
release | boolean | Present and true on an adopt that releases its import from state
rather than taking it over (MILESTONE_ | ||||
requestedAtrequired | string (date-time) | |||||
requestedByrequired | 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. | ||||
resourceId | string (uuid) | The provisioned row this destroys. Present for
destroy and absent for create, both enforced. | ||||
source | string | The pinned version's gitSha, inline or library,
denormalised onto the request because it is what the approver is
actually approving and it must still read correctly after the
template is archived. | ||||
staterequired | Provision | PROVISIONING_ §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_, rejected, expired, withdrawn, queued, running, cancelling, cancelled, succeeded, failed. | ||||
state | string | Where 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. | ||||
target | Resource | The one inventory resource an action acts on. Present
exactly for an action — stop — and absent for the lifecycle,
both enforced. | ||||
targetrequired | Account | Including its environment tag. "Production" is the single most decision-changing fact on an approval screen. | ||||
targetrequired | string | |||||
teamId | string (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). | ||||
teamName | string | |||||
templateIdrequired | string (uuid) | |||||
template | string | |||||
templaterequired | string | |||||
templaterequired | integer | |||||
terminalAt | string (date-time) | |||||
variablesrequired | object | The 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. | ||||
verification | Provision | 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
window has passed without one (unverified,
not_). 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 (window: 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_ —
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. | ||||
versionrequired | Template | The pinned version, including its source identity. inline
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 binding, 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 inline. A library version's
library and library 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 failure 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.
| Field | Type | Description |
|---|---|---|
droppedrequired | array of string | |
gapsrequired | array of string | |
phaserequired | string enum | One of generating, unplanned, generated. |
plan | Provision | 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. |
secret | Provision | 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. |
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.
| Field | Type | Description |
|---|---|---|
acceptedrequired | boolean | |
changesrequired | integer | How 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.
| Field | Type | Description |
|---|---|---|
cost | FindingCost | The finding's figure then, in its own currency — for Trusted Advisor, AWS's estimate. |
label | string | What the source called it, as the finding's label read then — a Trusted Advisor check's name. |
scoperequired | string | |
sourcerequired | Finding | Where a finding comes from, each read from its own table:
cost_, unknown_, trusted_, cost_, drift_, drift_, stale_, tag_policy, action_, action_. |
sourceIdrequired | string |
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.
| Field | Type | Description |
|---|---|---|
descriptionrequired | string | |
namerequired | string | |
staterequired | string enum | One of reported, withheld, missing. |
typerequired | string enum | One of string, number, boolean, list(string), map(string). |
value | Variable | One variable value, in one of the five types PROVISIONING_ §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
| Field | Type | Description |
|---|---|---|
itemsrequired | array of Provision | |
pagerequired | integer | |
pageSizerequired | integer | |
totalrequired | integer (int64) | |
totalPagesrequired | integer |
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.
| Field | Type | Description |
|---|---|---|
agentsrequired | array of Provision | |
attemptrequired | integer | |
neededrequired | array of Agent |
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.
| Field | Type | Description |
|---|---|---|
attemptrequired | integer | |
expiresAt | string (date-time) | |
neededrequired | array of Agent | |
sealedrequired | boolean |
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.
| Field | Type | Description |
|---|---|---|
blueprint | string (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. |
requestrequired | Provision | |
sourcesrequired | array of Provision | |
steppedUpAt | string (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. |
task | Provision | One attempt at one request, by one agent. |
verification | Provision | 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
window has passed without one (unverified,
not_). 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 (window: 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_ —
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
| Field | Type | Description |
|---|---|---|
details | array of object | One 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. |
errorrequired | object |
ProvisionSealedValue
| Field | Type | Description |
|---|---|---|
agentIdrequired | string (uuid) | |
sealedrequired | string | |
variablerequired | string |
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.
| Field | Type | Description |
|---|---|---|
attemptrequired | integer | |
valuesrequired | array of Provision |
ProvisionSealingAgent
| Field | Type | Description |
|---|---|---|
agentIdrequired | string (uuid) | |
namerequired | string | |
publicKeyrequired | Agent | The public half of the agent's sealing key (agent-protocol. §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.
| Field | Type | Description |
|---|---|---|
agentIdrequired | string (uuid) | |
attemptrequired | integer | |
claimedAtrequired | string (date-time) | |
finishedAt | string (date-time) | |
heartbeatAtrequired | string (date-time) | Falling behind heartbeat is what makes the scheduler
reap this task and return its request to queued. |
idrequired | string (uuid) | |
requestIdrequired | string (uuid) | |
stagerequired | string | Free 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. |
staterequired | Provision | 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. One of claimed, running, succeeded, failed, cancelled, abandoned. |
ProvisionTaskDetail
| Field | Type | Description |
|---|---|---|
agentIdrequired | string (uuid) | |
attemptrequired | integer | |
claimedAtrequired | string (date-time) | |
finishedAt | string (date-time) | |
heartbeatAtrequired | string (date-time) | Falling behind heartbeat is what makes the scheduler
reap this task and return its request to queued. |
idrequired | string (uuid) | |
requestIdrequired | string (uuid) | |
stagerequired | string | Free 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. |
staterequired | Provision | 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. One of claimed, running, succeeded, failed, cancelled, abandoned. |
summary | Provision | Written 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.
| Field | Type | Description |
|---|---|---|
createdAtrequired | string (date-time) | |
levelrequired | string enum | One of debug, info, warn, error. |
messagerequired | string | Already scrubbed by the agent when it was sent. |
seqrequired | integer (int64) | Agent-assigned and monotonic per task. Gaps mean the agent's own buffer dropped lines under pressure; they are not an error. |
stagerequired | string |
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.
| Field | Type | Description |
|---|---|---|
completionrequired | string enum | Whether 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, complete, incomplete, unverified. |
itemsrequired | array of Provision | |
nextCursorrequired | string | Always 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
| Field | Type | Description |
|---|---|---|
itemsrequired | array of Provision | |
pagerequired | integer | |
pageSizerequired | integer | |
totalrequired | integer (int64) | |
totalPagesrequired | integer |
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.
| Field | Type | Description | |||||||||||||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
agentrequired | string | ||||||||||||||||||||||
createdAtrequired | string (date-time) | ||||||||||||||||||||||
created | integer | ||||||||||||||||||||||
destroyed | integer | ||||||||||||||||||||||
failure | Provision | PROVISIONING_ §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 (Provision).
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. | |||||||||||||||||||||
failure | string | ||||||||||||||||||||||
log | integer (int64) | ||||||||||||||||||||||
outcomerequired | Provision | 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. One of claimed, running, succeeded, failed, cancelled, abandoned. | |||||||||||||||||||||
planDigest | string | sha256 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. | |||||||||||||||||||||
stage | object | Stage name to elapsed milliseconds. | |||||||||||||||||||||
tofuVersionrequired | string |
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.
| Field | Type | Description |
|---|---|---|
expiresAt | string (date-time) | |
hoprequired | string enum | One of request, approval, task, verification. |
idrequired | string | |
inspectablerequired | boolean | |
kindrequired | string enum | One of request, approval, decision_, blueprint_, task_, task_log, verdict, collection_, snapshot, resource_, transition. |
retentionrequired | string enum | One of permanent, limited, until_. |
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.
| Field | Type | Description |
|---|---|---|
accountIdrequired | string | |
create | string (uuid) | |
createdAtrequired | string (date-time) | |
destroy | string (uuid) | |
destroyedAt | string (date-time) | |
driftScope | DriftScope | What the drift diff compares of this resource. Present on
the resource list and on a drift finding, whenever
provenance is true; absent elsewhere, where it claims
nothing. |
expected | integer | Tagged resources found in state immediately after the apply —
everything the create stamped. The drift diff compares the part of
it the inventory can see, drift; 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. |
idrequired | string (uuid) | |
import | Resource | The import that recorded it, when origin is imported. |
managementrequired | Provisioned | 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. One of managed, recorded. |
namerequired | string | |
originrequired | Provisioned | Where 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. |
overridden | array of string | The 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. |
provenance | boolean | Whether the panorama: 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_, which
collides with the override. |
state | string | Where its Terraform state lives, as the agent reported it. |
status | Provisioned | 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 state, 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. |
teamIdrequired | string (uuid) | |
templateIdrequired | string (uuid) | |
template | string | |
templaterequired | integer |
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
| Field | Type | Description |
|---|---|---|
itemsrequired | array of Provisioned | |
pagerequired | integer | |
pageSizerequired | integer | |
totalrequired | integer (int64) | |
totalPagesrequired | integer |
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
| Field | Type | Description |
|---|---|---|
accountIdrequired | string | The 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. |
agent | string | |
createdAtrequired | string (date-time) | |
createdByrequired | 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. |
idrequired | string (uuid) | |
lastSeenAt | string (date-time) | The last call this token made. A stale value on an account whose requests never start is the answer to why. |
library | integer | The library release this agent last reported, on register or on a claim. Absent for an agent that runs no library templates. |
namerequired | string | |
revokedAt | string (date-time) | |
tofuVersion | string | |
versionrequired | Agent | 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. One of supported, outdated, unsupported. |
ProvisioningAgentInstall
| Field | Type | Description |
|---|---|---|
agentrequired | Provisioning | |
expiresAtrequired | string (date-time) | When the token in the link lapses if no agent has registered with it: an hour after issue. |
launchUrlrequired | string (password) | Returned once and never again. The AWS Cloudcloudpanorama-agent with the platform address, the pinned
image and the agent's token filled in. Open it signed in to that
account. |
ProvisioningAgentInstallCreate
| Field | Type | Description |
|---|---|---|
accountIdrequired | string | The AWS account the agent runs in: one of this tenant's accounts,
enabled, or 409 account_. AWS only: the link is a
Cloud |
dedicatedrequired | boolean | The 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_; recorded in the audit log.
A shared account uses the manual install with a policy the
customer scopes. |
namerequired | string | The agent's name, as for Provisioning. |
regionrequired | string | The AWS region the stack, and so the agent, runs in. |
ProvisioningAgentPage
| Field | Type | Description |
|---|---|---|
heartbeatrequired | integer | The server's agent_: 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. |
itemsrequired | array of Provisioning | |
pagerequired | integer | |
pageSizerequired | integer | |
totalrequired | integer (int64) | |
totalPagesrequired | integer |
ProvisioningAgentTokenCreate
| Field | Type | Description |
|---|---|---|
accountIdrequired | string | The 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 Account takes. It must be one of this
tenant's accounts and enabled, or the answer is 409 account_. |
namerequired | string | The 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_, "name: must not be blank". |
ProvisioningAgentTokenIssued
| Field | Type | Description |
|---|---|---|
agentrequired | Provisioning | |
tokenrequired | string (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.
| Field | Type | Description |
|---|---|---|
namerequired | string | |
servicesrequired | integer | |
servicesrequired | integer | |
servicesrequired | integer | |
servicesrequired | integer | Services with nothing confirmed lately: every resource in
them is stale, or unknown. Neither healthy nor
unhealthy, and not in services or services. |
statusrequired | Public | 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. One of failed, degraded, operational, unknown. |
unhealthy | string (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.
| Field | Type | Description |
|---|---|---|
demorequired | boolean | True when this status page describes a fabricated estate
(panorama admin seed-demo). The page says so on every load. |
environmentsrequired | array of Public | |
timelinerequired | array of Public | Aggregated. "3 instances became degraded", never a list of which. |
timelinerequired | boolean | True 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).
| Field | Type | Description |
|---|---|---|
demorequired | boolean | A demonstration tenant, as the status page marks it. |
tenantSlugrequired | string |
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.
| Field | Type | Description |
|---|---|---|
descriptionrequired | string | |
environmentrequired | string | |
occurredAtrequired | string (date-time) | |
servicerequired | string | |
statusrequired | 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. One of failed, degraded, operational, unknown. |
ReconciliationChargeType
| Field | Type | Description |
|---|---|---|
chargeTyperequired | string enum | One of usage, credit, refund, tax, fee, commitment, other. |
figuresrequired | array of Money | The same money as the places, cut again; not a constituent of the total. |
ReconciliationCoverage
| Field | Type | Description |
|---|---|---|
coveragerequired | string enum | One of in_, not_, stale, no_, account_, exempt, unknown. |
figuresrequired | array of Money | The same money as the places, cut again; not a constituent of the total. |
ReconciliationOwner
| Field | Type | Description |
|---|---|---|
resourcesrequired | integer | The row's billed resources this team owns. |
teamrequired | 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. |
ReconciliationPlace
| Field | Type | Description |
|---|---|---|
amountsrequired | array of Money | |
placerequired | string 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.
| Field | Type | Description |
|---|---|---|
accountIdrequired | string | |
apportionedrequired | boolean | The breakdowns were scaled to the row's total because the source rows sum to something else. |
chargeTypesrequired | object | Each charge type's amount, keyed by Reconciliation. |
coveragerequired | object | Each coverage value's amount, keyed by Reconciliation. |
currencyrequired | string | |
movesrequired | array of string enum | What would move the row's unhealthy values, each a kind the portal links to where it is done. |
placesrequired | object | Each place's amount, keyed by Reconciliation. |
servicerequired | string | |
totalrequired | string | |
untaggedrequired | array of Reconciliation | Where 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.
| Field | Type | Description |
|---|---|---|
accountrequired | 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. |
onboardingrequired | Account | 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
Test 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.
| Field | Type | Description |
|---|---|---|
generationrequired | integer (int64) | A run behind the current fence is superseded and may neither write nor finalize. |
regionrequired | string | |
resourcerequired | string | |
servicerequired | string | |
systemrequired | string |
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.
| Field | Type | Description |
|---|---|---|
accountIdrequired | string | |
cost | FindingCost | The finding's figure when the action was asked, in its own currency. |
label | string | What the source called it when the action was asked. |
requestrequired | Resolved | The request that resolved a finding, and the team that asked for it. |
scoperequired | string | |
sourcerequired | Finding | Where a finding comes from, each read from its own table:
cost_, unknown_, trusted_, cost_, drift_, drift_, stale_, tag_policy, action_, action_. |
sourceIdrequired | string | |
stillrequired | boolean | The 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. |
verificationrequired | Provision | 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
window has passed without one (unverified,
not_). 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 (window: 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_ —
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
| Field | Type | Description |
|---|---|---|
itemsrequired | array of Resolved | |
pagerequired | integer | |
pageSizerequired | integer | |
totalrequired | integer (int64) | |
totalPagesrequired | integer |
ResolvedFindingRequest
The request that resolved a finding, and the team that asked for it.
| Field | Type | Description |
|---|---|---|
actionrequired | Resource | An action on an existing resource — the closed set, one so
far. stop stops a running EC2 instance (aws.): its
template sets aws_ 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. |
idrequired | string (uuid) | |
namerequired | string | |
teamrequired | 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. |
Resource
| Field | Type | Description |
|---|---|---|
accountIdrequired | string | |
additional | object | Provider-specific detail. How each key is displayed comes from
/, not from the frontend. |
cloudrequired | string | The 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. |
collectedAt | string (date-time) | The bucket's last successful collection — when the resource was last confirmed. Absent when the bucket has never completed one. |
firstSeenAtrequired | string (date-time) | |
lastRunId | string (uuid) | The run that last observed this resource. |
lastSeenAtrequired | string (date-time) | |
ownership | 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_ |
regionrequired | string | global for global resources. |
resourceIdrequired | string | |
resourcerequired | string | |
resourcerequired | string | |
servicerequired | string | |
stalerequired | boolean | True 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_). 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. |
staterequired | string | The raw provider state the status was derived from. |
statusrequired | 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. One of failed, degraded, operational, unknown. |
systemrequired | string | The resource key's system (PLATFORM.k8s in its key and runs
in a cloud's account. What a person reads as the cloud is cloud. |
tags | object | The 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_ |
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.
| Field | Type | Description |
|---|---|---|
accountIdrequired | string | |
actionrequired | Resource | An action on an existing resource — the closed set, one so
far. stop stops a running EC2 instance (aws.): its
template sets aws_ 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. |
origin | Resource | 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_). |
regionrequired | string | |
resourceIdrequired | string | |
resourcerequired | string | |
servicerequired | string | |
systemrequired | string | |
templateIdrequired | string (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. |
templaterequired | integer | Explicit, 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.
| Field | Type | Description |
|---|---|---|
actionrequired | Resource | An action on an existing resource — the closed set, one so
far. stop stops a running EC2 instance (aws.): its
template sets aws_ 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. |
templateIdrequired | string (uuid) | |
templaterequired | string | |
templaterequired | integer |
ResourceActionOfferList
| Field | Type | Description |
|---|---|---|
itemsrequired | array of Resource |
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).
| Field | Type | Description |
|---|---|---|
scoperequired | string | |
sourcerequired | Finding | Where a finding comes from, each read from its own table:
cost_, unknown_, trusted_, cost_, drift_, drift_, stale_, tag_policy, action_, action_. |
sourceIdrequired | string |
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.
| Field | Type | Description |
|---|---|---|
accountIdrequired | string | |
regionrequired | string | |
resourceIdrequired | string | |
resourcerequired | string | |
servicerequired | string | |
systemrequired | string |
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
| Field | Type | Description |
|---|---|---|
history | Snapshot | How much of the resource's past is retained, so a missing
lastChange is explained rather than shown as an empty diff.
sole_: one observation is retained. older_:
the retained observations differ in nothing, and whatever changed
last did so before retention. |
lastChange | Snapshot | 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; selection says when the
request had them the other way round. |
resourcerequired | Resource | |
snapshotsrequired | array of Resource | The 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. |
transitionsrequired | array of Transition | Status 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.
| Field | Type | Description |
|---|---|---|
accountIdrequired | string | |
adopt | string (uuid) | The live or last request making it fully managed. |
idrequired | string (uuid) | |
needs | boolean | The 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_adopt; back is a release from state
(POST /). It
never returns to recorded on its own. |
recordedAtrequired | string (date-time) | |
recordedByrequired | 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. |
regionrequired | string | |
releasedAt | string (date-time) | |
releasedBy | 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. |
resourceIdrequired | string | |
resource | string | The resource's name as the inventory last saw it, when it has one. |
resourcerequired | string | |
servicerequired | string | |
staterequired | Resource | An import's lifecycle (MILESTONE_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. |
systemrequired | string | |
teamrequired | 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. |
templateIdrequired | string (uuid) | |
templaterequired | Resource | 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. One of catalog, generated. |
template | string | |
template | string | |
templaterequired | integer | The 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).
| Field | Type | Description |
|---|---|---|
accountIdrequired | string | |
regionrequired | string | |
resourceIdrequired | string | |
resourcerequired | string | |
servicerequired | string | |
systemrequired | string | |
teamIdrequired | string (uuid) | |
templateId | string (uuid) | Required unless template is generated, and refused with it. |
template | Resource | 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. One of catalog, generated. |
template | integer |
ResourceImportPage
| Field | Type | Description |
|---|---|---|
itemsrequired | array of Resource | |
pagerequired | integer | |
pageSizerequired | integer | |
totalrequired | integer (int64) | |
totalPagesrequired | integer |
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
| Field | Type | Description |
|---|---|---|
edgesrequired | array of Neighbour | |
elidedrequired | integer | Neighbours the viewer's team scope does not admit: counted, never rendered. |
lookedForrequired | array of Neighbour | Every kind the read tried, found or not. |
nodesrequired | array of Neighbour | |
rootrequired | Neighbour | 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. |
truncatedrequired | boolean | A cap was hit. |
truncatedrequired | object | How many edges of each kind a cap left out. |
unavailablerequired | array of object | The kinds that could not be answered for the time asked, each with its reason. |
ResourcePage
| Field | Type | Description |
|---|---|---|
itemsrequired | array of Resource | |
pagerequired | integer | |
pageSizerequired | integer | |
status | Status | Absent here. / returns Resource,
where it is required. |
totalrequired | integer (int64) | |
totalPagesrequired | integer |
ResourceRef
One resource: its account and its key, region/system/service/type/id.
| Field | Type | Description |
|---|---|---|
accountIdrequired | string | |
keyrequired | string |
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.
| Field | Type | Description |
|---|---|---|
nextCursor | string | Reads 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. |
statusrequired | Status | Computed over the full filtered set, not the current page. |
typeCountsrequired | array of TypeCount | How many resources each type holds, over the full filtered set
without the resource 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.
| Field | Type | Description |
|---|---|---|
accountIdrequired | string | |
additional | object | |
id | integer (int64) | The observation's handle for /.
Present on every observation the server returns. |
observedAtrequired | string (date-time) | |
regionrequired | string | |
resourceIdrequired | string | |
resource | string | |
resourcerequired | string | |
schema | integer | The 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. |
servicerequired | string | |
staterequired | string | The raw provider state the status was derived from. |
statusrequired | 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. One of failed, degraded, operational, unknown. |
systemrequired | string | |
tags | object | The 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
| Field | Type | Description |
|---|---|---|
categoryrequired | Resource | What kind of thing a resource type is (MILESTONE_other is the last bucket: a legacy templates. 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. Performance 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_, other. |
groupsrequired | array of FieldGroup | |
icon | string | |
importable | boolean | Whether 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. |
labelrequired | string | |
listColumnsrequired | array of string | Field keys to show as columns in the list view. |
measuredrequired | boolean | Whether 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. |
pluralLabelrequired | string | |
provisionable | boolean | Whether 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. |
resourcerequired | string | |
servicerequired | string | |
systemrequired | string |
ResourceTypeList
| Field | Type | Description |
|---|---|---|
resourcerequired | array of Resource |
SavingsAction
The succeeded action a claim came from: its request, its kind and its name.
| Field | Type | Description |
|---|---|---|
actionrequired | string | |
namerequired | string | |
requestIdrequired | string (uuid) |
SavingsClaim
| Field | Type | Description |
|---|---|---|
action | Savings | The succeeded action a claim came from: its request, its kind and its name. |
changedOnrequired | string (date) | |
claimant | ActorRef | Who made the claim (claimedBy), for display. Absent once the person is removed. |
claimedByrequired | string (uuid) | |
claimedrequired | integer | |
claimedRule | Savings | |
controlrequired | integer | |
controlRule | Savings | |
createdAtrequired | string (date-time) | |
currentrequired | Savings | 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. adjustments 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. |
descriptionrequired | string | |
idrequired | string (uuid) | |
recommendation | Savings | The Trusted Advisor recommendation a claim came from, as it stood when
the claim was made. estimated 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. |
teamId | string (uuid) | Absent on a tenant-wide claim. |
teamName | string | The claim's team's name; absent on a tenant-wide claim. |
windowDaysrequired | integer |
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.
| Field | Type | Description |
|---|---|---|
action | string (uuid) | The succeeded action the claim came from. |
changedOn | string (date) | The day of the change; it is in neither window. Required unless the claim is over an action. |
claimed | Savings | A list of resources, or a tag match — exactly one of them. |
control | Savings | A list of resources, or a tag match — exactly one of them. |
description | string | What was done. Required unless the claim is over an action. |
recommendation | Savings | The Trusted Advisor recommendation the claim came from, by the three
names GET / 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. |
teamId | string (uuid) | The team the claim is for; omitted, a tenant-wide claim (administrators only), or the action's team. |
windowDays | integer | Each 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.
| Field | Type | Description |
|---|---|---|
claimrequired | Savings | |
claimedrequired | array of Savings | |
controlrequired | array of Savings | |
versionsrequired | array of Savings |
SavingsClaimList
| Field | Type | Description |
|---|---|---|
itemsrequired | array of Savings |
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
| Field | Type | Description |
|---|---|---|
afterAmountrequired | string | |
beforerequired | string | |
componentrequired | string enum | adjustments are the report's Credit, Refund, Tax and Fee lines.
usage_ is one usage dimension (usage type, operation,
pricing unit) with the report's quantity before the change: its
reduction splits into usage (the change in
quantity at the before rate) and rate (the change
in price on the after quantity). Quantities in different units are
never added. amount_ 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_, usage_, adjustments. |
currencyrequired | string | |
operation | string | |
quantity | string | |
quantity | string | |
rate | string | |
reductionrequired | string | |
unit | string | The report's pricing/ |
usage | string | |
usageType | string |
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.
| Field | Type | Description |
|---|---|---|
comparisonrequired | Savings | 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_ (when no control set is usable): the Cost and Usage
Report's own quantities — the saving supported is the rate part of
the usage_ 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_. |
control | string | |
control | string | |
currencyrequired | string | |
denominators | array of string | The pricing units cost per unit was read in, when the comparison is cost_. |
figuresrequired | array of Savings | |
verified | string | |
wordrequired | SavingsWord | One of verified, partly_, reduced_, not_, no_. |
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.
| Field | Type | Description |
|---|---|---|
afterAmountrequired | string | |
beforerequired | string | |
componentsrequired | array of Savings | |
currencyrequired | string | |
figurerequired | string enum | One of amortized, unblended. |
reductionrequired | string |
SavingsLedger
| Field | Type | Description |
|---|---|---|
groupByrequired | string enum | One of team, app. |
groupsrequired | array of Savings | |
inNoTeamrequired | boolean | The reader is not an administrator and belongs to no team, so the ledger is empty for that reason; as Cost. |
totalsrequired | array of Savings | Over 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.
| Field | Type | Description |
|---|---|---|
claimrequired | Savings | |
countedrequired | boolean | |
inApp | string enum | One of whole, part. |
overlapsrequired | integer | How many overlapping claims the caller may not read. |
overlapsrequired | array 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.
| Field | Type | Description |
|---|---|---|
entriesrequired | array of Savings | |
key | string | The team's or the app's id. |
name | string | |
totalsrequired | array of Savings |
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.
| Field | Type | Description |
|---|---|---|
claimsrequired | integer | |
currencyrequired | string | |
observedrequired | string | |
verifiedrequired | string |
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.
| Field | Type | Description |
|---|---|---|
accountIdrequired | string | |
checkIdrequired | string | |
checkNamerequired | string | |
estimated | Money | One amount in one currency. The only way money appears in this
document. Test 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. |
resourceIdrequired | string |
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.
| Field | Type | Description |
|---|---|---|
accountIdrequired | string | |
checkIdrequired | string | |
resourceIdrequired | string |
SavingsResourceRef
| Field | Type | Description |
|---|---|---|
accountIdrequired | string | |
resourceKeyrequired | string | region/ |
SavingsSetSpec
A list of resources, or a tag match — exactly one of them.
| Field | Type | Description |
|---|---|---|
resources | array of Savings | |
tag | Savings |
SavingsTagRule
| Field | Type | Description |
|---|---|---|
keyrequired | string | |
valuerequired | string |
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.
| Field | Type | Description |
|---|---|---|
adjustmentsrequired | boolean | |
afterWindowrequired | CostPeriod | |
beforerequired | CostPeriod | |
computedAtrequired | string (date-time) | |
coveredrequired | integer | |
coveredrequired | integer | |
currenciesrequired | array of Savings | |
methodrequired | string | The method version the verdict was computed with. |
missingrequired | array of string (date) | |
missingrequired | array of string (date) | |
reason | string enum | Why it is not yet measurable. One of after_, no_evidence. |
settledrequired | string (date) | |
superseded | string (date-time) | |
versionrequired | integer | |
wordrequired | SavingsWord | One of verified, partly_, reduced_, not_, no_. |
SavingsWord
Type: string enum
SessionsEnded
| Field | Type | Description |
|---|---|---|
endedrequired | integer |
SignOnIdentitiesReleased
| Field | Type | Description |
|---|---|---|
releasedrequired | integer | The accounts whose provider identity was cleared. |
SingleSignOnOptions
| Field | Type | Description |
|---|---|---|
enabledrequired | boolean | |
enforcedrequired | boolean | Passwords work only for break-glass administrators. |
SnapshotBatchRequest
| Field | Type | Description |
|---|---|---|
itemsrequired | array of Snapshot | No duplicate resource keys within a batch: a duplicate makes the upsert order-dependent, which makes transitions non-deterministic. |
runIdrequired | string (uuid) | The run these observations belong to. Every item must fall inside a bucket that run registered, or the batch is rejected. |
SnapshotBatchResponse
| Field | Type | Description |
|---|---|---|
replayed | boolean | True when this key had already been processed, so nothing was written and these are the stored counts. |
storedrequired | integer | Snapshots written. |
transitionsrequired | integer | Status 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.
| Field | Type | Description |
|---|---|---|
attribution | Change | Who may have made the change, on a stored change — the
lastChange the ingest recorded and ?snapshot — and never on
a comparison of two observations picked on request, which is not
one change. |
entriesrequired | array of DiffEntry | |
fromrequired | SnapshotRef | One end of a diff — an observation by its id and time. |
schema | object | The two observations were written under different descriptor
versions, so additional was not compared field by field. |
selection | boolean | |
torequired | SnapshotRef | One 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.
| Field | Type | Description |
|---|---|---|
note | string enum | One of sole_, older_. |
oldest | string (date-time) | |
retainedrequired | integer | The 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.
| Field | Type | Description |
|---|---|---|
accountIdrequired | string | |
additional | object | Provider-specific detail, at most 256 KB when marshalled. This is also the deriver's input: status and state are computed from it. |
observedAt | string (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. |
regionrequired | string | |
resourceIdrequired | string | |
resource | string | |
resourcerequired | string | |
servicerequired | string | |
systemrequired | string | |
tags | object | The 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: on a
resource re-created outside provisioning. |
SnapshotPage
Cursor-paginated as TimelinePage is, newest first; nextCursor
absent means the last page.
| Field | Type | Description |
|---|---|---|
itemsrequired | array of Resource | |
nextCursor | string |
SnapshotRef
One end of a diff — an observation by its id and time.
| Field | Type | Description |
|---|---|---|
idrequired | integer (int64) | |
observedAtrequired | string (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.
| Field | Type | Description |
|---|---|---|
degradedrequired | integer (int64) | |
failedrequired | integer (int64) | |
operationalrequired | integer (int64) | |
stalerequired | integer (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. |
unknownrequired | integer (int64) | Resources whose health the inventory cannot observe; neither healthy nor a problem. |
StepUpAtProvider
| Field | Type | Description |
|---|---|---|
authorizationrequired | string | The provider's address the browser goes to. |
StepUpAtProviderRequest
| Field | Type | Description |
|---|---|---|
returnTo | string | A path on this site to come back to after the provider; anything else is the portal's root. |
StepUpRequest
| Field | Type | Description |
|---|---|---|
passwordrequired | string | The current user's password. |
SubscriptionRegistration
An Azure subscription an administrator is connecting.
| Field | Type | Description |
|---|---|---|
clientIdrequired | string | The application (client) id of the app registration this deployment
signs in as — the one deploy/ outputs. |
environmentrequired | string enum | One of production, staging, sdlc, shared, unknown. |
namerequired | string | |
subscriptionrequired | string | The subscription's id, a GUID; held lower-case. |
tenantIdrequired | string | The directory (tenant) the subscription trusts, by its id. |
TagComplianceBreakdown
The five classes with the money behind each, and the scope total they sum to.
| Field | Type | Description |
|---|---|---|
classesrequired | array of Tag | |
discrepancyrequired | array of Money | A 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_ 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. |
inNoTeamrequired | boolean | The reader is not an administrator and belongs to no team, so every class is empty for that reason; as Cost. |
totalsrequired | array of Money | The 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. |
windowrequired | CostPeriod |
TagComplianceClass
One class's share of the scope's spend.
| Field | Type | Description |
|---|---|---|
amountsrequired | array of Money | |
classrequired | string enum | One of compliant, missing_, value_, exempt, unknown_. |
resourcesrequired | integer | How many resources fell in this class. Zero for
unknown_, whose amount is a remainder rather than a
count of anything. |
TagComplianceGroup
One subject's share of the classes.
| Field | Type | Description |
|---|---|---|
classesrequired | array of Tag | |
keyrequired | string | The account id, team id or app id. Empty is the unowned subject of a team grouping. |
labelrequired | string |
TagComplianceGroupList
| Field | Type | Description |
|---|---|---|
groupByrequired | string enum | One of account, team, app. |
groupsrequired | array of Tag | |
windowrequired | CostPeriod |
TagFault
One fault a resource carries, named so a reader can act on it.
| Field | Type | Description |
|---|---|---|
keyrequired | string | |
kindrequired | string enum | One of missing_, value_. |
value | string | What 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.
| Field | Type | Description |
|---|---|---|
keyrequired | string | |
valuerequired | string |
TagPolicy
The tenant's tag policy. declared false means nobody wrote
one and this is the attribution keys, derived.
| Field | Type | Description |
|---|---|---|
declaredrequired | boolean | The tenant wrote this policy. False means it is derived from the attribution keys. |
exemptrequired | array of string | Resource types the policy does not apply to. An exempt resource is never compliant and never in violation — it is exempt, whatever it carries. |
requiredrequired | array of Tag |
TagPolicyKey
One key a resource must carry, and the values it admits.
| Field | Type | Description |
|---|---|---|
allowedrequired | array of string | Empty admits any non-empty value. The team key always carries the
tenant's team_, whatever was stored, so this page and
attribution cannot disagree about which value names a team. |
keyrequired | string |
TagPolicyUpdate
A whole-document replace of the policy.
| Field | Type | Description |
|---|---|---|
exemptrequired | array of string | |
requiredrequired | array of Tag |
TagViolation
One resource the policy is not satisfied by.
| Field | Type | Description |
|---|---|---|
accountIdrequired | string | |
amountsrequired | array of Money | |
classrequired | string enum | One of missing_, value_. |
faultsrequired | array of TagFault | |
ownStaterequired | string enum | The ownership verdict, empty where the resolver has not answered. One of , owned, unowned, conflict. |
proposedrequired | object | The 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_ 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. |
resourceKeyrequired | string | region/ |
TagViolationList
| Field | Type | Description |
|---|---|---|
itemsrequired | array of Tag | |
windowrequired | CostPeriod |
Team
| Field | Type | Description |
|---|---|---|
accountrequired | integer | |
archivedAt | string (date-time) | Present when the team is closed. Its history is still readable. |
createdAtrequired | string (date-time) | |
directory | string | The provider's group the team follows, as the groups claim names it. Present when source is directory. |
idrequired | string (uuid) | |
last | string (date-time) | The last sign-in that considered this team's binding. |
memberCountrequired | integer | |
namerequired | string | |
slugrequired | string | |
sourcerequired | string enum | Who keeps the membership: administrators (manual), or the identity
provider's group at each sign-in (directory). One of manual, directory. |
tagrequired | integer |
TeamAccount
| Field | Type | Description |
|---|---|---|
accountIdrequired | string | |
attributedrequired | array of Money | What the rollups attribute to this team through this account —
the account-level fallback, attribution team_ — by
currency, over all history. This is the number that moves to
the untagged bucket if the account is unassigned. |
namerequired | string | |
usageFrom | string (date) | |
usageTo | string (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.
| Field | Type | Description |
|---|---|---|
accountIdrequired | string | |
assignedrequired | boolean | |
rebuildrequired | boolean | |
spendMovingrequired | array of Money | |
teamIdrequired | string (uuid) | |
usageFrom | string (date) | |
usageTo | string (date) |
TeamCreate
| Field | Type | Description |
|---|---|---|
namerequired | string | |
slug | string | Derived from the name when omitted. |
TeamDetail
| Field | Type | Description |
|---|---|---|
accountsrequired | array of TeamAccount | |
membersrequired | array of TeamMember | |
tagValuesrequired | array of string | |
teamrequired | Team |
TeamDirectoryGroup
| Field | Type | Description |
|---|---|---|
grouprequired | string | The group exactly as the provider names it in the groups claim — an id or a name, whichever the provider emits. |
TeamList
| Field | Type | Description |
|---|---|---|
itemsrequired | array of Team |
TeamMember
| Field | Type | Description |
|---|---|---|
emailrequired | string | |
platformrequired | string enum | One of viewer, engineer, admin. |
teamRolerequired | TeamRole | A 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. |
userIdrequired | string (uuid) |
TeamMemberSet
| Field | Type | Description |
|---|---|---|
teamRolerequired | TeamRole | A 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.
| Field | Type | Description |
|---|---|---|
teamIdrequired | string (uuid) | |
teamNamerequired | string | |
teamRolerequired | TeamRole | A 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.
| Field | Type | Description |
|---|---|---|
idrequired | string (uuid) | |
namerequired | string |
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
| Field | Type | Description |
|---|---|---|
mappedrequired | boolean | |
rebuildrequired | boolean | |
tagValuerequired | string | |
teamIdrequired | string (uuid) |
TeamTagValueSet
| Field | Type | Description |
|---|---|---|
tagValuerequired | string |
TeamUpdate
| Field | Type | Description |
|---|---|---|
archived | boolean | |
name | string |
Template
| Field | Type | Description |
|---|---|---|
archivedAt | string (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. |
categoryrequired | Resource | The 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. |
cloud | Template | The cloud the template makes its resources in (MILESTONE_Template), 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. |
costNote | string | For a library template: what running requestable costs,
in the library's words (MILESTONE_ |
createdAtrequired | string (date-time) | |
createdByrequired | 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. |
descriptionrequired | string | |
gitRef | string | The default ref new versions are published from. |
gitUrl | string | Present 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. |
idrequired | string (uuid) | |
importablerequired | boolean | Whether requestable declares how an existing resource is
brought under it (MILESTONE_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. |
latest | integer | The highest published version, resolved or not. Absent when the template has none, which also means it cannot be requested. |
librarySlug | string | Present for library templates: the library template it was
enabled from (MILESTONE_ |
namerequired | string | |
requestable | integer | The 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 /, never created from. |
required | integer | With requestable: how many of its variables a requester
must fill in — required, with no default. The catalog says it
before the form asks. |
resourcerequired | array of string | What 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. |
retired | boolean | For a library template: whether the library no longer maintains
it (MILESTONE_ |
slugrequired | string | Stable, human-facing and immutable. It is what a URL names and what an audit record refers to years later, so it cannot be renamed. |
sourceTyperequired | Template | 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_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
| Field | Type | Description |
|---|---|---|
categoryrequired | Template | What a template creates, in the one taxonomy (MILESTONE_Resource 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_. |
cloud | Template | The cloud a template makes its resources in, as the account registry
names it (Account.): a library template's (MILESTONE_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. |
description | string | |
gitRef | string | Default ref for new versions. Only for git. |
gitUrl | string | Required for git, rejected for inline. |
namerequired | string | |
resource | array of string | What the template makes, <cloud>.<service>.<type>, each of
cloud, each once. Types need cloud. |
slugrequired | string | 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. |
sourceTyperequired | string enum | git 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.
| Field | Type | Description |
|---|---|---|
descriptionrequired | string | |
namerequired | string | |
sensitive | boolean | Never asked for, never reported, never wired. Absent is false. |
typerequired | string enum | One of string, number, boolean, list(string), map(string). |
TemplatePage
| Field | Type | Description |
|---|---|---|
itemsrequired | array of Template | |
pagerequired | integer | |
pageSizerequired | integer | |
totalrequired | integer (int64) | |
totalPagesrequired | integer |
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.
| Field | Type | Description |
|---|---|---|
archived | boolean | True archives, false restores. Archiving hides the template and closes it to new versions; it never touches what was provisioned from it. |
category | Template | What a template creates, in the one taxonomy (MILESTONE_Resource 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_. |
cloud | Template | Moves 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. |
description | string | |
gitRef | string | |
name | string | |
resource | array of string | Replaces 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).
| Field | Type | Description |
|---|---|---|
agent | string | For library versions, on the single-version read only: the first
agent version that carries library. Empty when this
platform's library no longer names the release. |
cloud | Template | The 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 (target,
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. |
gitRef | string | The ref that was asked for. What ran is gitSha. |
gitSha | string | The pinned commit. Absent until an agent resolves it, which is the
only reason a git version can sit pending. |
gitUrl | string | |
import | Import | What the version declares it can take over (MILESTONE_meta.json's. |
inline | string | sha256 of inline, computed at publish. |
inline | string | The exact bytes, for inline sources. Present on the single-version
read, which is what the admin editor loads, and on a request's
detail (Provision), 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. |
library | string | For 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_ |
library | integer | For library versions: the first library release that carried
library. Every later release carries it too, so an agent
reporting at least this release can run the version. |
library | string | For library versions, the source identity: the sha256 of the
library template's files in canonical order (MILESTONE_ |
library | string | For 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. |
needsSchemarequired | boolean | Imported from the selfservice donor without a variables schema. Listed in the catalog, refused at submit with
409 version_; an admin publishes the next version
with a schema. Versions are immutable, so this one stays marked. |
outputs | array of Template | What the version declares its apply produces (MILESTONE_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. |
publishedAtrequired | string (date-time) | |
publishedByrequired | 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. |
resolve | string | The 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. |
resolve | string | The 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. |
resolverequired | Template | 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. One of pending, resolved, failed. |
resolvedAt | string (date-time) | |
runnable | array of string | For 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 library — where a
request for this version runs, said before it is made (MILESTONE_agent or
later claims it. An account of another cloud is never listed, and
a create for one is refused. |
sourceTyperequired | Template | 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_git, inline, library. |
templateIdrequired | string (uuid) | |
variablesrequired | Variables | Stored on the version and immutable with it. The portal generates the
request form from this; the agent uses it to write
generated. as proper HCL through an encoder, never by
string concatenation, because hand-rolled escaping is how tfvars
injection happens. |
versionrequired | integer | |
warnings | array of string | Present 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_, which will collide with the provenance stamp the
agent writes (PROVISIONING_ §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
| Field | Type | Description |
|---|---|---|
itemsrequired | array of Template | |
pagerequired | integer | |
pageSizerequired | integer | |
totalrequired | integer (int64) | |
totalPagesrequired | integer |
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.
| Field | Type | Description |
|---|---|---|
fromLibrary | boolean | For library templates, and only true: publish the version the
library now carries for the template's library slug (MILESTONE_library. |
gitRef | string | For git templates. Defaults to the template's gitRef. This is
what an agent resolves; it is never what runs. |
import | Import | For inline templates: what this version declares it can take
over (MILESTONE_resource and additional (400 invalid_, naming the rule). Refused for git, whose source
is not known at publish, and for library, whose declaration is
the library's. |
inline | string | For 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. |
library | string | With 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_ and
no version is created. Refused on a git or inline publish. |
outputs | array of Template | For git and inline templates: the outputs this version
declares (MILESTONE_library, whose outputs are the library's. |
variables | Variables | Stored on the version and immutable with it. The portal generates the
request form from this; the agent uses it to write
generated. 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.
| Field | Type | Description |
|---|---|---|
itemsrequired | array of Transition | |
nextCursor | string | Pass 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.
| Field | Type | Description |
|---|---|---|
accountIdrequired | string | |
acknowledgement | Transition | 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). |
fromState | string | |
fromStatus | Transition | Absent for the first ever observation of a resource. Those rows are written pre-suppressed: onboarding 300 accounts must not emit 40,000 notifications. |
idrequired | integer (int64) | |
occurredAtrequired | string (date-time) | |
regionrequired | string | |
resourceIdrequired | string | |
resource | string | |
resourcerequired | string | |
runId | string (uuid) | |
servicerequired | string | |
systemrequired | string | |
toState | string | |
toStatusrequired | Transition | 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. 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).
| Field | Type | Description |
|---|---|---|
acknowledgedrequired | string (date-time) | |
acknowledgedrequired | 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. |
noterequired | string | |
reversedAt | string (date-time) | |
reversedBy | 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. |
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
| Field | Type | Description |
|---|---|---|
agreedrequired | integer | Of those, the suggestions that matched what people did. |
judgedrequired | integer | Shadow decisions a day old or more that the model answered and no floor decided. |
kindrequired | TriageKind | An event kind Triage decides (TRIAGE_status_, cost_, budget, provisioning, usage_, config_, access_lost, provisioned_. |
missedrequired | integer | Of those, the suggestions of none where people acted. |
TriageAgreementList
| Field | Type | Description |
|---|---|---|
itemsrequired | array of Triage |
TriageDecision
| Field | Type | Description |
|---|---|---|
accountId | string | |
appliedrequired | Triage | One of reviewer, notify, none. |
choice | Triage | One of reviewer, notify, none. |
confidence | number (double) | |
decidedAtrequired | string (date-time) | |
errorClass | string | |
eventRefrequired | string | The event's outbox row, <table>:<id>. |
idrequired | integer (int64) | |
kindrequired | TriageKind | An event kind Triage decides (TRIAGE_status_, cost_, budget, provisioning, usage_, config_, access_lost, provisioned_. |
latencyMsrequired | integer | |
moderequired | string enum | One of shadow, enforce. |
modelrequired | string | |
probabilities | object | |
providerrequired | string | |
questionrequired | string | |
reasonrequired | string enum | One of floor, model_error, model_, threshold, reviewer_, shadow, model. |
resourceKey | string | |
suggestedrequired | Triage | One of reviewer, notify, none. |
TriageDecisionPage
| Field | Type | Description |
|---|---|---|
itemsrequired | array of Triage | |
nextCursor | string |
TriageKind
An event kind Triage decides (TRIAGE_SPEC §5).
Type: string enum
TriageMode
Type: string enum
TriageOutcome
Type: string enum
TriageSetting
| Field | Type | Description |
|---|---|---|
enforced | string | The model the kind was switched to enforce against. |
enforced | string | The question version it was switched against. |
isDefaultrequired | boolean | No administrator has set this kind. |
kindrequired | TriageKind | An event kind Triage decides (TRIAGE_status_, cost_, budget, provisioning, usage_, config_, access_lost, provisioned_. |
moderequired | TriageMode | One of off, shadow, enforce. |
nonerequired | number (double) | |
reviewerrequired | number (double) | |
updatedAt | string (date-time) | |
updatedBy | string (uuid) |
TriageSettingList
| Field | Type | Description |
|---|---|---|
itemsrequired | array of Triage | |
providerrequired | boolean | The deployment configures a decision model (TRIAGE_ |
TriageSettingUpdate
| Field | Type | Description |
|---|---|---|
moderequired | TriageMode | One of off, shadow, enforce. |
none | number (double) | |
reviewer | number (double) |
TypeCount
How many resources of one type a filtered set holds.
| Field | Type | Description |
|---|---|---|
countrequired | integer (int64) | |
resourcerequired | string | |
servicerequired | string | |
systemrequired | string |
User
| Field | Type | Description |
|---|---|---|
clouds | array of string enum | The clouds this deployment reads (MILESTONE_GET /). 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_ lets the home reach AWS. gcp is listed
exactly when a Google Cloud sign-in is configured
(GCP_, 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_
names; under a Google home the platform service account's ID token
(GCP_); 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. |
demorequired | boolean | True 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. |
demo | string (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. |
displayName | string | The name the person gave themselves; absent when they gave none. |
emailrequired | string | The 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. |
idrequired | string (uuid) | |
operator | string | How this deployment's people reach whoever operates it, for help
and for access (MILESTONE_ops@example., or an https:// address, as the deployment's
OPERATOR_ 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. |
operator | boolean | True 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_ 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. |
preferences | Preferences | The person's portal preferences, applied by the portal; an absent field is the browser's own. |
rolerequired | string enum | What this user may do. Separate from entitlements, which are what
the tenant has bought. One of viewer, engineer, admin. |
tenantNamerequired | string | |
tenantSlugrequired | string |
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.
| Field | Type | Description |
|---|---|---|
default | Variable | Used 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. |
description | string | Form rendering only. |
enum | array of Variable | Closed 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. |
group | string | Form rendering only. |
max | number | Inclusive, for number. |
maxItems | integer | Inclusive, for list(string). |
maxLength | integer | Inclusive, for string. |
min | number | Inclusive, for number. |
minItems | integer | Inclusive, for list(string). |
minLength | integer | Inclusive, for string. |
namerequired | string | |
pattern | string | RE2, 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. |
patternRule | string | The template's own words for pattern, which it needs (PROVISIONING_ §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. |
required | boolean | Absent at submit and no default → rejected. |
sensitive | boolean | Masked in the UI and in variables, 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. |
title | string | The field's label in the template's own words (PROVISIONING_ §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. |
typerequired | string enum | Nothing 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.
| Field | Type | Description |
|---|---|---|
variablesrequired | array of Variable |
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.