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.
| Endpoint | Scope | What it does |
|---|---|---|
GET /v1/knowledge | knowledge:read | List the pages, with the pinned pages in full. |
GET /v1/knowledge/search | knowledge:read | Find the sections that match a few keywords. |
GET /v1/knowledge/pages?slug= | knowledge:read | Look a page up by its slug to get its id. |
GET /v1/knowledge/pages/:id | knowledge:read | Read a page, or one section of it. |
POST /v1/knowledge/pages | knowledge:write | Create a page. |
PATCH /v1/knowledge/pages/:id | knowledge:write | Change a page’s title, summary, tags or whole body. |
POST /v1/knowledge/pages/:id/section | knowledge:write | Replace 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.
What an API Key Can and Cannot Do
Section titled “What an API Key Can and Cannot Do”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.
lockedandpinnedin 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.
The two workspace switches
Section titled “The two workspace switches”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.
Pages, Slugs and Ids
Section titled “Pages, Slugs and Ids”A page has two names.
- Its
idaddresses it in a URL:/v1/knowledge/pages/:id. - Its
slugis the readable name you choose, such aspricingorplaybooks/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.
trust | Meaning |
|---|---|
pinned | An admin pinned the page: it holds the workspace’s standing rules for AI agents. |
verified | A person saved the current revision in the dashboard. |
unverified | The 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.
Revisions
Section titled “Revisions”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.
Sections
Section titled “Sections”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).
List Knowledge Pages
Section titled “List Knowledge Pages”GET /v1/knowledgeRequired scope: knowledge:read
Returns the pinned pages in full, then one row per page, sorted by slug.
Query Parameters
Section titled “Query Parameters”| Parameter | Type | Default | Description |
|---|---|---|---|
offset | number | 0 | Row to start from. Pass the previous response’s next_offset. |
limit | number | 40 | Rows per call (1 to 100). Fewer come back when they would not fit one response. |
tag | string | none | Only pages with this tag. |
Example
Section titled “Example”curl https://api.pathbound.ai/v1/knowledge \ -H "Authorization: Bearer $PATHBOUND_API_KEY"Response — 200 OK
Section titled “Response — 200 OK”{ "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", "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", "chars": 14260 } ], "total": 2, "offset": 0, "timestamp": "2026-10-13T09:30:00.000Z"}| Field | Description |
|---|---|
pinned | The 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. |
pages | One row per page. A pinned page is also a row here. |
pages[].summary | The page’s summary, cut to 240 characters. |
pages[].tags | Present when the page has tags. |
pages[].trust | See Trust. |
pages[].locked | true when only an admin may change the page. |
pages[].as_of | The date the page’s facts are correct for (YYYY-MM-DD), when the page states one. |
pages[].updated_by | Who 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[].chars | The length of the body in characters. |
total | Pages that match, across all calls. |
next_offset | Present 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.
Search Knowledge
Section titled “Search Knowledge”GET /v1/knowledge/searchRequired 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.
Query Parameters
Section titled “Query Parameters”| Parameter | Type | Default | Description |
|---|---|---|---|
q | string | none | Required. A few keywords, up to 200 characters. |
tag | string | none | Only search pages with this tag. |
limit | number | 8 | Most sections to return (1 to 20). |
Example
Section titled “Example”curl "https://api.pathbound.ai/v1/knowledge/search?q=discount+approval" \ -H "Authorization: Bearer $PATHBOUND_API_KEY"Response — 200 OK
Section titled “Response — 200 OK”{ "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", "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"}| Field | Description |
|---|---|
matched_by | text 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[].page | The page’s id and its slug. |
results[].section | The anchor of the matching section. |
results[].heading_path | The section’s heading with the headings above it. |
results[].text | The 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[].parts | Present when the section’s own text is too long for one response and was cut into parts. part is the one that matched. |
results[].chars | The 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.
Look Up a Page by Slug
Section titled “Look Up a Page by Slug”GET /v1/knowledge/pages?slug=Required scope: knowledge:read
Resolves a slug to its page. The slug is matched exactly, and slugs are lowercase.
Query Parameters
Section titled “Query Parameters”| Parameter | Type | Default | Description |
|---|---|---|---|
slug | string | none | Required. The page’s slug, up to 120 characters. URL-encode a /. |
Example
Section titled “Example”curl "https://api.pathbound.ai/v1/knowledge/pages?slug=playbooks%2Frenewals" \ -H "Authorization: Bearer $PATHBOUND_API_KEY"Response — 200 OK
Section titled “Response — 200 OK”{ "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", "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 a Page
Section titled “Get a Page”GET /v1/knowledge/pages/:idRequired 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.
Path Parameters
Section titled “Path Parameters”| Parameter | Type | Description |
|---|---|---|
id | string | The page’s id. |
Query Parameters
Section titled “Query Parameters”| Parameter | Type | Default | Description |
|---|---|---|---|
section | string | none | Read this section only: an anchor, a heading or a heading path. See Sections. |
part | number | 1 | For a section that comes in parts, the part to read. |
Example
Section titled “Example”curl https://api.pathbound.ai/v1/knowledge/pages/670b9c3f4c1a9d3e5f6071c8 \ -H "Authorization: Bearer $PATHBOUND_API_KEY"Response — 200 OK
Section titled “Response — 200 OK”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", "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"}| Field | Description |
|---|---|
section.section | The section’s anchor. |
section.text | The section: its own text and the sections under it. |
section.chars | The 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.
textholds its own text and as many of the sections under it as fit, in order.section.childrenlists the ones that are not intext, for you to read one at a time.section.children_omittedcounts any that did not fit in that list. - Its own text is too long.
textholds one part,section.partandsection.partssay which of how many, and you read the rest withpart=2,part=3and so on. Apartbeyond the last returns the last. The sections under it, if any, are all insection.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.
Create a Page
Section titled “Create a Page”POST /v1/knowledge/pagesRequired scope: knowledge:write
Request Body
Section titled “Request Body”| Field | Type | Required | Description |
|---|---|---|---|
slug | string | yes | The page’s slug. See Pages, Slugs and Ids. It cannot be changed later. |
body | string | no | The page as markdown, up to 256 KB. Use ## and ### headings: pages are read and edited one section at a time. Stored exactly as sent. |
title | string | no | Up to 200 characters. Defaults to the body’s first # heading, or else to the last part of the slug. |
summary | string | no | What 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. |
tags | string[] | no | Up to 50 tags. Each is trimmed, lowercased and cut to 64 characters. |
as_of | string | no | The date the page’s facts are correct for, such as 2026-10-01. |
Example
Section titled “Example”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" }'Response — 201 Created
Section titled “Response — 201 Created”{ "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", }, "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.
Update a Page
Section titled “Update a Page”PATCH /v1/knowledge/pages/:idRequired 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.
Path Parameters
Section titled “Path Parameters”| Parameter | Type | Description |
|---|---|---|
id | string | The page’s id. |
Request Body
Section titled “Request Body”| Field | Type | Required | Description |
|---|---|---|---|
body | string | no | The new body, which replaces the whole page. Needs expected_revision. |
title | string | no | A new title. A blank one is ignored. The title does not follow the body’s # heading: send title to change it. |
summary | string | no | A new summary. |
tags | string[] | no | Replaces the page’s tags. |
as_of | string | no | A new date. null removes it. |
expected_revision | number | with body | The 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.
Response — 200 OK
Section titled “Response — 200 OK”The page, in the shape of Create a Page, with the new revision. A request that changes nothing returns the page as it is.
Errors specific to this endpoint
Section titled “Errors specific to this endpoint”400 KNOWLEDGE_EXPECTED_REVISION_REQUIRED:bodydiffers from the stored body and noexpected_revisionwas sent.details.current_revisionholds the page’s revision.409 KNOWLEDGE_REVISION_CONFLICT:expected_revisionis 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.
Update a Section
Section titled “Update a Section”POST /v1/knowledge/pages/:id/sectionRequired 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.
Path Parameters
Section titled “Path Parameters”| Parameter | Type | Description |
|---|---|---|
id | string | The page’s id. |
Request Body
Section titled “Request Body”| Field | Type | Required | Description |
|---|---|---|---|
section | string | yes | The 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. |
content | string | yes | The new markdown for the section. |
create_if_missing | boolean | no | true 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_revision | number | no | The revision you read. The write is refused only if this section changed since then. |
What is replaced
Section titled “What is replaced”- 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
contentwith a heading of the section’s own level (## New namefor 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. introis the text before the first heading.- A new section (
create_if_missing) is added at the end under##and the text ofsection. Ifcontentitself opens with a#or##heading, it is added as it is.
expected_revision
Section titled “expected_revision”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.
Example
Section titled “Example”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 }'Response — 200 OK
Section titled “Response — 200 OK”{ "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", }, "section": "90-days-out", "created": false, "changed": true, "timestamp": "2026-10-13T09:52:03.288Z"}| Field | Description |
|---|---|
section | The section’s anchor after the write. It differs from the one you sent when the heading was renamed. |
created | true when the section was added. |
changed | false when content is what the section already held. No revision is made then. |
Limits
Section titled “Limits”| Limit | Value |
|---|---|
| Pages per workspace | Limited. A create past the limit is answered 403 QUOTA_EXCEEDED, and the message states the limit. |
| Page body | 256 KB |
| Title | 200 characters |
| Summary | 600 characters |
| Slug | 120 characters |
| Tags per page | 50, each up to 64 characters |
| Revisions kept per page | The most recent ones. An expected_revision older than those is answered 409 KNOWLEDGE_SECTION_CONFLICT. |
| Writes | Rate limited per workspace, across all keys and agents. A write over the limit is answered 429 with a Retry-After header. |
Errors
Section titled “Errors”Errors use the common error format. The message of each knowledge error says what to do next.
| Status | code | Meaning |
|---|---|---|
400 | none | A 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. |
400 | KNOWLEDGE_INVALID_SLUG | The slug does not fit the slug rule. |
400 | KNOWLEDGE_INVALID_AS_OF | as_of is not a date. |
400 | KNOWLEDGE_EXPECTED_REVISION_REQUIRED | A whole body was sent without expected_revision. |
400 | KNOWLEDGE_SECTION_AMBIGUOUS | Several sections have that heading. details.sections lists their anchors: pass one. |
403 | none | The key lacks knowledge:read or knowledge:write. required_scopes names it. |
403 | KNOWLEDGE_PAGE_LOCKED | The page is locked. Only an admin changes it, in the dashboard. |
403 | KNOWLEDGE_PAGE_PINNED | The page is pinned. Only an admin changes it, in the dashboard. |
403 | QUOTA_EXCEEDED | The workspace has reached its limit on pages. |
404 | KNOWLEDGE_PAGE_NOT_FOUND | No page has that id. |
404 | KNOWLEDGE_SECTION_NOT_FOUND | No section matches. details.sections lists the page’s anchors, and details.more counts any that did not fit in the list. |
409 | KNOWLEDGE_SLUG_TAKEN | A page with that slug exists. |
409 | KNOWLEDGE_REVISION_CONFLICT | The page changed since the revision you sent, or while the request was being written. |
409 | KNOWLEDGE_SECTION_CONFLICT | The section changed since the revision you sent. |
412 | KNOWLEDGE_TOOLS_DISABLED, KNOWLEDGE_WRITE_DISABLED | Only for calls made through the MCP server: the workspace’s switch for agents reading, or adding to, knowledge is off. |
413 | KNOWLEDGE_PAGE_TOO_LARGE | The body would be over 256 KB. Split it into several pages. |
429 | none | Too many knowledge writes in a minute in this workspace. Wait for the number of seconds in the Retry-After header. |
500 | none | The request could not be completed. |