Public API Reference
API only
Start path C
Read published content without the admin console. Site wiring: Framework recipes. Other paths: Agents (MCP) · Console quick start.
No authentication required by default. All responses are Content-Type: application/json (except /media/:assetId and XML endpoints).
Base URLs
| Mode | Base URL | When to use |
|---|---|---|
| projectId (recommended) | https://api.luno.rest/public/p/{projectId}/v1 | Local testing and multi-tenant; independent of Host resolution |
| Host-resolved | https://{your-domain}/public/v1 | Project public host / custom domain |
Get projectId from MCP get_public_api_info or project settings. On localhost, Host-based URLs fall back to DEFAULT_TENANT_ID — always use /public/p/{projectId}/v1 locally.
Public API keys (luno_pub_…) are used for Embed and Host resolution. Send header X-Luno-Public-Api-Key (or Authorization: Bearer / query). See Public API keys. Separate from agent keys (sk-agent-…).
Paths below are relative to either base.
Form Sets
GET /form-sets/:slug
Returns form set metadata and the published content of its primary entry (prefers slug main, then _legacy, then oldest).
Parameters
| Parameter | Location | Type | Description |
|---|---|---|---|
slug | path | string | Form set slug |
locale | query | string | Locale filter (e.g., en, ja) |
Request
curl "https://api.luno.rest/public/p/{projectId}/v1/form-sets/settings?locale=en"
# or
curl "https://your-domain.com/public/v1/form-sets/settings?locale=en"const BASE = 'https://api.luno.rest/public/p/{projectId}/v1'
const res = await fetch(`${BASE}/form-sets/settings?locale=en`)
const data = await res.json()# Agent prompt example: "Fetch the published settings form set"Response (200)
{
"formSet": {
"id": "550e8400-e29b-41d4-a716-446655440000",
"slug": "settings",
"name": "Site Settings",
"description": null
},
"entry": {
"id": "7c9e6679-7425-40de-944b-e07fc1f90ae7",
"slug": "main"
},
"revision": {
"id": "a2f3d4e5-...",
"revision": 3,
"updatedAt": "2025-01-15T10:00:00Z"
},
"data": {
"site_name": "My Website",
"tagline": "The best headless CMS",
"logo": "asset-uuid",
"primary_color": "#3b82f6"
},
"mediaUrls": {
"logo": "https://your-domain.com/public/v1/media/asset-uuid"
}
}GET /form-sets/:formSetSlug/entries
Returns a paginated list of published entries in a form set.
Parameters
| Parameter | Location | Type | Default | Description |
|---|---|---|---|---|
formSetSlug | path | string | — | Form set slug |
page | query | integer | 1 | Page number (1-based) |
limit | query | integer | 20 | Items per page (max 100) |
offset | query | integer | — | Offset (alternative to page) |
locale | query | string | — | Locale filter |
q | query | string | — | Full-text search (Business plan+) |
sort | query | string | — | Sort key, e.g., created_at:desc, updated_at:asc |
include_snapshot | query | boolean | false | Include field values and mediaUrls per item |
Request examples
# Default (first 20 entries)
curl "https://api.luno.rest/public/p/{projectId}/v1/form-sets/blog/entries"
# With field values included
curl "https://api.luno.rest/public/p/{projectId}/v1/form-sets/blog/entries?limit=5&include_snapshot=true"
# Full-text search (Business plan+) / sort
curl "https://api.luno.rest/public/p/{projectId}/v1/form-sets/blog/entries?q=cloudflare&locale=en"
curl "https://api.luno.rest/public/p/{projectId}/v1/form-sets/blog/entries?sort=updated_at:desc"const BASE = 'https://api.luno.rest/public/p/{projectId}/v1'
const qs = new URLSearchParams({
limit: '5',
include_snapshot: 'true',
sort: 'updated_at:desc',
})
const res = await fetch(`${BASE}/form-sets/blog/entries?${qs}`)
const data = await res.json()# Agent prompt example: "List 5 published blog entries with bodies, newest first"Response (200)
{
"formSet": {
"id": "uuid",
"slug": "blog",
"name": "Blog",
"description": null
},
"total": 42,
"limit": 20,
"offset": 0,
"items": [
{
"entry": {
"id": "7c9e6679-7425-40de-944b-e07fc1f90ae7",
"slug": "my-first-post"
},
"published": {
"revisionId": "a2f3d4e5-...",
"revision": 2,
"updatedAt": "2025-01-15T10:00:00Z"
}
}
]
}With include_snapshot=true, each published object also contains snapshot and mediaUrls:
{
"items": [
{
"entry": { "id": "uuid", "slug": "my-first-post" },
"published": {
"revisionId": "uuid",
"revision": 2,
"updatedAt": "2025-01-15T10:00:00Z",
"snapshot": {
"title": "My First Post",
"cover": "asset-uuid",
"category": "blog"
},
"mediaUrls": {
"cover": "https://your-domain.com/public/v1/media/asset-uuid"
}
}
}
]
}GET /form-sets/:formSetSlug/entries/:entrySlug
Returns the full published content for a specific entry. Returns HTTP 301 if the entry's slug has changed.
Parameters
| Parameter | Location | Type | Description |
|---|---|---|---|
formSetSlug | path | string | Form set slug |
entrySlug | path | string | Entry slug |
locale | query | string | Locale filter |
Request
curl https://your-domain.com/public/v1/form-sets/blog/entries/my-first-post
# With locale
curl "https://your-domain.com/public/v1/form-sets/blog/entries/my-first-post?locale=en"Response (200)
{
"formSet": {
"id": "uuid",
"slug": "blog",
"name": "Blog"
},
"entry": {
"id": "7c9e6679-7425-40de-944b-e07fc1f90ae7",
"slug": "my-first-post"
},
"revision": {
"id": "a2f3d4e5-...",
"revision": 2,
"updatedAt": "2025-01-15T10:00:00Z"
},
"data": {
"title": "My First Post",
"body": "<h2>Introduction</h2><p>Hello, world!</p>",
"cover": "asset-uuid-here",
"category": "blog",
"tags": ["cloudflare", "cms"],
"published_date": "2025-01-15",
"is_featured": true
},
"mediaUrls": {
"cover": "https://your-domain.com/public/v1/media/asset-uuid-here"
},
"widgetRoles": {
"title": "title",
"cover": "thumbnail",
"body": "description"
}
}Slug changed (301)
HTTP/1.1 301 Moved Permanently
Location: /public/v1/form-sets/blog/entries/new-slugContent Lookup
GET /content/by-path
Look up content by an import path. Used after migrating content from an external system to retrieve entries by their original URL path.
| Parameter | Location | Type | Description |
|---|---|---|---|
path | query | string | Import path (required) |
locale | query | string | Locale filter (optional) |
curl "https://your-domain.com/public/v1/content/by-path?path=/old-cms/articles/123"Returns the same structure as the single entry endpoint.
GET /content/by-slug
Fetch content using form set slug and entry slug as query parameters instead of path parameters.
| Parameter | Location | Type | Description |
|---|---|---|---|
formSetSlug | query | string | Form set slug (required) |
slug | query | string | Entry slug (required) |
locale | query | string | Locale filter (optional) |
curl "https://your-domain.com/public/v1/content/by-slug?formSetSlug=blog&slug=my-post"GET /content/by-external-id
Look up content by an external system entity ID. Set during content import.
| Parameter | Location | Type | Max length | Description |
|---|---|---|---|---|
sourceType | query | string | 50 | Source system name (e.g., wordpress, shopify) |
entityType | query | string | 200 | Entity type (e.g., post, product) |
externalId | query | string | 2000 | The external system's ID (required) |
locale | query | string | — | Locale filter (optional) |
curl "https://your-domain.com/public/v1/content/by-external-id?sourceType=wordpress&entityType=post&externalId=12345"Preview
GET /preview/revisions
Fetch an unpublished revision for preview purposes using a signed JWT token.
| Parameter | Location | Type | Description |
|---|---|---|---|
token | query | string | JWT token from the admin panel (required) |
curl "https://your-domain.com/public/v1/preview/revisions?token=eyJhbGciOiJIUzI1NiJ9..."| Case | Response |
|---|---|
| Valid token | 200 with entry details (all statuses, including draft) |
| Expired token | 401 UNAUTHORIZED |
| Invalid token | 401 UNAUTHORIZED |
Tokens are generated from the entry edit view and are valid for 15 minutes.
Media
GET /media/:assetId
Serve an uploaded media file from Cloudflare R2.
| Parameter | Location | Type | Description |
|---|---|---|---|
assetId | path | string (UUID) | The media asset ID |
# Fetch an image
curl https://your-domain.com/public/v1/media/550e8400-e29b-41d4-a716-446655440001
# Request WebP format (supported browsers)
curl -H "Accept: image/webp" \
https://your-domain.com/public/v1/media/550e8400-e29b-41d4-a716-446655440001Response headers
Content-Type: image/jpeg
Cache-Control: public, max-age=31536000
ETag: "abc123def456"Sitemaps
GET /sitemap.xml
XML sitemap for all published entries across all form sets.
curl https://your-domain.com/public/v1/sitemap.xml<?xml version="1.0" encoding="UTF-8"?>
<urlset xmlns="http://www.sitemaps.org/schemas/sitemap/0.9">
<url>
<loc>https://your-site.com/blog/my-first-post</loc>
<lastmod>2025-01-15T10:00:00Z</lastmod>
<changefreq>weekly</changefreq>
<priority>0.8</priority>
</url>
</urlset>GET /form-sets/:slug/sitemap.xml
XML sitemap for a single form set's published entries.
curl https://your-domain.com/public/v1/form-sets/blog/sitemap.xmlSEO
GET /form-sets/:formSetSlug/entries/:entrySlug/schema.json
Returns schema.org JSON-LD for the entry. Embed in <script type="application/ld+json">.
curl https://your-domain.com/public/v1/form-sets/blog/entries/my-post/schema.json{
"@context": "https://schema.org",
"@type": "Article",
"headline": "My First Post",
"description": "A concise description.",
"image": "https://your-domain.com/public/v1/media/cover-uuid",
"datePublished": "2025-01-15T10:00:00Z",
"dateModified": "2025-01-15T10:00:00Z",
"author": { "@type": "Organization", "name": "My Blog" }
}GET /form-sets/:formSetSlug/entries/:entrySlug/ogp.json
Returns Open Graph Protocol metadata as a JSON object.
curl https://your-domain.com/public/v1/form-sets/blog/entries/my-post/ogp.json{
"og:title": "My First Post | My Blog",
"og:description": "A concise description.",
"og:image": "https://your-domain.com/public/v1/media/cover-uuid",
"og:url": "https://your-site.com/blog/my-post",
"og:type": "article",
"og:site_name": "My Blog",
"twitter:card": "summary_large_image"
}Contact Forms
POST /contact-forms/:slug/submit
Submit a contact form. No authentication required.
curl -X POST https://your-domain.com/public/v1/contact-forms/contact/submit \
-H "Content-Type: application/json" \
-d '{
"name": "Jane Smith",
"email": "[email protected]",
"message": "Hello, I have a question about pricing."
}'Success (200)
{
"ok": true,
"submissionId": "7c9e6679-7425-40de-944b-e07fc1f90ae7"
}Validation error (400)
{
"error": {
"code": "VALIDATION_ERROR",
"message": "email is required"
}
}Masters (public)
List master entities and records that are published to the site (site_published_at set). Unpublished masters are omitted (and return 404 by key). Record value is locale-stable; label resolves with ?locale=. Overview: Masters.
curl https://api.luno.rest/public/p/{projectId}/v1/master-entities
curl "https://api.luno.rest/public/p/{projectId}/v1/master-entities/category/records?locale=ja"AI Agents
GET /llms.txt
AI-readable published content index in Markdown (llms.txt spec).
curl https://api.luno.rest/public/p/{projectId}/v1/llms.txt
# or
curl https://your-domain.com/public/v1/llms.txtconst text = await fetch(
'https://api.luno.rest/public/p/{projectId}/v1/llms.txt'
).then((r) => r.text())# Agent prompt example: "Read this project's llms.txt and summarize the public structure"docs-site llms-full.txt
The long-form API summary lives on this docs site: llms-full.txt. The product Public API does not expose /llms-full.txt.
For MCP setup, agent key scopes, and Admin API usage, see the AI Agents Guide.
Field Value Types
All field values live inside the data object of entry responses:
| Field type | Value type | Example |
|---|---|---|
text / url | string | "My First Post" |
textarea | string | "A brief summary." |
tiptap | Tiptap doc (JSON) or string | "<h2>Heading</h2><p>Body</p>" |
number | number | 1980 |
boolean | boolean | true |
date | string or { from, to } | "2025-01-15" |
select / radio | string (master value) | "blog" |
multiselect | string[] | ["cloudflare", "cms"] |
image / file | string (asset UUID) | "550e8400-..." |
image_gallery | UUID string or { assetId, caption? }[] | [{ "assetId": "…" }] |
video_embed | string (URL) | "https://youtube.com/..." |
entry_ref | string (referenced entry UUID) | "7c9e6679-..." |
image and file UUIDs resolve to full URLs via mediaUrls[fieldKey]. When fetched via /public/p/{projectId}/v1, mediaUrls use the same prefix.