List decisions for a project
const url = 'https://api.hydrate.sh/v1/decisions?project_id=2489E9AD-2EE2-8E00-8EC9-32D5F69181C0&limit=50&offset=0';const options = {method: 'GET', headers: {Authorization: 'Bearer <token>'}};
try { const response = await fetch(url, options); const data = await response.json(); console.log(data);} catch (error) { console.error(error);}curl --request GET \ --url 'https://api.hydrate.sh/v1/decisions?project_id=2489E9AD-2EE2-8E00-8EC9-32D5F69181C0&limit=50&offset=0' \ --header 'Authorization: Bearer <token>'One page of a project’s decisions, newest first.
Requires the decisions:read scope, which is not granted by
default — mint a key that asks for it. Only settled states are
served; captured and interviewing are mid-conversation and are
never returned. Results are further limited to decisions owned by an
altitude your account belongs to — a relevance filter, not a
confidentiality boundary (altitude membership is self-service).
The filters block echoes what was actually applied, so an empty
page is never ambiguous about which narrowing produced it.
Refusals beyond the standard envelopes:
403 no_altitude_membership— your account belongs to no altitude, so nothing here is readable. A statement about your account, not about the project.422 owner_layer_not_permitted—owner_layerasked outside your membership. The filter can only narrow.422 state_not_readable—stateasked for a state this surface does not serve.
Authorizations
Section titled “Authorizations”Parameters
Section titled “Parameters”Query Parameters
Section titled “Query Parameters”Required. Decisions are project-scoped and the read is authorized per project.
Required. Decisions are project-scoped and the read is authorized per project.
Narrow to these states. Only settled outcomes are served; anything else is refused with 422 state_not_readable.
Narrow to these altitudes. Must be a subset of the calling principal’s altitude membership; otherwise 422 owner_layer_not_permitted.
Responses
Section titled “Responses”Successful Response
One page of decisions for one project.
has_more is explicit rather than inferred from
len(decisions) == limit: a page that happens to end exactly on
the boundary is indistinguishable from a full one, and a caller that
guesses wrong either stops early or pages forever.
It is ADVISORY. The count and the page are two statements under read
committed, so a decision captured between them can make has_more
a row stale. Page until a page comes back short or empty rather than
trusting it as an invariant — and expect the ordering (newest first)
to shift a row onto a page you have already read if the ledger is
being written while you walk it.
object
A decision as the /v1 surface serves it.
Deliberately narrower than the in-app serializer. The fields it omits are omitted for a reason, and the omission is the mitigation — adding one back is a disclosure decision, not a serializer tweak:
history— the append-only audit trail, which names the actor of every transition. Accountability data for humans in the app, not context for an agent.assignee_user_id— a user id, same reasoning.draft_ids/originating_interview_ref— pointers into interview transcripts, which are the candid text itself.interview_session— the transcript. Never.snoozed_until— in-app queue mechanics with no meaning to an external caller.flag_reason— why a human flagged the row for approval. An in-app queue annotation about the review, not about the decision, and it names no behaviour an agent can act on.
object
Where a decision is anchored. id is a free-text reference
whose meaning depends on type (a node/boundary UUID, an error
payload key, or absent for a project-level decision).
type is an OPEN set, published as a plain string, for exactly the
reason Finding.code is — see that model. The anchor vocabulary is
server-owned and grows additively as new things become anchorable, so
a consumer must tolerate a type it does not recognize.
The narrowing the server ACTUALLY applied, echoed back.
Not a copy of the query string: owner_layer omitted means “your
whole altitude membership”, state omitted means “every settled
state”, and neither is knowable client-side. Without the echo an
empty page is indistinguishable from “this project has no
decisions”, which is a different fact and sends the caller looking
in the wrong place.
object
Example
{ "decisions": [ { "anchor_ref": { "type": "node" } } ]}No credentials, malformed credentials, or revoked credentials. The envelope is leak-parity (same shape across all 401 paths) so an attacker cannot distinguish revoked vs. unknown via the body.
object
Example
{ "detail": "unauthenticated"}Credentials are valid but lack the scope required for this route, or the principal lacks membership in the target project.
object
object
Examplegenerated
{ "detail": { "code": "example", "message": "example" }}Resource not found OR not accessible to this principal. Leak parity: the response is identical in both cases so an attacker cannot enumerate resources via 404-vs-403 timing.
object
object
Example
{ "detail": { "code": "not_found" }}Validation Error
object
object
object
Examplegenerated
{ "detail": [ { "ctx": {}, "input": "example", "loc": [ "example" ], "msg": "example", "type": "example" } ]}Per-bucket rate limit exceeded. The response carries Retry-After and the standard X-RateLimit-* headers (Limit / Remaining / Reset).
object
Example
{ "detail": "rate_limited"}