Skip to content

The REST API

Base URL /api/v1. Authentication is the X-API-Key header, compared against ENV["API_KEYS"] (comma-separated) with a constant-time comparison.

:id resolves slug first, then numeric id.

MethodPathNotes
GET/sessionsfilters: status, agent_runtime, show_archived, page, per_page
GET/sessions/searchq required (≤1000 chars), search_contents=true. Missing/oversized q → 400 (the only 400 in the API)
GET/sessions/:idinclude_transcript=true adds the raw transcript
POST/sessions→ 201. See below.
PATCH/sessions/:idpermits only title, slug, goal, is_autonomous, custom_metadata
DELETE/sessions/:id→ 204
POST/sessions/:id/archive{session, message, trash_after}
POST/sessions/:id/unarchive{session, clone_restored, message}
POST/sessions/:id/follow_upprompt (≤500,000), goal, force_immediate. 202 if the session is running (queued); 200 otherwise
POST/sessions/:id/pauserunning only
POST/sessions/:id/sleepneeds_input → sleeps; running → sets pending_sleep
POST/sessions/:id/restart
POST/sessions/:id/forkmessage_index required → 201
POST/sessions/:id/refreshre-read transcript from disk
POST/sessions/refresh_allmax 50 sessions
POST/sessions/bulk_archivesession_ids[]
PATCH/sessions/:id/mcp_serversmax 50, validated against the catalog
PATCH/sessions/:id/catalog_skills · /catalog_hooks · /catalog_pluginsmax 100 / 100 / 50
PATCH/sessions/:id/modelvalidated against ModelCatalog for the session’s runtime
PATCH/sessions/:id/notessession_notes ≤ 50,000
PATCH/sessions/:id/heartbeatenabled and/or interval_seconds (30–86,400)
PATCH/sessions/:id/set_categoryblank clears
POST/sessions/:id/toggle_favorite
GET/sessions/:id/transcriptformat=texttext/plain

Permitted params: agent_runtime, prompt, git_root, branch, subdirectory, title, slug, goal, execution_provider, is_autonomous, parent_session_id, auto_compact_window, mcp_servers[], catalog_skills[], catalog_hooks[], catalog_plugins[], config{}, custom_metadata{}.

One more: agent_root, which is read directly from params, outside the strong-params permit list. An invalid one → 422 {"error": "Invalid agent_root"}.

The AgentSessionJob is enqueued only if prompt is present.

id, slug, title, status, agent_runtime, prompt, git_root, branch, subdirectory, execution_provider, goal, mcp_servers, all_mcp_servers, injected_mcp_servers, catalog_skills, catalog_hooks, catalog_plugins, config, metadata, custom_metadata, is_autonomous, heartbeat_enabled, heartbeat_interval_seconds, auto_compact_window, category_id, category{}, session_id, job_id, running_job_id, archived_at, trash_after, created_at, updated_at, session_notes, session_notes_updated_at, favorited.

GET /triggers (filters condition_type, status) · GET /triggers/:id (+ recent_sessions, limit 10) · POST · PATCH · DELETE · POST /triggers/:id/toggle · GET /triggers/channels (Slack; 503 when Slack is unconfigured).

Conditions are nested via trigger_conditions_attributes. The web UI’s triggers#invoke route has no API equivalent.

GET /notifications (status=read|unread) · GET /notifications/:id · GET /notifications/badge{pending_count} · PATCH /notifications/:id/mark_read · PATCH /notifications/mark_all_read · DELETE /notifications/:id/dismiss (422 if unread) · DELETE /notifications/dismiss_all_read · POST /notifications/push (session_id + message).

GET /health{health_report, timestamp, rails_env, ruby_version} · POST /health/cleanup_processes · POST /health/retry_sessions · POST /health/archive_old (days, clamped 1–365, default 7).

  • POST /elicitationsUNAUTHENTICATED. Requires _meta["com.pulsemcp/request-id"] and message. → 201.
  • GET /elicitations/:request_idUNAUTHENTICATED. Auto-expires past expires_at.
  • PATCH /elicitations/:request_id/respond — authenticated. action_typeaccept | decline.

The first two skip auth because the MCP child process has no API key. See Elicitation.

Note the parameter is action_type, not actionaction is a Rails reserved param.

ResourceEndpoints
LogsFull CRUD at /sessions/:session_id/logs[/:id], level filter
Subagent transcriptsFull CRUD at /sessions/:session_id/subagent_transcripts[/:id]
Enqueued messagesCRUD + PATCH :id/reorder + POST :id/interrupt
CategoriesCRUD + POST /categories/reorder
CLIsGET /clis/status · POST /clis/refresh · POST /clis/clear_cache
Transcript archiveGET /transcript_archive/download (zip) · /status
Config (read-only)GET /configs · GET /mcp_servers · GET /skills

Three shapes, inconsistently applied:

{"error": "Not Found", "message": "The requested resource was not found"} // string
{"error": "Validation failed", "messages": ["Title can't be blank"]} // plural key, array
{"error": "...", "message": ["..."]} // singular key, ARRAY value

That third one comes from Api::BaseController#unprocessable_entity (the RecordInvalid rescue). Parse defensively.

Status codes in use: 200 · 201 · 202 (follow-up queued) · 204 · 400 (search only) · 401 · 404 · 409 (follow-up position collision, interrupt races) · 422 · 429 (health cooldown) · 500 · 503 (Slack unconfigured).

app/controllers/api/AGENTS.md requires that both doc surfaces — this page and app/views/api_docs/show.html.erb (the in-app /api_docs page) — be updated with every endpoint change. Both had drifted. app/views/api_docs/show.html.erb is still missing triggers, notifications, health, clis, and transcript_archive.

There is no generated OpenAPI spec. If you change a route, change this page in the same PR.