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.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.3
List threads for the sidebar
GET /v1/sessions on the instance URL lists the harness’s sessions, newest first, without history.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.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 returns409:
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}/cancelon the instance URL. Withstream: truethe 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 readsstatus: "in_progress"; the response settles tostatus: "cancelled"when the turn unwinds and the stream closes withresponse.completed. Whatever the agent already did is not undone. - Reattach instead of erroring: on a 409,
GET /v1/responses/{response_id}/streamreplays the running turn from its start and follows it live. Iferror.response_idis absent (an instance still on an older gateway), readactive_response_idfromGET /v1/sessions/{id}instead.