Knowledge
Read, write and search the workspace's knowledge base — folders, articles, website imports and the per-agent audience every AI reads from.
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), 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. 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,textingfor AI texting on every number,texting:<e164>for AI texting on one specific number.GET /v1/api/knowledgelists the workspace's consumers (repliesis a deprecated alias oftexting). - Audience —
all(every consumer, the default) orselected(only the consumer keys inconsumers).
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
iduuid — Unique identifier.titlestring — Shown in the app and used as the first line of every search chunk.kindstring —text(written) orurl(website import).contentstring | null — Markdown body. Omitted from list endpoints — fetch one article for it.sourceUrlstring | null — Website imports: the address that was read.statusstring —ready,pending(import in progress) orfailed.pageCountinteger | null — Website imports: pages read intocontent.fetchedAttimestamp | null — Website imports: when the site was last read.folderIduuid | null — Containing folder;nullat the top level.enabledboolean — The Available to AI switch.audienceall|selected— Who may read it.consumersarray of strings — Consumer keys granted whenaudienceisselected; empty otherwise.charsinteger — Length ofcontent.indexedAttimestamp | null — When the search index last matchedcontent;nulluntil first indexed.createdAt,updatedAttimestamp — ISO 8601.
{
"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
iduuid — Unique identifier.parentIduuid | null — Parent folder;nullat the top level.namestring — Folder name.descriptionstring | null — One line describing what goes inside.audienceall|selected— Who may read everything inside.consumersarray of strings — Consumer keys granted whenaudienceisselected.positioninteger — Sort order among siblings.articleCountinteger — Articles directly inside (not in sub-folders).createdAt,updatedAttimestamp — ISO 8601.
#The consumer object
keystring —agent:<agentId>for an AI agent;textingfor 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 exampletexting:+15125550100). Use it inconsumersarrays and insearch.repliesis a deprecated alias oftexting, still accepted on input.kindstring —agentortexting.namestring — The agent's name, "AI texting", or "AI texting · +1 (512) 555-0100".channelstring —voicefor agents,textfor texting.liveboolean — Agents only: published and switched on.numberE164string — Texting on one number: the line. Absent for every-number texting.detailstring — 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 the knowledge tree
/v1/api/knowledgeReturns 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 "https://api.ody.co/v1/api/knowledge" \
-H "Authorization: Bearer $ODY_API_KEY"const { folders, articles, consumers } = await ody.knowledge.get();tree = client.knowledge.get()
folders, articles, consumers = tree["folders"], tree["articles"], tree["consumers"]#Search knowledge
/v1/api/knowledge/searchReturns 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
qstring — Required. The question or phrase.consumerstring — Filter to one reader:agent:<agentId>,texting, ortexting:<e164>(what the texting AI sees on that line). Without it, every enabled article is searched.folderIduuid — Only articles inside this folder (and its sub-folders).limitinteger — 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 "https://api.ody.co/v1/api/knowledge/search?q=cancellation%20fee&consumer=texting" \
-H "Authorization: Bearer $ODY_API_KEY"const hits = await ody.knowledge.search("cancellation fee", { consumer: "texting" });hits = client.knowledge.search("cancellation fee", consumer="texting"){
"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
}
]
}#List articles
/v1/api/knowledge/articlesReturns every article without content, newest first. Pass folderId to list one folder's direct children.
Scope: knowledge:read
Query parameters
folderIduuid — Only articles directly inside this folder.
Returns { articles: ArticleSummary[] }.
curl "https://api.ody.co/v1/api/knowledge/articles?folderId=a1b2c3d4-0000-4000-8000-000000000001" \
-H "Authorization: Bearer $ODY_API_KEY"const articles = await ody.knowledge.listArticles({ folderId });articles = client.knowledge.list_articles(folder_id=folder_id)#Create an article
/v1/api/knowledge/articlesWrites a Markdown article. It is ready at once: indexed for search and pushed to every published agent its audience admits.
Scope: knowledge:write
Body
titlestring — Required. Up to 120 characters.contentstring — Required. Markdown, up to 20,000 characters.folderIduuid | null — Folder to put it in. Default: top level.enabledboolean — Defaulttrue.audienceall|selected— Defaultall.consumersarray of strings — Consumer keys; required whenaudienceisselected. Unknown keys are rejected withinvalid_argument.
Returns { article: Article } with status 201.
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"]
}'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"],
});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"],
)#Import a website
/v1/api/knowledge/articles/importReads 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
urlstring — Required. Anhttp(s)address of a public site. Private and loopback addresses are rejected.titlestring — Override the article title (default: the site's name).folderIduuid | 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 -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 }'const article = await ody.knowledge.importWebsite({ url: "https://www.yourbusiness.com" });article = client.knowledge.import_website("https://www.yourbusiness.com")#Retrieve an article
/v1/api/knowledge/articles/:idReturns one article including its full content.
Scope: knowledge:read
Returns { article: Article }, or 404 not_found.
curl "https://api.ody.co/v1/api/knowledge/articles/3f6d2a1e-8c4b-4f0e-9a7d-1b2c3d4e5f60" \
-H "Authorization: Bearer $ODY_API_KEY"const article = await ody.knowledge.getArticle(articleId);article = client.knowledge.get_article(article_id)#Update an article
/v1/api/knowledge/articles/:idChanges 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
titlestringcontentstring — Markdown, up to 20,000 characters. Editing a website article's content is allowed; a later refresh replaces it.folderIduuid | null — Move the article;nullmoves it to the top level.enabledboolean — The Available to AI switch.audienceall|selectedconsumersarray of strings — Consumer keys whenaudienceisselected.
Returns { article: Article }.
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 }'await ody.knowledge.updateArticle(articleId, { enabled: false });client.knowledge.update_article(article_id, enabled=False)#Refresh a website article
/v1/api/knowledge/articles/:id/refreshRe-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 -X POST "https://api.ody.co/v1/api/knowledge/articles/3f6d2a1e-8c4b-4f0e-9a7d-1b2c3d4e5f60/refresh" \
-H "Authorization: Bearer $ODY_API_KEY"const article = await ody.knowledge.refreshArticle(articleId);article = client.knowledge.refresh_article(article_id)#Delete an article
/v1/api/knowledge/articles/:idRemoves the article and its search chunks. Every AI stops using it immediately.
Scope: knowledge:write
Returns { deleted: true }, or 404 not_found.
curl -X DELETE "https://api.ody.co/v1/api/knowledge/articles/3f6d2a1e-8c4b-4f0e-9a7d-1b2c3d4e5f60" \
-H "Authorization: Bearer $ODY_API_KEY"await ody.knowledge.deleteArticle(articleId);client.knowledge.delete_article(article_id)#Create a folder
/v1/api/knowledge/foldersScope: knowledge:write
Body
namestring — Required. Up to 120 characters.descriptionstring | null — Up to 500 characters.parentIduuid | null — Nest inside another folder. Folders nest at most 3 levels deep (failed_preconditionbeyond that).audienceall|selected— Defaultall.consumersarray of strings — Consumer keys whenaudienceisselected.
Returns { folder: Folder } with status 201.
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"] }'const folder = await ody.knowledge.createFolder({
name: "Sales line",
audience: "selected",
consumers: [`agent:${salesAgentId}`],
});folder = client.knowledge.create_folder(
"Sales line", audience="selected", consumers=[f"agent:{sales_agent_id}"]
)#Update a folder
/v1/api/knowledge/folders/:idRename, 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 -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" }'await ody.knowledge.updateFolder(folderId, { name: "Sales desk" });client.knowledge.update_folder(folder_id, name="Sales desk")#Delete a folder
/v1/api/knowledge/folders/:idBy 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
deleteContentsboolean — Delete the articles and sub-folders too. Defaultfalse.
Returns { deleted: true, movedArticles: number, deletedArticles: number }.
curl -X DELETE "https://api.ody.co/v1/api/knowledge/folders/a1b2c3d4-0000-4000-8000-000000000001" \
-H "Authorization: Bearer $ODY_API_KEY"const { movedArticles } = await ody.knowledge.deleteFolder(folderId);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 —
knowledge:read,knowledge:write. - AI agent — the agent's config, which inlines the knowledge it can read when published.
- MCP tools › Knowledge — the same operations for assistants.