# GigaRAG API reference

The REST API reads, writes and searches the buckets, threads and memos in a GigaRAG workspace. Every request carries an API key, and the key decides which workspace it reaches.

- Base URL: `https://api.gigarag.com`
- Header on every request: `Authorization: Bearer <your-api-key>`
- The same reference as a web page: https://gigarag.com/docs/api

## Authentication

Create a key on the API Keys page in GigaRAG and send it on every request as a Bearer token. The `X-Api-Key` header works too.

```bash
curl https://api.gigarag.com/v1/buckets \
  -H "Authorization: Bearer $GIGARAG_API_KEY"
```

A key belongs to one workspace, so no route takes a workspace id. An admin key reaches every bucket in it, and a restricted key reaches only the buckets it was granted, either to view or to edit. Keep it in an environment variable and out of source code. A key sent in a query string is ignored, because URLs end up in logs and browser history.

Every authentication failure returns the same `401`, whether the key is mistyped, revoked or unknown.

## Requests and responses

Send bodies as JSON with `Content-Type: application/json`. Responses are JSON, and each one carries an `X-Request-Id` header worth quoting when you report a problem.

Buckets, threads and memos each have an `id`, which is a UUID, and a short `ref` handle such as `B:1`, `T:4` or `N:12` that never changes. All three carry `created_at` and `updated_at` as ISO 8601 timestamps.

Each workspace has a per-minute limit on every request and a monthly quota that counts searches and writes. Responses carry `X-RateLimit-Limit` and `X-RateLimit-Remaining` for the per-minute limit.

## Pagination

A listing returns a page of rows and a cursor. Pass `next_cursor` back as `after` to get the next page, and stop when it comes back `null`. Rows added while you read aren't skipped.

```json
{
  "items": [
    {
      "id": "018f2a1c-4b7e-7c11-9a3d-2f6b8e1d4c01",
      "ref": "B:1",
      "title": "Handbook"
    }
  ],
  "next_cursor": "018f2a1c-4b7e-7c11-9a3d-2f6b8e1d4c01"
}
```

## Errors

A failed request returns a readable `error` message and, for many failures, a `code` to branch on.

```json
{
  "error": "slug must be lowercase letters, digits, hyphens or underscores",
  "code": "invalid_request"
}
```

| Status | Code | Meaning |
| --- | --- | --- |
| 400 | `invalid_request` | The request is malformed. The message names the field. |
| 400 | `invalid_reference` | The body refers to a row that doesn't exist. |
| 401 | `invalid_api_key` | The key is missing, wrong or revoked. |
| 403 |  | The key wasn't granted that bucket or was granted it only to view, the id doesn't exist, or the workspace is suspended and refuses writes. A missing id returns 403 so an id can't be used to find out whether a row exists. |
| 403 | `limit_exceeded` | The write would pass a workspace limit. `limit`, `current` and `max` in the body say which. |
| 404 |  | No such route. |
| 409 | `conflict` | The slug is already taken where you put it. |
| 423 | `project_frozen` | The workspace is moving between servers and refuses writes for a few seconds. Retry after the seconds in `Retry-After`. |
| 429 | `rate_limited` | Too many requests this minute. Retry after the seconds in `Retry-After`. |
| 429 | `quota_exceeded` | The monthly request quota is used up. |

Of these, only `423` and `rate_limited` are worth retrying.

## Search

Find memos by meaning and by their exact words in one call.

### Search memos

`POST /v1/search`

Runs full-text and semantic search over every memo and every section inside one, then merges the two result lists, so an exact identifier and a paraphrase both find the memo.

Each result carries a snippet of the section that matched in `matched_section`, which can be `null`. The memo's body is left out, so read the memo when you need it.

`score` only compares results within one response. Ties break by id, so the same query returns the same order every time.

Body:

| Field | Type | Required | Description |
| --- | --- | --- | --- |
| `query` | string | yes | What to look for, up to 2,000 characters. |
| `limit` | integer | no | 1 to 200, default 20. |
| `bucket_id` | uuid | no | Only search inside this bucket. |
| `thread_id` | uuid | no | Only search inside this thread. |
| `node_type` | string | no | Only memos of this type. |

Request body:

```json
{
  "query": "how do we throttle clients",
  "limit": 20
}
```

Response `200`:

```json
{
  "results": [
    {
      "id": "018f2a1c-6e31-7f55-b2c4-1d8e3f4a9b23",
      "ref": "N:12",
      "title": "Rate limiting",
      "summary": "How we throttle API clients",
      "node_type": "note",
      "thread_id": "018f2a1c-5d20-7a44-8e1b-6c9f0a2b7d12",
      "bucket_id": "018f2a1c-4b7e-7c11-9a3d-2f6b8e1d4c01",
      "score": 0.0491,
      "matched_section": {
        "id": "018f2a1c-8a53-7177-94e6-3fa05b6c1d45",
        "heading": "Token buckets",
        "snippet": "Each client gets 60 requests a minute."
      }
    }
  ]
}
```

## Memos

A memo is a markdown document inside a thread. The API calls it a node, so memo routes live under `/v1/nodes`.

### Create a memo

`POST /v1/nodes`

Splits the content into sections at its headings, embeds each section and stores the result, so the memo is searchable as soon as the call returns.

Each `[label](N:12)` link to an existing memo becomes an edge, and `links` in the response counts them. `unresolved_links` lists the handles in the content that did not become one. See Links inside memos.

Counts toward the workspace limits on memos and vectors.

Body:

| Field | Type | Required | Description |
| --- | --- | --- | --- |
| `thread_id` | uuid | yes | The thread the memo goes in. |
| `slug` | string | yes | Lowercase letters, digits, hyphens and underscores, up to 64 characters, starting and ending with a letter or digit. Unique within the thread. |
| `title` | string | yes | Up to 500 characters. |
| `summary` | string | no | Up to 5,000 characters. Searched along with the title. |
| `content` | string | no | The markdown body, up to 10,000 characters. Anything longer returns `400`. |
| `node_type` | string | no | Your own label for the memo, such as `decision`, up to 64 characters. Defaults to `note`. Search and listings can filter by it. |
| `properties` | object | no | Any JSON object you want kept with the memo. |

Request body:

```json
{
  "thread_id": "018f2a1c-5d20-7a44-8e1b-6c9f0a2b7d12",
  "slug": "rate-limits",
  "title": "Rate limiting",
  "summary": "How we throttle API clients",
  "content": "# Token buckets\n\nEach client gets 60 requests a minute. See [retries](N:9).",
  "node_type": "note"
}
```

Response `201`:

```json
{
  "id": "018f2a1c-6e31-7f55-b2c4-1d8e3f4a9b23",
  "thread_id": "018f2a1c-5d20-7a44-8e1b-6c9f0a2b7d12",
  "ref": "N:12",
  "slug": "rate-limits",
  "title": "Rate limiting",
  "summary": "How we throttle API clients",
  "content": "# Token buckets\n\nEach client gets 60 requests a minute. See [retries](N:9).",
  "node_type": "note",
  "properties": {},
  "created_at": "2026-09-15T10:12:00.000Z",
  "updated_at": "2026-09-15T10:12:00.000Z",
  "sections": 1,
  "links": 1,
  "unresolved_links": []
}
```

### Read a memo

`GET /v1/nodes/:id`

The memo in full: its body, its sections, the memos linked to it in either direction, and the memos most similar to it.

`distance` in `similar` is cosine distance, so lower is closer. `similar` is empty for a memo with no vector yet, which is normal while a workspace is being re-embedded.

Query parameters:

| Field | Type | Required | Description |
| --- | --- | --- | --- |
| `links` | integer | no | 1 to 100, default 25. How many linked memos to return. |
| `similar` | integer | no | 0 to 10, default 5. How many similar memos to return. 0 skips the lookup. |

Response `200`:

```json
{
  "id": "018f2a1c-6e31-7f55-b2c4-1d8e3f4a9b23",
  "thread_id": "018f2a1c-5d20-7a44-8e1b-6c9f0a2b7d12",
  "ref": "N:12",
  "slug": "rate-limits",
  "title": "Rate limiting",
  "summary": "How we throttle API clients",
  "content": "# Token buckets\n\nEach client gets 60 requests a minute. See [retries](N:9).",
  "node_type": "note",
  "properties": {},
  "created_at": "2026-09-15T10:12:00.000Z",
  "updated_at": "2026-09-15T10:12:00.000Z",
  "bucket_id": "018f2a1c-4b7e-7c11-9a3d-2f6b8e1d4c01",
  "sections": [
    {
      "id": "018f2a1c-8a53-7177-94e6-3fa05b6c1d45",
      "position": 0,
      "heading": "Token buckets",
      "content": "Each client gets 60 requests a minute. See [retries](N:9)."
    }
  ],
  "links": [
    {
      "node_id": "018f2a1c-7f42-7066-a3d5-2e9f4a5b0c34",
      "label": "links_to",
      "description": "retries",
      "weight": 0.5,
      "direction": "out",
      "title": "Retries",
      "summary": "When a client should retry"
    }
  ],
  "similar": [
    {
      "node_id": "018f2a1c-7f42-7066-a3d5-2e9f4a5b0c34",
      "ref": "N:9",
      "title": "Retries",
      "summary": "When a client should retry",
      "node_type": "note",
      "distance": 0.21
    }
  ]
}
```

### List memos

`GET /v1/nodes`

Memos in the order they were created, without their bodies. See Pagination for reading every page.

Query parameters:

| Field | Type | Required | Description |
| --- | --- | --- | --- |
| `limit` | integer | no | 1 to 200, default 50. A value outside the range is clamped to it. |
| `after` | uuid | no | The `next_cursor` from the previous page. |
| `bucket_id` | uuid | no | Only memos in this bucket. |
| `thread_id` | uuid | no | Only memos in this thread. |
| `node_type` | string | no | Only memos of this type. |

Response `200`:

```json
{
  "items": [
    {
      "id": "018f2a1c-6e31-7f55-b2c4-1d8e3f4a9b23",
      "thread_id": "018f2a1c-5d20-7a44-8e1b-6c9f0a2b7d12",
      "bucket_id": "018f2a1c-4b7e-7c11-9a3d-2f6b8e1d4c01",
      "ref": "N:12",
      "slug": "rate-limits",
      "title": "Rate limiting",
      "summary": "How we throttle API clients",
      "node_type": "note",
      "properties": {},
      "created_at": "2026-09-15T10:12:00.000Z",
      "updated_at": "2026-09-15T10:12:00.000Z"
    }
  ],
  "next_cursor": null
}
```

### Find memos by title

`GET /v1/nodes/lookup`

For a link picker: the memos whose title or summary starts with what has been typed so far, across every bucket the key can read. A title that starts with `q` comes first. With no `q`, the most recently edited memos.

Nothing is embedded, so it is cheap enough to call on every keystroke. To find a memo by meaning, use Search instead. Results are ranked, not paged, so there is no cursor.

Query parameters:

| Field | Type | Required | Description |
| --- | --- | --- | --- |
| `q` | string | no | Up to 200 characters. Each word matches as the start of a word. |
| `limit` | integer | no | 1 to 25, default 8. |
| `exclude` | uuid | no | A memo to leave out, usually the one the link is being written in. |

Response `200`:

```json
{
  "items": [
    {
      "id": "018f2a1c-7f42-7066-a3d5-2e9f4a5b0c34",
      "thread_id": "018f2a1c-5d20-7a44-8e1b-6c9f0a2b7d12",
      "bucket_id": "018f2a1c-4b7e-7c11-9a3d-2f6b8e1d4c01",
      "ref": "N:9",
      "slug": "retries",
      "title": "Retries",
      "summary": "When a client should retry",
      "node_type": "note",
      "properties": {},
      "created_at": "2026-09-15T10:12:00.000Z",
      "updated_at": "2026-09-15T10:12:00.000Z"
    }
  ]
}
```

### Read several memos

`POST /v1/nodes/fetch`

Up to 50 memos with their bodies in one call, in the order you asked for them. Links and similar memos are left out.

An id that doesn't exist, or that sits in a bucket the key can't read, is left out of the result and the call still succeeds.

Body:

| Field | Type | Required | Description |
| --- | --- | --- | --- |
| `ids` | uuid[] | yes | 1 to 50 memo ids. |

Request body:

```json
{
  "ids": [
    "018f2a1c-6e31-7f55-b2c4-1d8e3f4a9b23",
    "018f2a1c-7f42-7066-a3d5-2e9f4a5b0c34"
  ]
}
```

Response `200`:

```json
{
  "nodes": [
    {
      "id": "018f2a1c-6e31-7f55-b2c4-1d8e3f4a9b23",
      "thread_id": "018f2a1c-5d20-7a44-8e1b-6c9f0a2b7d12",
      "ref": "N:12",
      "slug": "rate-limits",
      "title": "Rate limiting",
      "summary": "How we throttle API clients",
      "content": "# Token buckets\n\nEach client gets 60 requests a minute. See [retries](N:9).",
      "node_type": "note",
      "properties": {},
      "created_at": "2026-09-15T10:12:00.000Z",
      "updated_at": "2026-09-15T10:12:00.000Z",
      "bucket_id": "018f2a1c-4b7e-7c11-9a3d-2f6b8e1d4c01"
    }
  ]
}
```

### Create many memos

`POST /v1/nodes/bulk`

Up to 100 memos in one call, for indexing a repository or importing an archive. Each one is validated and written on its own.

This does not make writing cheaper. The rate limit counts operations rather than requests, so a batch of 100 costs 100. What it saves is the round trips, and it lifts the 20-message ceiling a batch over MCP runs into.

The status is `201` only when every memo was created. A partial success is `207`, and `results` carries an entry per memo in the order you sent them, so you can retry just the ones that failed. A refusal that applies to the whole call, such as running out of quota, stops the batch, and `results` is then shorter than what you sent.

Body:

| Field | Type | Required | Description |
| --- | --- | --- | --- |
| `nodes` | object[] | yes | 1 to 100 memos, each taking the same fields as a single create. |

Request body:

```json
{
  "nodes": [
    {
      "thread_id": "018f2a1c-5d20-7a44-8e1b-6c9f0a2b7d12",
      "slug": "rate-limits",
      "title": "Rate limits",
      "summary": "What the limits are and what happens at them",
      "content": "# Rate limits\n\nRequests are counted per minute."
    }
  ]
}
```

Response `201`:

```json
{
  "created": 1,
  "failed": 0,
  "results": [
    {
      "index": 0,
      "ok": true,
      "node": {
        "id": "018f2a1c-6e31-7f55-b2c4-1d8e3f4a9b23",
        "thread_id": "018f2a1c-5d20-7a44-8e1b-6c9f0a2b7d12",
        "ref": "N:12",
        "slug": "rate-limits",
        "title": "Rate limiting",
        "summary": "How we throttle API clients",
        "content": "# Token buckets\n\nEach client gets 60 requests a minute. See [retries](N:9).",
        "node_type": "note",
        "properties": {},
        "created_at": "2026-09-15T10:12:00.000Z",
        "updated_at": "2026-09-15T10:12:00.000Z"
      }
    }
  ]
}
```

### Update a memo

`PATCH /v1/nodes/:id`

Send only the fields you want to change. A field you leave out keeps its value.

Changing `content` rebuilds its sections, vectors and links in the same transaction as the update, and the response then carries `unresolved_links`. Changing only `title` or `summary` re-embeds the memo and leaves its sections alone, and a move re-embeds nothing.

Setting `thread_id` moves the memo. A slug already used in the destination thread returns `409`.

Body:

| Field | Type | Required | Description |
| --- | --- | --- | --- |
| `title` | string | no | Up to 500 characters. |
| `summary` | string | no | Up to 5,000 characters. |
| `content` | string | no | The new markdown body, up to 10,000 characters. A memo saved before that limit existed can still change its title, summary or thread without being shortened. |
| `thread_id` | uuid | no | Moves the memo to this thread. |

Request body:

```json
{
  "summary": "How we throttle API clients, per key"
}
```

Response `200`:

```json
{
  "id": "018f2a1c-6e31-7f55-b2c4-1d8e3f4a9b23",
  "thread_id": "018f2a1c-5d20-7a44-8e1b-6c9f0a2b7d12",
  "ref": "N:12",
  "slug": "rate-limits",
  "title": "Rate limiting",
  "summary": "How we throttle API clients, per key",
  "content": "# Token buckets\n\nEach client gets 60 requests a minute. See [retries](N:9).",
  "node_type": "note",
  "properties": {},
  "created_at": "2026-09-15T10:12:00.000Z",
  "updated_at": "2026-09-15T10:12:00.000Z",
  "sections": 1,
  "links": 1
}
```

### Delete a memo

`DELETE /v1/nodes/:id`

Deletes the memo with its sections, its vectors and every edge touching it. There is no undo.

Response `204` with no body.

## Buckets

Buckets are the top level of a workspace, and each one holds threads.

### Create a bucket

`POST /v1/buckets`

Creates an empty bucket. Needs an admin key. A slug already used in the workspace returns `409`.

Body:

| Field | Type | Required | Description |
| --- | --- | --- | --- |
| `slug` | string | yes | Lowercase letters, digits, hyphens and underscores, up to 64 characters, starting and ending with a letter or digit. |
| `title` | string | yes | Up to 200 characters. |
| `description` | string | no | Up to 2,000 characters. |

Request body:

```json
{
  "slug": "handbook",
  "title": "Handbook",
  "description": "How the team works"
}
```

Response `201`:

```json
{
  "id": "018f2a1c-4b7e-7c11-9a3d-2f6b8e1d4c01",
  "ref": "B:1",
  "slug": "handbook",
  "title": "Handbook",
  "description": "How the team works",
  "created_at": "2026-09-15T10:12:00.000Z",
  "updated_at": "2026-09-15T10:12:00.000Z"
}
```

### List buckets

`GET /v1/buckets`

Buckets the key can read, in the order they were created.

Query parameters:

| Field | Type | Required | Description |
| --- | --- | --- | --- |
| `limit` | integer | no | 1 to 200, default 50. A value outside the range is clamped to it. |
| `after` | uuid | no | The `next_cursor` from the previous page. |
| `q` | string | no | Only buckets whose title or slug contains this, up to 200 characters. `%` and `_` match themselves. |

Response `200`:

```json
{
  "items": [
    {
      "id": "018f2a1c-4b7e-7c11-9a3d-2f6b8e1d4c01",
      "ref": "B:1",
      "slug": "handbook",
      "title": "Handbook",
      "description": "How the team works",
      "created_at": "2026-09-15T10:12:00.000Z",
      "updated_at": "2026-09-15T10:12:00.000Z"
    }
  ],
  "next_cursor": null
}
```

### Update a bucket

`PATCH /v1/buckets/:id`

Send only the fields you want to change. A field you leave out keeps its value. A slug already used in the workspace returns `409`.

Body:

| Field | Type | Required | Description |
| --- | --- | --- | --- |
| `slug` | string | no | Lowercase letters, digits, hyphens and underscores, up to 64 characters, starting and ending with a letter or digit. |
| `title` | string | no | Up to 200 characters. |
| `description` | string | no | Up to 2,000 characters. |

Request body:

```json
{
  "title": "Team handbook"
}
```

Response `200`:

```json
{
  "id": "018f2a1c-4b7e-7c11-9a3d-2f6b8e1d4c01",
  "ref": "B:1",
  "slug": "handbook",
  "title": "Team handbook",
  "description": "How the team works",
  "created_at": "2026-09-15T10:12:00.000Z",
  "updated_at": "2026-09-15T10:12:00.000Z"
}
```

### Delete a bucket

`DELETE /v1/buckets/:id`

Deletes the bucket with every thread and memo inside it, and everything attached to those memos. Needs an admin key. There is no undo.

Response `204` with no body.

## Threads

Threads sit inside a bucket and hold memos.

### Create a thread

`POST /v1/threads`

Creates an empty thread inside a bucket. A slug already used in that bucket returns `409`.

Body:

| Field | Type | Required | Description |
| --- | --- | --- | --- |
| `bucket_id` | uuid | yes | The bucket the thread goes in. |
| `slug` | string | yes | Lowercase letters, digits, hyphens and underscores, up to 64 characters, starting and ending with a letter or digit. |
| `title` | string | yes | Up to 200 characters. |
| `description` | string | no | Up to 2,000 characters. |

Request body:

```json
{
  "bucket_id": "018f2a1c-4b7e-7c11-9a3d-2f6b8e1d4c01",
  "slug": "operations",
  "title": "Operations"
}
```

Response `201`:

```json
{
  "id": "018f2a1c-5d20-7a44-8e1b-6c9f0a2b7d12",
  "bucket_id": "018f2a1c-4b7e-7c11-9a3d-2f6b8e1d4c01",
  "ref": "T:4",
  "slug": "operations",
  "title": "Operations",
  "description": "",
  "created_at": "2026-09-15T10:12:00.000Z",
  "updated_at": "2026-09-15T10:12:00.000Z"
}
```

### List threads

`GET /v1/threads`

Threads in the order they were created. `node_count` is how many memos each one holds.

Query parameters:

| Field | Type | Required | Description |
| --- | --- | --- | --- |
| `limit` | integer | no | 1 to 200, default 50. A value outside the range is clamped to it. |
| `after` | uuid | no | The `next_cursor` from the previous page. |
| `bucket_id` | uuid | no | Only threads in this bucket. |

Response `200`:

```json
{
  "items": [
    {
      "id": "018f2a1c-5d20-7a44-8e1b-6c9f0a2b7d12",
      "bucket_id": "018f2a1c-4b7e-7c11-9a3d-2f6b8e1d4c01",
      "ref": "T:4",
      "slug": "operations",
      "title": "Operations",
      "description": "",
      "created_at": "2026-09-15T10:12:00.000Z",
      "updated_at": "2026-09-15T10:12:00.000Z",
      "node_count": 3
    }
  ],
  "next_cursor": null
}
```

### Update a thread

`PATCH /v1/threads/:id`

Send only the fields you want to change. A field you leave out keeps its value.

Setting `bucket_id` moves the thread and every memo in it. A slug already used in the destination bucket returns `409`.

Body:

| Field | Type | Required | Description |
| --- | --- | --- | --- |
| `slug` | string | no | Lowercase letters, digits, hyphens and underscores, up to 64 characters, starting and ending with a letter or digit. |
| `title` | string | no | Up to 200 characters. |
| `description` | string | no | Up to 2,000 characters. |
| `bucket_id` | uuid | no | Moves the thread to this bucket. |

Request body:

```json
{
  "title": "Ops"
}
```

Response `200`:

```json
{
  "id": "018f2a1c-5d20-7a44-8e1b-6c9f0a2b7d12",
  "bucket_id": "018f2a1c-4b7e-7c11-9a3d-2f6b8e1d4c01",
  "ref": "T:4",
  "slug": "operations",
  "title": "Ops",
  "description": "",
  "created_at": "2026-09-15T10:12:00.000Z",
  "updated_at": "2026-09-15T10:12:00.000Z"
}
```

### Delete a thread

`DELETE /v1/threads/:id`

Deletes the thread with every memo in it. There is no undo.

Response `204` with no body.

## Links between memos

An edge joins two memos with a named relationship. Links written inside a memo create edges too, as Links inside memos explains.

### Link two memos

`POST /v1/edges`

Creates an edge from one memo to another. An edge with the same `src_node_id`, `dst_node_id` and `rel_type` as an existing one changes nothing, even with a different weight or description, and returns `200` instead of `201`.

A memo can't link to itself.

Body:

| Field | Type | Required | Description |
| --- | --- | --- | --- |
| `src_node_id` | uuid | yes | The memo the edge starts at. |
| `dst_node_id` | uuid | yes | The memo the edge points to. |
| `rel_type` | string | yes | The relationship, such as `supersedes`, up to 64 characters. |
| `description` | string | no | Up to 2,000 characters. |
| `inverse_label` | string | no | How the edge reads from the other end, such as `superseded by`, up to 64 characters. |
| `weight` | number | no | 0 to 1000, default 1. Stronger edges are listed first. |

Request body:

```json
{
  "src_node_id": "018f2a1c-6e31-7f55-b2c4-1d8e3f4a9b23",
  "dst_node_id": "018f2a1c-7f42-7066-a3d5-2e9f4a5b0c34",
  "rel_type": "supersedes",
  "inverse_label": "superseded by"
}
```

Response `201`:

```json
{
  "created": true
}
```

### List linked memos

`GET /v1/nodes/:id/neighbors`

Memos one edge away in either direction, strongest edge first. `direction` says which way each edge points, and `label` is the edge's `rel_type` on an `out` row and its `inverse_label` on an `in` row.

Query parameters:

| Field | Type | Required | Description |
| --- | --- | --- | --- |
| `limit` | integer | no | 1 to 200, default 50. A value outside the range is clamped to it. |
| `rel_type` | string | no | Only edges of this type. |

Response `200`:

```json
{
  "neighbors": [
    {
      "node_id": "018f2a1c-7f42-7066-a3d5-2e9f4a5b0c34",
      "label": "links_to",
      "description": "retries",
      "weight": 0.5,
      "direction": "out",
      "title": "Retries",
      "summary": "When a client should retry"
    }
  ]
}
```

### Walk the graph

`POST /v1/nodes/:id/traverse`

Follows edges in both directions from a memo, one hop at a time, up to `depth` hops away. `via` is the memo each result was reached from.

Body:

| Field | Type | Required | Description |
| --- | --- | --- | --- |
| `depth` | integer | no | 1 to 6, default 2. |
| `limit` | integer | no | 1 to 500, default 50. |
| `rel_type` | string | no | Only follow edges of this type. |
| `min_weight` | number | no | Skip edges weaker than this, default 0. |

Request body:

```json
{
  "depth": 2,
  "limit": 50,
  "min_weight": 0
}
```

Response `200`:

```json
{
  "reached": [
    {
      "node_id": "018f2a1c-7f42-7066-a3d5-2e9f4a5b0c34",
      "title": "Retries",
      "summary": "When a client should retry",
      "node_type": "note",
      "depth": 1,
      "via": "018f2a1c-6e31-7f55-b2c4-1d8e3f4a9b23",
      "weight": 0.5
    }
  ]
}
```

### Read the whole graph

`GET /v1/graph`

Every memo the key can read, without bodies, and every edge between two of them. For drawing a map of the workspace, so it is not paged: a force layout needs every memo before it can place any of them.

Stops at 5,000 memos, oldest first, and 20,000 edges, strongest first. `truncated` is `true` when either cap was hit. An edge is only returned when both of its memos are in `nodes`.

Response `200`:

```json
{
  "nodes": [
    {
      "id": "018f2a1c-6e31-7f55-b2c4-1d8e3f4a9b23",
      "ref": "N:12",
      "thread_id": "018f2a1c-5d20-7a44-8e1b-6c9f0a2b7d12",
      "bucket_id": "018f2a1c-4b7e-7c11-9a3d-2f6b8e1d4c01",
      "title": "Rate limiting",
      "summary": "How requests are throttled",
      "node_type": "note"
    },
    {
      "id": "018f2a1c-7f42-7066-a3d5-2e9f4a5b0c34",
      "ref": "N:13",
      "thread_id": "018f2a1c-5d20-7a44-8e1b-6c9f0a2b7d12",
      "bucket_id": "018f2a1c-4b7e-7c11-9a3d-2f6b8e1d4c01",
      "title": "Retries",
      "summary": "When a client should retry",
      "node_type": "note"
    }
  ],
  "edges": [
    {
      "id": "018f2a1c-8a53-7177-94e6-3fa05b6c1d45",
      "src_node_id": "018f2a1c-6e31-7f55-b2c4-1d8e3f4a9b23",
      "dst_node_id": "018f2a1c-7f42-7066-a3d5-2e9f4a5b0c34",
      "rel_type": "links_to",
      "weight": 0.5,
      "auto": true
    }
  ],
  "truncated": false
}
```

### Resolve handles

`POST /v1/refs/resolve`

Turns handles such as `N:12` or `B:1` into the rows they name, which is what you need to render a `[label](N:12)` link as a real link.

A handle that names nothing, or names a row the key can't read, comes back in `unresolved`.

Body:

| Field | Type | Required | Description |
| --- | --- | --- | --- |
| `refs` | string[] | yes | 1 to 50 handles. |

Request body:

```json
{
  "refs": [
    "N:12",
    "B:1",
    "N:99999"
  ]
}
```

Response `200`:

```json
{
  "resolved": [
    {
      "ref": "N:12",
      "kind": "node",
      "id": "018f2a1c-6e31-7f55-b2c4-1d8e3f4a9b23",
      "title": "Rate limiting",
      "bucket_id": "018f2a1c-4b7e-7c11-9a3d-2f6b8e1d4c01"
    },
    {
      "ref": "B:1",
      "kind": "bucket",
      "id": "018f2a1c-4b7e-7c11-9a3d-2f6b8e1d4c01",
      "title": "Handbook",
      "bucket_id": "018f2a1c-4b7e-7c11-9a3d-2f6b8e1d4c01"
    }
  ],
  "unresolved": [
    "N:99999"
  ]
}
```

### Delete an edge

`DELETE /v1/edges/:id`

Removes one edge. The two memos it joined stay as they are.

No response returns an edge's id yet, so for now the id has to come from somewhere outside the API.

Response `204` with no body.

## Usage

What the workspace has used this month and what it stores.

### Read usage

`GET /v1/usage`

Operations counted since the start of the month, by kind: `search` covers searches and traversals, `fetch` covers reading one memo, and `write` covers writes. `totals` is what the workspace stores.

Response `200`:

```json
{
  "period": "2026-09-01",
  "counters": {
    "search": 120,
    "fetch": 64,
    "write": 8
  },
  "totals": {
    "vectors": 42,
    "documents": 5,
    "storage_bytes": 18432
  }
}
```

## Links inside memos

To have one memo mention another, write a markdown link to the other memo's `ref` in the content: `[Rate limits](N:12)`. Saving the memo creates the link, and removing the text removes it on the next save. There is no separate call to keep in step. In the GigaRAG app the link shows as a chip with the memo's current title, and the memo it points to lists this one under Mentioned by. Links work across buckets.

Get the `ref` from `GET /v1/nodes/lookup?q=` (the `find_nodes` tool over MCP) when you know the memo by name, or from any search, listing or read. Only a `ref` links: a UUID in the target, or a `B:1` or `T:4` handle, stays plain text.

```json
{
  "content": "Retries back off exponentially, within the limits in [Rate limits](N:12)."
}
```

The response counts the links made in `links`, and lists in `unresolved_links` every `ref` in the text that did not become one: a memo that does not exist, the memo itself, or a memo in a bucket the key cannot edit. The write still succeeds. `unresolved_links` is only present when the write changed the content.

A PATCH replaces the whole body, and the links are rebuilt from it. Keep the links that should stay when you rewrite a memo.

Use `POST /v1/edges` (`link_nodes`) only for a relationship the text does not state, such as `supersedes`. It is not visible in either memo.

These edges have type `links_to` and weight 0.5, below the 1.0 an edge you create gets by default, so `min_weight` on a traversal can tell the two apart. They never replace an edge you created between the same two memos.

Only the first 200 distinct handles in a memo are read. Handles inside code spans and fenced code blocks are ignored, and a handle that names nothing or names the memo itself is skipped without an error.

## MCP

Agents that speak the Model Context Protocol use the same key at `https://mcp.gigarag.com/mcp`, over Streamable HTTP, with the same `Authorization` header. Every call checks the key again, so a revoked key stops working on the next call.

Each tool runs the same code as its REST route. An id that REST takes in the path is a field instead, such as `node_id` or `edge_id`, and `fetch_nodes` takes `node_ids`.

| Tool | REST route |
| --- | --- |
| `search_nodes` | `POST /v1/search` |
| `fetch_node` | `GET /v1/nodes/:id` |
| `fetch_nodes` | `POST /v1/nodes/fetch` |
| `find_nodes` | `GET /v1/nodes/lookup` |
| `resolve_refs` | `POST /v1/refs/resolve` |
| `list_buckets` | `GET /v1/buckets` |
| `list_threads` | `GET /v1/threads` |
| `list_nodes` | `GET /v1/nodes` |
| `create_bucket` | `POST /v1/buckets` |
| `create_thread` | `POST /v1/threads` |
| `create_node` | `POST /v1/nodes` |
| `update_bucket` | `PATCH /v1/buckets/:id` |
| `update_thread` | `PATCH /v1/threads/:id` |
| `update_node` | `PATCH /v1/nodes/:id` |
| `link_nodes` | `POST /v1/edges` |
| `neighbors` | `GET /v1/nodes/:id/neighbors` |
| `traverse` | `POST /v1/nodes/:id/traverse` |
| `graph` | `GET /v1/graph` |
| `delete_node` | `DELETE /v1/nodes/:id` |
| `delete_thread` | `DELETE /v1/threads/:id` |
| `delete_bucket` | `DELETE /v1/buckets/:id` |
| `delete_edge` | `DELETE /v1/edges/:id` |
| `usage` | `GET /v1/usage` |

Setup guides for 99 AI tools are at https://gigarag.com/connect.

