Sitelet https://cdox.studio/guide/developers

Developers

Read and write your cDox data programmatically with the API.

Overview

The cDox API lets you read, create, update and delete your documents and sheets, and create and organize your 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 My Account > API key (your avatar in the top right of the dashboard).
  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 My Account > 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, PATCH, DELETE) 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 request that creates or grows content (POST, or a PATCH with bigger content) would push you over the quota, the API returns 403 QUOTA_EXCEEDED and changes nothing. 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
PATCH /documents/:idChange the fields you send; everything else is left alone
DELETE /documents/:idDelete a document, permanently
GET /sheetsList your sheets (paginated)
GET /sheets/:idA single sheet with its content
POST /sheetsCreate a sheet from CSV
PATCH /sheets/:idChange the fields you send; CSV content replaces the sheet
DELETE /sheets/:idDelete a sheet, permanently
GET /foldersYour folder tree
GET /folders/:idA single folder with its children
POST /foldersCreate a folder: name, optional parent_id (up to 5 levels deep)
PATCH /folders/:idRename or move a folder: name and/or parent_id
Tip

content is markdown for documents and CSV for sheets. What you read is what you can write back: a GET returns a document's markdown exactly as it was sent, and PATCHing it back changes nothing. Sheets answer with their JSON, which is accepted as-is on write; CSV replaces the whole sheet and cannot express tabs or formatting.

Note

Folders cannot be deleted through the API. Deleting a shared folder revokes every member's access at once, with nothing to confirm it: that one stays in the interface.

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, update or delete
  • 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 (Markdown)Single-item endpoint only. A few documents made by an early API version hold ProseMirror JSON instead; send it back as-is and it is kept.
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; folders nest at most 5 levels deep
owner_idnumber
document_countnumberItems directly inside this folder: documents, sheets and sub-folders
childrenarraySub-folders (the full tree on GET /folders, direct children on GET /folders/:id)
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, since 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 or that is shared with you. Defaults to root.
is_publicbooleanNoDefaults to false. If true, a public share link is created (Free accounts can have 3).
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_
  • Strikethrough: ~~strikethrough~~
  • Links: [texte](https://...)
  • Lists: - item or 1. item
  • Code: `inline` or fenced blocks with ```
  • Blockquotes: > quote
  • Horizontal rules: ---
  • Images: ![alt](url)
  • Tables (GFM pipe tables; the first row is the header): | A | B |

Not yet supported

  • Raw HTML (rendered as plain text)
  • Footnotes
  • Headings beyond H3 (they open in the editor as plain paragraphs)
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 or that is shared with you. Defaults to root.
is_publicbooleanNoDefaults to false. If true, a public share link is created (Free accounts can have 3).
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
  • Separator: comma, semicolon or tab, detected automatically
  • $1,200, 45% and 2026-03-31 become currency, percent and date; values with leading zeros (007) stay text
  • The created sheet has at least 100 rows × 26 cols; it expands if your data is larger
  • Up to 10,000 rows and columns A to ZZ; anything past that is left out

Not yet supported

  • Formulas cDox can't calculate yet (kept as the text you sent)
  • Multi-tab input in CSV (one tab per request; send the sheet JSON from a GET for tabs)
  • 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
400MAX_DEPTH_EXCEEDEDFolders nest at most 5 levels deep
400INVALID_OPERATIONA folder can't move inside itself
401UNAUTHORIZEDMissing or invalid API key
403FORBIDDENYou can open it but not do this (rename, move, publish, delete)
403PUBLIC_LINK_LIMITFree accounts can have 3 public links
403EMAIL_NOT_VERIFIEDVerify your email first
403QUOTA_EXCEEDEDStorage quota exceeded
404NOT_FOUNDResource doesn't exist or you don't have access to it
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);       // Markdown
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}` };

// Walk every page of the list, 100 sheets at a time
let page = 1;
let totalPages = 1;
do {
  const list = await fetch(
    `https://cdox.studio/api/v1/sheets?per_page=100&page=${page}`,
    { headers }
  ).then(r => r.json());
  totalPages = list.pagination.total_pages;

  // 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}`);
  }
  page++;
} while (page <= totalPages);

Mirror content from an external system

// Uses fetchWithRetry from the Rate limits section above
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) {
  // Waits out a 429 and retries this same article
  const res = await fetchWithRetry('https://cdox.studio/api/v1/documents', {
    method: 'POST',
    headers,
    body: JSON.stringify({ title: article.title, content: article.body })
  });

  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 (webhooks, anything else you need), don't hesitate to reach out.