API Reference
Complete REST API reference for CellCMS. All endpoints are under /api/v1/. Authentication is via Authorization: Bearer <token> (JWT or API token) unless noted otherwise.
Base URL
https://api.cellcms.com/api/v1
Rate Limiting
| Scope | Limit |
|---|---|
Auth endpoints (/auth/*) | 10 requests / minute |
| All other endpoints | 100 requests / minute |
Rate limit headers are included in every response:
X-RateLimit-Limit: 100
X-RateLimit-Remaining: 97
X-RateLimit-Reset: 1700000060
When exceeded, the API returns 429 Too Many Requests with a Retry-After header.
Error Responses
All errors follow this format:
{
"statusCode": 400,
"error": "Bad Request",
"message": "Detailed error description"
}
Common status codes:
| Code | Meaning |
|---|---|
| 400 | Invalid request body or parameters |
| 401 | Missing or invalid authentication |
| 403 | Insufficient permissions |
| 404 | Resource not found |
| 409 | Conflict (e.g., duplicate slug) |
| 429 | Rate limit exceeded |
| 500 | Internal server error |
Health
GET /health
Check API server health. No authentication required.
Response:
{
"status": "ok",
"timestamp": "2025-01-15T10:00:00.000Z",
"connections": 3
}
connections is the number of active WebSocket connections.
Authentication
POST /auth/login
Authenticate with email and password. Returns JWT tokens.
Rate limit: 10/minute
Request body:
{
"email": "admin@cellcms.com",
"password": "admin"
}
Response (200):
{
"accessToken": "eyJhbGciOiJIUzI1NiIs...",
"refreshToken": "a1b2c3d4e5f6...",
"user": {
"id": "uuid",
"email": "admin@cellcms.com",
"name": "Admin",
"role": "admin",
"created_at": "2025-01-01T00:00:00Z",
"updated_at": "2025-01-01T00:00:00Z"
}
}
Errors: 400 (missing fields), 401 (invalid credentials)
POST /auth/refresh
Exchange a refresh token for a new token pair. The old refresh token is invalidated.
Rate limit: 10/minute
Request body:
{
"refreshToken": "a1b2c3d4e5f6..."
}
Response (200):
{
"accessToken": "eyJhbGciOiJIUzI1NiIs...",
"refreshToken": "new-refresh-token...",
"user": { ... }
}
Errors: 400 (missing token), 401 (invalid or expired token)
POST /auth/logout
Revoke a refresh token.
Rate limit: 10/minute
Request body:
{
"refreshToken": "a1b2c3d4e5f6..."
}
Response (200):
{ "ok": true }
Projects
GET /projects
List all projects. Non-admin users only see projects they have access to.
Auth: Any authenticated user
Response (200):
[
{
"id": "uuid",
"name": "My Website",
"slug": "my-website",
"datasets": ["production", "staging"],
"locales": null,
"created_at": "2025-01-01T00:00:00Z",
"updated_at": "2025-01-01T00:00:00Z"
}
]
POST /projects
Create a new project.
Auth: Admin only
Request body:
{
"name": "My Website",
"slug": "my-website",
"datasets": ["production", "staging"]
}
| Field | Type | Required | Default |
|---|---|---|---|
name | string | Yes | — |
slug | string | Yes | — |
datasets | string[] | No | ["production"] |
Response (201):
{
"id": "uuid",
"name": "My Website",
"slug": "my-website",
"datasets": ["production", "staging"],
"created_at": "2025-01-01T00:00:00Z",
"updated_at": "2025-01-01T00:00:00Z"
}
Errors: 400 (missing fields), 409 (slug already exists)
GET /projects/:slug
Get a single project by slug.
Auth: Any authenticated user
Response (200): Project object
Errors: 404 (not found)
PUT /projects/:slug
Update a project.
Auth: Admin only
Request body (all fields optional):
{
"name": "Updated Name",
"datasets": ["production", "staging", "development"],
"locales": {
"default": "en",
"languages": [
{ "id": "en", "title": "English" },
{ "id": "nl", "title": "Dutch" }
]
}
}
Response (200): Updated project object
Errors: 404 (not found)
Users
GET /users
List all users.
Auth: Admin only
Response (200):
[
{
"id": "uuid",
"email": "admin@cellcms.com",
"name": "Admin",
"role": "admin",
"project_access": null,
"created_at": "2025-01-01T00:00:00Z",
"updated_at": "2025-01-01T00:00:00Z"
}
]
Password hashes are never returned.
POST /users
Create a new user.
Auth: Admin only
Request body:
{
"email": "editor@example.com",
"name": "Jane Editor",
"password": "secure-password",
"role": "editor"
}
| Field | Type | Required | Default |
|---|---|---|---|
email | string | Yes | — |
name | string | Yes | — |
password | string | Yes | — |
role | "admin" | "editor" | "viewer" | No | "editor" |
Response (201): SafeUser object
Errors: 400 (missing fields), 409 (email already exists)
GET /users/me
Get the currently authenticated user.
Auth: Any authenticated user
Response (200): SafeUser object
PUT /users/:id
Update a user. Non-admin users can only update their own profile (name, email, password).
Auth: Self or admin
Request body (all fields optional):
{
"name": "Updated Name",
"email": "new@example.com",
"password": "new-password",
"role": "admin",
"project_access": [
{ "projectId": "proj-uuid", "role": "editor" }
]
}
Only admins can change role and project_access.
Response (200): Updated SafeUser object
Errors: 403 (not admin and not self), 404 (not found)
Documents
POST /data/query/:dataset
Execute a GROQ query against a dataset.
Auth: Any authenticated user (viewer+)
Request body:
{
"query": "*[_type == \"post\" && publishedAt < now()] | order(publishedAt desc)[0...10]{title, slug, publishedAt}",
"params": {
"type": "post"
}
}
| Field | Type | Required | Description |
|---|---|---|---|
query | string | Yes | GROQ query string |
params | object | No | Named parameters ($param in query) |
Query parameter: ?language=en — resolve localized fields to the specified language.
Response (200):
{
"query": "*[_type == \"post\"]...",
"ms": 12,
"result": [
{ "title": "Hello World", "slug": { "current": "hello-world" } }
]
}
Errors: 400 (parse error, missing params), 500 (execution error)
GET /data/counts/:dataset
Get document counts per type.
Auth: Any authenticated user (viewer+)
Response (200):
{
"counts": {
"post": 42,
"author": 5,
"category": 12
}
}
Only includes published (non-draft) documents.
GET /data/doc/:dataset/:documentId
Get a single document by ID.
Auth: Any authenticated user (viewer+)
Query parameter: ?language=en — resolve localized fields.
Response (200):
{
"_id": "doc-uuid",
"_type": "post",
"_rev": "abc123",
"_createdAt": "2025-01-15T10:00:00Z",
"_updatedAt": "2025-01-15T12:00:00Z",
"title": "Hello World",
"slug": { "current": "hello-world" }
}
Errors: 404 (not found)
POST /data/mutate/:dataset
Create, update, or delete documents in a single atomic transaction.
Auth: Editor or above
Request body:
{
"mutations": [
{
"create": {
"_type": "post",
"title": "New Post",
"slug": { "current": "new-post" }
}
},
{
"patch": {
"id": "existing-doc-id",
"set": { "title": "Updated Title" }
}
},
{
"delete": {
"id": "doc-to-delete"
}
}
]
}
Mutation Types
create — Create a new document:
{
"create": {
"_type": "post",
"_id": "optional-custom-id",
"title": "Hello"
}
}
createOrReplace — Create or fully replace an existing document:
{
"createOrReplace": {
"_id": "my-doc-id",
"_type": "post",
"title": "Replaced content"
}
}
createIfNotExists — Create only if the ID doesn't exist:
{
"createIfNotExists": {
"_id": "my-doc-id",
"_type": "post",
"title": "Only created if new"
}
}
patch — Atomically modify fields on an existing document:
{
"patch": {
"id": "doc-id",
"ifRevisionID": "abc123",
"set": { "title": "Updated" },
"setIfMissing": { "views": 0 },
"unset": ["deprecated_field"],
"inc": { "views": 1 },
"dec": { "stock": 1 }
}
}
| Operation | Description |
|---|---|
set | Set fields to specific values |
setIfMissing | Set only if the field doesn't exist |
unset | Remove fields (array of dot-notation paths) |
inc | Increment numeric fields |
dec | Decrement numeric fields |
ifRevisionID | Optimistic concurrency — fail if revision doesn't match |
delete — Delete a document:
{
"delete": {
"id": "doc-id"
}
}
Response (200):
{
"transactionId": "txn-uuid",
"results": [
{ "id": "new-doc-id", "operation": "create" },
{ "id": "existing-doc-id", "operation": "update" },
{ "id": "doc-to-delete", "operation": "delete" }
]
}
Errors: 400 (validation errors), 404 (project not found)
POST /data/publish/:dataset/:documentId
Publish a draft document. Moves the draft (prefixed with drafts.) to the published version.
Auth: Editor or above
Response (200): Published document content
Errors: 404 (draft not found)
POST /data/unpublish/:dataset/:documentId
Unpublish a document. Converts the published version back to a draft.
Auth: Editor or above
Response (200): Draft document content
Errors: 404 (published document not found)
POST /data/schedule/:dataset/:documentId
Schedule a draft for automatic publishing at a future date.
Auth: Editor or above
Request body:
{
"scheduledAt": "2025-02-01T10:00:00Z"
}
Pass null to cancel a scheduled publish:
{
"scheduledAt": null
}
Response (200):
{
"documentId": "doc-id",
"scheduledAt": "2025-02-01T10:00:00Z"
}
Errors: 400 (invalid date), 404 (draft not found)
GET /data/history/:dataset/:documentId
Get revision history for a document.
Auth: Any authenticated user (viewer+)
Query parameters:
| Param | Type | Default | Max |
|---|---|---|---|
limit | number | 50 | 100 |
offset | number | 0 | — |
Response (200):
{
"documentId": "doc-id",
"revisions": [
{
"id": "rev-uuid",
"revision": 5,
"changed_by": "user-uuid",
"created_at": "2025-01-15T12:00:00Z"
}
]
}
GET /data/history/:dataset/:documentId/:revision
Get a specific revision's content.
Auth: Any authenticated user (viewer+)
Response (200): Revision object with full document content
Errors: 404 (document or revision not found)
POST /data/history/:dataset/:documentId/:revision/restore
Restore a document to a previous revision. Creates a new revision snapshot.
Auth: Editor or above
Response (200): Restored document content
Errors: 404 (document or revision not found)
Assets
POST /assets/:dataset
Upload an image or file.
Auth: Editor or above
Content-Type: multipart/form-data
Body: Form field file with the file data. Max size: 50MB.
curl -X POST https://api.cellcms.com/api/v1/assets/production \
-H "Authorization: Bearer YOUR_TOKEN" \
-F "file=@photo.jpg"
Response (201):
{
"_id": "asset-uuid",
"_type": "image",
"assetId": "asset-uuid",
"path": "project-id/production/asset-uuid.jpg",
"url": "/api/v1/assets/asset-uuid",
"metadata": {
"width": 1920,
"height": 1080,
"format": "jpeg",
"hasAlpha": false,
"palette": { "dominant": "#2b5797", "vibrant": "#ff6600" }
}
}
Errors: 400 (no file), 404 (project not found)
GET /assets/:id
Get an asset by ID. Supports on-the-fly image transforms.
Auth: Any authenticated user
Query parameters (images only):
| Param | Type | Description |
|---|---|---|
w | number | Width in pixels |
h | number | Height in pixels |
fit | string | cover, contain, or fill |
format | string | Output format (webp, avif, png, jpeg) |
q | number | Quality (1–100) |
blur | number | Blur radius |
dpr | number | Device pixel ratio multiplier |
Examples:
GET /api/v1/assets/abc123?w=400&h=300&fit=cover&format=webp&q=80
GET /api/v1/assets/abc123?w=100&blur=10
GET /api/v1/assets/abc123?w=800&dpr=2
Response: Binary file data with appropriate Content-Type header.
Caching: Transformed images are cached. Response includes ETag and Cache-Control: public, max-age=31536000, immutable headers. Returns 304 Not Modified when appropriate.
Errors: 404 (asset not found)
GET /assets/:dataset/list
List assets in a dataset.
Auth: Any authenticated user
Query parameters:
| Param | Type | Default | Description |
|---|---|---|---|
type | string | — | Filter by type (image or file) |
limit | number | 50 | Max items to return |
offset | number | 0 | Pagination offset |
Response (200):
{
"items": [
{
"id": "asset-uuid",
"type": "image",
"filename": "photo.jpg",
"mime_type": "image/jpeg",
"size": 245760,
"url": "/api/v1/assets/asset-uuid",
"metadata": { "width": 1920, "height": 1080 },
"created_at": "2025-01-15T10:00:00Z"
}
],
"total": 128
}
DELETE /assets/:dataset/:id
Delete an asset.
Auth: Editor or above
Response (200):
{ "deleted": true, "id": "asset-uuid" }
Errors: 404 (not found)
Webhooks
GET /webhooks/:project
List all webhooks for a project.
Auth: Admin only
Response (200):
[
{
"id": "webhook-uuid",
"name": "Deploy trigger",
"url": "https://api.vercel.com/deploy/hook/...",
"events": ["document.publish"],
"dataset": "production",
"active": true
}
]
Secrets are not included in the response.
POST /webhooks/:project
Create a webhook.
Auth: Admin only
Request body:
{
"name": "Deploy trigger",
"url": "https://api.vercel.com/deploy/hook/...",
"events": ["document.publish", "document.unpublish"],
"dataset": "production",
"secret": "optional-custom-secret",
"active": true
}
| Field | Type | Required | Default |
|---|---|---|---|
name | string | Yes | — |
url | string | Yes | — |
events | string[] | Yes | — |
dataset | string | Yes | — |
secret | string | No | Auto-generated (32 bytes hex) |
active | boolean | No | true |
Valid events: document.create, document.update, document.delete, document.publish, document.unpublish
Response (201): Webhook object
Errors: 400 (missing fields, invalid events), 409 (project not found)
PUT /webhooks/:project/:id
Update a webhook.
Auth: Admin only
Request body: Any of the creation fields (all optional).
Response (200): Updated webhook object
Errors: 400 (invalid events), 404 (not found)
DELETE /webhooks/:project/:id
Delete a webhook and all its delivery logs.
Auth: Admin only
Response (200):
{ "deleted": true, "id": "webhook-uuid" }
Errors: 404 (not found)
GET /webhooks/:project/:id/deliveries
View delivery history for a webhook.
Auth: Admin only
Query parameters:
| Param | Type | Default | Max |
|---|---|---|---|
limit | number | 50 | 100 |
Response (200):
[
{
"id": "delivery-uuid",
"event": "document.publish",
"payload": { "documentId": "...", "type": "post" },
"status_code": 200,
"response_body": "OK",
"attempts": 1,
"success": true,
"created_at": "2025-01-15T10:00:00Z"
}
]
API Tokens
GET /tokens/:project
List all API tokens for a project (without raw token values).
Auth: Admin only
Response (200):
[
{
"id": "token-uuid",
"name": "Frontend read-only",
"permissions": ["read"],
"dataset": "production",
"last_used_at": "2025-01-15T12:00:00Z",
"created_at": "2025-01-15T10:00:00Z"
}
]
POST /tokens/:project
Create an API token. The raw token is only returned once.
Auth: Admin only
Request body:
{
"name": "Frontend read-only",
"permissions": ["read"],
"dataset": "production"
}
| Field | Type | Required | Default |
|---|---|---|---|
name | string | Yes | — |
permissions | string[] | No | ["read"] |
dataset | string | No | null (all datasets) |
Valid permissions: read, write
Response (201):
{
"id": "token-uuid",
"name": "Frontend read-only",
"permissions": ["read"],
"dataset": "production",
"created_at": "2025-01-15T10:00:00Z",
"token": "cell_a1b2c3d4e5f6..."
}
Errors: 400 (invalid name or permissions), 404 (project not found)
DELETE /tokens/:project/:id
Revoke an API token.
Auth: Admin only
Response (200):
{ "deleted": true, "id": "token-uuid" }
Errors: 404 (not found)
Data Migration
GET /data/export/:dataset
Export all documents as NDJSON (newline-delimited JSON).
Auth: Viewer or above
Query parameters:
| Param | Type | Description |
|---|---|---|
types | string | Comma-separated list of types to export |
Response: Streaming NDJSON. Each line is a complete JSON document.
Content-Type: application/x-ndjson
Content-Disposition: attachment; filename="production-export.ndjson"
{"_id":"doc1","_type":"post","title":"Hello World","_createdAt":"2025-01-15T10:00:00Z"}
{"_id":"doc2","_type":"author","name":"Jane","_createdAt":"2025-01-14T09:00:00Z"}
POST /data/import/:dataset
Import documents from NDJSON format. Compatible with Sanity export format.
Auth: Admin only
Content-Type: application/x-ndjson or application/json
Query parameters:
| Param | Type | Default | Description |
|---|---|---|---|
replace | string | — | Set to "true" to replace existing documents |
Request body: NDJSON text (one document per line).
Response (200):
{
"total": 150,
"created": 142,
"replaced": 0,
"skipped": 8,
"errors": [
{ "line": 45, "error": "Invalid document format" }
]
}
Behavior:
- Sanity system documents (starting with
system.or_.) are skipped - Without
replace=true: existing documents are skipped - With
replace=true: existing documents are overwritten - Preserves
_createdAtif present in the source - Runs in a single atomic transaction
Schema
GET /schema/:project
Get the compiled schema for a project.
Auth: Any authenticated user
Response (200): Serialized schema with all document types and field definitions.
Note: This endpoint is only available if the server was started with schema types configured.
Related Documentation
- Authentication — Auth flows, tokens, roles
- GROQ Reference — Query language details
- Client SDK — Programmatic API access
- Webhooks — Webhook integration guide
- Assets & Images — Asset pipeline details