Skip to content

List decisions for a project

GET
/v1/decisions
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_permittedowner_layer asked outside your membership. The filter can only narrow.
  • 422 state_not_readablestate asked for a state this surface does not serve.
project_id
required
Project Id

Required. Decisions are project-scoped and the read is authorized per project.

string format: uuid

Required. Decisions are project-scoped and the read is authorized per project.

state
Any of:
Array<string>
<= 16 items

Narrow to these states. Only settled outcomes are served; anything else is refused with 422 state_not_readable.

owner_layer
Any of:
Array<string>
<= 16 items

Narrow to these altitudes. Must be a subset of the calling principal’s altitude membership; otherwise 422 owner_layer_not_permitted.

limit
Limit
integer
default: 50 >= 1 <= 200
offset
Offset
integer
0

Successful Response

Media typeapplication/json
DecisionsListResponse

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
decisions
required
Decisions
Array<object>
DecisionOut

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
anchor_ref
required
AnchorRef

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.

object
id
required
Any of:
string
type
required
Type

What the decision is anchored to. Known values: boundary, node, error_payload, project. This set is open and grows additively — treat an unrecognized type as an opaque string and render it; do not fail on it.

string
annotations
required
Any of:
string
capture_text
required
Capture Text
string
captured_at
required
Captured At
string
check
required
Any of:
string
id
required
Id
string format: uuid
owner_layer
required
Owner Layer
string
parent_decision_id
required
Any of:
string format: uuid
project_id
required
Project Id
string format: uuid
resulting_node_ids
required
Resulting Node Ids
Array<string>
source_layer
required
Source Layer
string
state
required
State
string
filters
required
AppliedDecisionFilters

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
owner_layer
required
Owner Layer
Array<string>
state
required
State
Array<string>
has_more
required
Has More
boolean
limit
required
Limit
integer
offset
required
Offset
integer
project_id
required
Project Id
string format: uuid
version
required
Version
string
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.

Media typeapplication/json
object
detail
required
string
Example
{
"detail": "unauthenticated"
}

Credentials are valid but lack the scope required for this route, or the principal lacks membership in the target project.

Media typeapplication/json
object
detail
object
code
string
message
string
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.

Media typeapplication/json
object
detail
object
code
string
message
string
Example
{
"detail": {
"code": "not_found"
}
}

Validation Error

Media typeapplication/json
HTTPValidationError
object
detail
Detail
Array<object>
ValidationError
object
ctx
Context
object
input
Input
loc
required
Location
Array
msg
required
Message
string
type
required
Error Type
string
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).

Media typeapplication/json
object
detail
string
Example
{
"detail": "rate_limited"
}
Retry-After
integer
X-RateLimit-Limit
integer
X-RateLimit-Remaining
integer
X-RateLimit-Reset
integer