> ## 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.

# Build a chat app

> Give every user their own always-on agent. The simplest thing to build on the Agent API, and where most teams start.

The pattern is four calls: create one instance per user at signup, start one session per chat thread, list sessions for the sidebar, and fetch one session for the open thread.

```text title="Paste this into your coding agent" wrap theme={null}
Read https://www.agent37.com/docs/llms-full.txt.
I want a chat UI on one always-on agent, with replies streamed token by token.
Create one instance with POST /v1/instances (leave template unset for the default agent37-hermes, set budget.credit_micros so it can answer from the first message), send turns to https://{instanceId}.agent37.app/v1/responses with the X-Agent37-Key header and stream: true, and pass the session_id from the first reply back on every later turn.
Done when response.output_text.delta events render in the UI as they arrive.
My key is in AGENT37_API_KEY.
```

<Card title="hermes-chat: this guide as a working app" icon="github" href="https://github.com/agent37-platform/examples/tree/main/hermes-chat" horizontal>
  Everything on this page, runnable: create instances from a table, stream replies token by token, list and reopen threads, cancel a turn. Express plus vanilla JS, no build step. Clone it, add your key, `npm start`.
</Card>

## One key, two base URLs

<Info>
  Two base URLs, one key, two headers: `https://api.agent37.com` manages instances and takes the key as `Authorization: Bearer`; each instance serves its own chat API at `https://{instanceId}.agent37.app` (the id is the hostname) and takes the same key as `X-Agent37-Key`, leaving `Authorization` free for your own app. See [Core concepts](/docs/agents-api/concepts).
</Info>

Each step below shows the call twice: as bare curl, and as `fetch` where it lands in your app's server code. The curl tabs use `ab12cd34ef` as the instance id; your app reads the id it stored at signup.

## The shape of it

<Steps>
  <Step title="One instance per user, on signup">
    When a user signs up, create one [instance](/docs/agents-api/instances) for them, tagged with your own user id. That instance is their agent from then on.

    <CodeGroup>
      ```bash curl theme={null}
      curl https://api.agent37.com/v1/instances \
        -H "Authorization: Bearer sk_live_..." \
        -H "Content-Type: application/json" \
        -d '{
          "user": "u_882",
          "name": "chat-u_882",
          "budget": { "credit_micros": 1000000 }
        }'
      ```

      ```javascript node theme={null}
      const inst = await (await fetch("https://api.agent37.com/v1/instances", {
        method: "POST",
        headers: {
          Authorization: `Bearer ${process.env.AGENT37_API_KEY}`,
          "Content-Type": "application/json",
        },
        body: JSON.stringify({
          user: "u_882",
          name: "chat-u_882",
          budget: { credit_micros: 1000000 },
        }),
      })).json();

      await db.users.update("u_882", { instanceId: inst.id });
      // inst.id is bare, e.g. "ab12cd34ef", and doubles as the hostname:
      // https://ab12cd34ef.agent37.app
      ```
    </CodeGroup>

    Omitting `template` gives you `agent37-hermes`, the default, on the default 2 vCPU / 4 GB RAM / 4 GB disk shape, billed from your workspace wallet (see [Billing](/docs/agents-api/billing)). Each create uses the template's newest published image; for a fleet where every signup must get the identical image, pass a [version-pinned template](/docs/agents-api/templates#pin-a-template-version) instead (`"template": "agent37-hermes@<tag>"`). The `budget.credit_micros` field grants one-time managed-spend headroom so the agent's LLM calls work from the first message; without it the per-instance [budget](/docs/agents-api/budgets) defaults to \$0.

    The call is synchronous and returns `201` with `status: "running"`: the instance's computer is up. The agent inside is still booting, usually seconds but up to a few minutes on a cold host, so before the first message poll `GET /v1/health` on the instance URL until it answers with `"healthy": true`; `ok` alone only means the gateway is up (see [Health & version](/docs/agents-api/health)). Store `inst.id` on the user row.
  </Step>

  <Step title="A session per chat thread">
    Each thread is a session on the user's instance. Send the first turn to the instance URL with no `session_id`; the reply mints one. Store it on your thread row, then send `session_id` plus the new `input` on every later turn. The session keeps the full history, so you never resend a transcript.

    <CodeGroup>
      ```bash curl theme={null}
      # new thread: first turn, no session_id; the reply carries the new one
      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." }'

      # later turns: session_id and the new input only
      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."
        }'
      ```

      ```javascript node theme={null}
      // new thread: first turn, no session_id
      const first = await (await fetch(
        `https://${user.instanceId}.agent37.app/v1/responses`,
        {
          method: "POST",
          headers: {
            "X-Agent37-Key": process.env.AGENT37_API_KEY,
            "Content-Type": "application/json",
          },
          body: JSON.stringify({
            input: "Research the top 3 EV makers, write a memo.",
          }),
        }
      )).json();
      await db.threads.create({ userId: "u_882", sessionId: first.session_id });
      // first.session_id, e.g. "7f3e0b6c52a949d2b1c4a8e9d0f31726"

      // later turns: session_id and the new input only
      const reply = await (await fetch(
        `https://${user.instanceId}.agent37.app/v1/responses`,
        {
          method: "POST",
          headers: {
            "X-Agent37-Key": process.env.AGENT37_API_KEY,
            "Content-Type": "application/json",
          },
          body: JSON.stringify({
            session_id: thread.sessionId,
            input: "Make it shorter, add a quote.",
          }),
        }
      )).json();
      render(reply.output_text);
      ```
    </CodeGroup>

    <Tip>
      Stream every reply so the UI fills in as the agent reasons, calls tools, and writes. Set `stream: true` and read the Server-Sent Events; see [Streaming](/docs/agents-api/streaming) for the full event list and a client parser.
    </Tip>
  </Step>

  <Step title="List threads for the sidebar">
    `GET /v1/sessions` on the instance URL lists the harness's sessions, newest first, without history.

    <CodeGroup>
      ```bash curl theme={null}
      curl https://ab12cd34ef.agent37.app/v1/sessions \
        -H "X-Agent37-Key: sk_live_..."
      ```

      ```javascript node theme={null}
      const { agent, data } = await (await fetch(
        `https://${user.instanceId}.agent37.app/v1/sessions`,
        { headers: { "X-Agent37-Key": process.env.AGENT37_API_KEY } }
      )).json();
      // every harness: [{ id, title, last_active, message_count, preview }]
      // fields the harness does not track are null; timestamps are epoch milliseconds
      ```
    </CodeGroup>

    Every row carries a `title`: Hermes fills one in on its own after the first exchange, while OpenClaw leaves it `null` until you set one. Set or replace it on either harness with `PATCH /v1/sessions/{id}` (`{ "title": "..." }`) when the user renames a thread. A title already used by another session returns `409 title_conflict`.
  </Step>

  <Step title="Load a thread when it opens">
    `GET /v1/sessions/{id}` returns the session with its full transcript in `history`, in order.

    <CodeGroup>
      ```bash curl theme={null}
      curl https://ab12cd34ef.agent37.app/v1/sessions/7f3e0b6c52a949d2b1c4a8e9d0f31726 \
        -H "X-Agent37-Key: sk_live_..."
      ```

      ```javascript node theme={null}
      const session = await (await fetch(
        `https://${user.instanceId}.agent37.app/v1/sessions/${thread.sessionId}`,
        { headers: { "X-Agent37-Key": process.env.AGENT37_API_KEY } }
      )).json();
      // session.history: [{ id, session_id, role, content, thinking?, created_at }]
      // session.active_response_id: the running response's id, or null when idle
      // session.context: { used_tokens, window_tokens }, or null until a turn reports one
      ```
    </CodeGroup>

    Render each message by `role` (`user`, `assistant`, or `system`). If `active_response_id` is set, a turn is still running and its messages are not in `history` yet, so reattach with `GET /v1/responses/{id}/stream` to render it live (this is how a page reload mid-turn recovers the stream). When a user deletes a thread, `DELETE /v1/sessions/{id}` removes it; see [Sessions](/docs/agents-api/sessions).
  </Step>
</Steps>

## Handle a busy session

A session runs one response at a time. Posting a new turn while one is in flight returns `409`:

```json theme={null}
{
  "error": {
    "code": "session_busy",
    "message": "A response is already running on this session.",
    "hint": "Reattach with GET /v1/responses/{response_id}/stream, cancel it, or start another session.",
    "response_id": "c91d2a7e84f04b6f9a3d5e1c0b87f4a2"
  }
}
```

`error.response_id` is the running response, so even a client that lost its state can reattach to it or cancel it. Three good ways to handle it in a chat UI:

* **Disable the composer** while a turn runs, and re-enable it when the reply arrives (the non-streaming call returning, or the terminal streaming event).
* **Offer a stop button** that calls `POST /v1/responses/{id}/cancel` on the instance URL. With `stream: true` the first event, `response.created`, hands you the response id immediately, which is what makes the button possible. Cancel returns 200 as soon as the stop is requested, so its body normally still reads `status: "in_progress"`; the response settles to `status: "cancelled"` when the turn unwinds and the stream closes with `response.completed`. Whatever the agent already did is not undone.
* **Reattach instead of erroring**: on a 409, `GET /v1/responses/{response_id}/stream` replays the running turn from its start and follows it live. If `error.response_id` is absent (an instance still on an older gateway), read `active_response_id` from `GET /v1/sessions/{id}` instead.

The [hermes-chat example](https://github.com/agent37-platform/examples/tree/main/hermes-chat) wires up the first two: the composer locks while a turn is in flight, and the stop button cancels it.

Other threads are unaffected: each session has its own lock, so one user can run turns in several threads at once.
