CDN API
Public API · Enterprise Edition
The CDN delivery endpoint is part of Studio's public, versioned, key-gated API (/api/cdn/v1/) — a stable contract intended for external integrations. CDN is an Enterprise-Edition feature (Starter+ on the managed service; not available in a self-hosted Community deployment).
Studio provides a built-in CDN for delivering content as JSON via API keys. Content is built from Git and stored in Cloudflare R2 object storage.
CDN Settings
Get CDN Settings
GET /api/workspaces/:workspaceId/projects/:projectId/cdn/settingsResponse
{
"cdn_enabled": true,
"cdn_branch": "main"
}Update CDN Settings
PATCH /api/workspaces/:workspaceId/projects/:projectId/cdn/settings| Field | Type | Required | Description |
|---|---|---|---|
cdn_enabled | boolean | No | Enable/disable CDN delivery |
cdn_branch | string | null | No | Branch the CDN builds from (defaults to the project's default branch) |
Returns the updated { cdn_enabled, cdn_branch }.
Builds
Trigger Build
Trigger a full rebuild of CDN content from the repository. This endpoint claims the project's single in-flight build slot and returns 409 (cdn.build_in_progress) if a build is already running.
POST /api/workspaces/:workspaceId/projects/:projectId/cdn/builds/triggerResponse — SSE progress stream
The response is a Server-Sent Events stream, not a single JSON body. Each event is a JSON-encoded progress update with a phase and message (and optional current/total counters):
{ "phase": "upload", "message": "Uploading manifest...", "current": 0, "total": 5 }The stream ends with a terminating complete event carrying the build result:
{
"phase": "complete",
"message": "Build complete — 12 files in 4200ms",
"result": {
"buildId": "uuid",
"filesUploaded": 12,
"totalSizeBytes": 52428800,
"durationMs": 4200,
"error": null
}
}If the build throws, an { "phase": "error", "message": "..." } event is emitted instead. The build process reads content from Git, serializes it to JSON, and uploads to R2. Stale objects are cleaned up via diff-based comparison.
List Builds
GET /api/workspaces/:workspaceId/projects/:projectId/cdn/buildsResponse
[
{
"id": "uuid",
"status": "completed",
"trigger_type": "manual",
"commit_sha": "abc123",
"started_at": "2026-01-15T12:00:00Z",
"completed_at": "2026-01-15T12:00:05Z"
}
]API Keys
List CDN Keys
GET /api/workspaces/:workspaceId/projects/:projectId/cdn/keysResponse
[
{
"id": "uuid",
"name": "Production Key",
"key_prefix": "crn_live_7K2mX9p",
"last_used_at": "2026-01-15T12:00:00Z",
"created_at": "2026-01-01T00:00:00Z"
}
]Create CDN Key
POST /api/workspaces/:workspaceId/projects/:projectId/cdn/keys| Field | Type | Required | Description |
|---|---|---|---|
name | string | Yes | Key display name |
scopes | string[] | No | Scopes the key holds. Valid values: delivery, media:read, media:write. Defaults to ['delivery']. Unknown scope strings are rejected with 400. |
Response
{
"id": "uuid",
"name": "Production Key",
"key": "crn_live_7K2mX9pQ4rT6vW3nY8sB1hJ5...",
"key_prefix": "crn_live_7K2mX9p"
}WARNING
The full API key is only returned once at creation time. Studio stores only the SHA-256 hash; the key_prefix (first 16 characters) is kept for display. Store the full key securely.
Revoke CDN Key
DELETE /api/workspaces/:workspaceId/projects/:projectId/cdn/keys/:keyIdPlan Limits
| Plan | API Keys | Bandwidth/Month |
|---|---|---|
| Starter | 3 | 2 GB |
| Pro | 10 | 20 GB |
| Enterprise | Unlimited | Unlimited |
Content Delivery (Public)
Fetch content through the CDN. Content JSON and manifests are authenticated via API key in the Authorization header. Media binaries (paths under media/) are the one exception: when the project has public media enabled they are served keyless (rate-limited per project + client IP) so a browser <img src> can load them.
GET /api/cdn/v1/:projectId/:pathHeaders
Authorization: Bearer crn_live_your-api-keyObject Paths
CDN objects are keyed by model ID (not domain) — the build pipeline writes them flat under content/, meta/, documents/, and models/:
| Path | Returns |
|---|---|
content/{modelId}/{locale}.json | Published entries for an i18n collection/singleton/dictionary, by locale (object map keyed by entry ID) |
content/{modelId}/data.json | Published entries for a non-i18n model |
meta/{modelId}/{locale}.json | Per-entry metadata for an i18n model, by locale (data.json for non-i18n) |
documents/{modelId}/{slug}/{locale}.json | A single document entry ({ frontmatter, body, html }) |
documents/{modelId}/_index/{locale}.json | Frontmatter index for a document model, by locale |
models/{modelId}.json | A model definition |
models/_index.json | Summary index of all models |
_manifest.json | Build manifest (commit SHA, locales, domains, model list) |
_media_manifest.json | Media manifest — asset metadata (dimensions, blurhash, alt text) |
_bundle/{locale}.json | Per-locale bundle — all JSON content for one locale in a single fetch (see below) |
media/{...} | Media binaries; served keyless when public media is enabled |
Locale bundle (_bundle/{locale}.json)
Emitted on every build, a bundle packs all of a locale's JSON content into a single keyed, ETag'd object so a client can prime its per-path cache with one conditional fetch instead of one request per model. Its shape:
{
"version": "1",
"commitSha": "abc123...",
"builtAt": "2026-07-10T12:00:00.000Z",
"locale": "en",
"paths": {
"content/authors/en.json": { "a1b2c3d4e5f6": { "name": "Jane" } },
"content/site-settings/data.json": { "title": "My Site" },
"documents/articles/_index/en.json": []
}
}paths is keyed by the exact delivery paths listed above, and each body is identical to the standalone artifact. All JSON-kind models ship their full body (non-i18n models under content/{modelId}/data.json in every locale bundle); document models contribute only their _index (per-slug bodies and meta/... are not bundled). Consumers must ignore a bundle whose version is not "1" and fall back to per-path fetches.
Examples
# Get a collection (English locale)
curl https://studio.example.com/api/cdn/v1/{projectId}/content/blog-post/en.json \
-H "Authorization: Bearer crn_live_..."
# Get a singleton (English locale)
curl https://studio.example.com/api/cdn/v1/{projectId}/content/hero/en.json \
-H "Authorization: Bearer crn_live_..."
# Get model definition
curl https://studio.example.com/api/cdn/v1/{projectId}/models/blog-post.json \
-H "Authorization: Bearer crn_live_..."Response
Returns the raw JSON content file from R2 storage with appropriate Content-Type and ETag headers.
Rate Limiting
Keyed CDN requests are rate-limited per API key; keyless public-media requests are rate-limited per project + client IP. Usage (request count and bandwidth) is metered per project — keyless public-media bandwidth counts toward the project's totals under a NULL-key bucket.
Related Pages
- Self-Hosting -- R2 storage configuration
- Environment Variables -- CDN-related variables
- Enterprise Edition -- CDN provider implementation