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 on127.0.0.1. To change the port, see Configuration. - Host: every request except
POST /hookandPOST /eventmust be addressed to127.0.0.1,localhostor[::1]. Anything else gets421. - Changes: every
POSTunder/api/needs the headerx-dotpals: 1, or it gets403. 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
| Route | What it does |
|---|---|
POST /event | Send an event from any agent. |
POST /hook | Claude Code hook events (what bridge/hook.js sends). |
POST /hook?agent=<id> | Another agent's hook events, read by its adapter. |
GET /events | The live feed, as Server-Sent Events. |
GET /api/activity | { entries }: every activity entry the bridge holds, oldest first. |
GET /api/status | What's running and connected, and where files are. |
GET /api/config | The settings (see keys). |
POST /api/config | Change settings. |
POST /api/history/clear | Clear the history. Returns { ok: true }. |
POST /api/checker/test | Test the test-result checker. |
GET /api/checker/laya | The status of the Laya dotpals set up. |
POST /api/checker/laya/setup | Set up Laya (and start it). /start, /stop and /uninstall too. |
GET /api/agents | { agents }: every integration. |
POST /api/agents/<id>/connect | Connect an agent (adds dotpals to its config). |
POST /api/agents/<id>/disconnect | Take dotpals out of its config again. |
POST /api/agents/<id>/test | Send a test event through the real hook. |
POST /api/sessions/<id>/dismiss | Put 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/recap | A note for an agent about what other agents did in the same project. |
POST /hook?guard=1 | Claude 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/agents | The 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 /dashboard | The 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 fromtoolorkind,atnow, and a start time when it'srunning. - A follow-up with the same id only changes the fields it sends.
bodyis merged key by key, and a row that'sokorfailedstays that way. helperreports a helper agent, and is sent on as ahelpersevent.stateandtextset 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_nameandsession_idis treated as a Claude Code hook event, so don't use those names for anything else. - If the
genericintegration 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 /hooktakes 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
| Event | Payload | Sent 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). |
activity | An activity entry | An 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 failed | A 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. |
approval | Pending: { 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. |
config | The full settings | On connect, and whenever settings change. |
agents | An array of agent objects | After 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>/connectand/disconnect(withx-dotpals: 1) return{ ok: true, file, backup, command?, note?, agent }, whereagentis the updated object. An agent that doesn't need connecting, or a config file that can't be read, gets400with the reason; an unknown id gets404.POST /api/agents/<id>/testruns 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 }, whereviaishook,pluginorbridge, 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" }'
decisionis"allow"or"deny"; anything else gets400.- Returns
{ ok: true, decision }. A request that was already answered or has timed out gets404.
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 statuslineis 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_atis 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"
| Parameter | Meaning |
|---|---|
session | The session asking. It's left out of its own note. |
label | The project: the name of its folder. Other sessions with the same label count. |
mode | start 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
| Status | Meaning |
|---|---|
400 | The body isn't valid (bad JSON, a bad decision), or a Connect couldn't be done. The reason is in error. |
403 | A change without the x-dotpals: 1 header, or an unknown GET under /api/. |
404 | No such agent, approval, page or file. |
421 | The request wasn't addressed to 127.0.0.1, localhost or [::1]. |