# Knowledge

> Read, write and search the workspace's knowledge base — folders, articles, website imports and the per-agent audience every AI reads from.

Source: https://dev.ody.co/api/knowledge

The **knowledge base** is what your AI knows about your business: articles organized in folders, shared by every AI agent in the workspace and by **AI texting** — the inbox's AI suggested replies and the agent's automatic replies ([Business AI Texting](https://dev.ody.co/api/ai-texting)), which retrieve the passages that answer each customer's text from this same base. The same data backs the **Knowledge** tab in the app and the [MCP knowledge tools](https://dev.ody.co/mcp/tools#knowledge). Use this API to sync a CMS or help center into Ody, to keep a website import fresh on a schedule, or to check what one agent would answer from.

Nothing here needs a publish step. Writing an article re-indexes it for search and updates every published agent its audience admits, within a second or two.

## Concepts

- **Article** — a Markdown body of up to 20,000 characters. Written through this API (`kind: "text"`) or read from a website (`kind: "url"`). An article has an **Available to AI** switch (`enabled`) and an **audience**.
- **Folder** — groups articles; nests up to three levels. A folder also has an audience, which applies to everything inside it.
- **Consumer** — anything that reads knowledge, identified by a **consumer key**: `agent:<agentId>` for an AI agent, `texting` for AI texting on every number, `texting:<e164>` for AI texting on one specific number. `GET /v1/api/knowledge` lists the workspace's consumers (`replies` is a deprecated alias of `texting`).
- **Audience** — `all` (every consumer, the default) or `selected` (only the consumer keys in `consumers`).

Two resolution rules decide who reads an article: a folder set to `selected` **restricts** everything inside it to its consumers (even an `all` article), and an article can **narrow** its folder's audience but never widen it. `enabled: false` hides the article from everyone.

## The article object

- `id` *uuid* — Unique identifier.
- `title` *string* — Shown in the app and used as the first line of every search chunk.
- `kind` *string* — `text` (written) or `url` (website import).
- `content` *string | null* — Markdown body. Omitted from list endpoints — fetch one article for it.
- `sourceUrl` *string | null* — Website imports: the address that was read.
- `status` *string* — `ready`, `pending` (import in progress) or `failed`.
- `pageCount` *integer | null* — Website imports: pages read into `content`.
- `fetchedAt` *timestamp | null* — Website imports: when the site was last read.
- `folderId` *uuid | null* — Containing folder; `null` at the top level.
- `enabled` *boolean* — The Available to AI switch.
- `audience` *`all` | `selected`* — Who may read it.
- `consumers` *array of strings* — Consumer keys granted when `audience` is `selected`; empty otherwise.
- `chars` *integer* — Length of `content`.
- `indexedAt` *timestamp | null* — When the search index last matched `content`; `null` until first indexed.
- `createdAt`, `updatedAt` *timestamp* — ISO 8601.

The article object:

```json
{
  "id": "3f6d2a1e-8c4b-4f0e-9a7d-1b2c3d4e5f60",
  "title": "Cancellation policy",
  "kind": "text",
  "content": "## Cancellations\nFree up to 24 hours before…",
  "sourceUrl": null,
  "status": "ready",
  "pageCount": null,
  "fetchedAt": null,
  "folderId": "a1b2c3d4-0000-4000-8000-000000000001",
  "enabled": true,
  "audience": "selected",
  "consumers": ["agent:7c9e6679-7425-40de-944b-e07fc1f90ae7", "texting"],
  "chars": 412,
  "indexedAt": "2026-10-28T14:03:10.000Z",
  "createdAt": "2026-10-28T14:03:09.000Z",
  "updatedAt": "2026-10-28T14:03:09.000Z"
}
```

## The folder object

- `id` *uuid* — Unique identifier.
- `parentId` *uuid | null* — Parent folder; `null` at the top level.
- `name` *string* — Folder name.
- `description` *string | null* — One line describing what goes inside.
- `audience` *`all` | `selected`* — Who may read everything inside.
- `consumers` *array of strings* — Consumer keys granted when `audience` is `selected`.
- `position` *integer* — Sort order among siblings.
- `articleCount` *integer* — Articles directly inside (not in sub-folders).
- `createdAt`, `updatedAt` *timestamp* — ISO 8601.

## The consumer object

- `key` *string* — `agent:<agentId>` for an AI agent; `texting` for **AI texting on every number** (the inbox's AI suggested replies today, the automated texting agent next — same engine); `texting:<e164>` for AI texting on **one** of the workspace's numbers only (for example `texting:+15125550100`). Use it in `consumers` arrays and in `search`. `replies` is a deprecated alias of `texting`, still accepted on input.
- `kind` *string* — `agent` or `texting`.
- `name` *string* — The agent's name, "AI texting", or "AI texting · +1 (512) 555-0100".
- `channel` *string* — `voice` for agents, `text` for texting.
- `live` *boolean* — Agents only: published and switched on.
- `numberE164` *string* — Texting on one number: the line. Absent for every-number texting.
- `detail` *string* — Texting on one number: the inbox name.

Per-number texting consumers are listed only when the workspace has more than one active number. When the texting AI answers on a line it reads every article granted to `texting` **or** to `texting:<that line>` — so an "every number" grant keeps working when you later narrow one article to one line. Folders restrict downward: a folder granted to `texting:+15125550100` hides everything inside it from the other lines (and from the voice agents).

**`GET /v1/api/knowledge`**

### Get the knowledge tree

Returns every folder, every article (without `content`) and the consumers you can grant access to — everything the Knowledge tab renders, in one call.

**Scope:** `knowledge:read`

**Returns** `{ folders: Folder[], articles: ArticleSummary[], consumers: Consumer[] }`.

cURL:

```bash
curl "https://api.ody.co/v1/api/knowledge" \
  -H "Authorization: Bearer $ODY_API_KEY"
```
TypeScript:

```ts
const { folders, articles, consumers } = await ody.knowledge.get();
```
Python:

```python
tree = client.knowledge.get()
folders, articles, consumers = tree["folders"], tree["articles"], tree["consumers"]
```

**`GET /v1/api/knowledge/search`**

### Search knowledge

Returns the passages that best answer `q`, with the article each comes from. The search is semantic (vector) when embeddings are configured and falls back to full-text otherwise. Pass `consumer` to see **only what that AI can read** — the audience rules and the enabled switch are applied — which is the quickest way to debug "why doesn't my agent know this".

**Scope:** `knowledge:read`

**Query parameters**

- `q` *string* — **Required.** The question or phrase.
- `consumer` *string* — Filter to one reader: `agent:<agentId>`, `texting`, or `texting:<e164>` (what the texting AI sees on that line). Without it, every enabled article is searched.
- `folderId` *uuid* — Only articles inside this folder (and its sub-folders).
- `limit` *integer* — 1–20, default 6.

**Returns** `{ hits: Hit[] }` where each hit has `articleId`, `title`, `folderId`, `kind`, `sourceUrl`, `chunkIndex`, `content` (the passage) and `score` (0–1, higher is better).

cURL:

```bash
curl "https://api.ody.co/v1/api/knowledge/search?q=cancellation%20fee&consumer=texting" \
  -H "Authorization: Bearer $ODY_API_KEY"
```
TypeScript:

```ts
const hits = await ody.knowledge.search("cancellation fee", { consumer: "texting" });
```
Python:

```python
hits = client.knowledge.search("cancellation fee", consumer="texting")
```

Response:

```json
{
  "hits": [
    {
      "articleId": "3f6d2a1e-8c4b-4f0e-9a7d-1b2c3d4e5f60",
      "title": "Cancellation policy",
      "folderId": "a1b2c3d4-0000-4000-8000-000000000001",
      "kind": "text",
      "sourceUrl": null,
      "chunkIndex": 0,
      "content": "Cancellation policy\n\nFree up to 24 hours before the appointment; $25 after that.",
      "score": 0.87
    }
  ]
}
```

**`GET /v1/api/knowledge/articles`**

### List articles

Returns every article without `content`, newest first. Pass `folderId` to list one folder's direct children.

**Scope:** `knowledge:read`

**Query parameters**

- `folderId` *uuid* — Only articles directly inside this folder.

**Returns** `{ articles: ArticleSummary[] }`.

cURL:

```bash
curl "https://api.ody.co/v1/api/knowledge/articles?folderId=a1b2c3d4-0000-4000-8000-000000000001" \
  -H "Authorization: Bearer $ODY_API_KEY"
```
TypeScript:

```ts
const articles = await ody.knowledge.listArticles({ folderId });
```
Python:

```python
articles = client.knowledge.list_articles(folder_id=folder_id)
```

**`POST /v1/api/knowledge/articles`**

### Create an article

Writes a Markdown article. It is ready at once: indexed for search and pushed to every published agent its audience admits.

**Scope:** `knowledge:write`

**Body**

- `title` *string* — **Required.** Up to 120 characters.
- `content` *string* — **Required.** Markdown, up to 20,000 characters.
- `folderId` *uuid | null* — Folder to put it in. Default: top level.
- `enabled` *boolean* — Default `true`.
- `audience` *`all` | `selected`* — Default `all`.
- `consumers` *array of strings* — Consumer keys; required when `audience` is `selected`. Unknown keys are rejected with `invalid_argument`.

**Returns** `{ article: Article }` with status `201`.

cURL:

```bash
curl -X POST "https://api.ody.co/v1/api/knowledge/articles" \
  -H "Authorization: Bearer $ODY_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "title": "Cancellation policy",
    "content": "## Cancellations\nFree up to 24 hours before the appointment; $25 after that.",
    "folderId": "a1b2c3d4-0000-4000-8000-000000000001",
    "audience": "selected",
    "consumers": ["agent:7c9e6679-7425-40de-944b-e07fc1f90ae7", "texting"]
  }'
```
TypeScript:

```ts
const article = await ody.knowledge.createArticle({
  title: "Cancellation policy",
  content: "## Cancellations\nFree up to 24 hours before the appointment; $25 after that.",
  folderId,
  audience: "selected",
  consumers: [`agent:${agentId}`, "texting"],
});
```
Python:

```python
article = client.knowledge.create_article(
    title="Cancellation policy",
    content="## Cancellations\nFree up to 24 hours before the appointment; $25 after that.",
    folder_id=folder_id,
    audience="selected",
    consumers=[f"agent:{agent_id}", "texting"],
)
```

**`POST /v1/api/knowledge/articles/import`**

### Import a website

Reads a public website — up to 20 of its main pages, found via the sitemap and links — and organizes it into **one** article. The request blocks until the read finishes (typically 20–40 seconds; set your client timeout to at least 90 seconds). The article is created with `enabled: true` and `audience: all`; `PATCH` it afterwards to change either.

**Scope:** `knowledge:write`

**Body**

- `url` *string* — **Required.** An `http(s)` address of a public site. Private and loopback addresses are rejected.
- `title` *string* — Override the article title (default: the site's name).
- `folderId` *uuid | null* — Folder to put it in.

**Returns** `{ article: Article }` with status `201`. Errors carry a `reason` field: `invalid_url`, `private_address` (400), or `unreachable`, `blocked`, `no_content` (409 `failed_precondition`).

cURL:

```bash
curl -X POST "https://api.ody.co/v1/api/knowledge/articles/import" \
  -H "Authorization: Bearer $ODY_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{ "url": "https://www.yourbusiness.com", "folderId": null }'
```
TypeScript:

```ts
const article = await ody.knowledge.importWebsite({ url: "https://www.yourbusiness.com" });
```
Python:

```python
article = client.knowledge.import_website("https://www.yourbusiness.com")
```

**`GET /v1/api/knowledge/articles/:id`**

### Retrieve an article

Returns one article including its full `content`.

**Scope:** `knowledge:read`

**Returns** `{ article: Article }`, or `404 not_found`.

cURL:

```bash
curl "https://api.ody.co/v1/api/knowledge/articles/3f6d2a1e-8c4b-4f0e-9a7d-1b2c3d4e5f60" \
  -H "Authorization: Bearer $ODY_API_KEY"
```
TypeScript:

```ts
const article = await ody.knowledge.getArticle(articleId);
```
Python:

```python
article = client.knowledge.get_article(article_id)
```

**`PATCH /v1/api/knowledge/articles/:id`**

### Update an article

Changes any of the article's fields. Send only what changes; at least one field is required. Changing `content` or `title` re-indexes the article; any change re-syncs published agents.

**Scope:** `knowledge:write`

**Body**

- `title` *string*
- `content` *string* — Markdown, up to 20,000 characters. Editing a website article's content is allowed; a later refresh replaces it.
- `folderId` *uuid | null* — Move the article; `null` moves it to the top level.
- `enabled` *boolean* — The Available to AI switch.
- `audience` *`all` | `selected`*
- `consumers` *array of strings* — Consumer keys when `audience` is `selected`.

**Returns** `{ article: Article }`.

cURL:

```bash
curl -X PATCH "https://api.ody.co/v1/api/knowledge/articles/3f6d2a1e-8c4b-4f0e-9a7d-1b2c3d4e5f60" \
  -H "Authorization: Bearer $ODY_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{ "enabled": false }'
```
TypeScript:

```ts
await ody.knowledge.updateArticle(articleId, { enabled: false });
```
Python:

```python
client.knowledge.update_article(article_id, enabled=False)
```

**`POST /v1/api/knowledge/articles/:id/refresh`**

### Refresh a website article

Re-reads a website article's `sourceUrl` in place — same id, folder, audience and enabled state; fresh `content`, `pageCount` and `fetchedAt`. Blocks like an import. Returns `failed_precondition` for articles that are not website imports.

**Scope:** `knowledge:write`

**Returns** `{ article: Article }`.

cURL:

```bash
curl -X POST "https://api.ody.co/v1/api/knowledge/articles/3f6d2a1e-8c4b-4f0e-9a7d-1b2c3d4e5f60/refresh" \
  -H "Authorization: Bearer $ODY_API_KEY"
```
TypeScript:

```ts
const article = await ody.knowledge.refreshArticle(articleId);
```
Python:

```python
article = client.knowledge.refresh_article(article_id)
```

**`DELETE /v1/api/knowledge/articles/:id`**

### Delete an article

Removes the article and its search chunks. Every AI stops using it immediately.

**Scope:** `knowledge:write`

**Returns** `{ deleted: true }`, or `404 not_found`.

cURL:

```bash
curl -X DELETE "https://api.ody.co/v1/api/knowledge/articles/3f6d2a1e-8c4b-4f0e-9a7d-1b2c3d4e5f60" \
  -H "Authorization: Bearer $ODY_API_KEY"
```
TypeScript:

```ts
await ody.knowledge.deleteArticle(articleId);
```
Python:

```python
client.knowledge.delete_article(article_id)
```

**`POST /v1/api/knowledge/folders`**

### Create a folder

**Scope:** `knowledge:write`

**Body**

- `name` *string* — **Required.** Up to 120 characters.
- `description` *string | null* — Up to 500 characters.
- `parentId` *uuid | null* — Nest inside another folder. Folders nest at most 3 levels deep (`failed_precondition` beyond that).
- `audience` *`all` | `selected`* — Default `all`.
- `consumers` *array of strings* — Consumer keys when `audience` is `selected`.

**Returns** `{ folder: Folder }` with status `201`.

cURL:

```bash
curl -X POST "https://api.ody.co/v1/api/knowledge/folders" \
  -H "Authorization: Bearer $ODY_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{ "name": "Sales line", "audience": "selected", "consumers": ["agent:7c9e6679-7425-40de-944b-e07fc1f90ae7"] }'
```
TypeScript:

```ts
const folder = await ody.knowledge.createFolder({
  name: "Sales line",
  audience: "selected",
  consumers: [`agent:${salesAgentId}`],
});
```
Python:

```python
folder = client.knowledge.create_folder(
    "Sales line", audience="selected", consumers=[f"agent:{sales_agent_id}"]
)
```

**`PATCH /v1/api/knowledge/folders/:id`**

### Update a folder

Rename, move (`parentId`), reorder (`position`) or change the audience. A folder cannot be moved into itself or one of its descendants.

**Scope:** `knowledge:write`

**Body** — any of `name`, `description`, `parentId` (`null` = top level), `audience`, `consumers`, `position`.

**Returns** `{ folder: Folder }`.

cURL:

```bash
curl -X PATCH "https://api.ody.co/v1/api/knowledge/folders/a1b2c3d4-0000-4000-8000-000000000001" \
  -H "Authorization: Bearer $ODY_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{ "name": "Sales desk" }'
```
TypeScript:

```ts
await ody.knowledge.updateFolder(folderId, { name: "Sales desk" });
```
Python:

```python
client.knowledge.update_folder(folder_id, name="Sales desk")
```

**`DELETE /v1/api/knowledge/folders/:id`**

### Delete a folder

By default the folder's articles and sub-folders **move up** to its parent — nothing is lost. Pass `deleteContents=true` to delete everything inside instead.

**Scope:** `knowledge:write`

**Query parameters**

- `deleteContents` *boolean* — Delete the articles and sub-folders too. Default `false`.

**Returns** `{ deleted: true, movedArticles: number, deletedArticles: number }`.

cURL:

```bash
curl -X DELETE "https://api.ody.co/v1/api/knowledge/folders/a1b2c3d4-0000-4000-8000-000000000001" \
  -H "Authorization: Bearer $ODY_API_KEY"
```
TypeScript:

```ts
const { movedArticles } = await ody.knowledge.deleteFolder(folderId);
```
Python:

```python
result = client.knowledge.delete_folder(folder_id)
```

## Recipes

**Sync a help center.** Create one folder per help-center category, then `POST /knowledge/articles` per article with `folderId`. Store the returned `id` next to your source id and `PATCH` on edits, `DELETE` on removals. Keep each article focused — the search index chunks by section and prefixes every chunk with the title.

**Keep a website fresh.** Import once, then call `POST /knowledge/articles/:id/refresh` from a nightly job. The article keeps its folder and audience.

**Give each phone number its own facts.** Create a folder per agent with `audience: "selected"` and that agent's key; put shared facts (hours, address) in a top-level `all` folder. Verify with `GET /knowledge/search?q=…&consumer=agent:<id>`.

## Related

- [Scopes](https://dev.ody.co/get-started/scopes) — `knowledge:read`, `knowledge:write`.
- [AI agent](https://dev.ody.co/api/agent) — the agent's config, which inlines the knowledge it can read when published.
- [MCP tools › Knowledge](https://dev.ody.co/mcp/tools#knowledge) — the same operations for assistants.
