Developers
Read and write your cDox data programmatically with the API.
Overview
The cDox API lets you read and create your documents, sheets, and folders programmatically. It's great for backups, integrations, or building custom tools around your data.
This is our first public release and things may evolve, but we're committed to not breaking things without notice.
This is an early-access API. The base URL is https://cdox.studio/api/v1/
Quickstart
Three steps to your first API call:
- Generate an API key in Settings > API Key.
- Confirm it works by hitting
/me. - Create your first document.
const KEY = 'cdox_your_key_here';
const headers = { 'Authorization': `Bearer ${KEY}`, 'Content-Type': 'application/json' };
// 1. Confirm your key works
const me = await fetch('https://cdox.studio/api/v1/me', { headers }).then(r => r.json());
console.log('Hello,', me.data.first_name);
// 2. Create your first document
const created = await fetch('https://cdox.studio/api/v1/documents', {
method: 'POST',
headers,
body: JSON.stringify({
title: 'Hello from the API',
content: '# It works\n\nWith **markdown** too.'
})
}).then(r => r.json());
console.log('Created:', created.data.id);If the second request returns an object with an id, you're set. The document will show up in your cDox list right away.
Getting your API key
You can generate an API key in Settings > API Key. The key starts with cdox_ and is only shown once. Copy it and keep it somewhere safe. Treat it like a password.
If you lose your key, you can regenerate a new one. This will immediately invalidate the old one.
Authentication
Send your key in the Authorization header on every request:
const response = await fetch('https://cdox.studio/api/v1/me', {
headers: {
'Authorization': 'Bearer cdox_your_key_here'
}
});
const { data } = await response.json();
console.log(data);If the key is valid, you'll get your user info back. If not, you'll get a 401 error. Your email must also be verified, or you'll get a 403.
Rate limits
The API is rate-limited to protect the service:
| Plan | Reads / minute | Writes / minute |
|---|---|---|
| Free | 100 | 30 |
| Pro | 300 | 100 |
Writes (POST) have their own bucket, separate from reads.
Every response includes headers so you can track your usage:
| Header | Description |
|---|---|
X-RateLimit-Limit | Your per-minute limit |
X-RateLimit-Remaining | Requests remaining in the window |
X-RateLimit-Reset | Unix timestamp when the window resets |
If you go over the limit, you'll get a 429 Too Many Requests response with a Retry-After header telling you how many seconds to wait.
Retry example
async function fetchWithRetry(url, options, maxRetries = 3) {
for (let attempt = 0; attempt <= maxRetries; attempt++) {
const res = await fetch(url, options);
if (res.status !== 429) return res;
const wait = parseInt(res.headers.get('Retry-After') ?? '60', 10);
if (attempt === maxRetries) return res;
await new Promise(r => setTimeout(r, wait * 1000));
}
}Pro users get 3x the reads and over 3x the writes per minute.
Storage and quotas
Every account has a storage limit that applies to your stored data, regardless of how it was created (UI or API):
| Plan | Limit |
|---|---|
| Free | 50 MB |
| Pro | 500 MB |
The total includes:
- Document content (converted markdown and rendered HTML)
- Sheet content (JSON cells)
- Uploaded images
If a POST request would push you over the quota, the API returns 403 QUOTA_EXCEEDED without creating the resource. Free up space by deleting items in the UI, or upgrade to Pro.
Documents are typically a few kilobytes each. The 50 MB limit is enough for thousands of typical documents.
Endpoints
All URLs start with https://cdox.studio/api/v1.
| Endpoint | Description |
|---|---|
GET /me | Your user info (good for testing your key) |
GET /documents | List your documents (paginated) |
GET /documents/:id | A single document with its content |
POST /documents | Create a document from markdown |
GET /sheets | List your sheets (paginated) |
GET /sheets/:id | A single sheet with its content |
POST /sheets | Create a sheet from CSV |
GET /folders | Your folder tree |
GET /folders/:id | A single folder with its children |
The list endpoints (/documents, /sheets) don't include content to keep responses light. Use the single-item endpoint to get the full content.
Query parameters for lists
| Parameter | Default | Description |
|---|---|---|
page | 1 | Page number |
per_page | 50 | Results per page (max 100) |
sort | updated_at | updated_at or created_at |
order | desc | desc or asc |
folder_id | Filter by folder (use `null` for root) |
Response format
All successful responses have the same shape: a wrapper object with a data key. List endpoints add a pagination key.
HTTP statuses
200— Successful read201— Resource created- 4xx / 5xx — See the Errors section
User object fields
Returned by GET /me
| Field | Type | Description |
|---|---|---|
id | number | Internal ID |
email | string | |
first_name | string | null | |
last_name | string | null | |
is_pro | boolean | Whether the user is on the Pro plan |
created_at | string (ISO 8601) |
Document object fields
Returned by /documents and /documents/:id
| Field | Type | Description |
|---|---|---|
id | string | Public ID (UUID). This is what you pass to endpoints |
type | string | Always "page" |
title | string | |
folder_id | number | null | `null` if at the root |
owner_id | number | |
is_public | boolean | Whether a public share link exists |
show_author | boolean | Whether the author name appears on the public page |
size | number | Length of the content field (characters) |
content | string (TipTap JSON) | Single-item endpoint only |
html_content | string | Rendered HTML. Single-item endpoint only |
created_at | string (ISO 8601) | |
updated_at | string (ISO 8601) |
Sheet object fields
Returned by /sheets and /sheets/:id
| Field | Type | Description |
|---|---|---|
id | string | Public ID (UUID) |
type | string | Always "sheet" |
title | string | |
folder_id | number | null | |
owner_id | number | |
is_public | boolean | |
show_author | boolean | |
size | number | Length of the content field (characters) |
content | string (JSON) | Sheet structure (tabs, cells). Single-item endpoint only |
created_at | string (ISO 8601) | |
updated_at | string (ISO 8601) |
Folder object fields
Returned by /folders and /folders/:id
| Field | Type | Description |
|---|---|---|
id | number | Numeric ID. This is what you pass as `folder_id` |
name | string | |
parent_id | number | null | `null` for root-level folders |
owner_id | number | |
document_count | number | Number of documents and sheets in this folder |
children | array | Sub-folders (recursive) |
created_at | string (ISO 8601) | |
updated_at | string (ISO 8601) |
Pagination
List endpoints return a pagination object with every response:
{
"data": [ ... ],
"pagination": {
"page": 1,
"per_page": 50,
"total": 127,
"total_pages": 3
}
} Single-item endpoints return one object in data:
{
"data": {
"id": "abc-123",
"type": "page",
"title": "My Document",
"content": "...",
...
}
}Folders
To create a document or sheet inside a specific folder, you need the folder's numeric id. Fetch your tree with GET /folders:
const tree = await fetch('https://cdox.studio/api/v1/folders', {
headers: { 'Authorization': 'Bearer cdox_your_key_here' }
}).then(r => r.json());
// Find a folder by name (top level)
const projects = tree.data.find(f => f.name === 'Projects');
console.log(projects.id); // → 42
// Pass it when creating
await fetch('https://cdox.studio/api/v1/documents', {
method: 'POST',
headers: {
'Authorization': 'Bearer cdox_your_key_here',
'Content-Type': 'application/json'
},
body: JSON.stringify({
title: 'New doc',
content: 'Hello',
folder_id: projects.id
})
});The tree is recursive: each folder has a children array of sub-folders. Walk the tree to find a nested folder.
If you omit folder_id or pass null, the new resource lands at the root.
Creating documents
Send a POST /documents with a title and markdown content. The content can be plain text with no formatting at all — any text is valid markdown. Sprinkle in markdown markers (#, **, -, etc.) if you want structure.
Request body
| Field | Type | Required | Description |
|---|---|---|---|
title | string | Yes | Max 255 characters |
content | string (markdown) | No | Max 256 KB. Empty creates a blank document. |
folder_id | number | null | No | ID of a folder you own. Defaults to root. |
is_public | boolean | No | Defaults to false. If true, a public share link is created. |
const response = await fetch('https://cdox.studio/api/v1/documents', {
method: 'POST',
headers: {
'Authorization': 'Bearer cdox_your_key_here',
'Content-Type': 'application/json'
},
body: JSON.stringify({
title: 'Meeting notes',
content: '# Agenda\n\n- Roadmap\n- **Q3 priorities**\n- Open questions'
})
});
const { data } = await response.json();
console.log(data.id);Supported markdown
The following markers are recognized and converted to editable elements:
- Headings:
# H1,## H2,### H3 - Bold and italic:
**gras**,_italique_ - Links:
[texte](https://...) - Lists:
- itemor1. item - Code:
`inline`or fenced blocks with``` - Blockquotes:
> quote - Horizontal rules:
--- - Images:

Not yet supported
- Raw HTML (rendered as plain text)
- Markdown tables
- Footnotes
- Headings beyond H3 (downgraded to H3)
- Strikethrough
The API isn't idempotent. Sending the same request twice creates two different documents. If you need deduplication, track your own IDs client-side and check before sending.
Creating sheets
Send a POST /sheets with a title and CSV content. A single value is valid. A full grid with headers is valid. Empty cells are skipped, numbers are auto-detected, and quoted values can contain commas and newlines.
Request body
| Field | Type | Required | Description |
|---|---|---|---|
title | string | Yes | Max 255 characters |
content | string (CSV) | No | Max 256 KB. Empty creates a blank sheet. |
folder_id | number | null | No | ID of a folder you own. Defaults to root. |
is_public | boolean | No | Defaults to false |
const csv = `name,email,signups
Alice,alice@x.com,42
Bob,bob@x.com,17`;
const response = await fetch('https://cdox.studio/api/v1/sheets', {
method: 'POST',
headers: {
'Authorization': 'Bearer cdox_your_key_here',
'Content-Type': 'application/json'
},
body: JSON.stringify({ title: 'Weekly signups', content: csv })
});
const { data } = await response.json();
console.log(data.id);CSV format
- First row = headers (row 1, refs A1, B1, etc.)
- Empty cells: skipped (sparse storage)
- Numbers: auto-detected and stored as numbers
- Commas in values: use quotes
"a, b" - Quotes in values: double them
"" - Newlines in values: yes, inside quotes
- The created sheet has at least 100 rows × 26 cols; it expands if your data is larger
Not yet supported
- Formulas (values starting with `=` are stored as text)
- Multi-tab input (one tab per request)
- Cell formatting (colors, alignment, borders)
Like documents, the API isn't idempotent. Sending the same CSV twice creates two different sheets.
Errors
Errors follow a consistent format:
{
"error": {
"code": "NOT_FOUND",
"message": "Document not found"
}
} | Status | Code | Meaning |
|---|---|---|
400 | VALIDATION_ERROR | Invalid request body or missing field |
401 | UNAUTHORIZED | Missing or invalid API key |
403 | EMAIL_NOT_VERIFIED | Verify your email first |
403 | QUOTA_EXCEEDED | Storage quota exceeded |
404 | NOT_FOUND | Resource doesn't exist or isn't yours |
413 | PAYLOAD_TOO_LARGE | Content too large (max 256 KB) |
429 | RATE_LIMITED | Too many requests, slow down |
503 | WRITE_DISABLED | Write API is temporarily disabled |
Versioning
The API is versioned in the URL (/api/v1/). As long as you stay on v1, we won't make breaking changes without notice.
Stable
- URL shape and HTTP methods
- Response shape (
data,pagination,error) - Fields documented above
- Error codes
- Bearer key authentication
May evolve
- New fields added (additive; ignore what you don't recognize)
- New endpoints
- Supported markdown subset (can grow, never shrink)
- Rate limits (can be relaxed, never tightened without notice)
Examples
List your recent documents
const response = await fetch(
'https://cdox.studio/api/v1/documents?per_page=10',
{ headers: { 'Authorization': 'Bearer cdox_your_key_here' } }
);
const { data, pagination } = await response.json();
console.log(`Page ${pagination.page} of ${pagination.total_pages}`);
data.forEach(doc => console.log(doc.title));Get a document with its content
const response = await fetch(
'https://cdox.studio/api/v1/documents/abc-123-def',
{ headers: { 'Authorization': 'Bearer cdox_your_key_here' } }
);
const { data } = await response.json();
console.log(data.title);
console.log(data.content); // raw editor JSON
console.log(data.html_content); // rendered HTMLBack up all your sheets
const API_KEY = 'cdox_your_key_here';
const headers = { 'Authorization': `Bearer ${API_KEY}` };
// Fetch all sheets
const list = await fetch(
'https://cdox.studio/api/v1/sheets?per_page=100',
{ headers }
).then(r => r.json());
// Download each sheet's content
for (const sheet of list.data) {
const full = await fetch(
`https://cdox.studio/api/v1/sheets/${sheet.id}`,
{ headers }
).then(r => r.json());
console.log(`Backed up: ${full.data.title}`);
}Mirror content from an external system
const API_KEY = 'cdox_your_key_here';
const headers = { 'Authorization': `Bearer ${API_KEY}`, 'Content-Type': 'application/json' };
// Pretend these came from your CMS / DB / LLM
const articles = [
{ title: 'Welcome', body: '# Hello world\n\nThis is **markdown**.' },
{ title: 'Pricing', body: '# Pricing\n\n- Free: $0\n- Pro: $8/mo' }
];
for (const article of articles) {
const res = await fetch('https://cdox.studio/api/v1/documents', {
method: 'POST',
headers,
body: JSON.stringify({ title: article.title, content: article.body })
});
if (res.status === 429) {
const wait = parseInt(res.headers.get('Retry-After') ?? '60', 10);
await new Promise(r => setTimeout(r, wait * 1000));
continue;
}
if (!res.ok) {
const err = await res.json();
console.error(`Failed: ${article.title} — ${err.error.code}`);
continue;
}
const { data } = await res.json();
console.log(`Created: ${data.id} — ${article.title}`);
}Add | python3 -m json.tool to the end of any curl command to pretty-print the JSON response.
The API is in early access and will evolve over time. If you have feedback or requests (updating documents, deleting, webhooks), don't hesitate to reach out.