---
title: Overview | Agent Graph API
description: 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`.

Terminal window

```
curl https://agentgraph.chofter.com/api/v1/graphs \
  -u ag_sk_live_51Hx7Qm2Vb9Kd4Tz0Ws3Ne8LrPq4Xa9T:
```

## 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`, a `root_id` (the top of its tree) and a `session_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

Authenticate with an API key, made on your [account page](https://agentgraph.chofter.com/account). Send it as the username of HTTP Basic auth (no password), or as a bearer token:

Terminal window

```
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

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.

Terminal window

```
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.

## Ids

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/k3P9xQ2mWt7a` is `gph_k3P9xQ2mWt7a`.
- A **node** id is `node_` and the base64url (no padding) of the id the agent tool gave it, which is returned as `provider_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`

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:

Terminal window

```
# 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](/docs/reference/api/resources/graphs/subresources/events/index.md): each records which nodes it changed, field by field.

## 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](https://agentgraph.chofter.com/docs)), its retained history can briefly shrink to less than 1,000 events.

## 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 (or `null`, 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 with `expand[]`. 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.

Terminal window

```
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](/docs/reference/api/resources/graphs/subresources/nodes/methods/retrieve/index.md).

## 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](/docs/reference/api/resources/graphs/subresources/nodes/methods/search/index.md) pages with `page` and `next_page` instead, as its results are ranked.

## 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

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

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](/docs/reference/api/resources/graphs/subresources/events/index.md) `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

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

- **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 `body` is `null`.
- **Processes and hosts.** The ids of processes and the names of machines are left out.
