Delivering content

API reference

A read-only JSON API over your published content. CORS is enabled, so browser clients can call it directly.

Quick start

  1. Open your CMS domain in a browser Visit /api/content and you should see JSON listing your published entries.
  2. Filter to one model Add the model slug: /api/content/model/product.
  3. Scope it to a space Prefix with the space slug: /api/uk-store/content/model/product.

The API returns published content only. Drafts and entries scheduled for the future are never included, so there is no risk of unreleased pricing or campaign copy leaking through it.

Authentication

Set an API key in Settings, then include it with every request.

Header — recommended
Authorization: Bearer YOUR_API_KEY
Query parameter — convenient, less private
/api/content?api_key=YOUR_API_KEY

With no API key configured the API is open. That is convenient in development and wrong in production — setting a key belongs on your go-live checklist.

Prefer the header form. Keys in query strings end up in access logs, browser history and referrer headers.

Endpoints

Method Endpoint Returns
GET /api/models Every content model, with field definitions
GET /api/models/{slug} A single model by slug
GET /api/content All published entries, paginated
GET /api/content/model/{slug} Published entries for one model
GET /api/content/{id} A single entry by ID

Any endpoint can be prefixed with a space slug: /api/{space-slug}/.... Without a prefix you get the default space.

Query parameters

Parameter Default What it does
page 1 Which page of results to return
per_page 20 Results per page, up to a maximum of 100
resolve 1 Set to 0 to return raw IDs instead of full media and relation objects
model Filter by content model slug

Use resolve=0 on listing pages. A product grid needs names and slugs, not the full media object for every image. Fetch the lightweight list, then resolve the detail on the product page itself.

Code samples

const response = await fetch(
  'https://cms.yourbrand.com/api/uk-store/content/model/product?per_page=50',
  { headers: { Authorization: 'Bearer YOUR_API_KEY' } }
);

const { data, meta } = await response.json();

for (const entry of data) {
  console.log(entry.data.name, entry.data.sku);
}

console.log(`Page ${meta.page} of ${meta.total_pages}`);
curl 'https://cms.yourbrand.com/api/uk-store/content/model/product' \
  -H 'Authorization: Bearer YOUR_API_KEY'
import requests

response = requests.get(
    'https://cms.yourbrand.com/api/uk-store/content/model/product',
    headers={'Authorization': 'Bearer YOUR_API_KEY'},
    params={'per_page': 50},
)
payload = response.json()

for entry in payload['data']:
    print(entry['data']['name'], entry['data']['sku'])
$ch = curl_init('https://cms.yourbrand.com/api/uk-store/content/model/product');
curl_setopt($ch, CURLOPT_HTTPHEADER, ['Authorization: Bearer YOUR_API_KEY']);
curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);

$payload = json_decode(curl_exec($ch), true);

foreach ($payload['data'] as $entry) {
    echo $entry['data']['name'], ' ', $entry['data']['sku'], PHP_EOL;
}

Response shape

Entry fields live under data. Media and relation fields resolve to full objects unless you pass resolve=0.

GET /api/uk-store/content/model/product
{
  "data": [
    {
      "id": 214,
      "model": "product",
      "model_name": "Product",
      "data": {
        "name": "Alpine Down Parka",
        "sku": "APK-2291-BLK",
        "slug": "alpine-down-parka",
        "short_description": "Recycled down, fully taped seams.",
        "featured": true,
        "category": {
          "id": 12,
          "name": "Winter outerwear"
        },
        "gallery": [
          {
            "id": 5,
            "url": "https://bucket.s3.amazonaws.com/uploads/parka-front.jpg",
            "filename": "parka-front.jpg",
            "mime_type": "image/jpeg",
            "width": 1600,
            "height": 2000,
            "alt_text": "Navy quilted parka, front view, hood up"
          }
        ]
      },
      "created_at": "2026-06-01 10:30:00",
      "updated_at": "2026-06-02 14:15:00"
    }
  ],
  "meta": {
    "total": 4182,
    "page": 1,
    "per_page": 20,
    "total_pages": 210
  }
}

Errors

Status Meaning Body
401 Missing or invalid API key {"error": "Unauthorized"}
404 Not found, or not published {"error": "Not found"}

A 404 on an entry you can see in the admin panel almost always means it is still a draft, or scheduled for a future time.

Production notes

  • Cache at the edge. Published content changes rarely. Serving it from CloudFront keeps your origin quiet during peak trading and cuts data transfer costs.
  • Paginate deliberately. per_page caps at 100, so a full catalogue sync means looping over pages. Read meta.total_pages rather than guessing.
  • Keep keys server-side where you can. CORS means a browser can call the API directly, but that also means the key is visible to anyone who opens developer tools.
  • Fail soft. If the API is unreachable, serve the last good response rather than an empty category page.
  • One key per space. It limits the blast radius if a key leaks.
Next Recipes Complete model sets you can copy for catalogues and campaigns.