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.api is the hosting API; agentOf(id) is one user’s agent.
node
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
node
template gives you agent37-hermes, the default, on the default 2 vCPU / 4 GB RAM / 6 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 "ok": true (see Health and 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.node
3
List threads for the sidebar
GET /v1/sessions on the instance URL lists the harness’s sessions, newest first, without history.node
title, and you can set it with PATCH /v1/sessions/{id} ({ "title": "..." }) — the first user message usually makes a good default. Harnesses that do not store a title answer 405; for those, keep titles in your own database keyed by session_id.4
Load a thread when it opens
GET /v1/sessions/{id} returns the session with its full transcript in history, in order.node
role (user, assistant, or system). If active_response_id is set, a turn is still running and its messages are not in history yet — 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 is best effort: the response ends withstatus: "cancelled", and 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 (a rare race, or an older gateway), readactive_response_idfromGET /v1/sessions/{id}instead.