Knowledge Base
Organize your ingested sources into folders, control access, and manage the compiled knowledge your agents query and generate from.
Root
├── Policies/
│ ├── refund-policy.pdf
│ └── lending-guidelines.docx
├── Training/
│ ├── onboarding-guide.md
│ └── product-faq.txt
└── rate-sheet-q1-2026.pdf
Every org starts with a root folder. You can nest folders as deep as you need. When you ingest a file or create raw text content, it gets compiled into a document inside a folder.
Terminology note: In the API, folders and documents are both called nodes — items in a tree. You'll seekb_node_idin responses and/org/kb/nodes/{id}in URLs. Just think of it as "the ID of this file or folder."
---
Creating folders
Organize your knowledge base by creating folders. If you omit parent_id, the folder is created at the root level.
import os, requests
KEY = os.environ["SENSO_API_KEY"]
BASE = "https://apiv2.senso.ai/api/v1"
HEADERS = {"X-API-Key": KEY, "Content-Type": "application/json"}
# Create a top-level folder
resp = requests.post(f"{BASE}/org/kb/folders", headers=HEADERS, json={
"name": "Policies",
})
folder = resp.json()
folder_id = folder["kb_node_id"]
print(f"Created folder: {folder['name']} ({folder_id})")
# Create a nested folder inside it
resp = requests.post(f"{BASE}/org/kb/folders", headers=HEADERS, json={
"name": "2026 Updates",
"parent_id": folder_id,
})
print(f"Created subfolder: {resp.json()['name']}")# Create a top-level folder
curl -X POST https://apiv2.senso.ai/api/v1/org/kb/folders \
-H "X-API-Key: $SENSO_API_KEY" \
-H "Content-Type: application/json" \
-d '{"name": "Policies"}'
# Create a nested folder
curl -X POST https://apiv2.senso.ai/api/v1/org/kb/folders \
-H "X-API-Key: $SENSO_API_KEY" \
-H "Content-Type: application/json" \
-d '{"name": "2026 Updates", "parent_id": "FOLDER_ID"}'---
Ingesting files into folders
When you ingest files with POST /org/kb/upload, you can specify which folder to place them in using kb_folder_node_id. Omit it and files go to the root.
import hashlib
file_bytes = open("refund-policy.pdf", "rb").read()
resp = requests.post(f"{BASE}/org/kb/upload", headers=HEADERS, json={
"kb_folder_node_id": folder_id, # ← place in the Policies folder
"files": [{
"filename": "refund-policy.pdf",
"file_size_bytes": len(file_bytes),
"content_type": "application/pdf",
"content_hash_md5": hashlib.md5(file_bytes).hexdigest(),
}]
})
result = resp.json()["results"][0]
# Upload to S3
requests.put(result["upload_url"], data=file_bytes)
print(f"Uploaded to {folder_id}: {result['content_id']}")curl -X POST https://apiv2.senso.ai/api/v1/org/kb/upload \
-H "X-API-Key: $SENSO_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"kb_folder_node_id": "FOLDER_ID",
"files": [{
"filename": "refund-policy.pdf",
"file_size_bytes": 245760,
"content_type": "application/pdf",
"content_hash_md5": "d41d8cd98f00b204e9800998ecf8427e"
}]
}'See Core Concepts — Ingestion for the full ingest flow including polling for processing status.
---
Creating raw text content
Not every raw source comes from a file. You can ingest content directly from text or markdown using POST /org/kb/raw:
resp = requests.post(f"{BASE}/org/kb/raw", headers=HEADERS, json={
"kb_folder_node_id": folder_id,
"title": "Return Policy Summary",
"summary": "Key points from our return and refund policy",
"text": "# Return Policy\n\nCustomers may return items within 30 days of purchase...",
})
content = resp.json()
print(f"Created: {content['id']} (status: {content['processing_status']})")curl -X POST https://apiv2.senso.ai/api/v1/org/kb/raw \
-H "X-API-Key: $SENSO_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"kb_folder_node_id": "FOLDER_ID",
"title": "Return Policy Summary",
"text": "# Return Policy\n\nCustomers may return items within 30 days..."
}'Raw text goes through the same compilation pipeline as ingested files — it's parsed, chunked, and embedded for querying.
Updating raw content
Use PATCH to update specific fields without replacing the whole document, or PUT to fully replace the content (creates a new version):
# Partial update — only changes what you send
requests.patch(f"{BASE}/org/kb/nodes/{node_id}/raw", headers=HEADERS, json={
"text": "# Updated Return Policy\n\nCustomers may return items within 60 days...",
})
# Full replace — requires title and text, creates a new version
requests.put(f"{BASE}/org/kb/nodes/{node_id}/raw", headers=HEADERS, json={
"title": "Return Policy v2",
"text": "# Return Policy v2\n\nCustomers may return items within 60 days...",
})# Partial update
curl -X PATCH https://apiv2.senso.ai/api/v1/org/kb/nodes/{id}/raw \
-H "X-API-Key: $SENSO_API_KEY" \
-H "Content-Type: application/json" \
-d '{"text": "# Updated Return Policy\n\nCustomers may return items within 60 days..."}'
# Full replace
curl -X PUT https://apiv2.senso.ai/api/v1/org/kb/nodes/{id}/raw \
-H "X-API-Key: $SENSO_API_KEY" \
-H "Content-Type: application/json" \
-d '{"title": "Return Policy v2", "text": "# Return Policy v2\n\n..."}'---
Browsing your knowledge base
List top-level contents
GET /org/kb/my-files returns the files and folders at the top level of your knowledge base:
resp = requests.get(f"{BASE}/org/kb/my-files", headers=HEADERS)
for item in resp.json()["nodes"]:
icon = "📁" if item["type"] == "folder" else "📄"
print(f" {icon} {item['name']} ({item['kb_node_id']})")curl https://apiv2.senso.ai/api/v1/org/kb/my-files \
-H "X-API-Key: $SENSO_API_KEY"Browse inside a folder
GET /org/kb/nodes/{id}/children lists the contents of a specific folder:
resp = requests.get(f"{BASE}/org/kb/nodes/{folder_id}/children", headers=HEADERS)
for item in resp.json()["nodes"]:
print(f" {item['type']}: {item['name']}")curl https://apiv2.senso.ai/api/v1/org/kb/nodes/{id}/children \
-H "X-API-Key: $SENSO_API_KEY"Search by name
GET /org/kb/find?q=refund searches your knowledge base by file or folder name:
resp = requests.get(f"{BASE}/org/kb/find", headers=HEADERS, params={"q": "refund"})
for item in resp.json()["nodes"]:
print(f" {item['name']} ({item['type']})")curl "https://apiv2.senso.ai/api/v1/org/kb/find?q=refund" \
-H "X-API-Key: $SENSO_API_KEY"Get the breadcrumb path
GET /org/kb/nodes/{id}/ancestors returns the full path from the root to a specific item — useful for building breadcrumb navigation:
resp = requests.get(f"{BASE}/org/kb/nodes/{node_id}/ancestors", headers=HEADERS)
path = " / ".join(a["name"] for a in resp.json()["ancestors"])
print(f"Path: {path}")curl https://apiv2.senso.ai/api/v1/org/kb/nodes/{id}/ancestors \
-H "X-API-Key: $SENSO_API_KEY"---
Renaming, moving, and deleting
Rename
requests.patch(f"{BASE}/org/kb/nodes/{node_id}/rename", headers=HEADERS, json={
"name": "Refund Policy (Updated)",
})curl -X PATCH https://apiv2.senso.ai/api/v1/org/kb/nodes/{id}/rename \
-H "X-API-Key: $SENSO_API_KEY" \
-H "Content-Type: application/json" \
-d '{"name": "Refund Policy (Updated)"}'Move to a different folder
requests.patch(f"{BASE}/org/kb/nodes/{node_id}/move", headers=HEADERS, json={
"new_parent_id": other_folder_id,
})curl -X PATCH https://apiv2.senso.ai/api/v1/org/kb/nodes/{id}/move \
-H "X-API-Key: $SENSO_API_KEY" \
-H "Content-Type: application/json" \
-d '{"new_parent_id": "OTHER_FOLDER_ID"}'Moving or deleting items triggers a background vector sync that updates search indexes. Check sync status with GET /org/kb/sync-status.Delete
Deleting a folder also deletes everything inside it. This is a soft delete — vectors are cleaned up asynchronously.
requests.delete(f"{BASE}/org/kb/nodes/{node_id}", headers=HEADERS)curl -X DELETE https://apiv2.senso.ai/api/v1/org/kb/nodes/{id} \
-H "X-API-Key: $SENSO_API_KEY"---
Downloading files
Get a time-limited download URL for any uploaded file:
resp = requests.get(f"{BASE}/org/kb/nodes/{node_id}/download-url", headers=HEADERS)
data = resp.json()
print(f"Download: {data['url']}")
print(f"Filename: {data['filename']}")
print(f"Expires: {data['expiry_utc_ms']}")curl https://apiv2.senso.ai/api/v1/org/kb/nodes/{id}/download-url \
-H "X-API-Key: $SENSO_API_KEY"You can also download a specific version by adding ?version=N.
---
Content versions
Every time you update a document (replace a file or update raw text), a new version is created. Retrieve a specific version by passing ?version=N:
# Get the current version
resp = requests.get(f"{BASE}/org/kb/nodes/{node_id}/content", headers=HEADERS)
# Get version 2 specifically
resp = requests.get(f"{BASE}/org/kb/nodes/{node_id}/content", headers=HEADERS,
params={"version": 2})# Current version
curl https://apiv2.senso.ai/api/v1/org/kb/nodes/{id}/content \
-H "X-API-Key: $SENSO_API_KEY"
# Specific version
curl "https://apiv2.senso.ai/api/v1/org/kb/nodes/{id}/content?version=2" \
-H "X-API-Key: $SENSO_API_KEY"---
Scoping API keys to specific folders
For a full overview of how user roles, API key scopes, and KB roles work together, see Permissions.
You can restrict an API key so it can only access specific parts of your knowledge base. This is useful for creating keys that only see certain folders — for example, giving an external integration access to "Public Docs" but not "Internal Policies."
# Scope an API key to a specific folder
requests.put(f"{BASE}/org/api-keys/{key_id}/kb-permissions", headers=HEADERS, json={
"grants": [
{"node_id": public_folder_id, "role": "viewer"},
]
})
# Check what a key has access to
resp = requests.get(f"{BASE}/org/api-keys/{key_id}/kb-permissions", headers=HEADERS)
for grant in resp.json():
print(f" {grant['node_id']}: {grant['role']}")
# Remove all restrictions (restore full access)
requests.delete(f"{BASE}/org/api-keys/{key_id}/kb-permissions", headers=HEADERS)# Scope to a folder
curl -X PUT https://apiv2.senso.ai/api/v1/org/api-keys/{keyId}/kb-permissions \
-H "X-API-Key: $SENSO_API_KEY" \
-H "Content-Type: application/json" \
-d '{"grants": [{"node_id": "FOLDER_ID", "role": "viewer"}]}'
# Check scope
curl https://apiv2.senso.ai/api/v1/org/api-keys/{keyId}/kb-permissions \
-H "X-API-Key: $SENSO_API_KEY"
# Remove scope
curl -X DELETE https://apiv2.senso.ai/api/v1/org/api-keys/{keyId}/kb-permissions \
-H "X-API-Key: $SENSO_API_KEY"Available roles: viewer (read-only), editor (read + write), owner, admin.
When a key is scoped, query results are automatically filtered to only include content within the granted folders and their subfolders.
---
Using the CLI
# List top-level files and folders
senso kb my-files --output json --quiet
# List contents of a folder
senso kb children <folder-id> --output json --quiet
# Create a folder
senso kb create-folder --name "Policies" --output json --quiet
# Get details for a file or folder
senso kb get <id> --output json --quiet
# Search by name
senso kb find --query "refund" --output json --quiet
# Rename
senso kb rename <id> --name "New Name" --output json --quiet
# Move to a different folder
senso kb move <id> --parent-id <folder-id> --output json --quiet
# Delete
senso kb delete <id> --quiet
# Get content details
senso kb get-content <id> --output json --quiet
# Get download URL
senso kb download-url <id> --output json --quiet---
Errors
| Status | Meaning |
|---|---|
400 | Invalid request — missing required fields, can't delete the root folder, or would create a circular folder structure |
402 | Insufficient credits or spend limit reached (for uploads) |
403 | No access — the API key doesn't have permission for this file or folder |
404 | File or folder not found |
422 | All files skipped — duplicates, conflicts, or invalid metadata |
