Skip to main content
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.

hermes-chat: this guide as a working app

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.

One key, two base URLs

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

1

One instance per user, on signup

When a user signs up, create one instance for them, tagged with your own user id. That instance is their agent from then on.
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). 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 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 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). Store inst.id on the user row.
2

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.
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 for the full event list and a client parser.
3

List threads for the sidebar

GET /v1/sessions on the instance URL lists the harness’s sessions, newest first, without history.
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.
4

Load a thread when it opens

GET /v1/sessions/{id} returns the session with its full transcript in history, in order.
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.

Handle a busy session

A session runs one response at a time. Posting a new turn while one is in flight returns 409:
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 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.