Skip to content
Pathbound DOCS

Knowledge

Knowledge pages are markdown pages a workspace keeps about its own business: product, pricing, ideal customers, terminology, playbooks. Your team reads and edits them on the Knowledge page of the dashboard. AI agents connected to the workspace read them before they write for you or reason about your customers.

Use these endpoints to publish pages from your own systems, to keep one section of a page current from a script, or to search what the workspace knows.

The MCP tools list_knowledge, search_knowledge, get_knowledge_page, save_knowledge_page and update_knowledge_section call the same endpoints. See the MCP tools reference.

EndpointScopeWhat it does
GET /v1/knowledgeknowledge:readList the pages, with the pinned pages in full.
GET /v1/knowledge/searchknowledge:readFind the sections that match a few keywords.
GET /v1/knowledge/pages?slug=knowledge:readLook a page up by its slug to get its id.
GET /v1/knowledge/pages/:idknowledge:readRead a page, or one section of it.
POST /v1/knowledge/pagesknowledge:writeCreate a page.
PATCH /v1/knowledge/pages/:idknowledge:writeChange a page’s title, summary, tags or whole body.
POST /v1/knowledge/pages/:id/sectionknowledge:writeReplace one section, or add one.

knowledge:write is an opt-in scope: a key has it only if it was selected when the key was created. See Scopes.

Knowledge calls read no customer record, so they never count against your plan.

A page that one caller writes is read by everyone in the workspace, and by their AI agents, as the workspace’s own position. So what a key may do is narrower than what a person may do in the dashboard.

A key with knowledge:write can:

  • create pages;
  • change pages that are neither locked nor pinned.

A key can never:

  • delete a page. There is no delete endpoint.
  • change a locked or a pinned page. The request is refused with 403.
  • lock, unlock, pin or unpin a page, or mark it verified. locked and pinned in a request body are ignored, like any other field the endpoint does not list.
  • name the author of a change. The author is taken from the key.

Everything a key writes is unverified until a person saves the page in the dashboard. Locking, pinning and deleting are done by a workspace admin in the dashboard.

A workspace admin has two switches for AI agents, both off by default: Read knowledge and Add to knowledge, under MCP Tools and on the Knowledge page of the dashboard. They apply to calls that reach the API through the MCP server, whether the agent signed in or uses an API key there. A direct call to these endpoints with an API key is governed by the key’s scopes alone, whichever way the switches are set.

A page has two names.

  • Its id addresses it in a URL: /v1/knowledge/pages/:id.
  • Its slug is the readable name you choose, such as pricing or playbooks/renewals: lowercase letters, digits and hyphens, in up to three parts joined by /, each part starting with a letter or a digit, 120 characters at most. A slug is unique among the workspace’s pages.

A slug can contain /, so it is never part of a URL path. To go from a slug to an id, call GET /v1/knowledge/pages?slug=. The list endpoint returns slugs without ids. Search results carry both.

Every page and every search hit carries trust, which says how much weight the text deserves.

trustMeaning
pinnedAn admin pinned the page: it holds the workspace’s standing rules for AI agents.
verifiedA person saved the current revision in the dashboard.
unverifiedThe current revision was written by an API key or an AI agent, and no person has saved the page since.

A change through the API to a verified page makes it unverified again.

revision starts at 1 and goes up by one each time a page’s title, summary, tags, body or date changes. A request that changes nothing makes no revision. Send the revision you read as expected_revision when you write, so that you never overwrite a version you have not seen. A page’s most recent revisions are kept, not all of them.

A page is read and edited by section. A section starts at a heading of level 1 to 3 (#, ##, ###) and holds that heading’s text and every deeper section under it, up to the next heading of the same or a higher level. Headings of level 4 and deeper do not start a section, and neither does a # inside a fenced code block.

Each section has an anchor: its heading in lowercase with punctuation removed and spaces turned into hyphens, so ## 90 days out is 90-days-out. A second section with the same heading gets -2, a third -3. Text before the first heading is the section intro.

One exception: when a page opens with its only # heading, that heading is the page’s title. Its section is just the text between the title and the next heading, never the whole page, and it is left out of the heading_path of the other sections.

Wherever an endpoint takes a section, you can pass the anchor, the heading text (with or without its # marks, in any case), or the heading path (Discounts > Discount approval rules).

GET /v1/knowledge

Required scope: knowledge:read

Returns the pinned pages in full, then one row per page, sorted by slug.

ParameterTypeDefaultDescription
offsetnumber0Row to start from. Pass the previous response’s next_offset.
limitnumber40Rows per call (1 to 100). Fewer come back when they would not fit one response.
tagstringnoneOnly pages with this tag.
Terminal window
curl https://api.pathbound.ai/v1/knowledge \
-H "Authorization: Bearer $PATHBOUND_API_KEY"
{
"status": "success",
"pinned": [
{
"slug": "agent-rules",
"title": "Rules for agents",
"revision": 4,
"body": "# Rules for agents\n\nNever quote a price that is not on the pricing page.\n"
}
],
"pages": [
{
"slug": "agent-rules",
"title": "Rules for agents",
"summary": "Never quote a price that is not on the pricing page.",
"trust": "pinned",
"locked": false,
"updated_at": "2026-10-06T08:41:17.000Z",
"updated_by": { "type": "user", "email": "[email protected]" },
"chars": 73
},
{
"slug": "pricing",
"title": "Pricing and commercial terms",
"summary": "What we charge, and what a rep may agree to.",
"tags": ["pricing", "sales"],
"trust": "verified",
"locked": true,
"as_of": "2026-10-01",
"updated_at": "2026-10-08T11:27:09.000Z",
"updated_by": { "type": "user", "email": "[email protected]" },
"chars": 14260
}
],
"total": 2,
"offset": 0,
"timestamp": "2026-10-13T09:30:00.000Z"
}
FieldDescription
pinnedThe pinned pages, each with its whole body. Returned with offset=0 only, whatever tag says. A workspace pins only a few short pages, so this part stays small.
pagesOne row per page. A pinned page is also a row here.
pages[].summaryThe page’s summary, cut to 240 characters.
pages[].tagsPresent when the page has tags.
pages[].trustSee Trust.
pages[].lockedtrue when only an admin may change the page.
pages[].as_ofThe date the page’s facts are correct for (YYYY-MM-DD), when the page states one.
pages[].updated_byWho last changed the page: type is user (a person in the dashboard), mcp (an AI agent), api_key or import (a bulk import), with the email of the account behind it when known.
pages[].charsThe length of the body in characters.
totalPages that match, across all calls.
next_offsetPresent when more rows follow. Pass it as offset. A hint beside it says the same in words.

A row has no id. Get it with Look Up a Page by Slug.

GET /v1/knowledge/search

Required scope: knowledge:read

Finds the sections that match a query, best first. This is a keyword search with no sense of meaning: discount approval rules finds the right section more reliably than what discount can a rep give without asking?. If a query misses, try the words a heading would use.

ParameterTypeDefaultDescription
qstringnoneRequired. A few keywords, up to 200 characters.
tagstringnoneOnly search pages with this tag.
limitnumber8Most sections to return (1 to 20).
Terminal window
curl "https://api.pathbound.ai/v1/knowledge/search?q=discount+approval" \
-H "Authorization: Bearer $PATHBOUND_API_KEY"
{
"status": "success",
"query": "discount approval",
"matched_by": "text",
"results": [
{
"page_id": "6701c9e25a3b4c7d8e9f0a1b",
"page": "pricing",
"title": "Pricing and commercial terms",
"trust": "verified",
"updated_at": "2026-10-08T11:27:09.000Z",
"updated_by": { "type": "user", "email": "[email protected]" },
"as_of": "2026-10-01",
"section": "discount-approval-rules",
"heading_path": "Discounts > Discount approval rules",
"chars": 99,
"text": "### Discount approval rules\n\nA rep may give up to 10% without asking. Above that, ask your manager."
}
],
"timestamp": "2026-10-13T09:30:00.000Z"
}
FieldDescription
matched_bytext when the words were found in the pages. heading when they were not, and the results come from a looser match on page titles and headings.
results[].page_id, results[].pageThe page’s id and its slug.
results[].sectionThe anchor of the matching section.
results[].heading_pathThe section’s heading with the headings above it.
results[].textThe section’s own text, without the sections under it. The best hits carry it. Once one response is full, the remaining hits come without text, and a hint says how many.
results[].part, results[].partsPresent when the section’s own text is too long for one response and was cut into parts. part is the one that matched.
results[].charsThe length of the matching text.

To read a hit that came without its text, call Get a Page with page_id, section and, when the hit has one, part.

When nothing matches, results is empty and hint suggests what to try.

GET /v1/knowledge/pages?slug=

Required scope: knowledge:read

Resolves a slug to its page. The slug is matched exactly, and slugs are lowercase.

ParameterTypeDefaultDescription
slugstringnoneRequired. The page’s slug, up to 120 characters. URL-encode a /.
Terminal window
curl "https://api.pathbound.ai/v1/knowledge/pages?slug=playbooks%2Frenewals" \
-H "Authorization: Bearer $PATHBOUND_API_KEY"
{
"status": "success",
"pages": [
{
"id": "670b9c3f4c1a9d3e5f6071c8",
"slug": "playbooks/renewals",
"title": "Renewal playbook",
"trust": "unverified",
"locked": false,
"revision": 1,
"as_of": "2026-10-01",
"updated_at": "2026-10-13T09:41:27.000Z",
"updated_by": { "type": "api_key", "email": "[email protected]" },
"chars": 120
}
],
"timestamp": "2026-10-13T09:45:10.000Z"
}

pages holds one page, or is empty when no page has that slug. An unknown slug is not an error. as_of is present only when the page states a date.

GET /v1/knowledge/pages/:id

Required scope: knowledge:read

Returns a page, or one section of it. Every response is sized to fit one AI agent tool result, about 10,000 characters of JSON, and a direct API call gets the same responses. So a short page comes back whole, and a long one comes back as its opening and an outline that you then read section by section. No call returns the whole body of a long page at once.

ParameterTypeDescription
idstringThe page’s id.
ParameterTypeDefaultDescription
sectionstringnoneRead this section only: an anchor, a heading or a heading path. See Sections.
partnumber1For a section that comes in parts, the part to read.
Terminal window
curl https://api.pathbound.ai/v1/knowledge/pages/670b9c3f4c1a9d3e5f6071c8 \
-H "Authorization: Bearer $PATHBOUND_API_KEY"

A page that fits:

{
"status": "success",
"page": {
"id": "670b9c3f4c1a9d3e5f6071c8",
"slug": "playbooks/renewals",
"title": "Renewal playbook",
"trust": "unverified",
"locked": false,
"revision": 1,
"as_of": "2026-10-01",
"updated_at": "2026-10-13T09:41:27.000Z",
"updated_by": { "type": "api_key", "email": "[email protected]" },
"chars": 120
},
"body": "# Renewal playbook\n\nHow we run a renewal, from 90 days out to signature.\n\n## 90 days out\n\nCheck usage against the plan.\n",
"timestamp": "2026-10-13T09:45:10.000Z"
}

page always has these fields, with as_of only when the page states a date. The next two examples cut it short.

A page that does not fit has no body. It has truncated: true, the opening of the page in lead, and an outline:

{
"status": "success",
"page": { "id": "6701c9e25a3b4c7d8e9f0a1b", "slug": "pricing", "revision": 7, "chars": 14260 },
"truncated": true,
"lead": "# Pricing and commercial terms\n\nWhat we charge, and what a rep may agree to.\n",
"outline": [
{ "section": "pricing-and-commercial-terms", "heading_path": "Pricing and commercial terms", "level": 1, "chars": 77 },
{ "section": "plans", "heading_path": "Plans", "level": 2, "chars": 6120 },
{ "section": "discounts", "heading_path": "Discounts", "level": 2, "chars": 56 },
{ "section": "discount-approval-rules", "heading_path": "Discounts > Discount approval rules", "level": 3, "chars": 100 },
{ "section": "contract-terms", "heading_path": "Contract terms", "level": 2, "chars": 7907 }
],
"hint": "This page is too long for one response. Read one section at a time: pass section=<a \"section\" value from outline>.",
"timestamp": "2026-10-13T09:30:00.000Z"
}

Each outline entry is a section: its anchor, its heading path, its heading level (0 for intro), and the length of its own text. On a page with so many headings that the outline itself would not fit, the outline lists the sections down to level 2 only, and hint says how many are left out.

With section, the response has a section object in place of body:

{
"status": "success",
"page": { "id": "6701c9e25a3b4c7d8e9f0a1b", "slug": "pricing", "revision": 7, "chars": 14260 },
"section": {
"section": "discounts",
"heading_path": "Discounts",
"text": "## Discounts\n\nDiscounts are given on annual plans only.\n\n### Discount approval rules\n\nA rep may give up to 10% without asking. Above that, ask your manager.\n",
"chars": 157
},
"timestamp": "2026-10-13T09:30:00.000Z"
}
FieldDescription
section.sectionThe section’s anchor.
section.textThe section: its own text and the sections under it.
section.charsThe size of the section in characters. When the response is truncated, it is more than the length of text.

A section that does not fit is returned in one of two ways, both with truncated: true and a hint that says which:

  • It has sections under it. text holds its own text and as many of the sections under it as fit, in order. section.children lists the ones that are not in text, for you to read one at a time. section.children_omitted counts any that did not fit in that list.
  • Its own text is too long. text holds one part, section.part and section.parts say which of how many, and you read the rest with part=2, part=3 and so on. A part beyond the last returns the last. The sections under it, if any, are all in section.children.

An entry of section.children has the shape of an outline entry, or only section and chars when the full form would not fit.

A section that matches no section is answered with 404 KNOWLEDGE_SECTION_NOT_FOUND, and details.sections lists the page’s anchors. A heading that several sections share is answered with 400 KNOWLEDGE_SECTION_AMBIGUOUS, and details.sections lists the anchors of those sections.

page.revision is the number to send as expected_revision when you change the page.

POST /v1/knowledge/pages

Required scope: knowledge:write

FieldTypeRequiredDescription
slugstringyesThe page’s slug. See Pages, Slugs and Ids. It cannot be changed later.
bodystringnoThe page as markdown, up to 256 KB. Use ## and ### headings: pages are read and edited one section at a time. Stored exactly as sent.
titlestringnoUp to 200 characters. Defaults to the body’s first # heading, or else to the last part of the slug.
summarystringnoWhat the page holds, up to 600 characters. Shown in the page list. Defaults to the page’s section headed In short, Summary, Overview or TL;DR, or else to its first paragraph.
tagsstring[]noUp to 50 tags. Each is trimmed, lowercased and cut to 64 characters.
as_ofstringnoThe date the page’s facts are correct for, such as 2026-10-01.
Terminal window
curl -X POST https://api.pathbound.ai/v1/knowledge/pages \
-H "Authorization: Bearer $PATHBOUND_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"slug": "playbooks/renewals",
"tags": ["playbook", "sales"],
"as_of": "2026-10-01",
"body": "# Renewal playbook\n\nHow we run a renewal, from 90 days out to signature.\n\n## 90 days out\n\nCheck usage against the plan.\n"
}'
{
"status": "success",
"page": {
"id": "670b9c3f4c1a9d3e5f6071c8",
"slug": "playbooks/renewals",
"title": "Renewal playbook",
"summary": "How we run a renewal, from 90 days out to signature.",
"tags": ["playbook", "sales"],
"trust": "unverified",
"locked": false,
"pinned": false,
"revision": 1,
"as_of": "2026-10-01",
"chars": 120,
"created_at": "2026-10-13T09:41:27.000Z",
"updated_at": "2026-10-13T09:41:27.000Z",
"updated_by": { "type": "api_key", "email": "[email protected]" }
},
"timestamp": "2026-10-13T09:41:27.412Z"
}

The response describes the page and does not repeat its body. as_of is null when the page states no date.

A slug that is taken is answered with 409 KNOWLEDGE_SLUG_TAKEN: update that page, or choose another slug.

PATCH /v1/knowledge/pages/:id

Required scope: knowledge:write

Changes a page’s title, summary, tags, date or whole body. Send only the fields you want to change. To change one part of a page, use Update a Section: it leaves the rest of the page as it is.

ParameterTypeDescription
idstringThe page’s id.
FieldTypeRequiredDescription
bodystringnoThe new body, which replaces the whole page. Needs expected_revision.
titlestringnoA new title. A blank one is ignored. The title does not follow the body’s # heading: send title to change it.
summarystringnoA new summary.
tagsstring[]noReplaces the page’s tags.
as_ofstringnoA new date. null removes it.
expected_revisionnumberwith bodyThe revision you read. If the page has moved on, nothing is written.

A summary that was derived from the body, and never written by hand, follows the new body unless you send summary.

The page, in the shape of Create a Page, with the new revision. A request that changes nothing returns the page as it is.

  • 400 KNOWLEDGE_EXPECTED_REVISION_REQUIRED: body differs from the stored body and no expected_revision was sent. details.current_revision holds the page’s revision.
  • 409 KNOWLEDGE_REVISION_CONFLICT: expected_revision is not the page’s current revision (details.current_revision), or someone else saved the page while this request was being written. Read the page again and apply your change to it.
POST /v1/knowledge/pages/:id/section

Required scope: knowledge:write

Replaces one section and leaves the rest of the page untouched, or adds a new section at the end. The edit is applied to the page as it is at that moment, so two callers who edit different sections both succeed.

ParameterTypeDescription
idstringThe page’s id.
FieldTypeRequiredDescription
sectionstringyesThe section to replace: an anchor, a heading or a heading path, up to 300 characters. With create_if_missing, the heading of the new section.
contentstringyesThe new markdown for the section.
create_if_missingbooleannotrue adds the section at the end of the page when no section matches. Default false: an unknown section is a 404 that lists the ones that exist.
expected_revisionnumbernoThe revision you read. The write is refused only if this section changed since then.
  • The section and everything under it is replaced by content: its own text and all the deeper sections it holds.
  • The section keeps its heading. Send only what goes under it: text, or its sub-sections as ### ….
  • To rename a section, start content with a heading of the section’s own level (## New name for a ## section). A heading of a higher level works the same way.
  • Naming the page’s # title replaces only the text between the title and the first section, never the rest of the page.
  • intro is the text before the first heading.
  • A new section (create_if_missing) is added at the end under ## and the text of section. If content itself opens with a # or ## heading, it is added as it is.

Without it, the edit goes through whatever happened to the page since you read it. With it, the page may have moved on, and the write still goes through as long as this section reads the same as it did at that revision. If the section changed, or that revision is no longer among the ones that are kept, the request is refused with 409 KNOWLEDGE_SECTION_CONFLICT and details.current_revision. It is not checked when a new section is added.

Terminal window
curl -X POST https://api.pathbound.ai/v1/knowledge/pages/670b9c3f4c1a9d3e5f6071c8/section \
-H "Authorization: Bearer $PATHBOUND_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"section": "90-days-out",
"content": "Check usage against the plan, and book the review call.",
"expected_revision": 1
}'
{
"status": "success",
"page": {
"id": "670b9c3f4c1a9d3e5f6071c8",
"slug": "playbooks/renewals",
"title": "Renewal playbook",
"summary": "How we run a renewal, from 90 days out to signature.",
"tags": ["playbook", "sales"],
"trust": "unverified",
"locked": false,
"pinned": false,
"revision": 2,
"as_of": "2026-10-01",
"chars": 145,
"created_at": "2026-10-13T09:41:27.000Z",
"updated_at": "2026-10-13T09:52:03.000Z",
"updated_by": { "type": "api_key", "email": "[email protected]" }
},
"section": "90-days-out",
"created": false,
"changed": true,
"timestamp": "2026-10-13T09:52:03.288Z"
}
FieldDescription
sectionThe section’s anchor after the write. It differs from the one you sent when the heading was renamed.
createdtrue when the section was added.
changedfalse when content is what the section already held. No revision is made then.
LimitValue
Pages per workspaceLimited. A create past the limit is answered 403 QUOTA_EXCEEDED, and the message states the limit.
Page body256 KB
Title200 characters
Summary600 characters
Slug120 characters
Tags per page50, each up to 64 characters
Revisions kept per pageThe most recent ones. An expected_revision older than those is answered 409 KNOWLEDGE_SECTION_CONFLICT.
WritesRate limited per workspace, across all keys and agents. A write over the limit is answered 429 with a Retry-After header.

Errors use the common error format. The message of each knowledge error says what to do next.

StatuscodeMeaning
400noneA parameter or a field failed validation: a missing q, slug, section or content, a value over its length, or an id that is not a page id. details lists each one.
400KNOWLEDGE_INVALID_SLUGThe slug does not fit the slug rule.
400KNOWLEDGE_INVALID_AS_OFas_of is not a date.
400KNOWLEDGE_EXPECTED_REVISION_REQUIREDA whole body was sent without expected_revision.
400KNOWLEDGE_SECTION_AMBIGUOUSSeveral sections have that heading. details.sections lists their anchors: pass one.
403noneThe key lacks knowledge:read or knowledge:write. required_scopes names it.
403KNOWLEDGE_PAGE_LOCKEDThe page is locked. Only an admin changes it, in the dashboard.
403KNOWLEDGE_PAGE_PINNEDThe page is pinned. Only an admin changes it, in the dashboard.
403QUOTA_EXCEEDEDThe workspace has reached its limit on pages.
404KNOWLEDGE_PAGE_NOT_FOUNDNo page has that id.
404KNOWLEDGE_SECTION_NOT_FOUNDNo section matches. details.sections lists the page’s anchors, and details.more counts any that did not fit in the list.
409KNOWLEDGE_SLUG_TAKENA page with that slug exists.
409KNOWLEDGE_REVISION_CONFLICTThe page changed since the revision you sent, or while the request was being written.
409KNOWLEDGE_SECTION_CONFLICTThe section changed since the revision you sent.
412KNOWLEDGE_TOOLS_DISABLED, KNOWLEDGE_WRITE_DISABLEDOnly for calls made through the MCP server: the workspace’s switch for agents reading, or adding to, knowledge is off.
413KNOWLEDGE_PAGE_TOO_LARGEThe body would be over 256 KB. Split it into several pages.
429noneToo many knowledge writes in a minute in this workspace. Wait for the number of seconds in the Retry-After header.
500noneThe request could not be completed.