Skip to content
Hoody.com

A turn is one user message dispatched into an agent session. This page covers the four ways to dispatch a turn, how to cancel one (the active turn on the session, or a named turn), how to send commands to a session that may already be working (a message, an interrupt, or a stop) and how to read their receipts, how to stop every running item in the realm, how to send input into a running workflow, and how to read the durable turn receipts that survive a disconnect or a daemon restart. All routes below honor per-request realm, container, cwd, and config-dir scoping through the standard X-Hoody-* headers (also accepted as query parameters for realm).

Four endpoints dispatch a turn, each with a different posture. Pick by what your caller can do, not by what feels familiar.

EndpointBlocks until doneStreams eventsRetry-safe with Idempotency-Key
POST /api/v1/agent/sessions/{id}/messagesNo (returns 202 immediately)No (observe the session stream separately)No
POST /api/v1/agent/sessions/{id}/prompt:syncUntil the turn ends, a gate needs a person, or 290 s (then 503 with details.turn_running)NoNo, but repeated calls against a parked session return the same pending_gate
POST /api/v1/agent/sessions/{id}/prompt:streamNo (opens SSE at once)Yes (SSE starting just before dispatch)No (replay returns 409 replay_unavailable)
POST /api/v1/agent/sessions/{id}/turnsNo (returns 202 receipt once admitted)No (observe the session stream separately)Yes (returns the same receipt for the same key)

Fire-and-observe. Dispatches a user turn and returns immediately with {job_id, session_id, turn_id}. Observe agent_done on the session stream (its turn_id matches the response) to learn the outcome. The session is single-writer: a parked gate (409 gate_parked) or a running turn (409 turn_in_flight) refuses a new dispatch. Not retry-safe; for retries, dispatch through POST /turns with an Idempotency-Key.

NameInTypeRequiredDescription
idpathstringYesThe session id.
X-Hoody-CwdheaderstringNoPer-request working-directory scope: the .hoody project layer / record cwd / tool+workflow cwd. Required by routes that resolve a cwd (e.g. POST /todos; createTodo also accepts a body cwd).
X-Hoody-Config-DirheaderstringNoPer-request --config-dir override selecting which on-disk .hoody install a stateless read/write resolves against.
X-Hoody-ContainerheaderstringNoPer-request bound remote container (omitted = local). Rejected (400) on routes with no container dimension.
X-Hoody-RealmheaderstringNoPer-request realm selector: “global” (not tied to a realm) or a 24-hex realm id (also accepted as ?realm=). On a session route it names the realm the session is looked up in: a session in another realm, or in a realm this login does not serve, is 404 not_found. Rejected (400 realm_scope_unsupported) on active-only / no-realm routes, and for any other realm on an agent pinned to one realm.
realmquerystringNoPer-request realm selector, the in:query alias of the X-Hoody-Realm header (read only when the header is absent): “global” (not tied to a realm) or a 24-hex realm id. On a session route it names the realm the session is looked up in: a session in another realm, or in a realm this login does not serve, is 404 not_found. Rejected (400 realm_scope_unsupported) on active-only / no-realm routes, and for any other realm on an agent pinned to one realm.
Idempotency-KeyheaderstringNoOpaque retry key (1–255 printable ASCII, no whitespace). A retry with the same key returns the same receipt and never re-runs the turn; the same key with a different request body is 422.
FieldTypeRequiredDescription
textstringYesRequired (may be empty only with attachments). The user message text for this turn: empty or whitespace-only text with no attachments is refused (400 empty_prompt) and no turn runs.
tool_modestringNoOptional per-turn tool mode override: standard or orchestrator. Any other value is refused 400 invalid_tool_mode.
dir_scopestringNoOptional per-turn directory-access scope override: home or full. Any other value is refused 400 invalid_dir_scope.
attachmentsarrayNoOptional images for this turn, as INLINE base64 bytes. At most 4 per turn, each at most 4 MiB DECODED (~5.33 MiB of base64); the request body limit applies on top and is authoritative, so several individually-legal images can still exceed it. A violation is refused with 400 BEFORE the turn is accepted, nothing is silently dropped. path is not supported here: it would name a file on the CALLER’s machine, and a path-bearing attachment is discarded outright on a container-bound session. If the active model cannot accept images the daemon reports that on the session stream.
{
"job_id": "job-9f2c4e1ab07d3355",
"session_id": "sess-9f2c4e1ab07d3355",
"turn_id": "turn-9f2c4e1ab07d3355"
}
import { HoodyClient } from 'hoody-sdk';
const client = new HoodyClient({ baseURL: 'https://{projectId}-{containerId}-agent-1.{server}.containers.hoody.com', token: process.env.HOODY_TOKEN });
await client.agent.sessions.startTurn('sess-9f2c4e1ab07d3355', {
text: 'Summarize the last 10 commits in the repo.'
});
Terminal window
curl -X POST "https://{projectId}-{containerId}-agent-1.{server}.containers.hoody.com/api/v1/agent/sessions/sess-9f2c4e1ab07d3355/messages" \
-H "Authorization: Bearer $HOODY_TOKEN" \
-H "Content-Type: application/json" \
-d '{ "text": "Summarize the last 10 commits in the repo." }'

POST /api/v1/agent/sessions/{id}/prompt:sync

Section titled “POST /api/v1/agent/sessions/{id}/prompt:sync”

Dispatches a turn and waits at most 290 seconds. Returns when the turn ends, or as soon as a turn parks on a confirm or question gate with 200 pending_gate (no status; the turn is still running). At the 290 s mark it returns 200 pending_gate if a gate needs a person, and otherwise 503 service_unavailable with details.turn_id and details.turn_running: true; the turn is NOT cancelled, follow it on the stream or with GET /sessions/{id}/turns/{turn_id}. Repeated prompt:sync against a parked session returns the same pending_gate (no duplicate parked turns). Pass X-Hoody-Gate-Policy: auto_approve (or ?policy=auto_approve) to adopt the headless posture and auto-answer confirm gates with approved=true (a confirm gate a tool-call rule raised is answered approved=false; question and plan gates still wait for /answer or /plan).

NameInTypeRequiredDescription
idpathstringYesThe session id.
policyquerystringNoauto_approve adopts the headless auto-answer posture (alias of the X-Hoody-Gate-Policy header); off by default; the gateway asks for no separate credentials to use it; access is decided by the container’s proxy permission policy.
X-Hoody-Gate-PolicyheaderstringNoConfirm-gate posture: “auto_approve” adopts the headless auto-answer posture (off by default; the in:query alias is ?policy=). Any other value is rejected 400.
X-Hoody-CwdheaderstringNoPer-request working-directory scope: the .hoody project layer / record cwd / tool+workflow cwd. Required by routes that resolve a cwd (e.g. POST /todos; createTodo also accepts a body cwd).
X-Hoody-Config-DirheaderstringNoPer-request --config-dir override selecting which on-disk .hoody install a stateless read/write resolves against.
X-Hoody-ContainerheaderstringNoPer-request bound remote container (omitted = local). Rejected (400) on routes with no container dimension.
X-Hoody-RealmheaderstringNoPer-request realm selector: “global” (not tied to a realm) or a 24-hex realm id (also accepted as ?realm=). On a session route it names the realm the session is looked up in: a session in another realm, or in a realm this login does not serve, is 404 not_found. Rejected (400 realm_scope_unsupported) on active-only / no-realm routes, and for any other realm on an agent pinned to one realm.
realmquerystringNoPer-request realm selector, the in:query alias of the X-Hoody-Realm header (read only when the header is absent): “global” (not tied to a realm) or a 24-hex realm id. On a session route it names the realm the session is looked up in: a session in another realm, or in a realm this login does not serve, is 404 not_found. Rejected (400 realm_scope_unsupported) on active-only / no-realm routes, and for any other realm on an agent pinned to one realm.
Idempotency-KeyheaderstringNoOpaque retry key (1–255 printable ASCII, no whitespace). The turn runs at most once per key: a retry answers 200 duplicate:true with the turn’s outcome (or pending_turn while it still runs) and never re-runs it; the same key with a different request body is 422.
FieldTypeRequiredDescription
textstringYesRequired (may be empty only with attachments). The user message text for this turn: empty or whitespace-only text with no attachments is refused (400 empty_prompt) and no turn runs.
tool_modestringNoOptional per-turn tool mode override: standard or orchestrator. Any other value is refused 400 invalid_tool_mode.
dir_scopestringNoOptional per-turn directory-access scope override: home or full. Any other value is refused 400 invalid_dir_scope.
attachmentsarrayNoOptional images for this turn, as INLINE base64 bytes. At most 4 per turn, each at most 4 MiB DECODED (~5.33 MiB of base64); the request body limit applies on top and is authoritative, so several individually-legal images can still exceed it. A violation is refused with 400 BEFORE the turn is accepted, nothing is silently dropped. path is not supported here: it would name a file on the CALLER’s machine, and a path-bearing attachment is discarded outright on a container-bound session. If the active model cannot accept images the daemon reports that on the session stream.
{
"status": "done",
"session_id": "sess-9f2c4e1ab07d3355",
"turn_id": "turn-9f2c4e1ab07d3355"
}

A parked turn returns {pending_gate} with no status. Other outcomes carry status: done, error, canceled, or quit. A 290 s timeout returns pending_gate if a gate needs a person, otherwise 503 service_unavailable with details.turn_id and details.turn_running: true.

import { HoodyClient } from 'hoody-sdk';
const client = new HoodyClient({ baseURL: 'https://{projectId}-{containerId}-agent-1.{server}.containers.hoody.com', token: process.env.HOODY_TOKEN });
const outcome = await client.agent.sessions.turns.run('sess-9f2c4e1ab07d3355', {
text: 'Summarize the last 10 commits in the repo.'
});
Terminal window
curl -X POST "https://{projectId}-{containerId}-agent-1.{server}.containers.hoody.com/api/v1/agent/sessions/sess-9f2c4e1ab07d3355/prompt:sync" \
-H "Authorization: Bearer $HOODY_TOKEN" \
-H "Content-Type: application/json" \
-d '{ "text": "Summarize the last 10 commits in the repo." }'

POST /api/v1/agent/sessions/{id}/prompt:stream

Section titled “POST /api/v1/agent/sessions/{id}/prompt:stream”

Stream the response. Dispatches a turn and opens an SSE stream positioned at the seq captured just before dispatch, so the first frames are this turn’s rather than the retained ring’s backlog.

Pass X-Hoody-Gate-Policy: auto_approve (or ?policy=auto_approve) to auto-answer confirm gates with approved=true for the life of the stream. Off by default.

NameInTypeRequiredDescription
idpathstringYesThe session id.
policyquerystringNoauto_approve auto-answers confirm gates for the life of the stream (alias of the X-Hoody-Gate-Policy header); off by default.
X-Hoody-Gate-PolicyheaderstringNoConfirm-gate posture: auto_approve adopts the headless auto-answer posture. Any other value is rejected with 400.
X-Hoody-CwdheaderstringNoPer-request working-directory scope.
X-Hoody-Config-DirheaderstringNoPer-request --config-dir override.
X-Hoody-ContainerheaderstringNoPer-request bound remote container (omitted = local).
X-Hoody-RealmheaderstringNoPer-request realm selector (global or a 24-hex id).
realmquerystringNoPer-request realm selector (query alias of X-Hoody-Realm).
Idempotency-KeyheaderstringNoOpaque retry key (1–255 printable ASCII, no whitespace). The turn runs at most once per key: a retry is 409 replay_unavailable with the turn’s receipt (details.turn), never a re-run and never a second stream; the same key with a different request body is 422.
FieldTypeRequiredDescription
textstringYesThe user message text for this turn.
tool_modestringNoOptional per-turn tool mode override.
dir_scopestringNoOptional per-turn directory-access scope override (home or full).
attachmentsarrayNoOptional inline base64 images (at most 4 per turn, each at most 4 MiB decoded).

Response is text/event-stream. Stop reading at the agent_done whose turn_id matches the X-Hoody-Turn-Id response header, then close the connection yourself.

event: agent_text_delta
data: {"text": "I'll look that up.\n"}
event: agent_tool_use
data: {"name": "shell", "input": {"cmd": "git log --oneline -10"}}
event: agent_text_delta
data: {"text": "Last 10 commits:\n"}
event: agent_done
data: {"turn_id": "turn-9f2c4e1ab07d3355", "state": "completed", "outcome": "completed"}
import { HoodyClient } from 'hoody-sdk';
const client = new HoodyClient({ baseURL: 'https://{projectId}-{containerId}-agent-1.{server}.containers.hoody.com', token: process.env.HOODY_TOKEN });
await client.agent.sessions.startTurnAndStream('sess-9f2c4e1ab07d3355', {
text: 'What is in this image?',
attachments: [
{
type: 'image',
media_type: 'image/png',
data: 'iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAQAAAC1HAwCAAAAC0lEQVR42mNkAAIAAAoAAv/lxKUAAAAASUVORK5CYII='
}
]
}, {
policy: 'auto_approve'
});
Terminal window
curl -X POST "https://{projectId}-{containerId}-agent-1.{server}.containers.hoody.com/api/v1/agent/sessions/sess-9f2c4e1ab07d3355/prompt:stream?policy=auto_approve" \
-H "Authorization: Bearer $HOODY_TOKEN" \
-H "Content-Type: application/json" \
-H "Accept: text/event-stream" \
-d '{ "text": "What is in this image?", "attachments": [{ "type": "image", "media_type": "image/png", "data": "iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAQAAAC1HAwCAAAAC0lEQVR42mNkAAIAAAoAAv/lxKUAAAAASUVORK5CYII=" }] }'

Retry-safe dispatch. Dispatches a user turn and returns 202 with a durable receipt once the daemon has admitted it.

The session is single-writer: a parked gate (409 gate_parked) or a running turn (409 turn_in_flight) refuses a new dispatch and records nothing. If the daemon does not acknowledge within the wait the answer is 503 admission_unconfirmed carrying turn_id and result_url; the turn may still have been accepted, so poll result_url before retrying (acceptance is never invented and never rolled back). A ledger write failure is 503 admission_write_failed (fail-closed). A turn interrupted by a daemon restart replays as state interrupted with effects_may_have_occurred and is never re-run.

NameInTypeRequiredDescription
idpathstringYesThe session id.
Idempotency-KeyheaderstringNoOpaque retry key (1 to 255 printable ASCII, no whitespace). A retry with the same key returns the same receipt and never re-runs the turn; the same key with a different request body returns 422.
X-Hoody-CwdheaderstringNoPer-request working-directory scope.
X-Hoody-Config-DirheaderstringNoPer-request --config-dir override.
X-Hoody-ContainerheaderstringNoPer-request bound remote container (omitted = local).
X-Hoody-RealmheaderstringNoPer-request realm selector (global or a 24-hex id).
realmquerystringNoPer-request realm selector (query alias of X-Hoody-Realm).
FieldTypeRequiredDescription
textstringYesThe user message text for this turn.
tool_modestringNoOptional per-turn tool mode override.
dir_scopestringNoOptional per-turn directory-access scope override (home or full).
attachmentsarrayNoOptional inline base64 images (at most 4 per turn, each at most 4 MiB decoded).
{
"turn_id": "turn-9f2c4e1ab07d3355",
"job_id": "job-9f2c4e1ab07d3355",
"state": "accepted",
"duplicate": false,
"accepted_at": "2025-04-12T14:32:11Z",
"result_url": "/api/v1/agent/sessions/sess-9f2c4e1ab07d3355/turns/turn-9f2c4e1ab07d3355",
"stream_url": "/api/v1/agent/sessions/sess-9f2c4e1ab07d3355/stream",
"notices": []
}
import { HoodyClient } from 'hoody-sdk';
const client = new HoodyClient({ baseURL: 'https://{projectId}-{containerId}-agent-1.{server}.containers.hoody.com', token: process.env.HOODY_TOKEN });
const receipt = await client.agent.sessions.turns.create('sess-9f2c4e1ab07d3355', {
text: 'Summarize the last 10 commits in the repo.'
}, {
IdempotencyKey: '01HX5G2EXAMPLE1234567890AB'
});
Terminal window
curl -X POST "https://{projectId}-{containerId}-agent-1.{server}.containers.hoody.com/api/v1/agent/sessions/sess-9f2c4e1ab07d3355/turns" \
-H "Authorization: Bearer $HOODY_TOKEN" \
-H "Content-Type: application/json" \
-H "Idempotency-Key: 01HX5G2EXAMPLE1234567890AB" \
-d '{ "text": "Summarize the last 10 commits in the repo." }'

Cancels the active turn on the session (the historical “Esc” behaviour), or, when a turn_id is supplied, cancels only that named turn.

NameInTypeRequiredDescription
idpathstringYesThe session id.
turn_idquerystringNoCancel only this turn (alternative to the body field).
X-Hoody-CwdheaderstringNoPer-request working-directory scope.
X-Hoody-Config-DirheaderstringNoPer-request --config-dir override.
X-Hoody-ContainerheaderstringNoPer-request bound remote container (omitted = local).
X-Hoody-RealmheaderstringNoPer-request realm selector (global or a 24-hex id).
realmquerystringNoPer-request realm selector (query alias of X-Hoody-Realm).
FieldTypeRequiredDescription
turn_idstringNoThe turn to cancel, as returned when it was dispatched. Cancels only that turn. Omit (or send {}) to cancel whatever is running.
{
"status": "ok",
"turn_id": "turn-9f2c4e1ab07d3355",
"matched": true
}

matched is present on a turn-scoped cancel: false when the named turn no longer held the slot, in which case nothing was cancelled.

import { HoodyClient } from 'hoody-sdk';
const client = new HoodyClient({ baseURL: 'https://{projectId}-{containerId}-agent-1.{server}.containers.hoody.com', token: process.env.HOODY_TOKEN });
// Cancel a specific turn by id.
const result = await client.agent.sessions.turns.cancel('sess-9f2c4e1ab07d3355', {
turn_id: 'turn-9f2c4e1ab07d3355'
});
// Cancel whatever turn is currently running on the session.
await client.agent.sessions.turns.cancel('sess-9f2c4e1ab07d3355', {});
Terminal window
# Cancel a specific turn.
curl -X POST "https://{projectId}-{containerId}-agent-1.{server}.containers.hoody.com/api/v1/agent/sessions/sess-9f2c4e1ab07d3355/cancel" \
-H "Authorization: Bearer $HOODY_TOKEN" \
-H "Content-Type: application/json" \
-d '{ "turn_id": "turn-9f2c4e1ab07d3355" }'
# Cancel whatever turn is currently running.
curl -X POST "https://{projectId}-{containerId}-agent-1.{server}.containers.hoody.com/api/v1/agent/sessions/sess-9f2c4e1ab07d3355/cancel" \
-H "Authorization: Bearer $HOODY_TOKEN" \
-H "Content-Type: application/json" \
-d '{}'

Commands: reaching a session that may already be working

Section titled “Commands: reaching a session that may already be working”

Reach a live session, working or idle, with a message, an interrupt, or a stop. Returns 202 with the command’s receipt once the agent has recorded it. The Idempotency-Key header is required: a retry with the same key and the same command returns the stored receipt (with duplicate: true) and admits nothing; the same key with a different command is 422 idempotency_key_reused.

Commands reach live sessions only: a closed session is 409 session_closed, an unknown session is 404, and a headless or internal session (a TODO run) is 409 command_unsupported. A configured UserPromptSubmit hook sees each message before delivery and can refuse it (state refused, reason hook). Stream events announce delivered commands (event.command_committed) and the ones that ended otherwise (event.command_settled).

Sends a command to a live session.

Kind message delivers text to the session at its next step. An idle session starts a turn for the message. text is required, non-empty, and at most 32 KiB (more is 400 bad_request).

Kind interrupt stops the work the session was running when the interrupt was sent, then delivers text as a new turn. Background tasks, background workflow runs, and background shells keep running (a stop ends them). The receipt says whether work was running (interrupted). text is required, non-empty, and at most 32 KiB.

Kind stop stops all of the session’s work, pauses its loops, and supersedes every message and interrupt sent before it that has not been delivered. A stop carries no text. The receipt’s stopped lists what the stop acted on and how each ended. With close: true the session is closed once its work is stopped, and later commands are 409 session_closed.

order (optional, at least 0) sequences one caller’s commands: a message or interrupt with a lower order than a stop already received is superseded at once (reason: order). from and trigger are attribution only (they change how the message is introduced to the model); the agent does not verify them. The text waiting on one session is at most 128 KiB (more is 429 command_queue_full with Retry-After).

NameInTypeRequiredDescription
idpathstringYesThe session id.
Idempotency-KeyheaderstringYesRetry key, required on this route; the SDK and CLI send one automatically. 1–255 printable ASCII, no whitespace. A retry with the same key and command returns the stored receipt and admits nothing; the same key with a different command is 422. The key is remembered while the command waits and for 24 hours after it settled. Past 4096 remembered keys a new key is 429 idempotency_keys_exhausted; a stop is never refused because of the remembered-key limit.
X-Hoody-CwdheaderstringNoPer-request working-directory scope: the .hoody project layer / record cwd / tool+workflow cwd. Required by routes that resolve a cwd (e.g. POST /todos; createTodo also accepts a body cwd).
X-Hoody-Config-DirheaderstringNoPer-request --config-dir override selecting which on-disk .hoody install a stateless read/write resolves against.
X-Hoody-ContainerheaderstringNoPer-request bound remote container (omitted = local). Rejected (400) on routes with no container dimension.
X-Hoody-RealmheaderstringNoPer-request realm selector: “global” (not tied to a realm) or a 24-hex realm id (also accepted as ?realm=). On a session route it names the realm the session is looked up in: a session in another realm, or in a realm this login does not serve, is 404 not_found. Rejected (400 realm_scope_unsupported) on active-only / no-realm routes, and for any other realm on an agent pinned to one realm.
realmquerystringNoPer-request realm selector, the in:query alias of the X-Hoody-Realm header (read only when the header is absent): “global” (not tied to a realm) or a 24-hex realm id. On a session route it names the realm the session is looked up in: a session in another realm, or in a realm this login does not serve, is 404 not_found. Rejected (400 realm_scope_unsupported) on active-only / no-realm routes, and for any other realm on an agent pinned to one realm.
FieldTypeRequiredDescription
kindstringYesmessage, interrupt, or stop.
textstringNoThe message text (message and interrupt: required, non-empty, at most 32 KiB). Delivered verbatim. A stop takes none.
closebooleanNoA stop only: close the session once its work is stopped.
on_gatestringNoA message only: deny (default) declines a confirmation or question the session is waiting on so the message is read now; wait leaves it waiting.
orderintegerNoOptional, at least 0: a sequence number for one caller’s commands. A message or interrupt with a lower order than a stop already received is superseded.
fromobjectNoWho sends it (kind enum: user, bot, delegate, system; optional id of at most 256 bytes). Attribution only, never authorization.
triggerstringNoWhat caused it (enum: human, wake, clear). Recorded on the receipt. Attribution only, never authorization.
{
"kind": "message",
"text": "Also update the README when you are done."
}
{
"admitted_at": "2026-10-08T14:03:11.204Z",
"command_id": "cmd_3f2a9c1e5b7d40a1c2e8f6d9",
"kind": "message",
"state": "queued"
}

A receipt carries the command’s state: queued, committed, superseded, refused, or failed. A committed receipt also carries turn_id and how (next_step, new_turn, or stop). A stop receipt’s stopped lists what it acted on (kind session, task, workflow_run, bash_job, or loop) and the outcome per item (stopped, already_done, or failed).

import { HoodyClient } from 'hoody-sdk';
const client = new HoodyClient({ baseURL: 'https://{projectId}-{containerId}-agent-1.{server}.containers.hoody.com', token: process.env.HOODY_TOKEN });
const receipt = await client.agent.sessions.commands.send('sess-9f2c4e1ab07d3355', {
kind: 'message',
text: 'Also update the README when you are done.'
}, {
IdempotencyKey: '01HX5G2EXAMPLE1234567890AB'
});
// Track a stop; the receipt's stopped tells you what it ended.
const stop = await client.agent.sessions.commands.send('sess-9f2c4e1ab07d3355', {
kind: 'stop',
close: true
}, {
IdempotencyKey: '01HX5G2EXAMPLE1234567890CD'
});
Terminal window
# Send a message.
curl -X POST "https://{projectId}-{containerId}-agent-1.{server}.containers.hoody.com/api/v1/agent/sessions/sess-9f2c4e1ab07d3355/commands" \
-H "Authorization: Bearer $HOODY_TOKEN" \
-H "Content-Type: application/json" \
-H "Idempotency-Key: 01HX5G2EXAMPLE1234567890AB" \
-d '{ "kind": "message", "text": "Also update the README when you are done." }'
# Stop everything and close the session.
curl -X POST "https://{projectId}-{containerId}-agent-1.{server}.containers.hoody.com/api/v1/agent/sessions/sess-9f2c4e1ab07d3355/commands" \
-H "Authorization: Bearer $HOODY_TOKEN" \
-H "Content-Type: application/json" \
-H "Idempotency-Key: 01HX5G2EXAMPLE1234567890CD" \
-d '{ "kind": "stop", "close": true }'

GET /api/v1/agent/sessions/{id}/commands/{command_id}

Section titled “GET /api/v1/agent/sessions/{id}/commands/{command_id}”

Returns the receipt of a command sent to this session. state is queued until the command is delivered, then committed (with turn_id and how), superseded, refused, or failed. The session must be live: a closed session is 409 session_closed. A command id this session never admitted is 404, and so is one whose receipt is no longer kept (a receipt is kept while its command waits and for 24 hours after it settled). Cache-Control: no-store.

NameInTypeRequiredDescription
idpathstringYesThe session id.
command_idpathstringYesThe command id.
X-Hoody-CwdheaderstringNoPer-request working-directory scope: the .hoody project layer / record cwd / tool+workflow cwd. Required by routes that resolve a cwd (e.g. POST /todos; createTodo also accepts a body cwd).
X-Hoody-Config-DirheaderstringNoPer-request --config-dir override selecting which on-disk .hoody install a stateless read/write resolves against.
X-Hoody-ContainerheaderstringNoPer-request bound remote container (omitted = local). Rejected (400) on routes with no container dimension.
X-Hoody-RealmheaderstringNoPer-request realm selector: “global” (not tied to a realm) or a 24-hex realm id (also accepted as ?realm=). On a session route it names the realm the session is looked up in: a session in another realm, or in a realm this login does not serve, is 404 not_found. Rejected (400 realm_scope_unsupported) on active-only / no-realm routes, and for any other realm on an agent pinned to one realm.
realmquerystringNoPer-request realm selector, the in:query alias of the X-Hoody-Realm header (read only when the header is absent): “global” (not tied to a realm) or a 24-hex realm id. On a session route it names the realm the session is looked up in: a session in another realm, or in a realm this login does not serve, is 404 not_found. Rejected (400 realm_scope_unsupported) on active-only / no-realm routes, and for any other realm on an agent pinned to one realm.
{
"admitted_at": "2026-10-08T14:03:11.204Z",
"command_id": "cmd_3f2a9c1e5b7d40a1c2e8f6d9",
"kind": "message",
"state": "queued"
}

A committed receipt carries turn_id (the running turn when how is next_step, or the turn it started when how is new_turn; absent for a stop) and committed_at. A superseded receipt carries superseded_by (the command_id of the stop that superseded it); reason is stop (a stop sent after it) or order (a stop with a higher order). A refused receipt carries reason (hook, closed, or unsupported) and an optional detail (for example, the hook’s message). A failed receipt carries reason: write_failed. A stop receipt’s stopped lists what it acted on and how each ended; interrupted appears on an interrupt receipt (whether running work was stopped for it).

import { HoodyClient } from 'hoody-sdk';
const client = new HoodyClient({ baseURL: 'https://{projectId}-{containerId}-agent-1.{server}.containers.hoody.com', token: process.env.HOODY_TOKEN });
// Poll the receipt returned by POST /commands.
const r = await client.agent.sessions.commands.get('sess-9f2c4e1ab07d3355', 'cmd_3f2a9c1e5b7d40a1c2e8f6d9');
const state = r.data.state;
Terminal window
curl -X GET "https://{projectId}-{containerId}-agent-1.{server}.containers.hoody.com/api/v1/agent/sessions/sess-9f2c4e1ab07d3355/commands/cmd_3f2a9c1e5b7d40a1c2e8f6d9" \
-H "Authorization: Bearer $HOODY_TOKEN"

Stops, in one call, all running work in the realm the caller may see. It covers running or parked session turns (every live session, including system, resident, and headless ones; each is stopped like POST /sessions/{id}/cancel with the turn_id of the turn that was running when the call listed it, so a turn that starts later is never stopped, only reported under started_during_call), background tasks, workflow runs, todo runs (the lease is released as cancelled, as cancelTodoRun does), detached local background bash jobs, detached background bash jobs on bound containers, loops (PAUSED, never deleted: resume one with PATCH /sessions/{id}/loops/{loopId} {"paused":false}), and this gateway’s detached headless and gated tool jobs. Work it does not reach is named in not_covered.

NameInTypeRequiredDescription
X-Hoody-RealmheaderstringNoPer-request realm selector: “global” (not tied to a realm) or a 24-hex realm id (also accepted as ?realm=). On a session route it names the realm the session is looked up in: a session in another realm, or in a realm this login does not serve, is 404 not_found. Rejected (400 realm_scope_unsupported) on active-only / no-realm routes, and for any other realm on an agent pinned to one realm.
realmquerystringNoPer-request realm selector, the in:query alias of the X-Hoody-Realm header (read only when the header is absent): “global” (not tied to a realm) or a 24-hex realm id. On a session route it names the realm the session is looked up in: a session in another realm, or in a realm this login does not serve, is 404 not_found. Rejected (400 realm_scope_unsupported) on active-only / no-realm routes, and for any other realm on an agent pinned to one realm.
FieldTypeRequiredDescription
afterstringNoA next_cursor from an earlier call, unchanged (the cursor is opaque): act only on the items after it. Any other value is 400 bad_request.
{
"behind_cursor": [],
"complete": true,
"items": [
{
"id": "5f0c2a91d3b44e7f",
"kind": "session",
"outcome": "stopped"
},
{
"id": "loop-3",
"kind": "loop",
"outcome": "stopped"
}
],
"not_covered": [
{
"kind": "sync_request",
"reason": "synchronous and streaming tool and headless requests end when their HTTP request is closed"
}
],
"started_during_call": []
}
import { HoodyClient } from 'hoody-sdk';
const client = new HoodyClient({ baseURL: 'https://{projectId}-{containerId}-agent-1.{server}.containers.hoody.com', token: process.env.HOODY_TOKEN });
// Stop everything running in the realm (start from the first item).
const result = await client.agent.stopAllWork({});
// Continue with the items behind an earlier next_cursor.
const next = await client.agent.stopAllWork({ after: result.data.next_cursor });
Terminal window
# Stop everything running in the realm.
curl -X POST "https://{projectId}-{containerId}-agent-1.{server}.containers.hoody.com/api/v1/agent/stop" \
-H "Authorization: Bearer $HOODY_TOKEN" \
-H "Content-Type: application/json" \
-d '{}'
# Continue with the items behind an earlier next_cursor.
curl -X POST "https://{projectId}-{containerId}-agent-1.{server}.containers.hoody.com/api/v1/agent/stop" \
-H "Authorization: Bearer $HOODY_TOKEN" \
-H "Content-Type: application/json" \
-d '{ "after": "eyJraW5kIjoic2Vzc2lvbiIsImlkIjoiNWYwYzJhOTFkM2I0NGU3ZiJ9" }'

POST /api/v1/agent/sessions/{id}/workflow/messages

Section titled “POST /api/v1/agent/sessions/{id}/workflow/messages”

Injects user feedback or input into a running workflow on the session. The message is forwarded to the live session’s workflowMsgChan (not commandChan), so the running workflow receives it as input without being treated as a slash command. Use this only when a workflow is running on the session.

NameInTypeRequiredDescription
idpathstringYesThe session id.
X-Hoody-CwdheaderstringNoPer-request working-directory scope.
X-Hoody-Config-DirheaderstringNoPer-request --config-dir override.
X-Hoody-ContainerheaderstringNoPer-request bound remote container (omitted = local).
X-Hoody-RealmheaderstringNoPer-request realm selector (global or a 24-hex id).
realmquerystringNoPer-request realm selector (query alias of X-Hoody-Realm).
FieldTypeRequiredDescription
textstringNoFeedback or input text fed to the running workflow.
{
"status": "ok"
}

When a gate is parked, the response carries deferred: true and a note explaining that the message may not have taken effect yet (it is applied only between turns).

import { HoodyClient } from 'hoody-sdk';
const client = new HoodyClient({ baseURL: 'https://{projectId}-{containerId}-agent-1.{server}.containers.hoody.com', token: process.env.HOODY_TOKEN });
await client.agent.workflows.sendMessage('sess-9f2c4e1ab07d3355', {
text: 'Use the staged branch for the next attempt.'
});
Terminal window
curl -X POST "https://{projectId}-{containerId}-agent-1.{server}.containers.hoody.com/api/v1/agent/sessions/sess-9f2c4e1ab07d3355/workflow/messages" \
-H "Authorization: Bearer $HOODY_TOKEN" \
-H "Content-Type: application/json" \
-d '{ "text": "Use the staged branch for the next attempt." }'

Lists the durable receipts of the session’s turns, newest first. Each receipt is the same record returned by GET /sessions/{id}/turns/{turn_id}. This is how a client that lost its turn ids after a disconnect finds them again. Works for a live or dormant session. Pass ?limit to cap the count (1 to 1000; the default is all retained rows). Keyed and client-identified rows are never pruned. Responses carry Cache-Control: no-store.

NameInTypeRequiredDescription
idpathstringYesThe session id.
limitqueryintegerNoReturn at most this many receipts, newest first (1 to 1000). A cap, not a page size: there is no next page.
X-Hoody-CwdheaderstringNoPer-request working-directory scope.
X-Hoody-Config-DirheaderstringNoPer-request --config-dir override.
X-Hoody-ContainerheaderstringNoPer-request bound remote container (omitted = local).
X-Hoody-RealmheaderstringNoPer-request realm selector (global or a 24-hex id).
realmquerystringNoPer-request realm selector (query alias of X-Hoody-Realm).
{
"session_id": "sess-9f2c4e1ab07d3355",
"turns": [
{
"turn_id": "turn-9f2c4e1ab07d3355",
"job_id": "job-9f2c4e1ab07d3355",
"state": "completed",
"duplicate": false,
"outcome": "completed",
"effects_may_have_occurred": false,
"accepted_at": "2025-04-12T14:32:11Z",
"terminal_at": "2025-04-12T14:33:02Z",
"result_url": "/api/v1/agent/sessions/sess-9f2c4e1ab07d3355/turns/turn-9f2c4e1ab07d3355",
"stream_url": "/api/v1/agent/sessions/sess-9f2c4e1ab07d3355/stream",
"notices": []
},
{
"turn_id": "turn-3a2f1e0d9c8b7a6b",
"job_id": "job-3a2f1e0d9c8b7a6b",
"state": "failed",
"duplicate": false,
"outcome": "failed",
"error_code": "no_credentials",
"effects_may_have_occurred": false,
"accepted_at": "2025-04-12T14:18:47Z",
"terminal_at": "2025-04-12T14:19:01Z",
"result_url": "/api/v1/agent/sessions/sess-9f2c4e1ab07d3355/turns/turn-3a2f1e0d9c8b7a6b",
"stream_url": "/api/v1/agent/sessions/sess-9f2c4e1ab07d3355/stream",
"notices": []
}
]
}
import { HoodyClient } from 'hoody-sdk';
const client = new HoodyClient({ baseURL: 'https://{projectId}-{containerId}-agent-1.{server}.containers.hoody.com', token: process.env.HOODY_TOKEN });
// Most recent 50 receipts.
const recent = await client.agent.sessions.turns.list('sess-9f2c4e1ab07d3355', { limit: 50 });
const turns = recent.data.turns;
// All retained receipts.
const all = await client.agent.sessions.turns.list('sess-9f2c4e1ab07d3355');
Terminal window
curl -X GET "https://{projectId}-{containerId}-agent-1.{server}.containers.hoody.com/api/v1/agent/sessions/sess-9f2c4e1ab07d3355/turns?limit=50" \
-H "Authorization: Bearer $HOODY_TOKEN"

GET /api/v1/agent/sessions/{id}/turns/{turn_id}

Section titled “GET /api/v1/agent/sessions/{id}/turns/{turn_id}”

Returns the durable record of a turn: state, terminal outcome, error_code once terminal, and effects_may_have_occurred for a turn interrupted by a restart after a tool may have acted. Works for a live or dormant session. A turn the ledger never recorded returns 404 turn_not_found. Responses carry Cache-Control: no-store.

The state field is one of accepted, dispatched, completed, failed, cancelled, or interrupted. outcome and error_code are absent until the turn is terminal.

NameInTypeRequiredDescription
idpathstringYesThe session id.
turn_idpathstringYesThe turn id.
X-Hoody-CwdheaderstringNoPer-request working-directory scope.
X-Hoody-Config-DirheaderstringNoPer-request --config-dir override.
X-Hoody-ContainerheaderstringNoPer-request bound remote container (omitted = local).
X-Hoody-RealmheaderstringNoPer-request realm selector (global or a 24-hex id).
realmquerystringNoPer-request realm selector (query alias of X-Hoody-Realm).
{
"turn_id": "turn-9f2c4e1ab07d3355",
"job_id": "job-9f2c4e1ab07d3355",
"state": "completed",
"duplicate": false,
"outcome": "completed",
"effects_may_have_occurred": false,
"accepted_at": "2025-04-12T14:32:11Z",
"terminal_at": "2025-04-12T14:33:02Z",
"result_url": "/api/v1/agent/sessions/sess-9f2c4e1ab07d3355/turns/turn-9f2c4e1ab07d3355",
"stream_url": "/api/v1/agent/sessions/sess-9f2c4e1ab07d3355/stream",
"notices": []
}
import { HoodyClient } from 'hoody-sdk';
const client = new HoodyClient({ baseURL: 'https://{projectId}-{containerId}-agent-1.{server}.containers.hoody.com', token: process.env.HOODY_TOKEN });
const receipt = await client.agent.sessions.turns.get('sess-9f2c4e1ab07d3355', 'turn-9f2c4e1ab07d3355');
Terminal window
curl -X GET "https://{projectId}-{containerId}-agent-1.{server}.containers.hoody.com/api/v1/agent/sessions/sess-9f2c4e1ab07d3355/turns/turn-9f2c4e1ab07d3355" \
-H "Authorization: Bearer $HOODY_TOKEN"