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.

Note

This is an early-access API. The base URL is https://cdox.studio/api/v1/

Quickstart

Three steps to your first API call:

  1. Generate an API key in Settings > API Key.
  2. Confirm it works by hitting /me.
  3. 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:

PlanReads / minuteWrites / minute
Free10030
Pro300100

Writes (POST) have their own bucket, separate from reads.

Every response includes headers so you can track your usage:

HeaderDescription
X-RateLimit-LimitYour per-minute limit
X-RateLimit-RemainingRequests remaining in the window
X-RateLimit-ResetUnix 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

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):

PlanLimit
Free50 MB
Pro500 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.

Note

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.

EndpointDescription
GET /meYour user info (good for testing your key)
GET /documentsList your documents (paginated)
GET /documents/:idA single document with its content
POST /documentsCreate a document from markdown
GET /sheetsList your sheets (paginated)
GET /sheets/:idA single sheet with its content
POST /sheetsCreate a sheet from CSV
GET /foldersYour folder tree
GET /folders/:idA single folder with its children
Tip

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

ParameterDefaultDescription
page1Page number
per_page50Results per page (max 100)
sortupdated_atupdated_at or created_at
orderdescdesc or asc
folder_idFilter 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 read
  • 201 — Resource created
  • 4xx / 5xx — See the Errors section

User object fields

Returned by GET /me

FieldTypeDescription
idnumberInternal ID
emailstring
first_namestring | null
last_namestring | null
is_probooleanWhether the user is on the Pro plan
created_atstring (ISO 8601)

Document object fields

Returned by /documents and /documents/:id

FieldTypeDescription
idstringPublic ID (UUID). This is what you pass to endpoints
typestringAlways "page"
titlestring
folder_idnumber | null`null` if at the root
owner_idnumber
is_publicbooleanWhether a public share link exists
show_authorbooleanWhether the author name appears on the public page
sizenumberLength of the content field (characters)
contentstring (TipTap JSON)Single-item endpoint only
html_contentstringRendered HTML. Single-item endpoint only
created_atstring (ISO 8601)
updated_atstring (ISO 8601)

Sheet object fields

Returned by /sheets and /sheets/:id

FieldTypeDescription
idstringPublic ID (UUID)
typestringAlways "sheet"
titlestring
folder_idnumber | null
owner_idnumber
is_publicboolean
show_authorboolean
sizenumberLength of the content field (characters)
contentstring (JSON)Sheet structure (tabs, cells). Single-item endpoint only
created_atstring (ISO 8601)
updated_atstring (ISO 8601)

Folder object fields

Returned by /folders and /folders/:id

FieldTypeDescription
idnumberNumeric ID. This is what you pass as `folder_id`
namestring
parent_idnumber | null`null` for root-level folders
owner_idnumber
document_countnumberNumber of documents and sheets in this folder
childrenarraySub-folders (recursive)
created_atstring (ISO 8601)
updated_atstring (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

FieldTypeRequiredDescription
titlestringYesMax 255 characters
contentstring (markdown)NoMax 256 KB. Empty creates a blank document.
folder_idnumber | nullNoID of a folder you own. Defaults to root.
is_publicbooleanNoDefaults 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: - item or 1. item
  • Code: `inline` or fenced blocks with ```
  • Blockquotes: > quote
  • Horizontal rules: ---
  • Images: ![alt](url)

Not yet supported

  • Raw HTML (rendered as plain text)
  • Markdown tables
  • Footnotes
  • Headings beyond H3 (downgraded to H3)
  • Strikethrough
Note

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

FieldTypeRequiredDescription
titlestringYesMax 255 characters
contentstring (CSV)NoMax 256 KB. Empty creates a blank sheet.
folder_idnumber | nullNoID of a folder you own. Defaults to root.
is_publicbooleanNoDefaults 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)
Note

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"
  }
}
StatusCodeMeaning
400VALIDATION_ERRORInvalid request body or missing field
401UNAUTHORIZEDMissing or invalid API key
403EMAIL_NOT_VERIFIEDVerify your email first
403QUOTA_EXCEEDEDStorage quota exceeded
404NOT_FOUNDResource doesn't exist or isn't yours
413PAYLOAD_TOO_LARGEContent too large (max 256 KB)
429RATE_LIMITEDToo many requests, slow down
503WRITE_DISABLEDWrite 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 HTML

Back 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}`);
}
Tip

Add | python3 -m json.tool to the end of any curl command to pretty-print the JSON response.

Note

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.