Coda
Access the Coda API with managed OAuth authentication. Manage docs, pages, tables, rows, formulas, and controls with full CRUD operations.
Quick Start
# List your docs
python <<'EOF'
import urllib.request, os, json
req = urllib.request.Request('https://gateway.maton.ai/coda/apis/v1/docs')
req.add_header('Authorization', f'Bearer {os.environ["MATON_API_KEY"]}')
print(json.dumps(json.load(urllib.request.urlopen(req)), indent=2))
EOF
Base URL
https://gateway.maton.ai/coda/apis/v1/{resource}
Replace {resource} with the actual Coda API endpoint path. The gateway proxies requests to coda.io/apis/v1 and automatically injects your OAuth token.
Authentication
All requests require the Maton API key in the Authorization header:
Authorization: Bearer $MATON_API_KEY
Environment Variable: Set your API key as MATON_API_KEY:
export MATON_API_KEY="YOUR_API_KEY"
Getting Your API Key
- Sign in or create an account at maton.ai
- Go to maton.ai/settings
- Copy your API key
Connection Management
Manage your Coda OAuth connections at https://ctrl.maton.ai.
List Connections
python <<'EOF'
import urllib.request, os, json
req = urllib.request.Request('https://ctrl.maton.ai/connections?app=coda&status=ACTIVE')
req.add_header('Authorization', f'Bearer {os.environ["MATON_API_KEY"]}')
print(json.dumps(json.load(urllib.request.urlopen(req)), indent=2))
EOF
Create Connection
python <<'EOF'
import urllib.request, os, json
data = json.dumps({'app': 'coda'}).encode()
req = urllib.request.Request('https://ctrl.maton.ai/connections', data=data, method='POST')
req.add_header('Authorization', f'Bearer {os.environ["MATON_API_KEY"]}')
req.add_header('Content-Type', 'application/json')
print(json.dumps(json.load(urllib.request.urlopen(req)), indent=2))
EOF
Get Connection
python <<'EOF'
import urllib.request, os, json
req = urllib.request.Request('https://ctrl.maton.ai/connections/{connection_id}')
req.add_header('Authorization', f'Bearer {os.environ["MATON_API_KEY"]}')
print(json.dumps(json.load(urllib.request.urlopen(req)), indent=2))
EOF
Response:
{
"connection": {
"connection_id": "f46d34b1-3735-478a-a0d7-54115a16cd46",
"status": "ACTIVE",
"creation_time": "2026-02-12T01:38:10.500238Z",
"last_updated_time": "2026-02-12T01:38:33.545353Z",
"url": "https://connect.maton.ai/?session_token=...",
"app": "coda",
"metadata": {}
}
}
Open the returned url in a browser to complete OAuth authorization.
Delete Connection
python <<'EOF'
import urllib.request, os, json
req = urllib.request.Request('https://ctrl.maton.ai/connections/{connection_id}', method='DELETE')
req.add_header('Authorization', f'Bearer {os.environ["MATON_API_KEY"]}')
print(json.dumps(json.load(urllib.request.urlopen(req)), indent=2))
EOF
Specifying Connection
If you have multiple Coda connections, specify which one to use with the Maton-Connection header:
python <<'EOF'
import urllib.request, os, json
req = urllib.request.Request('https://gateway.maton.ai/coda/apis/v1/docs')
req.add_header('Authorization', f'Bearer {os.environ["MATON_API_KEY"]}')
req.add_header('Maton-Connection', 'f46d34b1-3735-478a-a0d7-54115a16cd46')
print(json.dumps(json.load(urllib.request.urlopen(req)), indent=2))
EOF
If omitted, the gateway uses the default (oldest) active connection.
API Reference
Account
Get Current User
GET /coda/apis/v1/whoami
Returns information about the authenticated user.
Docs
List Docs
GET /coda/apis/v1/docs
Query parameters:
isOwner- Show only owned docs (true/false)query- Search querysourceDoc- Filter by source doc IDisStarred- Show only starred docsinGallery- Show only gallery docsworkspaceId- Filter by workspacefolderId- Filter by folderlimit- Page size (default: 25, max: 200)pageToken- Pagination token
Create Doc
POST /coda/apis/v1/docs
Content-Type: application/json
{
"title": "My New Doc",
"sourceDoc": "optional-source-doc-id",
"timezone": "America/Los_Angeles",
"folderId": "optional-folder-id"
}
Get Doc
GET /coda/apis/v1/docs/{docId}
Delete Doc
DELETE /coda/apis/v1/docs/{docId}
Pages
List Pages
GET /coda/apis/v1/docs/{docId}/pages
Query parameters:
limit- Page sizepageToken- Pagination token
Create Page
POST /coda/apis/v1/docs/{docId}/pages
Content-Type: application/json
{
"name": "New Page",
"subtitle": "Optional subtitle",
"parentPageId": "optional-parent-page-id"
}
Get Page
GET /coda/apis/v1/docs/{docId}/pages/{pageIdOrName}
Update Page
PUT /coda/apis/v1/docs/{docId}/pages/{pageIdOrName}
Content-Type: application/json
{
"name": "Updated Page Name",
"subtitle": "Updated subtitle"
}
Delete Page
DELETE /coda/apis/v1/docs/{docId}/pages/{pageIdOrName}
Tables
List Tables
GET /coda/apis/v1/docs/{docId}/tables
Query parameters:
limit- Page sizepageToken- Pagination tokensortBy- Sort by fieldtableTypes- Filter by table type
Get Table
GET /coda/apis/v1/docs/{docId}/tables/{tableIdOrName}
Columns
List Columns
GET /coda/apis/v1/docs/{docId}/tables/{tableIdOrName}/columns
Query parameters:
limit- Page sizepageToken- Pagination token
Get Column
GET /coda/apis/v1/docs/{docId}/tables/{tableIdOrName}/columns/{columnIdOrName}
Rows
List Rows
GET /coda/apis/v1/docs/{docId}/tables/{tableIdOrName}/rows
Query parameters:
query- Filter rows by search queryuseColumnNames- Use column names instead of IDs in response (true/false)valueFormat- Value format (simple, simpleWithArrays, rich)sortBy- Sort by columnlimit- Page sizepageToken- Pagination token
Get Row
GET /coda/apis/v1/docs/{docId}/tables/{tableIdOrName}/rows/{rowIdOrName}
Query parameters:
useColumnNames- Use column names instead of IDsvalueFormat- Value format
Insert/Upsert Rows
POST /coda/apis/v1/docs/{docId}/tables/{tableIdOrName}/rows
Content-Type: application/json
{
"rows": [
{
"cells": [
{"column": "Column Name", "value": "Cell Value"},
{"column": "Another Column", "value": 123}
]
}
],
"keyColumns": ["Column Name"]
}
- Use
keyColumnsfor upsert behavior (update if exists, insert if not) - Row inserts/upserts are processed asynchronously (returns requestId)
Update Row
PUT /coda/apis/v1/docs/{docId}/tables/{tableIdOrName}/rows/{rowIdOrName}
Content-Type: application/json
{
"row": {
"cells": [
{"column": "Column Name", "value": "Updated Value"}
]
}
}
Delete Row
DELETE /coda/apis/v1/docs/{docId}/tables/{tableIdOrName}/rows/{rowIdOrName}
Formulas
List Formulas
GET /coda/apis/v1/docs/{docId}/formulas
Get Formula
GET /coda/apis/v1/docs/{docId}/formulas/{formulaIdOrName}
Controls
List Controls
GET /coda/apis/v1/docs/{docId}/controls
Get Control
GET /coda/apis/v1/docs/{docId}/controls/{controlIdOrName}
Permissions
Get Sharing Metadata
GET /coda/apis/v1/docs/{docId}/acl/metadata
List Permissions
GET /coda/apis/v1/docs/{docId}/acl/permissions
Add Permission
POST /coda/apis/v1/docs/{docId}/acl/permissions
Content-Type: application/json
{
"access": "readonly",
"principal": {
"type": "email",
"email": "user@example.com"
}
}
Access values: readonly, write, comment
Delete Permission
DELETE /coda/apis/v1/docs/{docId}/acl/permissions/{permissionId}
Categories
List Categories
GET /coda/apis/v1/categories
Utilities
Resolve Browser Link
GET /coda/apis/v1/resolveBrowserLink?url={encodedUrl}
Converts a Coda browser URL to API resource information.
Get Mutation Status
GET /coda/apis/v1/mutationStatus/{requestId}
Check the status of an asynchronous mutation operation.
Analytics
List Doc Analytics
GET /coda/apis/v1/analytics/docs
Query parameters:
isPublished- Filter by published statussinceDate- Start date (YYYY-MM-DD)untilDate- End date (YYYY-MM-DD)limit- Page sizepageToken- Pagination token
List Pack Analytics
GET /coda/apis/v1/analytics/packs
Get Analytics Update Time
GET /coda/apis/v1/analytics/updated
Pagination
Coda uses cursor-based pagination with pageToken:
GET /coda/apis/v1/docs?limit=25
Response includes nextPageToken when more results exist:
{
"items": [...],
"href": "https://coda.io/apis/v1/docs?pageToken=...",
"nextPageToken": "eyJsaW1..."
}
Use the nextPageToken value as pageToken in subsequent requests.
Asynchronous Mutations
Create, update, and delete operations return HTTP 202 with a requestId:
{
"id": "canvas-abc123",
"requestId": "mutate:9f038510-be42-4d16-bccf-3468d38efd57"
}
Check mutation status:
GET /coda/apis/v1/mutationStatus/mutate:9f038510-be42-4d16-bccf-3468d38efd57
Response:
{
"completed": true
}
Mutations are generally processed within a few seconds.
Code Examples
JavaScript - List Docs
const response = await fetch(
'https://gateway.maton.ai/coda/apis/v1/docs?limit=10',
{
headers: {
'Authorization': `Bearer ${process.env.MATON_API_KEY}`
}
}
);
const data = await response.json();
console.log(data.items);
Python - List Docs
import os
import requests
response = requests.get(
'https://gateway.maton.ai/coda/apis/v1/docs',
headers={'Authorization': f'Bearer {os.environ["MATON_API_KEY"]}'},
params={'limit': 10}
)
data = response.json()
for doc in data['items']:
print(f"{doc['name']}: {doc['id']}")
Python - Create Doc and Page
import os
import requests
headers = {'Authorization': f'Bearer {os.environ["MATON_API_KEY"]}'}
base_url = 'https://gateway.maton.ai/coda/apis/v1'
# Create doc
doc_response = requests.post(
f'{base_url}/docs',
headers=headers,
json={'title': 'My New Doc'}
)
doc = doc_response.json()
print(f"Created doc: {doc['id']}")
# Create page
page_response = requests.post(
f'{base_url}/docs/{doc["id"]}/pages',
headers=headers,
json={'name': 'First Page', 'subtitle': 'Created via API'}
)
page = page_response.json()
print(f"Created page: {page['id']}")
Python - Insert Rows
import os
import requests
headers = {'Authorization': f'Bearer {os.environ["MATON_API_KEY"]}'}
response = requests.post(
'https://gateway.maton.ai/coda/apis/v1/docs/{docId}/tables/{tableId}/rows',
headers=headers,
json={
'rows': [
{
'cells': [
{'column': 'Name', 'value': 'John Doe'},
{'column': 'Email', 'value': 'john@example.com'}
]
}
]
}
)
result = response.json()
print(f"Request ID: {result['requestId']}")
Notes
- Doc IDs look like
s0ekj2vV-v - Page IDs start with
canvas- - Table and column names can be used instead of IDs
- Row operations require a base table (not views)
- Create/Update/Delete operations are asynchronous (return requestId)
- Newly created docs may take a moment to be accessible via API (409 error)
- Page-level analytics require Enterprise plan
- IMPORTANT: When using curl commands, use
curl -gwhen URLs contain brackets to disable glob parsing - IMPORTANT: When piping curl output to
jq, environment variables may not expand correctly. Use Python examples instead.
Rate Limits
| Operation | Limit |
|---|---|
| Reading data | 100 requests per 6 seconds |
| Writing data | 10 requests per 6 seconds |
| Writing doc content | 5 requests per 10 seconds |
| Listing docs | 4 requests per 6 seconds |
| Reading analytics | 100 requests per 6 seconds |
Error Handling
| Status | Meaning |
|---|---|
| 400 | Missing Coda connection or invalid request |
| 401 | Invalid or missing Maton API key |
| 404 | Resource not found |
| 409 | Doc not yet accessible (just created) |
| 429 | Rate limited |
| 4xx/5xx | Passthrough error from Coda API |
Troubleshooting: API Key Issues
- Check that the
MATON_API_KEYenvironment variable is set:
echo $MATON_API_KEY
- Verify the API key is valid by listing connections:
python <<'EOF'
import urllib.request, os, json
req = urllib.request.Request('https://ctrl.maton.ai/connections')
req.add_header('Authorization', f'Bearer {os.environ["MATON_API_KEY"]}')
print(json.dumps(json.load(urllib.request.urlopen(req)), indent=2))
EOF
Troubleshooting: Invalid App Name
- Ensure your URL path starts with
coda. For example:
- Correct:
https://gateway.maton.ai/coda/apis/v1/docs - Incorrect:
https://gateway.maton.ai/apis/v1/docs