Sutram REST API v1
Overview
The Sutram REST API provides JSON endpoints for external consumption of project content (folders, files, links). It uses the same dual-key authentication as the MCP Server (x-project-key + x-user-key), so any project with Remote Access enabled becomes automatically accessible through the REST API.
Use cases
- Institutional site (dogfooding) — Serving the blog posts and documentation pages of the sutram.io site itself, with Sutram as the single source of truth (SSOT)
- Custom dashboards — Building content listing pages outside of Sutram
- Mobile apps — Fetching files and links with pre-signed download URLs
- Automation — Programmatic content discovery and retrieval via scripts or CI/CD
Base URL
https://sutram.io/api/v1
How it works
External Client (app, script, dashboard)
|
| HTTPS + JSON
v
Sutram REST API
/api/v1/*
|
| Dual Key Authentication
v
Project Content (folders, files, links, tags)
Authentication
Every request must include two headers:
| Header | Value | Description |
|---|---|---|
x-project-key |
sk_proj_... |
Identifies the project |
x-user-key |
sk_user_... |
Identifies the user |
Both keys are created through the Sutram web interface.
Example
curl -H "x-project-key: sk_proj_ej8NWMisd2rJgMwAJ22T..." \
-H "x-user-key: sk_user_zQD0BjH56nQhOgX3uxni..." \
https://sutram.io/api/v1/project
Error responses
When authentication fails, the API returns HTTP 401:
{ "error": "Missing x-project-key header" }
{ "error": "Missing x-user-key header" }
{ "error": "Invalid project key" }
{ "error": "Invalid user key" }
{ "error": "Not a member of this project" }
Permissions
| Role | Access |
|---|---|
| Owner | Full read access |
| Admin | Full read access |
| Member | Full read access |
| Viewer | Read access (the v1 API is read-only for all roles) |
The v1 REST API is read-only for all roles. Since the entire API is read-only, any active member — including viewers — can consume it with valid keys.
Endpoints
GET /api/v1/project
Returns information about the authenticated project.
Example request:
curl -H "x-project-key: $PROJECT_KEY" \
-H "x-user-key: $USER_KEY" \
https://sutram.io/api/v1/project
Response:
{
"project": {
"id": "a1b2c3d4-...",
"name": "Construction Site Alpha",
"description": "Main project documentation",
"your_role": "owner",
"created_at": "2026-01-15T10:00:00Z",
"updated_at": "2026-02-19T14:30:00Z"
}
}
GET /api/v1/folders
Lists the project's root-level folders.
Example request:
curl -H "x-project-key: $PROJECT_KEY" \
-H "x-user-key: $USER_KEY" \
https://sutram.io/api/v1/folders
Response:
{
"folders": [
{
"id": "f1a2b3c4-...",
"name": "Reports",
"parent_id": null,
"tags": {"category": "monthly"},
"created_at": "2026-01-20T10:00:00Z"
},
{
"id": "f5e6d7c8-...",
"name": "Photos",
"parent_id": null,
"tags": {},
"created_at": "2026-01-22T08:00:00Z"
}
],
"meta": {
"total_count": 2
}
}
GET /api/v1/folders/:id
Returns the details of a folder, its subfolders, and its content items.
Path parameters:
| Parameter | Type | Description |
|---|---|---|
id |
UUID | Folder ID |
Example request:
curl -H "x-project-key: $PROJECT_KEY" \
-H "x-user-key: $USER_KEY" \
https://sutram.io/api/v1/folders/f1a2b3c4-...
Response:
{
"folder": {
"id": "f1a2b3c4-...",
"name": "Reports",
"parent_id": null,
"path": "/Reports",
"tags": {"category": "monthly"},
"created_at": "2026-01-20T10:00:00Z"
},
"subfolders": [
{
"id": "a1b2c3d4-...",
"name": "2024",
"parent_id": "f1a2b3c4-...",
"tags": {"year": "2024"},
"created_at": "2026-01-25T12:00:00Z"
}
],
"items": [
{
"id": "d5e6f7a8-...",
"type": "file",
"name": "summary",
"description": null,
"tags": {"status": "final"},
"folder_id": "f1a2b3c4-...",
"created_at": "2026-02-01T10:00:00Z",
"file": {
"filename": "summary.pdf",
"content_type": "application/pdf",
"file_size": 245000
}
}
],
"meta": {
"subfolder_count": 1,
"item_count": 1
}
}
Error responses:
| Status | Body | When |
|---|---|---|
| 404 | {"error": "Folder not found"} |
The folder does not exist or belongs to another project |
GET /api/v1/content
Lists content items with filtering, tag search, text search, and pagination.
Query parameters:
| Parameter | Type | Default | Description |
|---|---|---|---|
folder_id |
UUID | — | Filter by folder. Without this parameter, returns only root-level items. |
all_folders |
boolean | false |
If true, searches across all folders (not just the root). Required for project-wide tag/search queries. |
type |
string | — | Filter by content type: "file", "web_link", "video_link", "audio_link" |
tag |
string | — | Single tag key to filter by (e.g., tag=category) |
tag_value |
string | — | Tag value to match (used with tag). Substring match, case-insensitive. |
tags |
JSON | — | Multiple AND conditions as a JSON array (e.g., tags=[{"key":"topic","value":"neuro"}]) |
search |
string | — | Search by item name (substring match, case-insensitive) |
page |
integer | 1 |
Page number |
per_page |
integer | 50 |
Items per page (maximum: 100) |
Tag search behavior:
- Key match: Exact match, case-insensitive (
"Category"matches"category") - Value match: Substring match, case-insensitive (
"neuro"matches"Neurologia Avançada") - Multiple conditions (
tagsparameter): All conditions must be satisfied (AND logic) - No value: When
tag_valueis omitted, matches any item that has the tag key, regardless of value
Example — list root items
curl -H "x-project-key: $PROJECT_KEY" \
-H "x-user-key: $USER_KEY" \
"https://sutram.io/api/v1/content"
Example — list items in a folder
curl -H "x-project-key: $PROJECT_KEY" \
-H "x-user-key: $USER_KEY" \
"https://sutram.io/api/v1/content?folder_id=f1a2b3c4-..."
Example — filter by type
curl -H "x-project-key: $PROJECT_KEY" \
-H "x-user-key: $USER_KEY" \
"https://sutram.io/api/v1/content?type=video_link&all_folders=true"
Example — single-tag search
curl -H "x-project-key: $PROJECT_KEY" \
-H "x-user-key: $USER_KEY" \
"https://sutram.io/api/v1/content?tag=category&tag_value=cientifico&all_folders=true"
Example — AND search with multiple tags
curl -H "x-project-key: $PROJECT_KEY" \
-H "x-user-key: $USER_KEY" \
"https://sutram.io/api/v1/content?tags=%5B%7B%22key%22%3A%22patient%22%2C%22value%22%3A%22jo%C3%A3o%22%7D%2C%7B%22key%22%3A%22year%22%2C%22value%22%3A%222024%22%7D%5D&all_folders=true"
The tags parameter above is the URL-encoded version of:
[{"key": "patient", "value": "joão"}, {"key": "year", "value": "2024"}]
Example — search by name
curl -H "x-project-key: $PROJECT_KEY" \
-H "x-user-key: $USER_KEY" \
"https://sutram.io/api/v1/content?search=report&all_folders=true"
Example — paginated listing
curl -H "x-project-key: $PROJECT_KEY" \
-H "x-user-key: $USER_KEY" \
"https://sutram.io/api/v1/content?page=2&per_page=20"
Response:
{
"items": [
{
"id": "d5e6f7a8-...",
"type": "file",
"name": "site-plan",
"description": null,
"tags": {"category": "planta", "year": "2024"},
"folder_id": "f1a2b3c4-...",
"created_at": "2026-02-19T10:00:00Z",
"file": {
"filename": "site-plan.pdf",
"content_type": "application/pdf",
"file_size": 2450000
}
},
{
"id": "e5f6a7b8-...",
"type": "video_link",
"name": "Site walkthrough",
"description": null,
"tags": {"topic": "neurologia"},
"folder_id": null,
"created_at": "2026-02-18T14:00:00Z",
"video_link": {
"url": "https://youtube.com/watch?v=abc",
"platform": "youtube",
"video_id": "abc",
"thumbnail_url": "https://img.youtube.com/vi/abc/hqdefault.jpg"
}
},
{
"id": "a1b2c3d4-...",
"type": "web_link",
"name": "Documentation",
"description": "Project reference docs",
"tags": {},
"folder_id": null,
"created_at": "2026-02-17T09:00:00Z",
"web_link": {
"url": "https://docs.example.com",
"fetched_title": "Documentation Hub",
"favicon_url": "https://docs.example.com/favicon.ico"
}
},
{
"id": "c9d0e1f2-...",
"type": "audio_link",
"name": "Weekly recap",
"description": null,
"tags": {"series": "weekly"},
"folder_id": null,
"created_at": "2026-02-16T16:00:00Z",
"audio_link": {
"url": "https://open.spotify.com/episode/xyz",
"platform": "spotify",
"artist_or_author": "Team Alpha",
"thumbnail_url": "https://i.scdn.co/image/abc"
}
}
],
"meta": {
"page": 1,
"per_page": 50,
"total_count": 4,
"total_pages": 1
}
}
Contentable fields by type:
The contentable is serialized under a key matching the item's type:
| Type | Key | Fields |
|---|---|---|
file |
file |
filename, content_type, file_size |
web_link |
web_link |
url, fetched_title, favicon_url |
video_link |
video_link |
url, platform, video_id, thumbnail_url |
audio_link |
audio_link |
url, platform, artist_or_author, thumbnail_url |
GET /api/v1/content/:id
Returns the full details of a single content item, including pre-signed download URLs for files.
Path parameters:
| Parameter | Type | Description |
|---|---|---|
id |
UUID | Content item ID |
Example request:
curl -H "x-project-key: $PROJECT_KEY" \
-H "x-user-key: $USER_KEY" \
https://sutram.io/api/v1/content/d5e6f7a8-...
Response (file):
{
"item": {
"id": "d5e6f7a8-...",
"type": "file",
"name": "site-plan",
"description": null,
"tags": {"category": "planta", "year": "2024"},
"folder_id": "f1a2b3c4-...",
"folder_path": "/Reports/2024",
"created_at": "2026-02-19T10:00:00Z",
"file": {
"filename": "site-plan.pdf",
"content_type": "application/pdf",
"file_size": 2450000,
"version": 1,
"download_url": "https://storage.example.com/projects/.../site-plan.pdf?X-Amz-..."
}
}
}
Response (video link):
{
"item": {
"id": "e5f6a7b8-...",
"type": "video_link",
"name": "Site walkthrough",
"description": null,
"tags": {"topic": "neurologia"},
"folder_id": null,
"folder_path": "/",
"created_at": "2026-02-18T14:00:00Z",
"video_link": {
"url": "https://youtube.com/watch?v=abc",
"platform": "youtube",
"video_id": "abc",
"thumbnail_url": "https://img.youtube.com/vi/abc/hqdefault.jpg",
"duration_seconds": null,
"fetched_title": "Construction Site Alpha - Full Tour"
}
}
}
Response (audio link):
{
"item": {
"id": "c9d0e1f2-...",
"type": "audio_link",
"name": "Weekly recap",
"description": null,
"tags": {"series": "weekly"},
"folder_id": null,
"folder_path": "/",
"created_at": "2026-02-16T16:00:00Z",
"audio_link": {
"url": "https://open.spotify.com/episode/xyz",
"platform": "spotify",
"artist_or_author": "Team Alpha",
"thumbnail_url": "https://i.scdn.co/image/abc",
"duration_seconds": 1800,
"fetched_title": "Weekly Standup Recap - Feb 16"
}
}
}
Response (web link):
{
"item": {
"id": "a1b2c3d4-...",
"type": "web_link",
"name": "Documentation",
"description": "Project reference docs",
"tags": {},
"folder_id": null,
"folder_path": "/",
"created_at": "2026-02-17T09:00:00Z",
"web_link": {
"url": "https://docs.example.com",
"fetched_title": "Documentation Hub",
"fetched_description": "Complete project documentation",
"favicon_url": "https://docs.example.com/favicon.ico"
}
}
}
Differences between the detail and listing responses:
The /content/:id endpoint returns additional fields compared to the listing endpoint:
| Field | Listing (/content) |
Detail (/content/:id) |
|---|---|---|
folder_path |
No | Yes |
file.version |
No | Yes |
file.download_url |
No | Yes (pre-signed, temporary) |
web_link.fetched_description |
No | Yes |
video_link.duration_seconds |
No | Yes |
video_link.fetched_title |
No | Yes |
audio_link.duration_seconds |
No | Yes |
audio_link.fetched_title |
No | Yes |
Error responses:
| Status | Body | When |
|---|---|---|
| 404 | {"error": "Content item not found"} |
The item does not exist or belongs to another project |
GET /api/v1/document-classes/:id/documents
Lists the governed documents of a Document Class (a governance feature, Max plan). By default it returns one row per family — the latest version you can read — and honors need-to-know visibility: only documents in states you have permission to read are returned.
Path parameters:
| Parameter | Type | Description |
|---|---|---|
id |
UUID or string | The Document Class UID or the class's category_id (e.g., docs) |
Query parameters:
| Parameter | Syntax | Effect |
|---|---|---|
tag[key] |
tag[category]=getting-started&tag[lang]=PT |
Generic filter by any document metadata (e.g., category, lang). Multiple keys combine with AND. |
state / states |
state[]=draft&state[]=approved or state=draft,approved |
Filters by lifecycle state; multiple states = union (OR). Always intersected with the states you can read (need-to-know) — it never widens visibility. |
all_versions |
all_versions=true |
false (default) = one row per family (the latest readable version); true = one row per version. |
page / per_page |
page=2&per_page=100 |
Pagination. per_page defaults to 50, maximum 200. |
Readable states and default: without
state, the endpoint returns the latest version you can read of each document — which may be an unpublished draft. To consume only published content (e.g., a public site), filter withstate=approved.
Example request:
curl -H "x-project-key: $PROJECT_KEY" \
-H "x-user-key: $USER_KEY" \
"https://sutram.io/api/v1/document-classes/docs/documents?state=approved"
Response:
{
"category_id": "docs",
"category_name": "Documentation",
"documents": [
{
"document_id": "a6a01eb3-...",
"version_id": "117665e1-...",
"slug": "guia-de-inicio-rapido",
"name": "guia-de-inicio-rapido",
"lifecycle_state": "approved",
"state_label": "Approved",
"version_key": "1.38.a",
"metadata": {
"title": "Guia de Início Rápido",
"category": "getting-started",
"excerpt": "Comece a usar o Sutram em poucos minutos...",
"lang": "PT",
"published_at": "2026-03-16",
"sort_order": "10",
"translation_key": "quick-start-guide"
}
}
],
"page": 1,
"per_page": 50,
"total_count": 27,
"total_pages": 1
}
Fields per document:
| Field | Description |
|---|---|
document_id |
The document family ID (stable across versions) |
version_id |
ID of the returned version. It is also a folder ID — use GET /api/v1/folders/{version_id} to obtain the body file (e.g., the .md) |
slug / name |
Computed identity of the document |
lifecycle_state / state_label |
Lifecycle state (id and human-readable label) |
version_key |
Version label (e.g., 1.38.a) |
metadata |
Typed class metadata (title, category, excerpt, lang, sort_order, etc.) |
Getting a document's body:
A document's body (e.g., the .md) lives in the version folder. Combine this endpoint with the folder and content endpoints:
# 1. List the governed documents
curl ... "https://sutram.io/api/v1/document-classes/docs/documents?state=approved"
# 2. For each document, open the version folder by version_id
curl ... "https://sutram.io/api/v1/folders/117665e1-..."
# 3. Get the pre-signed download URL for the body file
curl ... "https://sutram.io/api/v1/content/{file_id}"
Error responses:
| Status | Body | When |
|---|---|---|
| 404 | {"error": "Document class not found"} |
The class does not exist or belongs to another project |
Content item tags
Tags are free-text key-value pairs attached to content items and folders. They enable structured metadata and advanced search features.
Tag constraints
| Constraint | Limit |
|---|---|
| Maximum tags per item | 50 |
| Maximum key length | 100 characters |
| Maximum value length | 500 characters |
| Key and value types | Strings only |
Managing tags
Content item tags are managed through the MCP Server tools:
| MCP tool | Description |
|---|---|
sutram_set_item_tags |
Set/replace all tags on a content item |
sutram_search_items |
Search items by tag (single condition or AND with multiple conditions) |
sutram_get_item_tag_keys |
List all distinct tag keys across items |
Querying tags via the REST API
Use the tag, tag_value, and tags query parameters on GET /api/v1/content:
# Find all items tagged with "category"
curl ... "https://sutram.io/api/v1/content?tag=category&all_folders=true"
# Find items where category contains "report"
curl ... "https://sutram.io/api/v1/content?tag=category&tag_value=report&all_folders=true"
# Find items matching multiple conditions (AND)
curl ... "https://sutram.io/api/v1/content?tags=[{\"key\":\"patient\",\"value\":\"joão\"},{\"key\":\"year\",\"value\":\"2024\"}]&all_folders=true"
Comparison: REST API vs. MCP Server
| Feature | REST API (/api/v1) |
MCP Server (/mcp) |
|---|---|---|
| Protocol | HTTP/JSON | JSON-RPC 2.0 (MCP) |
| Read content | Yes | Yes |
| Search by tags | Yes | Yes |
| List governed documents | Yes | Yes |
| Download URLs | Yes (pre-signed) | Yes (pre-signed) |
| Upload files | No | Yes |
| Create links | No | Yes |
| Create folders | No | Yes |
| Set tags | No | Yes |
| Move/rename/delete | No | Yes |
| Authentication | Same dual key | Same dual key |
| Best for | External apps, dashboards, scripts | AI assistants, automation |
Rate limits
The REST API enforces per-project and per-IP rate limits:
- Per project (per hour) — Determined by the project owner's plan (
api_rate_limit_per_hour). The default is 1,000 requests/hour, reaching 10,000/hour on higher plans. When exceeded, the API responds with HTTP 429 and the body{"error": "Rate limit exceeded", "retry_after": <seconds>}. - Limit headers — Successful responses include
x-ratelimit-limitandx-ratelimit-remaining; the 429 response includesretry-after(in seconds). - Per IP — An additional per-IP-address limit protects against abuse and brute force.
When you receive a 429, respect the retry-after header and apply backoff before retrying the request.
Versioning
The API is versioned by URL path (/api/v1). Breaking changes will be introduced in new versions (/api/v2). Non-breaking additions (new fields, new optional parameters) may be added to the current version without prior notice.
Examples
Fetch all video links of a project
curl -H "x-project-key: $PROJECT_KEY" \
-H "x-user-key: $USER_KEY" \
"https://sutram.io/api/v1/content?type=video_link&all_folders=true"
Fetch content tagged with a specific topic
curl -H "x-project-key: $PROJECT_KEY" \
-H "x-user-key: $USER_KEY" \
"https://sutram.io/api/v1/content?tag=topic&tag_value=neurologia&all_folders=true"
Navigate the folder hierarchy
# 1. List root folders
curl ... "https://sutram.io/api/v1/folders"
# 2. Open a specific folder
curl ... "https://sutram.io/api/v1/folders/f1a2b3c4-..."
# 3. Get download URL for a file
curl ... "https://sutram.io/api/v1/content/d5e6f7a8-..."
Paginate through all content
# Page 1
curl ... "https://sutram.io/api/v1/content?page=1&per_page=20&all_folders=true"
# Page 2
curl ... "https://sutram.io/api/v1/content?page=2&per_page=20&all_folders=true"
# Continue until page >= total_pages from meta
JavaScript integration (fetch)
const PROJECT_KEY = "sk_proj_...";
const USER_KEY = "sk_user_...";
const BASE_URL = "https://sutram.io/api/v1";
async function fetchContent(params = {}) {
const query = new URLSearchParams(params).toString();
const url = `${BASE_URL}/content${query ? "?" + query : ""}`;
const response = await fetch(url, {
headers: {
"x-project-key": PROJECT_KEY,
"x-user-key": USER_KEY,
},
});
if (!response.ok) {
const error = await response.json();
throw new Error(error.error);
}
return response.json();
}
// List all video links
const videos = await fetchContent({ type: "video_link", all_folders: "true" });
console.log(`Found ${videos.meta.total_count} videos`);
// Search by tag
const tagged = await fetchContent({
tag: "topic",
tag_value: "neurologia",
all_folders: "true",
});
Python integration (requests)
import requests
PROJECT_KEY = "sk_proj_..."
USER_KEY = "sk_user_..."
BASE_URL = "https://sutram.io/api/v1"
headers = {
"x-project-key": PROJECT_KEY,
"x-user-key": USER_KEY,
}
# List root folders
folders = requests.get(f"{BASE_URL}/folders", headers=headers).json()
# Get content in a folder
folder_id = folders["folders"][0]["id"]
content = requests.get(
f"{BASE_URL}/content",
headers=headers,
params={"folder_id": folder_id},
).json()
# Search by multiple tags
import json
tags = json.dumps([
{"key": "patient", "value": "joão"},
{"key": "year", "value": "2024"},
])
results = requests.get(
f"{BASE_URL}/content",
headers=headers,
params={"tags": tags, "all_folders": "true"},
).json()
print(f"Found {results['meta']['total_count']} items")
Troubleshooting
401 Unauthorized
- Check that both the
x-project-keyandx-user-keyheaders are present - Confirm that the keys have not been revoked
- Make sure you are an active member of the project (viewers have read access; only non-members receive 401)
404 Not Found
- Check that the folder/content ID belongs to the authenticated project
- Confirm that the item has not been deleted
429 Too Many Requests
- You have exceeded the project's per-hour rate limit (see Rate limits)
- Read the
retry-afterheader from the response and wait the indicated number of seconds before retrying - Track
x-ratelimit-remainingin responses to avoid hitting the limit - If the volume is legitimate, consider a plan with a larger quota
Empty results in tag search
- Add
all_folders=trueto search across all folders (without it, only root items are returned) - Tag search is case-insensitive — check for typos in key names
- Use
GET /api/v1/content?tag=YOUR_KEY&all_folders=truewithout a value first to verify that the tag key exists
tags parameter returns an error
- The
tagsparameter must be a valid JSON array:[{"key": "...", "value": "..."}] - URL-encode the JSON string when passing it as a query parameter
- Each condition must have a
keyfield;valueis optional