Skip to content

Reporting API endpoints ​

The Reporting API is specified by api/reporting/openapi.yaml. make generate produces the server's routing and its models from that document, and the service validates every request against it before a handler sees it. The tables and sections below are rendered from the same document.

The document is OpenAPI 3.0.3 and carries no servers block, so a client sets the base URL of the deployment it talks to.

Conventions ​

The four conventions below are fixed by section 7 of roadmap/00-conventions.md. Each paragraph states what the document declares for one of them.

Every timestamp the document declares is a string with format: date-time, in a query parameter as in a body. Section 7 asks for ISO 8601 in UTC, in and out.

Four lists page: GET /api/v1/events, GET /api/v1/resources, GET /api/v1/projects and GET /api/v1/rejected-events. Each takes limit, whose bounds and default stand on the row of the operation below, and cursor, an opaque string a caller passes back as it received it. A page is answered as {"items": [...], "next_cursor": ...}, where next_cursor is a string or null and null is the last page. GET /api/v1/resource-types answers those two members and takes neither parameter: its next_cursor is always null.

Every response of every operation declares the header X-Request-ID. The server adopts the id a caller sends and generates one otherwise. The Headers column of an operation lists what a status carries beyond it.

Every error status of every operation references one shared response, which carries a problem document as application/problem+json.

Authentication ​

SchemeTypeDescription
apiTokenhttp bearerAn API token passed as Authorization: Bearer <token>. Each operation names the role it needs; a token below it is answered 403.
ingestTokenhttp bearerAn ingest credential, issued per (platform, cloud) and passed as Authorization: Bearer <token>. It may report events for that pair alone.
internalTokenhttp bearerThe shared secret the other Tally components call the internal routes with, passed as Authorization: Bearer <token>. It is not issued to public callers.

Each operation below names the scheme it takes. GET /healthz, GET /readyz and GET /metrics take none. A token is issued under one scheme and refused by the operations of the other two.

An API token carries a role and a project scope. A request above the token's role, and a request for a project outside its scope, are both answered 403. An operation that needs more than the lowest role says so in its description.

Errors ​

Every error is one RFC 9457 problem document. It always carries type, title and status; it carries detail where there is more to say, and errors, a list of loc and msg pairs, when a request failed validation. The members are listed under Problem.

A client branches on type. The values are the constants of internal/reporting/httpapi/problem.

NameValueMeaning
TypeValidationurn:tally:error:validationTypeValidation marks a request the OpenAPI contract rejects.
TypeUnauthorizedurn:tally:error:unauthorizedTypeUnauthorized marks a request that carries no usable credential.
TypeForbiddenurn:tally:error:forbiddenTypeForbidden marks an authenticated caller that may not do this.
TypeNotFoundurn:tally:error:not_foundTypeNotFound marks a path or resource that does not exist.
TypeMethodNotAllowedurn:tally:error:method_not_allowedTypeMethodNotAllowed marks a known path addressed with the wrong method.
TypeConflicturn:tally:error:conflictTypeConflict marks a write that collides with existing state, such as a project whose (cloud, external_id) is already registered or a relation triple that is already active.
TypePayloadTooLargeurn:tally:error:payload_too_largeTypePayloadTooLarge marks a request that carries more than the endpoint takes at once, such as an event batch above the item limit.
TypeHistoryTooLongurn:tally:error:history_too_longTypeHistoryTooLong marks a resource or project whose stored history is longer than the unpaginated per-resource reads answer at once.
TypeResultTooLargeurn:tally:error:result_too_largeTypeResultTooLarge marks a query whose answer would be larger than this API serves unpaginated, such as an event grouping over too wide a window.
TypeNotImplementedurn:tally:error:not_implementedTypeNotImplemented marks a parameter or a capability a later phase delivers, such as counting resources at a historic instant.
TypeRelationCycleurn:tally:error:relation_cycleTypeRelationCycle marks a relation creation that would close a cycle over the relation types that attribute cost.
TypeInternalurn:tally:error:internalTypeInternal marks a failure the caller cannot do anything about.
TypeUnavailableurn:tally:error:unavailableTypeUnavailable marks a dependency the service needs and cannot reach.

Operations ​

One section per operation, in the order the router matches the paths. A section names the credential the operation takes, the parameters it reads off the request, the body it accepts and every status it answers with.

GET /readyz ​

Readiness probe

Reports whether the service can serve traffic. It fails while the database is unreachable, which takes the pod out of rotation without restarting it.

No credential.

StatusDescriptionBodyHeaders
200The service is ready to serve traffic.text/plainnone
500The request failed. The body says how.application/problem+jsonnone
503The request failed. The body says how.application/problem+jsonnone

GET /metrics ​

Service metrics

Serves the instruments of this service in the Prometheus exposition format: the nine tally_ series over the ingest, projection, and reconciliation paths, together with the Go runtime and process collectors.

The route carries no credential, which is what roadmap/00-conventions.md section 7 asks of every service, and the Gateway publishes /api/v1 alone, so it stays reachable from inside the cluster only. A deployment whose configuration turns the instrumentation off is answered 404 here.

No credential.

StatusDescriptionBodyHeaders
200The current values of this service's instruments.text/plainnone
404The request failed. The body says how.application/problem+jsonnone
500The request failed. The body says how.application/problem+jsonnone

POST /internal/projection/rebuild ​

Rebuild the projection

Replays the event history of every resource the filter selects into the projection and answers once it is done. It is the operational guarantee behind the derived rows: whatever a projection row holds, the history it comes from can produce it again.

Security: internalToken

The request body is application/json, a RebuildRequest.

StatusDescriptionBodyHeaders
200The rebuild finished.RebuildResultnone
400The request failed. The body says how.application/problem+jsonnone
401The request failed. The body says how.application/problem+jsonnone
413The request failed. The body says how.application/problem+jsonnone
500The request failed. The body says how.application/problem+jsonnone

GET /healthz ​

Liveness probe

Reports whether the process should keep running. It fails only after the service has been unhealthy for longer than the configured threshold, so that a transient database outage restarts nothing.

No credential.

StatusDescriptionBodyHeaders
200The service is alive.text/plainnone
500The request failed. The body says how.application/problem+jsonnone
503The request failed. The body says how.application/problem+jsonnone

GET /api/v1/stats/resources ​

Count the current resources per group

Returns the projection counted along the dimensions group_by names, one item per combination of values that carries at least one resource. A combination no resource carries is left out rather than served as a zero. The order is (cloud, resource_type, state, platform, project_id) ascending, where a dimension outside the grouping compares as the empty string.

The answer is never paginated: one item stands for however many resources the group holds, so the answer grows with the number of combinations the fleet shows rather than with the fleet. Four of the five dimensions carry a handful of values each; project_id carries one per tenant, so a fleet showing more combinations than one answer holds is refused 422 (urn:tally:error:result_too_large) rather than served truncated. The counts are taken along all five dimensions whatever group_by names, so a coarser grouping does not lower that bound; the part of the fleet status selects is what does.

A project token counts the resources whose (cloud, project_id) pair one of its projects names, which is the pair the resource list narrows its rows by.

Security: apiToken

NameInRequiredTypeDescription
group_byqueryyesarray of cloud, resource_type, state, platform, project_idWhich dimensions the counts are grouped by, as a comma-separated list. The parameter is given once: a request that repeats it, such as group_by=cloud&group_by=resource_type, is answered 400. cloud and resource_type have to be among them, because they are what an item is read by; a grouping that leaves either out is answered 400. The rule spans the members of one list, which this schema cannot express, so the handler is what enforces it.
statusquerynoactive, deleted, all, default activeWhich part of the fleet to count. active counts the rows whose state is not deleted, deleted counts those alone, and all counts both.
atquerynostring, date-timeThe instant the counts describe. Leaving it out asks for the current counts, which is what the projection holds. Any value at all is answered 501 (urn:tally:error:not_implemented): counting a past instant means replaying the histories, and the Phase 3 usage records are what answer that. A value meaning "now" cannot be told from a historic one, because the two differ by however long the request took, so omitting the parameter rather than sending a timestamp is how the current counts are asked for.
StatusDescriptionBodyHeaders
200The counts, one item per group.ResourceStatsListnone
400The request failed. The body says how.application/problem+jsonnone
401The request failed. The body says how.application/problem+jsonnone
403The request failed. The body says how.application/problem+jsonnone
422The request failed. The body says how.application/problem+jsonnone
500The request failed. The body says how.application/problem+jsonnone
501The request failed. The body says how.application/problem+jsonnone

GET /api/v1/stats/events ​

Count the stored events per time bucket

Returns the stored events of the window counted per time bucket and per combination of the dimensions group_by names. from and to select the half-open window [from, to) on the event timestamp: an event at exactly from is counted, one at exactly to is not, and a from at or past to holds nothing. The buckets are aligned on UTC hours and days rather than on from, so the first one can start before the window; every bucket counts the events inside the window alone.

The order is (bucket, cloud, event_type, source) ascending, and a bucket no event falls into is left out rather than served as a zero. The answer is never paginated: a request that groups into more rows than one answer carries is refused 422 (urn:tally:error:result_too_large) rather than answered with a truncated count. The bound counts the finest grouping there is, which is what the read returns, so a narrower window or a coarser interval is what gets such a request through; leaving source out of group_by does not lower it.

A second bound is read on the window itself, to - from, and it is decided before anything is counted: the aggregate behind the count walks every event of the window before its row limit discards anything, so what a request costs is set by the events the window holds rather than by the buckets it names. A window wider than a year and a month is refused 422 whatever the interval, rather than aggregated across the archive and then thrown away.

The counts are event-scoped the way the event list is: a project token counts every event whose project_id names one of its projects, including the events a resource carried before it was transferred away.

Security: apiToken

NameInRequiredTypeDescription
group_byqueryyesarray of cloud, event_type, sourceWhich dimensions the counts are grouped by, as a comma-separated list. The parameter is given once: a request that repeats it, such as group_by=cloud&group_by=event_type, is answered 400. cloud and event_type have to be among them, because they are what an item is read by; a grouping that leaves either out is answered 400. The rule spans the members of one list, which this schema cannot express, so the handler is what enforces it.
fromqueryyesstring, date-timeCount only the events at or after this instant, the inclusive bound of the window.
toqueryyesstring, date-timeCount only the events before this instant, the exclusive bound of the window.
intervalqueryyes1h, 1dHow wide one bucket is.
StatusDescriptionBodyHeaders
200The counts, one item per bucket and group.EventStatsListnone
400The request failed. The body says how.application/problem+jsonnone
401The request failed. The body says how.application/problem+jsonnone
403The request failed. The body says how.application/problem+jsonnone
422The request failed. The body says how.application/problem+jsonnone
500The request failed. The body says how.application/problem+jsonnone

GET /api/v1/resources ​

List the current resources

Returns the projection rows, one per resource, narrowed by whichever filters the request carries. The order is (cloud, resource_type, resource_id) ascending.

One call answers one page. items carries the rows, and next_cursor is what the next call passes as cursor; a null next_cursor is the end of the walk. The cursor holds a position and nothing else, so presenting it together with other filters is well defined: the filters of that call apply from that position onwards.

A deleted resource keeps its row, with state deleted and deleted_at set. Rows are never removed, so a resource that is gone stays readable here and through its history.

A project token reads the rows whose (cloud, project_id) pair one of its projects names. The pair is what decides, so a project id another cloud uses for something else stays out of the answer.

Security: apiToken

NameInRequiredTypeDescription
cloudquerynostringServe only the resources of this cloud.
platformquerynostringServe only the resources of this platform.
project_idquerynostringServe only the resources of this project, named the way its cloud names it. A token asking for a project outside its scope is answered 403.
resource_typequerynostringServe only the resources of this type.
statequerynostringServe only the resources whose current state is exactly this, shutoff for example.
statusquerynoactive, deleted, all, default activeWhich part of the fleet to serve. active serves the rows whose state is not deleted, deleted serves those alone, and all serves both. state and status are independent filters, so a contradictory pair such as state=active&status=deleted yields the empty page.
limitquerynointeger, 1 to 1000, default 100How many resources one page carries at most.
cursorquerynostringThe next_cursor of the page before this one. It is opaque: a client passes it back as it received it and reads nothing out of it.
StatusDescriptionBodyHeaders
200One page of current resources.ResourceListnone
400The request failed. The body says how.application/problem+jsonnone
401The request failed. The body says how.application/problem+jsonnone
403The request failed. The body says how.application/problem+jsonnone
500The request failed. The body says how.application/problem+jsonnone

GET /api/v1/resource-types ​

List the registered resource types

Returns every registered (platform, resource_type) pair together with the size schema its events are checked against.

Security: apiToken

StatusDescriptionBodyHeaders
200The registered resource types.ResourceTypeListnone
401The request failed. The body says how.application/problem+jsonnone
500The request failed. The body says how.application/problem+jsonnone

GET /api/v1/rejected-events ​

List the dead-lettered events

Returns every ingest item this API refused server-side, with the reason it was refused and the raw body as it was submitted. The order is (received_at, id) ascending. from and to select the half-open window [from, to) on received_at, which is when the item was refused rather than when its event claims to have happened: an item received at exactly from is served, one received at exactly to is not.

One call answers one page. items carries the items, and next_cursor is what the next call passes as cursor; a null next_cursor is the end of the walk. The cursor holds a position and nothing else, so presenting it together with other filters is well defined: the filters of that call apply from that position onwards.

The operation is admin-only: callers below the admin role are answered 403. No project scope applies, because the raw JSON of a refused item carries no reliable project attribution.

Security: apiToken

NameInRequiredTypeDescription
fromquerynostring, date-timeServe only the items refused at or after this instant, the inclusive bound of the window.
toquerynostring, date-timeServe only the items refused before this instant, the exclusive bound of the window.
limitquerynointeger, 1 to 1000, default 100How many items one page carries at most.
cursorquerynostringThe next_cursor of the page before this one. It is opaque: a client passes it back as it received it and reads nothing out of it.
StatusDescriptionBodyHeaders
200One page of dead-lettered events.DeadLetterListnone
400The request failed. The body says how.application/problem+jsonnone
401The request failed. The body says how.application/problem+jsonnone
403The request failed. The body says how.application/problem+jsonnone
500The request failed. The body says how.application/problem+jsonnone

GET /api/v1/projects ​

List the registered projects

Returns the registered projects, narrowed by whichever filters the request carries. The order is (cloud, external_id) ascending.

One call answers one page. items carries the projects, and next_cursor is what the next call passes as cursor; a null next_cursor is the end of the walk. The cursor holds a position and nothing else, so presenting it together with other filters is well defined: the filters of that call apply from that position onwards.

Security: apiToken

NameInRequiredTypeDescription
platformquerynostringServe only the projects of this platform.
cloudquerynostringServe only the projects of this cloud.
external_idquerynostringServe only the projects their cloud names this way. It is not a key on its own, so two clouds using the same external id are both served unless cloud narrows the answer.
limitquerynointeger, 1 to 1000, default 100How many projects one page carries at most.
cursorquerynostringThe next_cursor of the page before this one. It is opaque: a client passes it back as it received it and reads nothing out of it.
StatusDescriptionBodyHeaders
200One page of registered projects.ProjectListnone
400The request failed. The body says how.application/problem+jsonnone
401The request failed. The body says how.application/problem+jsonnone
403The request failed. The body says how.application/problem+jsonnone
500The request failed. The body says how.application/problem+jsonnone

POST /api/v1/projects ​

Register a project

Registers one project. The registry is keyed by (cloud, external_id): a pair it already holds is answered 409 rather than replaced, so a repeated registration never overwrites what an operator entered before.

A registration whose platform is meta or partner has to carry that same literal as its cloud, and no other platform may carry meta or partner as its cloud. A registration breaking that rule is answered 422 without being written.

The id the answer carries is how the other operations address the project. An external id alone does not, because two clouds may name different projects the same way.

Security: apiToken

The request body is application/json, a CreateProject.

StatusDescriptionBodyHeaders
201The project as it is now registered.ProjectLocation
400The request failed. The body says how.application/problem+jsonnone
401The request failed. The body says how.application/problem+jsonnone
403The request failed. The body says how.application/problem+jsonnone
409The request failed. The body says how.application/problem+jsonnone
422The request failed. The body says how.application/problem+jsonnone
500The request failed. The body says how.application/problem+jsonnone

GET /api/v1/events ​

List stored events

Returns every stored event, narrowed by whichever filters the request carries. The order is (timestamp, event_id) ascending. from and to select the half-open window [from, to) on the event timestamp: an event at exactly from is served, one at exactly to is not.

One call answers one page. items carries the events, and next_cursor is what the next call passes as cursor; a null next_cursor is the end of the walk. The cursor holds a position and nothing else, so presenting it together with other filters is well defined: the filters of that call apply from that position onwards.

This list is event-scoped rather than resource-scoped. A project token reads every event whose project_id names one of its projects, including the events a resource carried before it was transferred away. The per-resource reads scope every event the same way, so a transfer moves the resource to its new project without handing that project the history the resource carried under the old one.

Security: apiToken

NameInRequiredTypeDescription
cloudquerynostringServe only the events of this cloud.
platformquerynostringServe only the events of this platform.
project_idquerynostringServe only the events of this project, named the way its cloud names it. A token asking for a project outside its scope is answered 403.
resource_typequerynostringServe only the events about resources of this type.
event_typequerynostringServe only the events of this type, volume.create for example.
sourcequerynocollector, reconciliationServe only the events the named pipeline produced.
fromquerynostring, date-timeServe only the events at or after this instant, the inclusive bound of the window.
toquerynostring, date-timeServe only the events before this instant, the exclusive bound of the window.
limitquerynointeger, 1 to 1000, default 100How many events one page carries at most.
cursorquerynostringThe next_cursor of the page before this one. It is opaque: a client passes it back as it received it and reads nothing out of it.
StatusDescriptionBodyHeaders
200One page of stored events.EventListnone
400The request failed. The body says how.application/problem+jsonnone
401The request failed. The body says how.application/problem+jsonnone
403The request failed. The body says how.application/problem+jsonnone
500The request failed. The body says how.application/problem+jsonnone

POST /api/v1/events ​

Ingest events

Stores a batch of canonical events. The body is either one event or an array of at most 1000 of them.

The answer is 200 whenever the request itself was authorized and readable, whatever the individual items did: an item this API refuses is kept server-side with the reason it was refused and reported in rejected. A collector may therefore drop the whole batch from its buffer once the call returns, and must not retry a rejected item.

Security: ingestToken

The request body is application/json, an EventInput or an array of EventInput.

StatusDescriptionBodyHeaders
200The batch was processed. The body says what happened to it.IngestResultnone
400The request failed. The body says how.application/problem+jsonnone
401The request failed. The body says how.application/problem+jsonnone
413The request failed. The body says how.application/problem+jsonnone
500The request failed. The body says how.application/problem+jsonnone

POST /internal/sync/{cloud} ​

Reconcile one cloud

Runs one sync of the cloud. A sync asks the platform's adapter what the cloud currently holds, diffs that observation against the projection, and feeds the difference back as synthetic events through the ordinary ingest path, so a resource a run corrected ends up with the same kind of history as one that was never missed.

The run is synchronous: the answer carries the stats of the run that just happened rather than a handle to poll. A cloud the configuration does not name is answered 404, and a cloud another run is holding is answered 409, because two syncs of one cloud would diff the same projection rows against two overlapping observations.

The request may carry a body naming the instant the run is at. Only a deployment that sets TALLY_REPORTING_SYNC_ALLOW_AT accepts it; anywhere else such a body is answered 400 and no run starts.

Security: internalToken

NameInRequiredTypeDescription
cloudpathyesstringThe installation to reconcile, os-prod-eu1 for example.

The request body is application/json, a SyncRequest.

StatusDescriptionBodyHeaders
200The sync run finished.SyncResultnone
400The request failed. The body says how.application/problem+jsonnone
401The request failed. The body says how.application/problem+jsonnone
404The request failed. The body says how.application/problem+jsonnone
409The request failed. The body says how.application/problem+jsonnone
500The request failed. The body says how.application/problem+jsonnone

GET /api/v1/projects/{id}/summary ​

Summarize what one project ran inside a window

Returns the project together with what each of its resource types did inside the half-open window [from, to): how many resources of the type began or ended their life in it, how many minutes they ran within it, and how many of the type the project holds right now.

The three window numbers come from folding the histories of the project's resources with the fold every other read of a history runs, clipped to the window, and an interval still open is counted up to the instant of the request. A resource that changed hands counts here over the part of the window this project held it: the transfer ends its minutes here and begins the new owner's, so a resource is billed to one project at a time. created and deleted are read off the project's own events, so a transfer moves neither. active_now is counted off the projection instead, so it describes the present whatever the window covers. A from at or past to is an empty window: created, deleted, and the minutes come out zero, while active_now stays what it is.

A project this registry does not hold and a project outside the token's scope are answered the same 404, body for body, so a caller cannot tell the projects another token holds from ids that name nothing.

The embedded project names the project and nothing more. This route is reachable with a project token, and the registry row it hangs off is not: the name and the operator-set metadata are served by getProject, which takes read_all.

A project whose history is longer than one summary folds at once is answered 422 (urn:tally:error:history_too_long) rather than a summary folded from part of it. The Phase 3 usage records are what answer a history that long.

Security: apiToken

NameInRequiredTypeDescription
idpathyesUuidThe project the summary is about.
fromqueryyesstring, date-timeWhen the window starts, the inclusive bound.
toqueryyesstring, date-timeWhen the window ends, the exclusive bound.
StatusDescriptionBodyHeaders
200The project and what its resource types did in the window.ProjectSummarynone
400The request failed. The body says how.application/problem+jsonnone
401The request failed. The body says how.application/problem+jsonnone
403The request failed. The body says how.application/problem+jsonnone
404The request failed. The body says how.application/problem+jsonnone
422The request failed. The body says how.application/problem+jsonnone
500The request failed. The body says how.application/problem+jsonnone

GET /api/v1/projects/{id}/relations ​

List the relations of one project

Returns the relations of the project that are valid at at, ordered by (created_at, id). A relation is valid at t iff valid_from <= t AND (valid_to IS NULL OR valid_to > t), so the answer is the point-in-time view of the neighborhood and history is read by passing a past at.

The answer is never paginated. A project this registry does not hold is answered 404, which is what tells an empty neighborhood from an unknown project.

Security: apiToken

NameInRequiredTypeDescription
idpathyesUuidThe project the relations are addressed under.
directionquerynooutgoing, incoming, both, default bothWhich relations to serve: outgoing the ones leaving the project, incoming the ones reaching it, both either.
relation_typequerynostringServe only the relations of this type.
atquerynostring, date-timeThe instant the answer describes. It defaults to now.
StatusDescriptionBodyHeaders
200The relations valid at that instant.RelationListnone
400The request failed. The body says how.application/problem+jsonnone
401The request failed. The body says how.application/problem+jsonnone
403The request failed. The body says how.application/problem+jsonnone
404The request failed. The body says how.application/problem+jsonnone
500The request failed. The body says how.application/problem+jsonnone

POST /api/v1/projects/{id}/relations ​

Relate one project to another

Creates one relation leaving the project the path names. A relation is valid at t iff valid_from <= t AND (valid_to IS NULL OR valid_to > t), and valid_from defaults to the instant the relation is written.

One triple of source, target and relation_type carries a single open relation at a time. A triple that is already active is answered 409; the same triple after a close is created again.

A source project this registry does not hold is answered 404. A target it does not hold, and a target that is the source itself, are answered 422.

A relation of one of the configured attributing types keeps attribution a forest. Its creation walks the active attributing relations out of target_id first, and a relation that reaches the source again is answered 422 (urn:tally:error:relation_cycle) without being written. A type outside that list is created without the walk.

A metadata.pricing_adjustments array the adjustments schema refuses is answered 422 (urn:tally:error:validation) with one field error per violation, located as body.metadata.pricing_adjustments.<index>.<member>.

A relation to or from a virtual project is created, listed, updated and closed like any other.

Security: apiToken

NameInRequiredTypeDescription
idpathyesUuidThe project the relations are addressed under.

The request body is application/json, a CreateRelation.

StatusDescriptionBodyHeaders
201The relation as it is now stored.RelationLocation
400The request failed. The body says how.application/problem+jsonnone
401The request failed. The body says how.application/problem+jsonnone
403The request failed. The body says how.application/problem+jsonnone
404The request failed. The body says how.application/problem+jsonnone
409The request failed. The body says how.application/problem+jsonnone
422The request failed. The body says how.application/problem+jsonnone
500The request failed. The body says how.application/problem+jsonnone

List the projects one project reaches

Walks the outgoing relations that are valid at at, up to depth relations out, and returns every project the walk reaches. A relation is valid at t iff valid_from <= t AND (valid_to IS NULL OR valid_to > t), so the answer is the point-in-time view of the graph and history is read by passing a past at.

The walk is breadth-first and visits a project once, which terminates a cycle and keeps the project of the path out of the answer. The items come in the order they were visited, and path names the relations the walk took to reach each of them.

The answer is never paginated. A project this registry does not hold is answered 404.

Security: apiToken

NameInRequiredTypeDescription
idpathyesUuidThe project the traversal starts from.
depthquerynointeger, 1 to 10, default 1How many relations out the walk goes.
relation_typequerynostringWalk only the relations of this type.
atquerynostring, date-timeThe instant the answer describes. It defaults to now.
StatusDescriptionBodyHeaders
200The projects the walk reached.RelatedProjectListnone
400The request failed. The body says how.application/problem+jsonnone
401The request failed. The body says how.application/problem+jsonnone
403The request failed. The body says how.application/problem+jsonnone
404The request failed. The body says how.application/problem+jsonnone
500The request failed. The body says how.application/problem+jsonnone

GET /api/v1/projects/{id} ​

Read one registered project

Security: apiToken

NameInRequiredTypeDescription
idpathyesUuidThe project, as this API names it.
StatusDescriptionBodyHeaders
200The registered project.Projectnone
400The request failed. The body says how.application/problem+jsonnone
401The request failed. The body says how.application/problem+jsonnone
403The request failed. The body says how.application/problem+jsonnone
404The request failed. The body says how.application/problem+jsonnone
500The request failed. The body says how.application/problem+jsonnone

PATCH /api/v1/projects/{id} ​

Update one registered project

Changes the name or the metadata of one project. A member the request leaves out stays as it is, and metadata is replaced wholesale rather than merged, so a request carrying it carries every member the project keeps.

Neither the platform nor the (cloud, external_id) key is writable here. They are what the registry is keyed by, and a project that moves to another cloud is a different project.

Security: apiToken

NameInRequiredTypeDescription
idpathyesUuidThe project, as this API names it.

The request body is application/json, an UpdateProject.

StatusDescriptionBodyHeaders
200The project as it now stands.Projectnone
400The request failed. The body says how.application/problem+jsonnone
401The request failed. The body says how.application/problem+jsonnone
403The request failed. The body says how.application/problem+jsonnone
404The request failed. The body says how.application/problem+jsonnone
500The request failed. The body says how.application/problem+jsonnone

GET /api/v1/resource-types/{platform}/{resource_type} ​

Read one registered resource type

Security: apiToken

NameInRequiredTypeDescription
platformpathyesstringThe platform the resource type belongs to, openstack for example.
resource_typepathyesstringThe resource type within that platform, instance for example.
StatusDescriptionBodyHeaders
200The registered resource type.ResourceTypenone
401The request failed. The body says how.application/problem+jsonnone
404The request failed. The body says how.application/problem+jsonnone
500The request failed. The body says how.application/problem+jsonnone

PUT /api/v1/resource-types/{platform}/{resource_type} ​

Register a resource type

Registers the size schema of one (platform, resource_type) pair, or replaces the schema already registered for it. The document is compiled before it is stored, so a schema that does not compile is refused and nothing changes.

Security: apiToken

NameInRequiredTypeDescription
platformpathyesstringThe platform the resource type belongs to, openstack for example.
resource_typepathyesstringThe resource type within that platform, instance for example.

The request body is application/json, a RegisterResourceType.

StatusDescriptionBodyHeaders
200The resource type as it is now registered.ResourceTypenone
400The request failed. The body says how.application/problem+jsonnone
401The request failed. The body says how.application/problem+jsonnone
403The request failed. The body says how.application/problem+jsonnone
413The request failed. The body says how.application/problem+jsonnone
500The request failed. The body says how.application/problem+jsonnone

PATCH /api/v1/projects/{id}/relations/{relation_id} ​

Update one relation

Changes the metadata or the end of one relation. A member the request leaves out stays as it is, and metadata is replaced wholesale rather than merged.

valid_to has to be after valid_from, which is 422 otherwise. It is also where a closed relation gets its close instant corrected; the member is not nullable, so reopening a closed relation is not supported.

A relation the path does not name, and a relation that does not leave the project of the path, are both answered 404.

A metadata.pricing_adjustments array the adjustments schema refuses is answered 422, and a document whose pricing_adjustments differs from the stored one is answered 409 (urn:tally:error:conflict).

Security: apiToken

NameInRequiredTypeDescription
idpathyesUuidThe project the relation leaves.
relation_idpathyesUuidThe relation itself, as this API names it.

The request body is application/json, an UpdateRelation.

StatusDescriptionBodyHeaders
200The relation as it now stands.Relationnone
400The request failed. The body says how.application/problem+jsonnone
401The request failed. The body says how.application/problem+jsonnone
403The request failed. The body says how.application/problem+jsonnone
404The request failed. The body says how.application/problem+jsonnone
409The request failed. The body says how.application/problem+jsonnone
422The request failed. The body says how.application/problem+jsonnone
500The request failed. The body says how.application/problem+jsonnone

DELETE /api/v1/projects/{id}/relations/{relation_id} ​

Close one relation

Closes the relation by setting valid_to to now. The row is never deleted, so a read at an earlier at still finds the relation.

A relation that is already closed is answered 204 as well, and the stored valid_to does not move: the close instant a relation was given is the one it keeps. A relation this API does not hold, and one that does not leave the project of the path, are answered 404.

Security: apiToken

NameInRequiredTypeDescription
idpathyesUuidThe project the relation leaves.
relation_idpathyesUuidThe relation itself, as this API names it.
StatusDescriptionBodyHeaders
204The relation is closed.nonenone
400The request failed. The body says how.application/problem+jsonnone
401The request failed. The body says how.application/problem+jsonnone
403The request failed. The body says how.application/problem+jsonnone
404The request failed. The body says how.application/problem+jsonnone
500The request failed. The body says how.application/problem+jsonnone

GET /api/v1/resources/{cloud}/{resource_type}/{resource_id}/lifecycle ​

Read the folded lifecycle of one resource

Returns the resource's history folded into the half-open billable intervals of roadmap/00-conventions.md section 5, next to the projection row and the events the fold ran on. It is the fold the projection replay runs, so the intervals here are the ones every derived row comes from.

warnings names what the fold could not trust, a history that starts without a create for example.

The read is gated and scoped the way the per-resource history is: a resource this API holds no projection row for and a resource outside the token's scope are answered the same 404, the events a project token folds are its own projects' events, and a resource with more than 10000 stored events is answered 422 (urn:tally:error:history_too_long). Folding a scoped history is what keeps the intervals a project reads the spans it is billed for: a transferred resource is folded from the transfer onwards, which warnings reports as a history that starts without a create.

The embedded resource is the projection row and is not scoped, so its created_at can predate the reader's ownership while the intervals start at the transfer. Bill from intervals.

Security: apiToken

NameInRequiredTypeDescription
cloudpathyesstringThe installation the resource lives in, os-prod-eu1 for example.
resource_typepathyesstringThe kind of resource, instance or volume for example.
resource_idpathyesstringThe resource itself, as its cloud names it.
StatusDescriptionBodyHeaders
200The resource, its history, and the intervals it folds into.Lifecyclenone
401The request failed. The body says how.application/problem+jsonnone
404The request failed. The body says how.application/problem+jsonnone
422The request failed. The body says how.application/problem+jsonnone
500The request failed. The body says how.application/problem+jsonnone

GET /api/v1/resources/{cloud}/{resource_type}/{resource_id}/events ​

Read the event history of one resource

Returns the history of one resource, ordered by (timestamp, received_at, event_id). The answer is never paginated: one call carries every event the request may see, and next_cursor is always null. A resource with more than 10000 stored events is answered 422 (urn:tally:error:history_too_long) rather than a truncated history; GET /api/v1/events pages such a history.

The read is gated on the project the resource's projection row names today. A project token whose scope does not hold that (cloud, project_id) pair is answered the 404 an unknown resource gets, so a resource outside the scope cannot be told from one that does not exist. Every served event is scoped the same way, so a project reads the part of the history its own projects carried and no more: after a transfer the old project stops reading the resource here, and the new one reads the events stored since the transfer rather than the whole history. GET /api/v1/events is where the old project keeps reading the events it carried.

Security: apiToken

NameInRequiredTypeDescription
cloudpathyesstringThe installation the resource lives in, os-prod-eu1 for example.
resource_typepathyesstringThe kind of resource, instance or volume for example.
resource_idpathyesstringThe resource itself, as its cloud names it.
StatusDescriptionBodyHeaders
200The ordered history of the resource, with next_cursor null.EventListnone
401The request failed. The body says how.application/problem+jsonnone
404The request failed. The body says how.application/problem+jsonnone
422The request failed. The body says how.application/problem+jsonnone
500The request failed. The body says how.application/problem+jsonnone