Knowledge API
Create knowledge bases, upload files, send records and web pages, follow each document, and search with filters and citations — the same paths on self-hosted and Cloud.
Everything the Knowledge page does, and a few things it does not, is under /api/v1/knowledge. The paths, bodies and answers are the same on a self-hosted install and on SynapseOrch Cloud; where the two differ, this page says. Every call takes an API key as a Bearer token. Each endpoint's fields and answers, with samples in four languages, are in the reference, starting at List knowledge bases.
While Knowledge is switched off, every route here answers 404, before the key is even checked.
Endpoints
| Method | Path | Cloud scope | |
|---|---|---|---|
GET | /api/v1/knowledge/bases | List bases, each with its state | read |
POST | /api/v1/knowledge/bases | Create a base — 201 | build |
GET | /api/v1/knowledge/bases/{kb_id} | Get a base | read |
PATCH | /api/v1/knowledge/bases/{kb_id} | Change a base | build |
DELETE | /api/v1/knowledge/bases/{kb_id} | Delete a base; its files stay in the Vault — 202 | build |
POST | /api/v1/knowledge/bases/{kb_id}/cost-preview | Price the base's indexing, with or without changed settings | read |
POST | /api/v1/knowledge/bases/{kb_id}/uploads | Start an upload — 201 | build |
PUT | the part URL the start gave | Send one part of an upload | — |
POST | /api/v1/knowledge/uploads/{upload_id}/complete | Complete an upload — 202 | build |
DELETE | /api/v1/knowledge/uploads/{upload_id} | Cancel an upload — 204 | build |
POST | /api/v1/knowledge/bases/{kb_id}/records | Send records — 202 | build |
DELETE | /api/v1/knowledge/bases/{kb_id}/records/{record_id} | Delete a record — 202 | build |
POST | /api/v1/knowledge/bases/{kb_id}/urls | Add web pages — 202 | build |
GET | /api/v1/knowledge/bases/{kb_id}/documents | List a base's documents | read |
GET | /api/v1/knowledge/documents/{doc_id} | Get a document, with its report | read |
POST | /api/v1/knowledge/documents/{doc_id}/retry | Read a document again — 202 | build |
DELETE | /api/v1/knowledge/documents/{doc_id} | Delete a document and its file — 202 | build |
POST | /api/v1/knowledge/search | Search | read |
GET | /api/v1/knowledge/queries/{query_id} | Read a search's trace | read |
GET | /api/v1/knowledge/bases/{kb_id}/costs | What a base has cost | read |
The scope column is what a call needs on SynapseOrch Cloud: every key has read and run, and build only when it was made with Can also build (What a key can do). A self-hosted key reaches every route. The page's web-source progress and Evaluate are not on the API.
A request body with a field the endpoint does not know is refused with 422. Lists are plain JSON arrays. Errors are {"detail": …}.
Bases
curl -X POST http://localhost:3000/api/v1/knowledge/bases \
-H "Authorization: Bearer sk-syn-..." -H "Content-Type: application/json" \
-d '{"name": "Contracts", "folder": "contracts",
"description": "Signed supplier contracts, 2022 on.",
"retrieval": {"top_k": 10}}'Only name and folder are required; everything else takes the defaults described in Search and settings:
| Field | Default | |
|---|---|---|
name | — | 1–200 characters |
description | "" | Up to 2,000 characters |
folder | — | A Vault folder, created if missing; it may not overlap another base's |
status | "active" | "disabled" switches the base off: not searched, and uploads, records and web pages are refused with 409 |
language | "simple" | For keyword matching: simple or a language such as english, french, german |
visitor_access | false | Whether widget and channel visitors may search it |
budget_monthly_usd | null | No budget |
chunking | {"mode": "structure", "target_tokens": 450, "max_tokens": 800, "overlap_pct": 0} | mode is structure, fixed, page or none |
embedding | {"model": "", "dims": null} | "" is the workspace default |
rerank | {"enabled": null, "model": null, "top_n": 8, "candidates": 50} | enabled: null reranks where a reranker is configured |
fusion | {"rrf_k": 60, "overfetch": 50, "weights": {"lexical": 1, "vector": 1}} | |
retrieval | {"top_k": 8, "score_threshold": null, "parent_expansion": "off", "neighbor_window": 1, "max_context_tokens": 6000} | parent_expansion is off, neighbors or section |
enrichment | {"mode": "prefix", "contextual_model": null, "extract_metadata": false} | mode: "contextual" turns on Contextual Retrieval |
web | {"max_depth": 3, "max_pages": 500, "rps_per_host": 1, "recrawl_days": 7, "sources": []} | Crawl limits, within the deployment's; recrawl_days: 0 fetches only when asked |
enrichment.extract_metadata is not built yet: true is refused with a 422, on a create and on an edit that turns it on. A base stored with it keeps it.
PATCH sends the base's revision — the one your edit started from — and only the fields to change. A stale revision is refused with 409, so two edits never overwrite each other silently. "clear_budget": true removes the budget. A change to chunking, embedding, enrichment or language rebuilds the index beside the serving one, and the answer says "reindex_started": true.
A base's state reports counts_by_status, this month's spend (month_spend_usd), why it is paused if it is (paused_reason), the serving and building index versions, and — during a rebuild — build: docs_total, docs_done, docs_failed, passages_done, passages_total, the current stage, eta_s in seconds, and waiting when a rate limit is being waited out.
Uploads
A file goes up in three steps, so a large one never travels in a single request:
# 1. Start: name, size and type
curl -X POST http://localhost:3000/api/v1/knowledge/bases/kb_…/uploads \
-H "Authorization: Bearer sk-syn-..." -H "Content-Type: application/json" \
-d '{"filename": "msa-globex.pdf", "size_bytes": 12001743, "mime": "application/pdf"}'{
"upload_id": "kup_…",
"part_size": 8388608,
"parts": [
{"part_number": 1, "url": "…", "method": "PUT", "headers": {"X-Synapse-Upload-Token": "…"}},
{"part_number": 2, "url": "…", "method": "PUT", "headers": {"X-Synapse-Upload-Token": "…"}}
],
"expires_at": "2026-10-05T09:12:00Z"
}That is a self-hosted answer; on SynapseOrch Cloud each url points straight at storage and headers is {}.
- Send each part with
PUTto itsurl: exactlypart_sizebytes (the last part is the rest), with exactly the part'sheaders— and nothing else, noAuthorization. Keep each answer'sETagheader as it came, quotes included. - Complete with the ETags:
curl -X POST http://localhost:3000/api/v1/knowledge/uploads/kup_…/complete \
-H "Authorization: Bearer sk-syn-..." -H "Content-Type: application/json" \
-d '{"parts": [{"part_number": 1, "etag": "\"9b2c…\""}, {"part_number": 2, "etag": "\"41ef…\""}]}'The answer is 202 with the new document, queued. One rule covers both products — PUT each part URL with exactly its headers — although the URLs differ:
| Self-hosted | SynapseOrch Cloud | |
|---|---|---|
| Part URL | Synapse's own, /api/v1/knowledge/uploads/{upload_id}/parts/{n} | Straight to storage |
| Part headers | X-Synapse-Upload-Token, a token for this upload alone | None ({}) |
| A part URL lasts | Until expires_at (24 hours) | An hour; past it, cancel and start again |
| Who completes or cancels | Any key | Only the key or app that started it |
| Uploads open at once | No limit | 5 per workspace (429 beyond) |
path in the start puts the file in a subfolder of the base (not _web or _records), and metadata sets its metadata. A type the base does not take is 415; a file over the size limit, 413; a switched-off base, 409. A part whose upload is unknown, or whose token is wrong, answers 404.
Cancel an upload you will not complete — a part failed, or the complete was refused — with DELETE /api/v1/knowledge/uploads/{upload_id}. Its parts are deleted, and on SynapseOrch Cloud it stops counting against the five.
Records
curl -X POST http://localhost:3000/api/v1/knowledge/bases/kb_…/records \
-H "Authorization: Bearer sk-syn-..." -H "Content-Type: application/json" \
-d '{"records": [
{"id": "T-1042", "title": "Refund after cancellation",
"text": "Customer cancelled within 14 days and asked for a refund…",
"doc_date": "2026-09-30", "metadata": {"team": "billing", "priority": "high"}},
{"id": "SKU-77", "json": {"name": "Desk lamp", "price": 49, "colours": ["black", "white"]}}
]}'Each record has either text or json, and optionally id, title, doc_date (YYYY-MM-DD) and metadata. It is stored as _records/<id>.json in the base's folder, and sending the same id again replaces it. A record without an id gets a new one each time it is sent. Ids are letters, digits, ., _ and -, up to 128 characters. Up to 100 records go in one request, each up to 1,000,000 bytes. DELETE …/records/{record_id} deletes one.
Web pages
curl -X POST http://localhost:3000/api/v1/knowledge/bases/kb_…/urls \
-H "Authorization: Bearer sk-syn-..." -H "Content-Type: application/json" \
-d '{"urls": ["https://docs.example.com/guide/"], "kind": "site", "max_depth": 2, "max_pages": 100}'kind is page (the default), sitemap or site; max_depth and max_pages replace the base's for these addresses, within the deployment's limits. The answer is 202 with crawl_id and how many addresses were accepted, and the crawl runs on — see Web pages for what it reads. Each address becomes a source in the base's web.sources. To remove one, PATCH web.sources without it; a source id that another base holds is refused with 422.
Documents
GET /api/v1/knowledge/bases/{kb_id}/documents lists a base's documents, paged by after (the last id seen) and limit (up to 1,000), and narrowed by status or path_prefix. GET /api/v1/knowledge/documents/{doc_id} adds the document's report: each stage of the last attempt, its warnings, and whether a retry can help.
| Field | |
|---|---|
status | queued, parsing, indexing, indexed, partial, failed, skipped, paused, blocked or deleting — see Document status |
status_reason, error | Why, as a code and in words |
source, source_url | upload, web, record or vault; a web page's address |
path, title, doc_date, metadata, mime, size_bytes | The file |
pages, chunks, tokens, version, indexed_at | What was indexed, and when |
progress | While the document is read or indexed: stage (parse, ocr, enrich, embed, index), done and total with their unit (pages or passages), eta_s, and waiting — {"reason": "rate_limited", "provider": …, "until": …} — while a rate limit is waited out. On SynapseOrch Cloud a wait past two minutes puts the document back in queued; its progress keeps the stage it reached, with waiting.until when it carries on |
POST …/retry reads a resting document again (409 while it is being worked on); DELETE deletes the document and its file.
Knowing when a document is ready
Self-hosted: there are no Knowledge webhooks. Poll GET /api/v1/knowledge/documents/{doc_id} until status is one of indexed, partial, failed, skipped, paused or blocked, or watch a base's state.counts_by_status.
SynapseOrch Cloud: a webhook can tell you when a document is indexed, partly indexed or failed. Deliveries are best effort, so the document is still the truth.
Search
curl -X POST http://localhost:3000/api/v1/knowledge/search \
-H "Authorization: Bearer sk-syn-..." -H "Content-Type: application/json" \
-d '{"query": "When can the supplier end the contract?",
"kb_ids": ["kb_…"],
"filters": {"doc_type": "contract", "year": {"$gte": 2024}},
"top_k": 5}'| Field | Default | |
|---|---|---|
query | — | Up to 2,000 characters |
kb_ids | — | 1 to 10 bases |
filters | none | See Filters |
path_prefix | none | Only documents under this folder of the base, matched whole folder by whole folder |
top_k | The bases' own | 1–50 |
rerank | The bases' own | true or false for this search |
score_threshold | The base's own, for one base | Drop hits below it |
neighbors | 0 | 0–3 passages either side of each hit, in its text |
mode | "hybrid" | "lexical" or "vector" for one search alone |
snapshot_ids | Each base's serving index | {"kb_…": "ksnap_…"} pins a base to one version of its index |
include_trace | false | Adds the trace to the answer |
{
"query_id": "kq_…",
"hits": [
{
"label": "[K1]",
"content": "Either party may terminate this agreement with 30 days' written notice…",
"context": "Globex Payroll MSA (2025) › 12 Term and termination",
"score": 0.91,
"scores": {"lexical": 0.42, "vector": 0.81, "rerank": 0.91},
"metadata": {"doc_type": "contract", "year": 2025},
"citation": {
"label": "[K1]", "path": "contracts/msa-globex.pdf", "source": "upload",
"title": "Master Services Agreement", "page_start": 7, "page_end": 7,
"heading_path": ["12 Term and termination"], "snippet": "Either party may terminate…",
"indexed_at": "2026-10-02T11:04:09Z", "doc_id": "kdoc_…", "chunk_id": "kch_…"
}
}
],
"degraded": [],
"cost": {"total_est_usd": 0.002501, "total_charged_microusd": 2501, "by_stage": {"embed": 0.000001, "rerank": 0.0025}}
}Each hit's citation also carries char_start/char_end, source_url, sha256, doc_version and snapshot_id. degraded lists what the search did without — vector_unavailable, rerank_failed, rerank_unconfigured, embed_failed, budget_paused, snapshot_retired — each explained in Test search. total_charged_microusd is what SynapseOrch Cloud's wallet paid; on a self-hosted install it is null.
GET /api/v1/knowledge/queries/{query_id} returns a search's trace — the request, each search's candidates with their ranks and scores, the cuts and the hits — for 30 days. An unknown or expired one is 404. A search naming an unknown base, or a pinned version that is gone, is 404 too.
Filters
A filter is a JSON object over a document's metadata and a few fields every document has. The search, the Retrieve step and an agent's search_knowledge all use the same language.
{"doc_type": "contract", "year": {"$gte": 2024}, "$or": [{"region": "EU"}, {"tags": "urgent"}]}- Each key of an object must match: the object is an AND.
- A plain value means equal; a list must be spelled
{"$in": [...]}. - Operators:
$eq,$ne,$gt,$gte,$lt,$lte,$in,$nin,$exists, and$and,$or(each a list) and$not(one object). - Every document has
path,source(upload,web,recordorvault),mime,title,doc_date(YYYY-MM-DD) andindexed_at(an ISO time). $nematches a document that lacks the field. Ranges compare numbers with numbers and text with text. A list field equals a value when it contains it, so{"tags": "urgent"}matches["legal", "urgent"].- At most 4 levels deep, 50 conditions and 100 values in an
$in.
The filter runs inside the search, so top_k hits always match it. A malformed filter is refused with 422, its type naming the problem — filter.unknown_operator, filter.too_deep, filter.type_mismatch and so on.
An agent's search_knowledge takes the same filter as a JSON string, because some models cannot call a tool whose argument may be more than one type.
Costs
POST …/cost-preview prices indexing the base — with its settings, or with {"overrides": {…}} in the shape of a PATCH — without saving anything:
{"docs": 24, "pages": 311, "total_est_usd": 0.0042, "budget_remaining_usd": 4.2,
"stages": [{"stage": "embed", "model": "synapse.titan-embed-v2", "units": 168000, "unit": "tokens",
"est_usd": 0.0042, "cost_source": "platform"}]}GET …/costs reports what the base has spent, grouped by group_by — day (the default), stage, document or model — from since to until (ISO dates or times; this month by default). Each group has key, est_usd, charged_microusd (SynapseOrch Cloud), input_tokens and units. See Costs and budgets.
Limits on SynapseOrch Cloud
- Searches: 120 a minute per key, shared with MCP's
search_knowledge. Past it:429Rate limit exceeded: Knowledge searches per minute for this key. Retry after … s. - Starting work — an upload's start and complete, records, web pages, a retry — counts toward the key's 60 starts a minute.
- Every call counts toward the key's 300 requests a minute and the organisation's 1,200; see the API overview.
A self-hosted install applies none of these.
Workflows API
List orchestrations, run them synchronously or as a stream, read a run back after its stream is gone, resume runs that paused at a human gate, and cancel them.
Synapse as an MCP Server
Connect Claude Code, Cursor or any MCP client to Synapse: build agents and workflows, run them, answer their questions and read the results — every workflow is a tool of its own.