Docs / Build on CRHQ/ Integration API Reference

Integration API Reference

The endpoint-by-endpoint contract for driving CRHQ sessions from your product — creation, turns, messages, streaming, status and errors.

Everything below works with a scoped API key (sessions:write / sessions:read). Base URL is your instance (https://<instance> or http://127.0.0.1:3456 on-host).

  1. POST /api/sessions — create + configure without spending tokens; persist the id in your own DB transaction first (crash-safe).
  2. PATCH /api/conversations/:id and POST /api/conversations/:id/agent — title, model, agent.
  3. POST /api/agent — start the first turn.
  4. Stream over WebSocket; poll /metadata for status.
  5. POST /api/sessions/:id/message for follow-ups.

Create a session (no turn) — POST /api/sessions

{ "sessionId": "session-1785…-abc",   // optional — server generates if absent
  "name": "Intake — Acme Corp",       // optional
  "agent": "prd-builder",             // optional; must be a registered, active agent
  "model": "opus" }                   // optional; validated against the model catalog

Responses: 201 {"success":true,"sessionId":"…","created":true} · 200 …"created":false (idempotent re-POST — provided fields are applied). A retry is always safe. Bad model or unknown agent → 400 with a readable message. Session ids are yours to generate — any string works.

Start a turn — POST /api/agent

{ "instruction": "…", "sessionId": "session-1785…-abc" }

Two possible outcomes:

{ "success": true, "status": "processing", "queued": false, "processed": true, … }
{ "success": true, "status": "queued", "queued": true,
  "messageId": "msg-…", "queuePosition": 1, "queueStatus": { … } }

queued:true means the session was busy — the message runs after the current turn ends. Concurrency is guaranteed: two calls can never start two simultaneous turns on one session.

The retry contract — read this twice. If you retry an instruction (network timeout, worker crash) and get queued:true, the queued copy is a duplicate that will run again later. Void it: DELETE /api/conversations/:sessionId/queue/:messageId. Rule of thumb: queued:true on a retry means "already running — drop the duplicate and keep waiting."

A user-stopped session responds queued:true, parked:true, reason:"recipient_stopped_by_user" — the message waits for a human to resume that session.

Send a follow-up — POST /api/sessions/:sessionId/message

{ "content": "…" }

Same processed / queued / parked contract as POST /api/agent. The session must exist (404 otherwise).

Title, model, agent

  • PATCH /api/conversations/:sessionId with { "name": "…" } and/or { "model": "…" } (validated).
  • POST /api/conversations/:sessionId/agent binds a registered agent — its instructions, skills and platform context are assembled server-side into every subsequent turn.

Status & liveness — GET /api/conversations/:sessionId/metadata

{ "metadata": { "status": "idle" | "running" | "error" | "interrupted" | "stopped", … } }

Returns 404 for unknown ids — this is the canonical liveness endpoint. The transcript route below returns 200-empty for unknown ids by default; add ?strict=1 if you use it for liveness.

Read the transcript — GET /api/conversations/:sessionId?limit=N&offset=M

Paginated message history plus metadata. Add &strict=1 to get 404 on unknown ids.

Live streaming — WebSocket

wss://<instance>/?sessionId=session-1785…-abc      (header: x-api-key: crhq_sk_…)
→ send: { "action": "subscribe", "sessionId": "session-1785…-abc" }
→ receive events keyed on "type": text, tool_use, session_run_status, processing_queued, …

Subscribing requires sessions:read; starting a turn over WS (action: "instruction") requires sessions:write. Recent events are buffered and replayed on reconnect; reconnection logic is your responsibility.

Error semantics

SignalMeaningWhat to do
401Unknown, revoked or missing keyStop; the key may have been revoked in Settings
403Valid key, insufficient scope or off-limits endpoint (message names the reason)Fix the call or request a broader scope — don't retry as-is
404Unknown session (/metadata, or ?strict=1)Treat as not-running / deleted
400Validation error (bad model, unknown agent, missing field)Fix the request — messages are human-readable
queued:trueSession busy; message queued for after the current turnOn a retry: void the duplicate

Agent allowlists affect your calls

If your key is pinned to specific agents (recommended — see Scoped API Keys):

  • POST /api/sessions without an agent binds your first allowed agent automatically.
  • Naming an agent outside your list → 403.
  • POST /api/conversations/:id/agent with an agent outside your list → 403.
  • Starting a turn on a session running an agent outside your list → 403.

What the platform handles for you

  • Concurrency — atomic per-session start claims; the loser of a race queues instead of double-running.
  • Restarts — in-flight sessions are auto-resumed (staggered) after an instance restart.
  • Provider limits — usage-cap rotation across the operator's subscription accounts is automatic.
  • Cost — every turn records the concrete model that ran and its cost, attributed to your key.

Smoke test

K="crhq_sk_…"; B="https://<instance>"
SID="session-$(date +%s000)-smoke1"
curl -s -X POST $B/api/sessions -H "x-api-key: $K" -H 'Content-Type: application/json' \
  -d "{\"sessionId\":\"$SID\",\"name\":\"smoke\"}"                      # → 201 created:true
curl -s -X POST $B/api/agent -H "x-api-key: $K" -H 'Content-Type: application/json' \
  -d "{\"sessionId\":\"$SID\",\"instruction\":\"Reply exactly: OK\"}"   # → processing
sleep 20
curl -s $B/api/conversations/$SID/metadata -H "x-api-key: $K"           # → status: idle
curl -s "$B/api/conversations/$SID?limit=5" -H "x-api-key: $K"          # → transcript incl. "OK"
curl -s $B/api/settings/agents -H "x-api-key: $K"                       # → 403 — the wall works