Overview
Read the graphs your coding agents make, at any point in their history.
The Agent Graph API gives your own servers, and the AI systems they run, read access to the graphs your coding agents make: every session and subagent, what each is doing, who is waiting on whom, and how that changed over time.
It is a REST API with predictable, resource-oriented URLs. It returns JSON, uses standard HTTP verbs, status codes and authentication, and is read-only: every endpoint is a GET.
curl https://agentgraph.chofter.com/api/v1/graphs \ -u ag_sk_live_51Hx7Qm2Vb9Kd4Tz0Ws3Ne8LrPq4Xa9T:What’s in a graph
Section titled “What’s in a graph”A graph is one of your account’s live shares: what agent-graph watch-remote streams from your machine. It holds nodes and the events that made them.
- A node is a session (one run of Claude Code, Codex, Cursor, …) or an agent that a session or another agent started. Nodes form trees: every node has a
parent_id, aroot_id(the top of its tree) and asession_id(the session it runs in). - An event is one thing that happened: a session started, an agent was spawned, a node changed state, a task list was updated. Events are immutable and ordered; each one records which nodes it changed, and how.
The graph is derived from the events by the same reducer the viewer runs, so the API and the viewer always agree.
Authentication
Section titled “Authentication”Authenticate with an API key, made on your account page. Send it as the username of HTTP Basic auth (no password), or as a bearer token:
curl https://agentgraph.chofter.com/api/v1/graphs \ -H "Authorization: Bearer ag_sk_live_51Hx7Qm2Vb9Kd4Tz0Ws3Ne8LrPq4Xa9T"| Prefix | Kind | Can read |
|---|---|---|
ag_sk_live_ | Secret key | Every graph your account owns |
ag_rk_live_ | Restricted key | Only the graphs chosen when it was made |
Keys are shown once, when made, and stored only as a hash: keep them secret, on your server, never in a browser or a repository. Revoke a key on the account page and it stops working at once. All requests must be made over HTTPS.
Versioning
Section titled “Versioning”The API is versioned by date. Each key is pinned to the version current when it was made, so a change that would break your integration never reaches you unannounced. To try another version for one request, send it in the Agent-Graph-Version header; every response says which version answered it.
curl https://agentgraph.chofter.com/api/v1/graphs \ -u ag_sk_live_51Hx7Qm2Vb9Kd4Tz0Ws3Ne8LrPq4Xa9T: \ -H "Agent-Graph-Version: 2026-10-09"Adding a field, an endpoint, an optional parameter, an event type, an enum value or an error code is not a breaking change. Your code should ignore what it doesn’t recognise.
Every object has an id with a prefix that says what it is.
| Object | Prefix | Example |
|---|---|---|
| Graph | gph_ | gph_k3P9xQ2mWt7a |
| Node | node_ | node_Y2xhdWRlLWNvZGU6NWYyYzFlOWE… |
| Event | evt_ | evt_01M4G2FMPN7KD3N1PZ0QX8RM4A |
- A graph id is
gph_and the id in the share’s link:/l/k3P9xQ2mWt7aisgph_k3P9xQ2mWt7a. - A node id is
node_and the base64url (no padding) of the id the agent tool gave it, which is returned asprovider_ref:claude-code:<session>for a session,claude-code:<session>/<agent>for an agent. The encoding is reversible, needs no lookup, and keeps ids safe in a URL path. - An event id is
evt_and the event’s ULID. ULIDs sort by time, so event ids sort in the order events happened, and any event id names a point in time (below).
Ids are case-sensitive. Treat them as opaque strings: don’t parse them, except to decode node_ ids if you need the provider’s own id and don’t have provider_ref at hand.
Time travel: as_of
Section titled “Time travel: as_of”Every graph, node and search endpoint can answer as of any point in the graph’s retained history. Pass as_of:
| Value | Answers with |
|---|---|
An event id, evt_… | The state just after that event was applied |
| A Unix time in milliseconds | The state after the last event at or before that time |
| Omitted | The state now |
Every object and list in a response carries as_of_event_id: the event it was answered at. Pass it on later requests to read one consistent moment, even while the graph keeps changing:
# What's stuck right now?curl -G https://agentgraph.chofter.com/api/v1/graphs/gph_k3P9xQ2mWt7a/nodes \ -u $AGENT_GRAPH_KEY: -d stale=true# → "as_of_event_id": "evt_01M4G3G4E8Q9R2M5X4T6W8Y0B1"
# Everything under the first one, at that same moment.curl -G https://agentgraph.chofter.com/api/v1/graphs/gph_k3P9xQ2mWt7a/nodes/node_Y2xhdWRlLWNvZGU6NWYyYzFlOWEtOGQxYi00YjZlLTlhNTEtMGY2ZjJjN2QzZTEwL2I4MWMyMmUwOWY3YTNjNTU \ -u $AGENT_GRAPH_KEY: \ -d as_of=evt_01M4G3G4E8Q9R2M5X4T6W8Y0B1 \ -d "expand[]=descendants"A response for a fixed point never changes, so it’s sent with Cache-Control: private, max-age=31536000, immutable.
To walk through history rather than look at one point, list events: each records which nodes it changed, field by field.
Retention
Section titled “Retention”A live share keeps its recent history: between 1,000 and 2,000 events, trimmed from the start as new ones arrive. A graph’s retention says what it holds now. An as_of before retention.first_event_id is refused with 410 and the code outside_retention_window, and the error names the oldest point you can ask for.
When a share restarts on a new machine state (see restart in the design notes), its retained history can briefly shrink to less than 1,000 events.
Ids versus objects, and expanding
Section titled “Ids versus objects, and expanding”A field that refers to another object comes in two forms, and they never mix:
parent_id,root_id,session_id,requested_by_id,on_ids, …: always a string id (ornull, or an array of ids), and always present.parent,root,session,requested_by,on, …: always the full object, and present only when you ask for it withexpand[]. A field that wasn’t expanded is absent, never an id in its place.
So a field never changes type, and whether something was expanded is simply whether its field is there.
curl -G https://agentgraph.chofter.com/api/v1/graphs/gph_k3P9xQ2mWt7a/nodes/node_Y2xhdWRlLWNvZGU6NWYyYzFlOWEtOGQxYi00YjZlLTlhNTEtMGY2ZjJjN2QzZTEwL2EzZmQwN2MyYjE5ZTRkNjE \ -u $AGENT_GRAPH_KEY: \ -d "expand[]=parent" \ -d "expand[]=children"Some fields exist only when expanded, because they can be large: children, descendants, tasks, spawns, waits and messages.
Paths. Expand an expanded object’s fields with dots: expand[]=parent.parent, expand[]=children.children, expand[]=blocked.on. A path goes at most 4 levels deep, and a request can expand at most 20 paths. In a list, expand each item’s fields with the data. prefix: expand[]=data.parent.
Whole subtrees. children is nested, one level per path segment. For everything under a node, however deep, expand descendants instead: a single flat list in tree order, each node with its parent_id, paged like any other list. See Retrieve a node.
Pagination
Section titled “Pagination”Every list endpoint returns a list object and takes the same cursor parameters:
{ "object": "list", "url": "/api/v1/graphs/gph_k3P9xQ2mWt7a/nodes", "as_of_event_id": "evt_01M4G3G4E8Q9R2M5X4T6W8Y0B1", "has_more": true, "data": [ … ]}| Parameter | |
|---|---|
limit | How many objects to return, 1 to 1,000. Defaults to 100. |
starting_after | An object id: return the page after it. Pass the last id of the page you have. |
ending_before | An object id: return the page before it. Pass the first id of the page you have. |
Keep as_of the same across the pages of one list (the first page’s as_of_event_id), so that nodes added in the meantime don’t shift the pages under you. An embedded list’s url already includes it, and every parameter that defined the list: add only starting_after.
Search pages with page and next_page instead, as its results are ranked.
Conditional requests
Section titled “Conditional requests”Every successful response has an ETag. Send it back as If-None-Match and, if nothing has changed, you get 304 Not Modified with no body. That’s the cheap way to poll the state now: it costs no rate limit and almost no bandwidth.
Responses are compressed with Brotli or gzip when you send Accept-Encoding.
Errors
Section titled “Errors”The API uses conventional HTTP status codes: 2xx succeeded, 4xx means something about the request was wrong, 5xx means something went wrong on our side (they’re rare). Every error has the same shape:
{ "error": { "type": "invalid_request_error", "code": "outside_retention_window", "message": "This graph holds events from evt_01M4FXYWR82B8XK0V5QJ7M1N3R to evt_01M4G3G4E8Q9R2M5X4T6W8Y0B1.", "param": "as_of", "doc_url": "https://agentgraph.chofter.com/docs/reference#errors", "first_event_id": "evt_01M4FXYWR82B8XK0V5QJ7M1N3R", "last_event_id": "evt_01M4G3G4E8Q9R2M5X4T6W8Y0B1" }}| Status | Meaning |
|---|---|
200 | OK. |
304 | Not modified since the ETag you sent. |
400 | A parameter is missing, unknown or invalid. |
401 | No API key, or one that’s invalid or revoked. |
403 | The key can’t read this graph. |
404 | No such graph, node or event, or the node doesn’t exist at as_of. |
410 | as_of is older than the graph’s retained history. |
429 | Too many requests: wait for Retry-After. |
500, 502, 503 | Something went wrong on our side. Safe to retry: every request is read-only. |
| Type | |
|---|---|
invalid_request_error | The request was invalid. |
authentication_error | The API key is missing, invalid or revoked. |
permission_error | The key isn’t allowed to do this. |
rate_limit_error | Too many requests. |
api_error | Something went wrong on our side. |
| Code | Status | |
|---|---|---|
parameter_missing | 400 | A required parameter is missing. param names it. |
parameter_invalid | 400 | A parameter’s value is invalid. param names it. |
parameter_unknown | 400 | A parameter that the endpoint doesn’t take. param names it. |
version_invalid | 400 | Agent-Graph-Version isn’t a version. |
expand_invalid | 400 | An expand[] path names a field that can’t be expanded. |
expand_too_deep | 400 | An expand[] path goes more than 4 levels deep. |
expand_not_allowed | 400 | That expansion isn’t available here: descendants and children can’t be expanded on the items of a list. |
search_query_invalid | 400 | The search query couldn’t be parsed. message says where. |
api_key_missing | 401 | No API key was sent. |
api_key_invalid | 401 | The API key isn’t one, or has been revoked. |
api_key_restricted | 403 | A restricted key that can’t read this graph. |
resource_missing | 404 | No such graph, node or event (for this key). |
node_not_yet_created | 404 | The node exists, but not yet at as_of. first_event_id is the event that creates it. |
outside_retention_window | 410 | as_of is before the graph’s retained history. first_event_id and last_event_id say what it holds. |
rate_limit | 429 | Too many requests. |
api_error | 500 | Something went wrong on our side. |
Every response, including errors, has a Request-Id header. Include it if you contact us about a request.
Rate limits
Section titled “Rate limits”Each key can make 25 requests a second, in bursts of up to 100. Responses carry RateLimit-Limit, RateLimit-Remaining and RateLimit-Reset (seconds until the limit resets); a refused request has Retry-After. A 304 doesn’t count.
The limit is kept by each of the site’s servers, so a key whose requests are spread over several can go a little faster. Don’t rely on it: keep to the limit.
The cheapest way to follow a graph is to list events starting_after the last one you saw, or to poll with If-None-Match, not to fetch the whole graph again.
Text in nodes is untrusted
Section titled “Text in nodes is untrusted”Fields like title, summary, headline, purpose, attention, task text and message summaries are written by coding agents, from what they were working on. They can contain anything, including text written to manipulate a model that reads it.
If you pass them to an AI system, mark them as data, not instructions, and don’t let them trigger actions on their own.
What’s not included
Section titled “What’s not included”- Prompts. No endpoint returns what a person typed to an agent.
- Message bodies are present only if the share was made with body capture on; otherwise
bodyisnull. - Processes and hosts. The ids of processes and the names of machines are left out.