roam open source

roam-hub API reference

The hosted hub (hub.roam.intrane.fr) is the control plane for scheduled, metered agent runs. Workers hold no API keys — every LLM call proxies through the hub, which counts exact tokens, accumulates cost, and charges per run via peage.

Authentication

Two token types, both returned once at signup:

Token Prefix Used by Scope
Tenant token rh_ You (the owner) Agent CRUD, run management, approve/deny
Worker token rhw_ The roam worker process Pull work, LLM proxy, stream events, report done

Both are sent as Authorization: Bearer <token>.

Signup

POST /v1/signup

curl -X POST https://hub.roam.intrane.fr/v1/signup \
  -H "Content-Type: application/json" \
  -d '{"email":"you@example.com","peage_wallet":"pw_..."}'

Response:

{"ok":true,"token":"rh_...","worker_token":"rhw_..."}

One tenant per email. The peage_wallet is a peage wallet token (pw_...) for billing — see peage.intrane.fr/llms.txt.

GET /v1/whoami (cli-trial-spec)

Any Bearer token auto-provisions a tenant — no email, no signup needed. The token IS the credential. Generate any token and call whoami:

curl https://hub.roam.intrane.fr/v1/whoami \
  -H "Authorization: Bearer rh_my_random_token_123"

First call (auto-provisions):

{"v":"1","tenant_id":"...","email":"","wallet_attached":false,
 "claimed":false,"plan":"pay_per_run",
 "worker_token":"rhw_...","auto_provisioned":true}

Subsequent calls (returns existing):

{"v":"1","tenant_id":"...","email":"","wallet_attached":false,
 "claimed":false,"plan":"pay_per_run"}

POST /app/claim

Attach an email to an existing tenant (recovery + billing):

curl -X POST https://hub.roam.intrane.fr/app/claim \
  -H "Authorization: Bearer rh_my_random_token_123" \
  -H "Content-Type: application/json" \
  -d '{"email":"you@example.com"}'

Response:

{"v":"1","claimed":true,"email":"you@example.com"}

POST /v1/wallet

Set or update your peage wallet:

curl -X POST https://hub.roam.intrane.fr/v1/wallet \
  -H "Authorization: Bearer rh_..." \
  -H "Content-Type: application/json" \
  -d '{"wallet_token":"pw_..."}'

Agent management

POST /v1/agents

Create an agent spec (a spec, not a process — runs are the executions):

curl -X POST https://hub.roam.intrane.fr/v1/agents \
  -H "Authorization: Bearer rh_..." \
  -H "Content-Type: application/json" \
  -d '{
    "name": "nightly-tests",
    "prompt": "clone repo X, run tests, report failures",
    "model": "claude-sonnet-4-20250514",
    "every_secs": 86400,
    "budget_microeur": 500000,
    "report_url": "https://your-webhook.example.com/run-done",
    "kind": "agent",
    "trigger_mode": "ignore"
  }'

Response: {"ok":true,"id":"ag_...","trigger_secret":"ts_..."}

Fields:

GET /v1/agents

List your agents:

curl https://hub.roam.intrane.fr/v1/agents \
  -H "Authorization: Bearer rh_..."

POST /v1/agent/pause · /v1/agent/resume · /v1/agent/delete

curl -X POST https://hub.roam.intrane.fr/v1/agent/pause \
  -H "Authorization: Bearer rh_..." \
  -H "Content-Type: application/json" \
  -d '{"id":"ag_..."}'

Webhook trigger

POST /t//

Queue a run immediately (bypasses the interval schedule):

curl -X POST https://hub.roam.intrane.fr/t/ag_.../ts_... \
  -H "Content-Type: application/json" \
  -d '{"event":"push","repo":"myorg/myrepo"}'

If the agent’s trigger_mode is inject, the body is appended to the prompt.

Run management

GET /v1/runs?agent=

List recent runs (last 50) for an agent:

curl "https://hub.roam.intrane.fr/v1/runs?agent=ag_..." \
  -H "Authorization: Bearer rh_..."

GET /v1/run?id=

Run detail with full journal:

curl "https://hub.roam.intrane.fr/v1/run?id=run_..." \
  -H "Authorization: Bearer rh_..."

POST /v1/run/approve · /v1/run/deny

Approve or deny a parked run (when the worker called needs-human):

curl -X POST https://hub.roam.intrane.fr/v1/run/approve \
  -H "Authorization: Bearer rh_..." \
  -H "Content-Type: application/json" \
  -d '{"id":"run_..."}'

POST /v1/run/kill

Kill a running or queued run (starves the LLM loop within one turn):

curl -X POST https://hub.roam.intrane.fr/v1/run/kill \
  -H "Authorization: Bearer rh_..." \
  -H "Content-Type: application/json" \
  -d '{"id":"run_..."}'

Worker protocol

These endpoints are used by the roam worker process, not by humans directly.

GET /v1/work

Pull the oldest queued run. Returns 204 when idle. Claiming flips queued → running (atomic, guards against double-claim).

Worker routing: workers can pass ?labels=k=v,k=v and ?name=prod-01 as query params. The hub only hands a run whose agent selector is satisfied by the worker’s labels. Empty selector = any worker. Worker with no labels = only gets runs with empty selector (backward compatible).

# any worker (backward compatible)
curl https://hub.roam.intrane.fr/v1/work \
  -H "Authorization: Bearer rhw_..."

# labeled worker — only gets runs whose selector matches
curl "https://hub.roam.intrane.fr/v1/work?labels=gpu=true,docker=true&name=gpu-box-01" \
  -H "Authorization: Bearer rhw_..."

From the roam CLI:

roam worker --hub https://hub.roam.intrane.fr --token rhw_... \
  --label gpu=true --label docker=true --name gpu-box-01

Response (200):

{
  "ok": true,
  "run_id": "run_...",
  "agent_id": "ag_...",
  "name": "nightly-tests",
  "prompt": "clone repo X, run tests...",
  "model": "claude-sonnet-4-20250514",
  "budget_microeur": 500000,
  "kind": "agent"
}

Multiple workers: N workers can poll the same hub with the same rhw_ token. Each gets a different run from the queue (the atomic claim guard prevents double-claiming). This is horizontal scaling — more workers = more parallel runs. Use --label and --name to route specific agents to specific workers within a single tenant.

POST /v1/llm

Metered LLM proxy. The worker sends the upstream messages payload; the hub forwards to the provider with its own key, counts tokens, and accumulates cost. The run ID travels in the X-Roam-Run header:

curl -X POST https://hub.roam.intrane.fr/v1/llm \
  -H "Authorization: Bearer rhw_..." \
  -H "X-Roam-Run: run_..." \
  -H "Content-Type: application/json" \
  -d '{"model":"claude-sonnet-4-20250514","messages":[...],"max_tokens":16000}'

The hub refuses if the run is killed or the budget is exhausted.

POST /v1/run/events

Stream a journal entry:

curl -X POST https://hub.roam.intrane.fr/v1/run/events \
  -H "Authorization: Bearer rhw_..." \
  -H "Content-Type: application/json" \
  -d '{"run_id":"run_...","kind":"tool_call","data":"{\"tool\":\"run_shell\",\"cmd\":\"ls\"}"}'

POST /v1/run/needs-human

Park the run for owner approval (sets status to awaiting):

curl -X POST https://hub.roam.intrane.fr/v1/run/needs-human \
  -H "Authorization: Bearer rhw_..." \
  -H "Content-Type: application/json" \
  -d '{"run_id":"run_...","command":"rm -rf /var/log/old"}'

GET /v1/run/decision?run=

Poll for the owner’s decision (the worker polls this every second while parked):

curl "https://hub.roam.intrane.fr/v1/run/decision?run=run_..." \
  -H "Authorization: Bearer rhw_..."

Response: {"ok":true,"decision":""} (empty = still waiting), "approve", "deny", or "stop" (if killed).

POST /v1/run/done

Close the run, bill it, and fire the report webhook:

curl -X POST https://hub.roam.intrane.fr/v1/run/done \
  -H "Authorization: Bearer rhw_..." \
  -H "Content-Type: application/json" \
  -d '{"run_id":"run_...","exit_reason":"complete","result":"all tests passed"}'

Billing

Per finished run:

charge_cents = ceil(cost_microeur × 1.3 / 10000) + 1

Discovery