Skip to content

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/settings

Response

json
{
  "cdn_enabled": true,
  "cdn_branch": "main"
}

Update CDN Settings

PATCH /api/workspaces/:workspaceId/projects/:projectId/cdn/settings
FieldTypeRequiredDescription
cdn_enabledbooleanNoEnable/disable CDN delivery
cdn_branchstring | nullNoBranch 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/trigger

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

json
{ "phase": "upload", "message": "Uploading manifest...", "current": 0, "total": 5 }

The stream ends with a terminating complete event carrying the build result:

json
{
  "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/builds

Response

json
[
  {
    "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/keys

Response

json
[
  {
    "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
FieldTypeRequiredDescription
namestringYesKey display name
scopesstring[]NoScopes the key holds. Valid values: delivery, media:read, media:write. Defaults to ['delivery']. Unknown scope strings are rejected with 400.

Response

json
{
  "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/:keyId

Plan Limits

PlanAPI KeysBandwidth/Month
Starter32 GB
Pro1020 GB
EnterpriseUnlimitedUnlimited

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/:path

Headers

http
Authorization: Bearer crn_live_your-api-key

Object Paths

CDN objects are keyed by model ID (not domain) — the build pipeline writes them flat under content/, meta/, documents/, and models/:

PathReturns
content/{modelId}/{locale}.jsonPublished entries for an i18n collection/singleton/dictionary, by locale (object map keyed by entry ID)
content/{modelId}/data.jsonPublished entries for a non-i18n model
meta/{modelId}/{locale}.jsonPer-entry metadata for an i18n model, by locale (data.json for non-i18n)
documents/{modelId}/{slug}/{locale}.jsonA single document entry ({ frontmatter, body, html })
documents/{modelId}/_index/{locale}.jsonFrontmatter index for a document model, by locale
models/{modelId}.jsonA model definition
models/_index.jsonSummary index of all models
_manifest.jsonBuild manifest (commit SHA, locales, domains, model list)
_media_manifest.jsonMedia manifest — asset metadata (dimensions, blurhash, alt text)
_bundle/{locale}.jsonPer-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:

json
{
  "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

bash
# 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.

Released under the AGPL-3.0 License.