Skip to content

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

ModeBase URLWhen to use
projectId (recommended)https://api.luno.rest/public/p/{projectId}/v1Local testing and multi-tenant; independent of Host resolution
Host-resolvedhttps://{your-domain}/public/v1Project 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_IDalways 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

ParameterLocationTypeDescription
slugpathstringForm set slug
localequerystringLocale filter (e.g., en, ja)

Request

bash
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"
ts
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()
bash
# Agent prompt example: "Fetch the published settings form set"

Response (200)

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

ParameterLocationTypeDefaultDescription
formSetSlugpathstringForm set slug
pagequeryinteger1Page number (1-based)
limitqueryinteger20Items per page (max 100)
offsetqueryintegerOffset (alternative to page)
localequerystringLocale filter
qquerystringFull-text search (Business plan+)
sortquerystringSort key, e.g., created_at:desc, updated_at:asc
include_snapshotquerybooleanfalseInclude field values and mediaUrls per item

Request examples

bash
# 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"
ts
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()
bash
# Agent prompt example: "List 5 published blog entries with bodies, newest first"

Response (200)

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

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

ParameterLocationTypeDescription
formSetSlugpathstringForm set slug
entrySlugpathstringEntry slug
localequerystringLocale filter

Request

bash
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)

json
{
  "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
HTTP/1.1 301 Moved Permanently
Location: /public/v1/form-sets/blog/entries/new-slug

Content 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.

ParameterLocationTypeDescription
pathquerystringImport path (required)
localequerystringLocale filter (optional)
bash
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.

ParameterLocationTypeDescription
formSetSlugquerystringForm set slug (required)
slugquerystringEntry slug (required)
localequerystringLocale filter (optional)
bash
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.

ParameterLocationTypeMax lengthDescription
sourceTypequerystring50Source system name (e.g., wordpress, shopify)
entityTypequerystring200Entity type (e.g., post, product)
externalIdquerystring2000The external system's ID (required)
localequerystringLocale filter (optional)
bash
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.

ParameterLocationTypeDescription
tokenquerystringJWT token from the admin panel (required)
bash
curl "https://your-domain.com/public/v1/preview/revisions?token=eyJhbGciOiJIUzI1NiJ9..."
CaseResponse
Valid token200 with entry details (all statuses, including draft)
Expired token401 UNAUTHORIZED
Invalid token401 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.

ParameterLocationTypeDescription
assetIdpathstring (UUID)The media asset ID
bash
# 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-446655440001

Response headers

http
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.

bash
curl https://your-domain.com/public/v1/sitemap.xml
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.

bash
curl https://your-domain.com/public/v1/form-sets/blog/sitemap.xml

SEO

GET /form-sets/:formSetSlug/entries/:entrySlug/schema.json

Returns schema.org JSON-LD for the entry. Embed in <script type="application/ld+json">.

bash
curl https://your-domain.com/public/v1/form-sets/blog/entries/my-post/schema.json
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.

bash
curl https://your-domain.com/public/v1/form-sets/blog/entries/my-post/ogp.json
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.

bash
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)

json
{
  "ok": true,
  "submissionId": "7c9e6679-7425-40de-944b-e07fc1f90ae7"
}

Validation error (400)

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

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

bash
curl https://api.luno.rest/public/p/{projectId}/v1/llms.txt
# or
curl https://your-domain.com/public/v1/llms.txt
ts
const text = await fetch(
  'https://api.luno.rest/public/p/{projectId}/v1/llms.txt'
).then((r) => r.text())
bash
# 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 typeValue typeExample
text / urlstring"My First Post"
textareastring"A brief summary."
tiptapTiptap doc (JSON) or string"<h2>Heading</h2><p>Body</p>"
numbernumber1980
booleanbooleantrue
datestring or { from, to }"2025-01-15"
select / radiostring (master value)"blog"
multiselectstring[]["cloudflare", "cms"]
image / filestring (asset UUID)"550e8400-..."
image_galleryUUID string or { assetId, caption? }[][{ "assetId": "…" }]
video_embedstring (URL)"https://youtube.com/..."
entry_refstring (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.