Contents
Content Delivery API
Read published pages, collections, menus and redirects of a Nucleo CMS site.
The headless CMS inside Nucleo Brain publishes websites through this read-only API. Your website's server (Next.js, Astro, Nuxt…) fetches the published content of one site: pages resolved by path, collection entries (blog posts, news…), menus, redirects and a sitemap. Drafts are never returned.
Authentication is a per-site delivery token sent as Authorization: Bearer cmsdt_…. A token
reads exactly one site. Use it only server-side: the API does not allow cross-origin browser calls
and the token must never reach the visitor's browser.
After every publish Nucleo can call your site back with a signed request, retried until your site answers (see Webhooks → cmsRevalidate), so you can refresh your cache.
Authentication
deliveryTokenHTTP bearerPer-site delivery token (
cmsdt_+ 40 characters). Create it in Nucleo: CMS → site → Settings → For developers → Create token (requires thecms.managepermission). The value is shown once; Nucleo stores only its hash. Revoke it from the same card. Only theAuthorizationheader is accepted (no?token=query parameter).
- Base URL
- https://api-brain.nucleoplatform.com/api/delivery/v1/{workspace}/sites/{siteId}Production. The full base URL of a site is shown in Nucleo under CMS → *site* → Settings → For developers.
- Who calls it
- Your servers
- Endpoints
- 8 + 1 webhook
- OpenAPI 3.1 specification
- content.yaml
{workspace} — Slug of your Nucleo Brain workspace (lowercase letters, digits and dashes). (examples use acme; full URL https://api-brain.nucleoplatform.com/api/delivery/v1/acme/sites/12){siteId} — Numeric id of the CMS site. (examples use 12; full URL https://api-brain.nucleoplatform.com/api/delivery/v1/acme/sites/12)Site
Basic data of the site the token belongs to.
/Get the site
Id, slug, name, status, default locale, enabled locales and the public URL of the site's
frontend (as configured in Nucleo; null if not set).
Rate limit (all endpoints): 120 requests per minute per IP address.
Responses
200The site.application/json
dataSiteChild attributes
idintegerslugstringnamestringstatusstringdefault_localestringlocalesarray of stringfrontend_urlstring (uri) | null
401Missing or unknown delivery token.application/json
messagestring
404Not found. Also returned when the workspace slug is unknown or when the token belongs to another site (Nucleo never confirms that a site exists to a token that cannot read it). application/json
messagestring
429More than 120 requests in a minute from the same IP.application/json
HeadersRetry-AfterX-RateLimit-LimitX-RateLimit-Remaining
messagestring
curl -X GET https://api-brain.nucleoplatform.com/api/delivery/v1/acme/sites/12/ \
-H "Authorization: Bearer $NUCLEO_ACCESS_TOKEN" \
-H 'Accept: application/json'const res = await fetch("https://api-brain.nucleoplatform.com/api/delivery/v1/acme/sites/12/", {
method: "GET",
headers: {
Authorization: `Bearer ${process.env.NUCLEO_ACCESS_TOKEN}`,
Accept: "application/json",
},
});
const data = await res.json();
console.log(res.status, data);<?php
require 'vendor/autoload.php';
$client = new GuzzleHttp\Client();
$response = $client->request('GET', 'https://api-brain.nucleoplatform.com/api/delivery/v1/acme/sites/12/', [
'headers' => [
'Authorization' => 'Bearer ' . getenv('NUCLEO_ACCESS_TOKEN'),
'Accept' => 'application/json',
],
'http_errors' => false,
]);
echo $response->getStatusCode(), PHP_EOL;
echo $response->getBody();{
"data": {
"id": 12,
"slug": "acme-website",
"name": "Acme Apparel website",
"status": "active",
"default_locale": "en",
"locales": [
"en",
"it"
],
"frontend_url": "https://www.acme.example"
}
}{
"message": "Delivery token required (Authorization: Bearer)."
}{
"message": "Invalid delivery token."
}{
"message": "Not found."
}{
"message": "Too Many Attempts."
}cmsRevalidateContent published on the site
Sent by Nucleo right after a page or an entry is published, to the Revalidate URL configured in CMS → site → Settings, signed with the site's Revalidate secret.
- Page published:
pathis/(refresh the whole site). - Entry published:
pathis/<collection-slug>.
Signature. Nucleo-Signature: t=<unix time>,v1=<signature>, where the signature is the
lowercase hex HMAC-SHA256 of <t>.<raw body> keyed with the Revalidate secret. Verify it on the
exact bytes received, compare in constant time and reject a t older than 5 minutes. Without a
secret on the site the request is not signed. The secret body field is deprecated: it is still
sent for older receivers and will be removed.
Retries. 5-second timeout, redirects not followed. Any 2xx is a success (the body is
ignored); any other answer or no answer is retried after 1 min, 5 min, 30 min, 2 h and 6 h — at
most 6 attempts, then the delivery is dropped and logged. Every attempt has the same id (also in
Nucleo-Webhook-Id) and its number in Nucleo-Webhook-Attempt, so you can ignore duplicates.
Headers
Nucleo-Signaturestringt=<unix time>,v1=<hex HMAC-SHA256 of <t>.<raw body>>keyed with the Revalidate secret. Absent when the site has no secret.Example:t=1791115200,v1=5f2b6c0e9a8d4f7b1c3e2a6d9b0f8e7c6a5d4b3c2e1f0a9b8c7d6e5f4a3b2c1dNucleo-Webhook-Idstring (uuid)requiredDelivery id, the same on every attempt (equals
idin the body).Example:3f6c1a52-8e0b-4d7a-9c2e-1b5d7f9a0c34Nucleo-Webhook-EventstringrequiredOne ofcontent.publishedExample:content.publishedNucleo-Webhook-AttemptintegerrequiredAttempt number, from 1 to 6.
min 1max 6Example:1
Request Nucleo sendsapplication/json
idstring (uuid)requiredDelivery id
eventstringrequiredOne ofcontent.publishedsitestringrequiredSite slug.
localestringrequiredpathstringrequiredpublished_atstring (date-time)requiredWhen the content was published.
secretstringdeprecatedThe Revalidate secret of the site (empty string if not set). Deprecated, verify
Nucleo-Signatureinstead.
Expected response
200Acknowledged. Any
2xxstops the retries.
{
"id": "3f6c1a52-8e0b-4d7a-9c2e-1b5d7f9a0c34",
"event": "content.published",
"site": "acme-website",
"locale": "en",
"path": "/blog",
"published_at": "2026-10-04T12:00:00+00:00",
"secret": "whsec_example_123"
}HTTP 200 — Acknowledged. Any `2xx` stops the retries.Pages
Pages resolved by URL path and locale.
/pageGet a published page by path
Resolves path segment by segment through the translated slugs of the page tree in the given
locale. / (or no path) returns the page marked as home. Returns 404 when no page matches,
when the page has no translation in that locale, or when that translation is not published.
blocks and seo are returned exactly as edited in Nucleo (free-form JSON defined by the
page template): render them with your own components.
Query parameters
pathstringURL path, with or without leading/trailing slashes. Default
/.default /Example:/about/teamlocalestringLocale code enabled on the site. Default the site's default locale.
Example:en
Responses
200The page.application/json
dataPageChild attributes
idintegertemplatestringis_homebooleantitlestringslugstringblocksarray of any | object | nullPage content as edited in Nucleo (template-defined JSON).
seoobject | nulllocalestringpublished_atstring (date-time) | null
401Missing or unknown delivery token.application/json
messagestring
404Page not found, not translated or not published (or wrong site/workspace).application/json
messagestring
429More than 120 requests in a minute from the same IP.application/json
HeadersRetry-AfterX-RateLimit-LimitX-RateLimit-Remaining
messagestring
curl -X GET 'https://api-brain.nucleoplatform.com/api/delivery/v1/acme/sites/12/page?path=%2Fabout%2Fteam&locale=en' \
-H "Authorization: Bearer $NUCLEO_ACCESS_TOKEN" \
-H 'Accept: application/json'const res = await fetch("https://api-brain.nucleoplatform.com/api/delivery/v1/acme/sites/12/page?path=%2Fabout%2Fteam&locale=en", {
method: "GET",
headers: {
Authorization: `Bearer ${process.env.NUCLEO_ACCESS_TOKEN}`,
Accept: "application/json",
},
});
const data = await res.json();
console.log(res.status, data);<?php
require 'vendor/autoload.php';
$client = new GuzzleHttp\Client();
$response = $client->request('GET', 'https://api-brain.nucleoplatform.com/api/delivery/v1/acme/sites/12/page?path=%2Fabout%2Fteam&locale=en', [
'headers' => [
'Authorization' => 'Bearer ' . getenv('NUCLEO_ACCESS_TOKEN'),
'Accept' => 'application/json',
],
'http_errors' => false,
]);
echo $response->getStatusCode(), PHP_EOL;
echo $response->getBody();{
"data": {
"id": 87,
"template": "default",
"is_home": false,
"title": "Our team",
"slug": "team",
"blocks": [
{
"type": "hero",
"props": {
"heading": "Meet the people behind Acme Apparel",
"media_id": 311
}
},
{
"type": "rich_text",
"props": {
"html": "<p>Designed in Milano since 2009.</p>"
}
}
],
"seo": {
"title": "Our team · Acme Apparel",
"description": "The people behind Acme Apparel."
},
"locale": "en",
"published_at": "2026-09-30T10:15:00.000000Z"
}
}{
"message": "Delivery token required (Authorization: Bearer)."
}{
"message": "Invalid delivery token."
}{
"message": "Page not found."
}{
"message": "Page not translated."
}{
"message": "Page not published."
}{
"message": "Too Many Attempts."
}Collections
Entries of a collection (blog, news, case studies…), paginated.
/collections/{collection}/entriesList published entries of a collection
Entries that have a published translation in the locale, newest first (by publish date,
then id). Standard pagination: data plus current_page, last_page, per_page,
total, links and the page URLs. Unknown collection → 404.
Path parameters
collectionstringrequiredCollection slug.
Example:blog
Query parameters
localestringLocale code enabled on the site. Default the site's default locale.
Example:enpageintegermin 1default 1Example:1per_pageintegerClamped to 1–50.
min 1max 50default 12Example:12
Responses
200A page of entries.application/json
current_pageintegerdataarray of Entry & objectAttributes of each item
idintegertitlestringslugstringfieldsobject | nullCollection-defined fields.
seoobject | nullpublished_atstring (date-time) | nullsortinteger
first_page_urlstring (uri)frominteger | nulllast_pageintegerlast_page_urlstring (uri)linksarray of objectnext_page_urlstring (uri) | nullpathstring (uri)per_pageintegerprev_page_urlstring (uri) | nulltointeger | nulltotalinteger
401Missing or unknown delivery token.application/json
messagestring
404Not found. Also returned when the workspace slug is unknown or when the token belongs to another site (Nucleo never confirms that a site exists to a token that cannot read it). application/json
messagestring
429More than 120 requests in a minute from the same IP.application/json
HeadersRetry-AfterX-RateLimit-LimitX-RateLimit-Remaining
messagestring
curl -X GET 'https://api-brain.nucleoplatform.com/api/delivery/v1/acme/sites/12/collections/blog/entries?locale=en&page=1&per_page=12' \
-H "Authorization: Bearer $NUCLEO_ACCESS_TOKEN" \
-H 'Accept: application/json'const res = await fetch("https://api-brain.nucleoplatform.com/api/delivery/v1/acme/sites/12/collections/blog/entries?locale=en&page=1&per_page=12", {
method: "GET",
headers: {
Authorization: `Bearer ${process.env.NUCLEO_ACCESS_TOKEN}`,
Accept: "application/json",
},
});
const data = await res.json();
console.log(res.status, data);<?php
require 'vendor/autoload.php';
$client = new GuzzleHttp\Client();
$response = $client->request('GET', 'https://api-brain.nucleoplatform.com/api/delivery/v1/acme/sites/12/collections/blog/entries?locale=en&page=1&per_page=12', [
'headers' => [
'Authorization' => 'Bearer ' . getenv('NUCLEO_ACCESS_TOKEN'),
'Accept' => 'application/json',
],
'http_errors' => false,
]);
echo $response->getStatusCode(), PHP_EOL;
echo $response->getBody();{
"current_page": 1,
"data": [
{
"id": 501,
"sort": 0,
"title": "Autumn drop is live",
"slug": "autumn-drop",
"fields": {
"excerpt": "Ten new tees in organic cotton.",
"cover_media_id": 340
},
"seo": {
"title": "Autumn drop · Acme Apparel"
},
"published_at": "2026-09-28T08:00:00.000000Z"
}
],
"first_page_url": "https://api-brain.nucleoplatform.com/api/delivery/v1/acme/sites/12/collections/blog/entries?page=1",
"from": 1,
"last_page": 4,
"last_page_url": "https://api-brain.nucleoplatform.com/api/delivery/v1/acme/sites/12/collections/blog/entries?page=4",
"links": [],
"next_page_url": "https://api-brain.nucleoplatform.com/api/delivery/v1/acme/sites/12/collections/blog/entries?page=2",
"path": "https://api-brain.nucleoplatform.com/api/delivery/v1/acme/sites/12/collections/blog/entries",
"per_page": 12,
"prev_page_url": null,
"to": 12,
"total": 41
}{
"message": "Delivery token required (Authorization: Bearer)."
}{
"message": "Invalid delivery token."
}{
"message": "Not found."
}{
"message": "Too Many Attempts."
}/collections/{collection}/entries/{entry}Get a published entry by slug
One entry by its translated slug in the locale; 404 if it does not exist or is not published in that locale.
Path parameters
collectionstringrequiredCollection slug.
Example:blogentrystringrequiredEntry slug in the requested locale.
Example:autumn-drop
Query parameters
localestringLocale code enabled on the site. Default the site's default locale.
Example:en
Responses
200The entry.application/json
dataEntryChild attributes
idintegertitlestringslugstringfieldsobject | nullCollection-defined fields.
seoobject | nullpublished_atstring (date-time) | null
401Missing or unknown delivery token.application/json
messagestring
404Not found. Also returned when the workspace slug is unknown or when the token belongs to another site (Nucleo never confirms that a site exists to a token that cannot read it). application/json
messagestring
429More than 120 requests in a minute from the same IP.application/json
HeadersRetry-AfterX-RateLimit-LimitX-RateLimit-Remaining
messagestring
curl -X GET 'https://api-brain.nucleoplatform.com/api/delivery/v1/acme/sites/12/collections/blog/entries/autumn-drop?locale=en' \
-H "Authorization: Bearer $NUCLEO_ACCESS_TOKEN" \
-H 'Accept: application/json'const res = await fetch("https://api-brain.nucleoplatform.com/api/delivery/v1/acme/sites/12/collections/blog/entries/autumn-drop?locale=en", {
method: "GET",
headers: {
Authorization: `Bearer ${process.env.NUCLEO_ACCESS_TOKEN}`,
Accept: "application/json",
},
});
const data = await res.json();
console.log(res.status, data);<?php
require 'vendor/autoload.php';
$client = new GuzzleHttp\Client();
$response = $client->request('GET', 'https://api-brain.nucleoplatform.com/api/delivery/v1/acme/sites/12/collections/blog/entries/autumn-drop?locale=en', [
'headers' => [
'Authorization' => 'Bearer ' . getenv('NUCLEO_ACCESS_TOKEN'),
'Accept' => 'application/json',
],
'http_errors' => false,
]);
echo $response->getStatusCode(), PHP_EOL;
echo $response->getBody();{
"data": {
"id": 501,
"title": "Autumn drop is live",
"slug": "autumn-drop",
"fields": {
"excerpt": "Ten new tees in organic cotton.",
"body": "<p>Available from today in Milano and online.</p>",
"cover_media_id": 340
},
"seo": {
"title": "Autumn drop · Acme Apparel",
"description": "Ten new tees in organic cotton."
},
"published_at": "2026-09-28T08:00:00.000000Z"
}
}{
"message": "Delivery token required (Authorization: Bearer)."
}{
"message": "Invalid delivery token."
}{
"message": "Not found."
}{
"message": "Too Many Attempts."
}Media
Resolve a media id to its public URL.
/media/{mediaId}Resolve a media id to its URL
302 redirect to the stable public URL of a media file of this site. Meant for your server to
turn a media id found in blocks/fields into a URL. Never use this endpoint as src of
an <img>/<video>: the browser would need the token. Put the Location URL in your HTML instead.
Path parameters
mediaIdintegerrequiredExample:311
Responses
302Redirect to the public file.
HeadersLocation
401Missing or unknown delivery token.application/json
messagestring
404Not found. Also returned when the workspace slug is unknown or when the token belongs to another site (Nucleo never confirms that a site exists to a token that cannot read it). application/json
messagestring
429More than 120 requests in a minute from the same IP.application/json
HeadersRetry-AfterX-RateLimit-LimitX-RateLimit-Remaining
messagestring
curl -X GET https://api-brain.nucleoplatform.com/api/delivery/v1/acme/sites/12/media/311 \
-H "Authorization: Bearer $NUCLEO_ACCESS_TOKEN"const res = await fetch("https://api-brain.nucleoplatform.com/api/delivery/v1/acme/sites/12/media/311", {
method: "GET",
headers: {
Authorization: `Bearer ${process.env.NUCLEO_ACCESS_TOKEN}`,
},
});
const data = await res.json();
console.log(res.status, data);<?php
require 'vendor/autoload.php';
$client = new GuzzleHttp\Client();
$response = $client->request('GET', 'https://api-brain.nucleoplatform.com/api/delivery/v1/acme/sites/12/media/311', [
'headers' => [
'Authorization' => 'Bearer ' . getenv('NUCLEO_ACCESS_TOKEN'),
],
'http_errors' => false,
]);
echo $response->getStatusCode(), PHP_EOL;
echo $response->getBody();{
"message": "Delivery token required (Authorization: Bearer)."
}{
"message": "Invalid delivery token."
}{
"message": "Not found."
}{
"message": "Too Many Attempts."
}