dotpals Docs

Reference

Bridge HTTP API

The bridge is a small HTTP server on 127.0.0.1:5175. Agents post events to it, and the pal, the notch and the dashboard read its live feed. You can use the same API from your own tools.

Basics

  • Address: http://127.0.0.1:5175. The bridge listens only on 127.0.0.1. To change the port, see Configuration.
  • Host: every request except POST /hook and POST /event must be addressed to 127.0.0.1, localhost or [::1]. Anything else gets 421.
  • Changes: every POST under /api/ needs the header x-dotpals: 1, or it gets 403. Browsers won't send a custom header to another site without asking first, and the bridge never agrees, so websites can't change your settings. See Privacy and security.
  • Bodies are JSON. Responses are JSON with cache-control: no-store, and errors look like { "error": "…" }.

All routes

RouteWhat it does
POST /eventSend an event from any agent.
POST /hookClaude Code hook events (what bridge/hook.js sends).
POST /hook?agent=<id>Another agent's hook events, read by its adapter.
GET /eventsThe live feed, as Server-Sent Events.
GET /api/activity{ entries }: every activity entry the bridge holds, oldest first.
GET /api/statusWhat's running and connected, and where files are.
GET /api/configThe settings (see keys).
POST /api/configChange settings.
POST /api/history/clearClear the history. Returns { ok: true }.
POST /api/checker/testTest the test-result checker.
GET /api/checker/layaThe status of the Laya dotpals set up.
POST /api/checker/laya/setupSet up Laya (and start it). /start, /stop and /uninstall too.
GET /api/agents{ agents }: every integration.
POST /api/agents/<id>/connectConnect an agent (adds dotpals to its config).
POST /api/agents/<id>/disconnectTake dotpals out of its config again.
POST /api/agents/<id>/testSend a test event through the real hook.
POST /api/sessions/<id>/dismissPut a session to sleep everywhere.
GET /api/approvals{ approvals }: permission requests waiting for an answer.
POST /api/approvals/<id>Answer one: allow or deny.
GET /api/usage{ agents }: plan usage limits.
GET /api/recapA note for an agent about what other agents did in the same project.
POST /hook?guard=1Claude Code's PreToolUse for Edit, Write, MultiEdit or NotebookEdit (sent by bridge/guard-hook.js): if another active session just changed that file, Claude Code's { hookSpecificOutput: { hookEventName: "PreToolUse", permissionDecision: "ask" | "deny", permissionDecisionReason } }, else {}. See Two agents, one file.
GET /api/handoff/agentsThe agents a session can be handed to that are installed here: { agents: [{ id, name }] }.
POST /api/handoff{ session, agent }, agent one of codex, claude, gemini or copy: writes the hand-off note and opens that agent in a new terminal in the session's own folder (copy only returns the note). { ok, note, file, dir }; errors still carry note. 400 for another agent or one that isn't installed, 404 for an unknown session, 409 when the folder isn't known.
GET /The pal page. ?character=<id> picks the first pal.
GET /dashboardThe dashboard.
GET /bridge/…, /src/…, /desktop/…The pages' own files (for example /bridge/notch.html). Nothing outside these folders is served.

POST /event

Any agent can send its state, activity rows, or both:

curl -s http://127.0.0.1:5175/event -H "content-type: application/json" -d '{
  "session": "run-42", "harness": "my-agent", "label": "my-project",
  "state": "working", "text": "Running tests",
  "activity": { "id": "call-1", "kind": "run", "title": "npm test", "status": "running",
                "body": { "command": "npm test" } }
}'

The answer is always 200 with {}, even when the event wasn't understood. The fields are described in the event format. What the bridge does with them:

  • Each row's id becomes <session>:<id>, so ids only need to be unique within a session. A row without an id gets a new one.
  • A new row gets defaults: kind: "tool" (or your kind, if it's valid), status: "ok", a title from tool or kind, at now, and a start time when it's running.
  • A follow-up with the same id only changes the fields it sends. body is merged key by key, and a row that's ok or failed stays that way.
  • helper reports a helper agent, and is sent on as a helpers event.
  • state and text set the pal. Instead, you can send any event the pal understands, such as Anthropic, OpenAI or Agent SDK stream events (see the mapping).
  • A body with hook_event_name and session_id is treated as a Claude Code hook event, so don't use those names for anything else.
  • If the generic integration is switched off, events here are ignored.

POST /hook

Where hook commands send their events. bridge/hook.js reads the event from standard input and posts it here, so you rarely call this yourself.

  • POST /hook takes Claude Code's hook payloads (hook_event_name, session_id, cwd, transcript_path, tool_name, tool_input…). The first event from a session also loads its transcript, so its history is complete.
  • POST /hook?agent=<id> takes another agent's payload as that agent sends it, and hands it to the adapter for <id> (cursor, gemini, opencode, copilot).

The answer is {}, which tells Claude Code to carry on as usual. The one exception is a Claude Code PermissionRequest while Approve from the pal is on and a pal, the notch or the dashboard is open: then the bridge waits up to approvalWait seconds, and if you answer, replies in Claude Code's hook format:

{ "hookSpecificOutput": { "hookEventName": "PermissionRequest", "decision": { "behavior": "allow" } } }
{ "hookSpecificOutput": { "hookEventName": "PermissionRequest", "decision": { "behavior": "deny", "reason": "The user said no from dotpals." } } }

GET /events

A Server-Sent Events stream of everything that happens. When you connect, it first replays the current picture (the settings, every activity entry, each session's state, context and helpers, and pending approvals), then sends changes as they happen. A comment line (: ping) every 15 seconds keeps the connection open.

const feed = new EventSource('http://127.0.0.1:5175/events');
feed.onmessage = (e) => console.log('state', JSON.parse(e.data));          // unnamed events
feed.addEventListener('activity', (e) => console.log('activity', JSON.parse(e.data)));
feed.addEventListener('context', (e) => console.log('context', JSON.parse(e.data)));

A page on another website can't read this stream (the bridge sends no CORS headers). Use it from the bridge's own pages, a script, or a tool on your computer.

Event types

EventPayloadSent when
(unnamed) message{ session, harness, label, state, text?, at }A session's pal changes state. state: "sleeping" means the session is over (or was dismissed).
activityAn activity entryAn entry is added or updated. The whole entry is sent each time.
context{ session, harness, label, used, size, known, at }A session's context window changes. used and size are tokens; known is false when the size is a guess.
helpers{ session, harness, helpers: [{ id, name, task, status, doing, startedAt, endedAt }] }, where status is running, done or failedA session's helper agents change. The whole list is sent each time; finished helpers drop off after 60 s, and the list goes when the session ends.
laya{ installed, running, phase, message, line, error, port, log }The Laya dotpals set up changes: install and start progress (line is the latest line pip or Laya printed), running, stopped or failed.
conflict{ id, at, path, session, harness, label, other: { session, harness, label, at }, text, guard? }An agent is changing a file another active session changed a few minutes ago (once per file and pair per 10 minutes). guard is "ask" or "tell" when Claude Code was paused.
approvalPending: { id, session, harness, label, tool, kind, title, detail, command, patch, helper, risks, at, expiresAt, status: "pending" }. Then: { id, session, status: "allow" | "deny" | "expired" }A permission request is waiting for you, then answered or timed out.
configThe full settingsOn connect, and whenever settings change.
agentsAn array of agent objectsAfter Connect or Disconnect.
reset{}History was cleared.

The activity entry

{
  "id": "5f2c…:toolu_01A…",    // stable; the same id is sent again as the entry changes
  "session": "5f2c…",           // other agents' sessions are prefixed: "codex:…", "cursor:…"
  "harness": "claude",          // claude, codex, cursor, gemini, opencode, copilot, or your harness
  "label": "my-project",        // usually the project folder
  "at": 1790000000000,          // ms since 1970
  "kind": "edit",               // prompt read edit write run search web agent mcp skill plan tool done error compact
  "tool": "Edit",               // the agent's own tool name
  "title": "src/app.js",        // one line
  "detail": "…",                // optional second line
  "files": [{ "path": "C:/work/my-project/src/app.js", "change": "edit" }],
  "body": { "patch": "-old\n+new" },   // command, patch, output, args: plain text, clipped
  "status": "ok",               // running waiting ok failed stopped info
  "startedAt": 1790000000000, "ms": 120, "error": "…",
  "summary": "…",               // on done entries: what the agent said it did
  "plan": [{ "text": "Add tests", "status": "in_progress" }],  // on plan entries
  "check": { "by": "jev", "state": "passed", "p": 0.94, "ms": 310 }  // on unclear test runs, when a checker answered
}

Lines starting with // are explanations, not part of the JSON. Fields that don't apply are left out.

GET /api/status

{
  "version": "0.7.0", "port": 5175, "startedAt": 1790000000000,
  "home": "/home/you/.dotpals",
  "configFile": "/home/you/.dotpals/config.json",
  "historyFile": "/home/you/.dotpals/history.json",
  "historySize": 481233,       // bytes
  "entries": 812,              // entries the bridge holds right now
  "adapters": { "claude": { … }, "codex": { … }, "generic": { "endpoint": "http://127.0.0.1:5175/event", … } },
  "agents": [ … ]              // as in GET /api/agents
}

POST /api/config

Send only the settings you want to change. The bridge checks them, saves them to config.json, applies them (starting or stopping the Codex and Claude transcript followers as needed), broadcasts a config event and returns the full settings. Invalid values are ignored.

curl -s http://127.0.0.1:5175/api/config -H "x-dotpals: 1" -H "content-type: application/json" \
  -d '{ "historyDays": 14, "agents": { "cursor": false } }'

A body that isn't JSON gets 400.

The checker's API key is write-only: send { "checker": { "jevKey": "…" } } to save one (an empty string keeps the saved key, and something that can't be a key gets 400), or { "checker": { "removeKey": true } } to delete it. Responses and the config event never include it, only keySet, keyLast4 and keyFrom.

POST /api/checker/test

With x-dotpals: 1 and an optional { "mode": "local" | "cloud" } (default: the saved mode), sends one tiny request to the test-result checker: GET /health on Laya's server, or the list of models from TypeSafe. Returns { ok: true, by, ms, model? } or { ok: false, by, ms, error }, where by is laya or jev and error is a short reason such as “couldn't reach Laya: is laya-serve running?”.

Laya on this computer

With x-dotpals: 1: POST /api/checker/laya/setup installs Laya into ~/.dotpals/laya if it isn't yet, starts it on 127.0.0.1, and answers at once with 202 and { ok, laya }; the progress comes as laya events. When it's installed, the settings get checker.mode: "local" and checker.layaManaged: true. /start (202, or 400 when it isn't set up), /stop and /uninstall (stops it, deletes the folder and turns the checker off if it was on Local) answer { ok, laya }. GET /api/checker/laya returns the status alone; it's also in GET /api/config as checker.laya.

Agents

GET /api/agents returns { agents: [...] }, one object per integration:

{
  "id": "cursor", "name": "Cursor",
  "via": "Hooks (~/.cursor/hooks.json)",
  "how": "Connect adds a small command to Cursor's hooks. …",
  "docs": "https://cursor.com/docs/hooks",
  "setup": "connect",          // plugin (Claude Code), auto (Codex), connect, or http (any agent)
  "file": "/home/you/.cursor/hooks.json",
  "enabled": true,             // the on/off switch
  "found": true, "where": "/home/you/.cursor",
  "connected": true,           // null unless setup is "connect"
  "lastEventAt": 1790000000000
}

Claude Code's entry also has install (the /plugin commands), and the generic entry has endpoint, the address for POST /event.

  • POST /api/agents/<id>/connect and /disconnect (with x-dotpals: 1) return { ok: true, file, backup, command?, note?, agent }, where agent is the updated object. An agent that doesn't need connecting, or a config file that can't be read, gets 400 with the reason; an unknown id gets 404.
  • POST /api/agents/<id>/test runs the agent's real hook command (or loads its plugin) with a sample event, waits up to 6 seconds for it to arrive, then plays a short scene on a pal. It returns { ok: true, via }, where via is hook, plugin or bridge, or { ok: false, via, error }. Test events aren't recorded as activity.

POST /api/sessions/<id>/dismiss

Puts a session to sleep for every viewer, like the × on its tab. Its history isn't touched, and it comes back if the agent does something new. Needs x-dotpals: 1. Returns { ok: true, wasActive }. URL-encode the session id.

Approvals

GET /api/approvals returns { approvals: [...] }, the permission requests waiting for an answer, in the same shape as the pending approval event. To answer one:

curl -s -X POST http://127.0.0.1:5175/api/approvals/<id> -H "x-dotpals: 1" \
  -H "content-type: application/json" -d '{ "decision": "allow" }'
  • decision is "allow" or "deny"; anything else gets 400.
  • Returns { ok: true, decision }. A request that was already answered or has timed out gets 404.

GET /api/usage

Plan usage limits, read from files on your computer and cached for a few seconds:

{
  "agents": [
    {
      "harness": "claude",
      "window": { "used_percent": 42, "resets_at": 1790003600000, "window_minutes": 300 },
      "weekly": { "used_percent": 18, "resets_at": 1790400000000, "window_minutes": 10080 },
      "limits": [
        { "label": "5-hour", "used_percent": 42, "resets_at": 1790003600000, "window_minutes": 300 },
        { "label": "Week", "used_percent": 18, "resets_at": 1790400000000, "window_minutes": 10080 }
      ],
      "context": 31, "model": "Opus", "updatedAt": 1790000000000
    },
    { "harness": "codex", "window": { … }, "weekly": { … }, "limits": [ … ], "plan": "plus", "updatedAt": 1790000000000 }
  ]
}
  • Until dotpals statusline is set up, Claude's entry has "setup": "statusline" and no limits.
  • Codex appears once its logs have limits in them.
  • A limit whose window has already reset reads 0%. resets_at is in milliseconds.

GET /api/recap

A short note for an agent about what your other agents did in the same project, for Share with your agents. The Claude Code plugin's bridge/context-hook.js calls it and hands the note to Claude.

curl -s "http://127.0.0.1:5175/api/recap?session=5f2c9e&label=my-app&mode=start"
ParameterMeaning
sessionThe session asking. It's left out of its own note.
labelThe project: the name of its folder. Other sessions with the same label count.
modestart for the whole note, or prompt for a note only when there's news since the last one this session got.

It returns { "text": "…" }, or {} when there's nothing to say or the setting (shareRecap) is off. The note covers up to four other sessions active in the last two hours that changed files or ran tests: what they changed, whether their last test run passed, what they were asked, and whether they're working now.

Errors

StatusMeaning
400The body isn't valid (bad JSON, a bad decision), or a Connect couldn't be done. The reason is in error.
403A change without the x-dotpals: 1 header, or an unknown GET under /api/.
404No such agent, approval, page or file.
421The request wasn't addressed to 127.0.0.1, localhost or [::1].

Edit this page on GitHub