Read one decision
const url = 'https://api.hydrate.sh/v1/decisions/2489E9AD-2EE2-8E00-8EC9-32D5F69181C0';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/2489E9AD-2EE2-8E00-8EC9-32D5F69181C0 \ --header 'Authorization: Bearer <token>'One decision by id.
Requires the decisions:read scope. A decision you may not read —
because it belongs to another project, another altitude, or is still
mid-conversation — returns the same 404 as one that does not
exist.
The URL is flat (no project component) because a decision id is globally unique. Authorization is still per project: the caller’s altitude membership is resolved FIRST, then the row is loaded, its project resolved through the membership gate, and only then the altitude + state filters — every one of those failures producing the SAME 404 a nonexistent id gets, so this route is not an oracle for “that decision exists but you may not see it”.
403 no_altitude_membership is the one refusal that is not a 404,
and it is raised before any id is looked at, so it says nothing about
the id.
Authorizations
Section titled “Authorizations”Parameters
Section titled “Parameters”Path Parameters
Section titled “Path Parameters”Responses
Section titled “Responses”Successful Response
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.
Example
{ "decision": { "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"}