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
-
Open your CMS domain in a browser
Visit
/api/contentand you should see JSON listing your published entries. -
Filter to one model
Add the model slug:
/api/content/model/product. -
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.
Authorization: Bearer YOUR_API_KEY
/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.
{
"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_pagecaps at 100, so a full catalogue sync means looping over pages. Readmeta.total_pagesrather 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.