> ## Documentation Index
> Fetch the complete documentation index at: https://www.agent37.com/docs/llms.txt
> Use this file to discover all available pages before exploring further.

# Send a message

> The core call: POST /v1/responses on your instance URL runs a turn through your agent, streams or returns the result, and continues the thread.

`POST /v1/responses` is the core call. You make it against your instance, not against `api.agent37.com`: every instance serves its own chat API at `https://{instanceId}.agent37.app`, the `url` of the default port in the create response. This page uses `https://ab12cd34ef.agent37.app`. Authenticate with the same `sk_live_` key you use on the hosting API, sent as the `X-Agent37-Key` header.

The call is agentic by default: the agent can browse, run code, use a terminal, read and write files, call connected tools, and reason across many steps before answering.

## Request body

Request bodies are capped at 2 MB; anything larger returns `413 payload_too_large`.

<ParamField body="input" type="string" required>
  The message or task, a plain string. There is no image field; to attach files, upload them first and list their paths in `files`. See [Sessions](/docs/agents-api/sessions) for how history carries across turns.
</ParamField>

<ParamField body="files" type="string[]">
  Paths of files on the instance to attach to this turn, typically the `path` returned by [`PUT /v1/files/content`](/docs/agents-api/files). Each must name an existing file on the instance, or the call returns `400 validation_error`. The paths are appended to the input, and the agent reads them from disk.
</ParamField>

<ParamField body="session_id" type="string">
  Continue an existing conversation. Omit it to start a new one; the response returns the new session's id. The harness owns sessions and creates one on first use, so an id it has not seen simply starts a fresh thread under that id rather than erroring. The exception is a harness that mints its own session ids: `codex` and `opencode` reject a first turn on an id they did not issue with `400 validation_error` (`param: session_id`), so on those two always start a thread by omitting `session_id` and reuse the id the response returns.
</ParamField>

<ParamField body="stream" type="boolean" default="false">
  `true` returns a Server-Sent Events stream; `false` returns the finished response as one JSON body. See [Streaming](/docs/agents-api/streaming).
</ParamField>

<ParamField body="model" type="string">
  The LLM to run this turn on, an id from `GET /v1/models` (on OpenClaw the ids are `provider/model`). On Hermes it applies to this turn only: omit it and the turn runs on the instance's default model, whatever earlier turns chose. On OpenClaw a named model is written to the session and sticks for the turns that follow; a turn that omits it leaves the session's model as is. A `model` OpenClaw does not allow (an id outside `GET /v1/models`) never starts the turn: the response settles with `status: "failed"` and `error.code` `agent_error`, with OpenClaw's reason in `message`, and the session keeps its previous model. See [Models](/docs/agents-api/models).
</ParamField>

<ParamField body="provider" type="string">
  The model's provider, for example `anthropic`. Hermes only, and per turn like `model`: it tells Hermes which configured provider to run the named model on. OpenClaw ignores it (the response still echoes it); there the provider is the prefix of the `provider/model` id you pass as `model`.
</ParamField>

<ParamField body="reasoning_effort" type="string">
  How hard the model thinks: `none`, `minimal`, `low`, `medium`, `high`, `xhigh`, `max`, or `ultra`. `max` is the strongest plain thinking level; `ultra` is the harness's own Ultra mode, top thinking plus its agentic orchestration (on OpenClaw it also turns on proactive subagent orchestration). It applies to this turn only on both harnesses. Omit it and Hermes uses the instance's configured effort, while OpenClaw uses the session's own thinking level. On OpenClaw the values map one to one onto its thinking levels, with `none` sent as `off`. `max` and `ultra` need gateway `0.10.0`: older instances reject them with `validation_error` until [updated](/docs/agents-api/instances#update).
</ParamField>

<ParamField body="metadata" type="object">
  Up to 16 key/value pairs, at most 64 KB serialized. Echoed back on the response object, never interpreted.
</ParamField>

<ParamField body="agent" type="string">
  Which agent harness runs the turn, `hermes`, `openclaw`, `claude-code`, `codex`, `grok`, or `opencode`. Omit it to use the instance's configured default (`hermes` on `agent37-hermes`, `openclaw` on `agent37-openclaw`, `claude-code` on `agent37-claude-code`, `codex` on `agent37-codex`, `grok` on `agent37-grok`, `opencode` on `agent37-opencode`). Routing is per request: the gateway keeps no session-to-agent binding, so if a session runs on a non-default harness, send `agent` on every turn of it. Targeting a harness the instance was not provisioned with does not reject the call: the turn settles with `status: "failed"` and `error.code` `agent_unavailable`.
</ParamField>

<ParamField body="mode" type="string" default="chat">
  `chat` runs one turn and replies. `goal` is reserved: sending it returns `400 validation_error` today.
</ParamField>

<Note>
  `instance_id` in the body is accepted and ignored. The URL names the instance: one gateway per instance, so there is nothing to route.
</Note>

## Response

The response object. Ids are 32-character hex strings; timestamps from the gateway are epoch milliseconds.

<ResponseField name="id" type="string">
  The response id. Use it to reconnect or cancel the turn.
</ResponseField>

<ResponseField name="session_id" type="string">
  The conversation this turn belongs to. Reuse it on the next call to continue the thread.
</ResponseField>

<ResponseField name="status" type="string">
  `in_progress`, then a terminal `completed`, `failed`, or `cancelled`.
</ResponseField>

<ResponseField name="agent" type="string">
  The agent that ran the turn, `hermes`, `openclaw`, `claude-code`, `codex`, or `opencode`.
</ResponseField>

<ResponseField name="model" type="string | null">
  The `model` you sent on this request, echoed back. `null` when you omitted it: the turn still ran on the harness's default or session model, but the gateway does not report which model the harness resolved.
</ResponseField>

<ResponseField name="provider" type="string | null">
  The `provider` you sent on this request, echoed back; `null` when you omitted it.
</ResponseField>

<ResponseField name="output_text" type="string">
  The agent's final answer. Always a string, empty if the turn produced none.
</ResponseField>

<ResponseField name="usage" type="object | null">
  Token counts and cost for the turn: `{ input_tokens, output_tokens, cost_usd }`. `cost_usd` is absent or `null` when the provider did not report a cost.
</ResponseField>

<ResponseField name="context" type="object | null">
  The session's context window as of this turn: `{ used_tokens, window_tokens }`, the tokens occupying the model's window against the window's size. `null` when the harness didn't report a measurement; a cancelled turn reports none on either harness. Hermes reports it on every completed turn. OpenClaw reports none, so the gateway derives it from the turn: `used_tokens` is the turn's token total (input, output, and cache tokens) and `window_tokens` is the model's context window from OpenClaw's model catalog, or `128000` (OpenClaw's own default budget) when the catalog lists no window for the model. On OpenClaw it is `null` when the turn reported no usage, no model, a zero token total, or a model that is not in the catalog.
</ResponseField>

<ResponseField name="error" type="object | null">
  Set when the turn failed: `{ code, message, param?, hint? }`. See [Errors](/docs/agents-api/errors).
</ResponseField>

<ResponseField name="metadata" type="object | null">
  Your request metadata, echoed back verbatim.
</ResponseField>

<ResponseField name="created" type="number">
  When the turn started, epoch milliseconds.
</ResponseField>

<Note>
  A failed turn does not reject the HTTP call. The POST still returns 200 with `status: "failed"` and `error` set. Branch on `status`, not on the HTTP code.
</Note>

<Note>
  On a non-streaming call the gateway sends the `200` and headers as soon as the turn starts, then keeps the connection alive with a whitespace tick every 25 seconds while the agent works. There is no time limit on a turn; the connection stays open until the agent is done. The JSON body arrives when the turn finishes, prefixed by that whitespace. Leading whitespace is valid JSON, so standard parsers handle it unchanged. Don't treat the early headers as the response being ready. On a sleeping instance no headers arrive at all until the wake completes, so give the request a first-byte timeout of at least three minutes; past the edge's three-minute wake budget the request gets `503 wake_timeout` instead.
</Note>

## Example

<CodeGroup>
  ```bash curl theme={null}
  curl https://ab12cd34ef.agent37.app/v1/responses \
    -H "X-Agent37-Key: sk_live_..." \
    -H "Content-Type: application/json" \
    -d '{
      "input": "Research the top 3 EV makers, write a memo."
    }'
  ```

  ```python python theme={null}
  import requests

  r = requests.post(
      "https://ab12cd34ef.agent37.app/v1/responses",
      headers={
          "X-Agent37-Key": "sk_live_...",
          "Content-Type": "application/json",
      },
      json={"input": "Research the top 3 EV makers, write a memo."},
  ).json()
  ```

  ```javascript node theme={null}
  const r = await (await fetch("https://ab12cd34ef.agent37.app/v1/responses", {
    method: "POST",
    headers: {
      "X-Agent37-Key": "sk_live_...",
      "Content-Type": "application/json",
    },
    body: JSON.stringify({
      input: "Research the top 3 EV makers, write a memo.",
    }),
  })).json();
  ```

  ```json response theme={null}
  {
    "id": "c91d2a7e84f04b6f9a3d5e1c0b87f4a2",
    "session_id": "7f3e0b6c52a949d2b1c4a8e9d0f31726",
    "status": "completed",
    "agent": "hermes",
    "model": null,
    "provider": null,
    "output_text": "Memo: the top 3 EV makers...",
    "usage": { "input_tokens": 1840, "output_tokens": 920, "cost_usd": 0.0137 },
    "context": { "used_tokens": 22600, "window_tokens": 256000 },
    "error": null,
    "metadata": null,
    "created": 1781136000000
  }
  ```
</CodeGroup>

<Tip>
  Set `stream: true` to receive Server-Sent Events as the agent reasons, calls tools, and writes its answer. The terminal event carries the final `output_text`, `usage`, and `context`. See [Streaming](/docs/agents-api/streaming).
</Tip>

## Continue a conversation

The first message omits `session_id` and starts a session. The reply returns a `session_id`; pass it on the next message to continue the same thread. The session holds the full history, so you never resend a transcript: you send only the new input.

<CodeGroup>
  ```bash curl theme={null}
  # 1. start a session: omit session_id
  curl https://ab12cd34ef.agent37.app/v1/responses \
    -H "X-Agent37-Key: sk_live_..." \
    -H "Content-Type: application/json" \
    -d '{
      "input": "Research the top 3 EV makers, write a memo."
    }'
  # -> { "id": "c91d2a7e84f04b6f9a3d5e1c0b87f4a2",
  #      "session_id": "7f3e0b6c52a949d2b1c4a8e9d0f31726",
  #      "status": "completed", ... }

  # 2. continue it: reuse the session_id, send only the new input
  curl https://ab12cd34ef.agent37.app/v1/responses \
    -H "X-Agent37-Key: sk_live_..." \
    -H "Content-Type: application/json" \
    -d '{
      "session_id": "7f3e0b6c52a949d2b1c4a8e9d0f31726",
      "input": "Make it shorter, add a quote."
    }'
  ```

  ```python python theme={null}
  import requests

  BASE = "https://ab12cd34ef.agent37.app"
  H = {"X-Agent37-Key": "sk_live_...", "Content-Type": "application/json"}

  # 1. start a session: omit session_id
  first = requests.post(
      f"{BASE}/v1/responses",
      headers=H,
      json={"input": "Research the top 3 EV makers, write a memo."},
  ).json()

  # 2. continue it: reuse the session_id, send only the new input
  requests.post(
      f"{BASE}/v1/responses",
      headers=H,
      json={
          "session_id": first["session_id"],
          "input": "Make it shorter, add a quote.",
      },
  )
  ```

  ```javascript node theme={null}
  const BASE = "https://ab12cd34ef.agent37.app";
  const H = {
    "X-Agent37-Key": "sk_live_...",
    "Content-Type": "application/json",
  };

  // 1. start a session: omit session_id
  const first = await (await fetch(`${BASE}/v1/responses`, {
    method: "POST",
    headers: H,
    body: JSON.stringify({
      input: "Research the top 3 EV makers, write a memo.",
    }),
  })).json();

  // 2. continue it: reuse the session_id, send only the new input
  await fetch(`${BASE}/v1/responses`, {
    method: "POST",
    headers: H,
    body: JSON.stringify({
      session_id: first.session_id,
      input: "Make it shorter, add a quote.",
    }),
  });
  ```
</CodeGroup>

<Note>
  **One active turn per session.** A session runs one response at a time. Sending new input while one is in flight returns `409 session_busy`, normally with the running response's id in `error.response_id`. Use another session, reattach to the running turn, or cancel it first.
</Note>

To list a user's threads, read a thread's history, or delete one, see [Sessions](/docs/agents-api/sessions). To pick a model per turn, see [Models](/docs/agents-api/models).

### When the agent needs a decision from you

The agent asks by ending its turn: the response comes back `completed` with the question (and the options, if any) as `output_text`, and the turn is over. Answer by sending the reply as the next `input` on the same `session_id`. There is no in-turn prompt to answer while a response is `in_progress`, so a chat client needs nothing special: render the question like any other reply, and send the user's answer like any other message.

### When the agent delegates to subagents

On Hermes, subagents run inside the turn: the response stays `in_progress` until they finish, and the agent's final answer, their results included, arrives in that same response. A delegation can run for minutes with nothing on the stream but `:keepalive` comments between the `delegate_task` tool call's `started` and `completed` events, so keep the connection open or [reattach](#follow-up-on-a-response). Cancelling the turn stops the subagents too. This needs gateway `0.15.1`: older instances end the turn as soon as the subagents are dispatched and hand their results to the agent at the start of your next message, until [updated](/docs/agents-api/instances#update).

## Follow up on a response

Every response has an id you can use after the call returns.

| Method | Path                        | Returns                                                     |
| ------ | --------------------------- | ----------------------------------------------------------- |
| `GET`  | `/v1/responses/{id}/stream` | `200` an SSE stream: replays all events, then attaches live |
| `POST` | `/v1/responses/{id}/cancel` | `200` the current response object                           |

`GET /v1/responses/{id}/stream` replays every event so far in order, then stays attached live, so a dropped connection never loses the answer, including after the turn has finished, while the response is still retained. See [Streaming](/docs/agents-api/streaming) for the replay window. Lost the id (page reload, new device)? [`GET /v1/sessions/{id}`](/docs/agents-api/sessions#retrieve-a-session-with-history) returns the running response as `active_response_id`.

`POST /v1/responses/{id}/cancel` takes no body and stops a running turn (on OpenClaw it aborts the run inside OpenClaw). It returns 200 with the current response object as soon as the stop is requested, not once the agent has actually stopped, so the body normally still reads `status: "in_progress"`; the response settles to `cancelled` when the turn unwinds. To see it end, read `GET /v1/responses/{id}/stream`: a cancelled turn still closes with `response.completed`, carrying the `output_text` accumulated so far. Cancelling a finished response is a no-op that returns its terminal state, still 200.

<Warning>
  Cancel does not rewind. Whatever the agent has already done (files written, emails sent, tools called) is not undone. The response ends with `status: "cancelled"`.
</Warning>

## Status values

A response moves from `in_progress` to exactly one terminal status.

| Status        | Meaning                                                                                                                                               |
| ------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------- |
| `in_progress` | The turn is running.                                                                                                                                  |
| `completed`   | The turn finished and `output_text` holds the answer.                                                                                                 |
| `failed`      | The turn ended on an error; `error` says why.                                                                                                         |
| `cancelled`   | You stopped the turn with `cancel`, or (OpenClaw only) the input was one of OpenClaw's own stop commands such as `stop` or `exit`, so no run started. |
