Senso
Sign in

Knowledge Base

Organize your ingested sources into folders, control access, and manage the compiled knowledge your agents query and generate from.

Your knowledge base is where all your ingested raw sources get compiled and stored. It works like a file system: folders organize your content, and documents (ingested files or raw text) hold the verified knowledge that queries and content generation draw 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 see kb_node_id in 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

StatusMeaning
400Invalid request — missing required fields, can't delete the root folder, or would create a circular folder structure
402Insufficient credits or spend limit reached (for uploads)
403No access — the API key doesn't have permission for this file or folder
404File or folder not found
422All files skipped — duplicates, conflicts, or invalid metadata
---

What's next

  • Permissions — Understand user roles and KB access scoping in detail
  • Core Concepts — Understand how the ingest → compile → query → generate pipeline works
  • Quickstart — Ingest a raw source and start querying in 5 minutes
  • API Reference — Full endpoint specs for all knowledge base operations