Headless delivery API
This is a developer reference. If you’re setting up a headless space for the first time, start with Create a headless space.
Every headless space serves its published posts from a public, CDN-cacheable read API. There is no authentication and no API key — the API returns published posts only (drafts and scheduled posts never appear), and a blog is public by definition.
Your space’s base URL — including its workspace and site identifiers — is shown on the site’s Delivery API page in the customer portal, along with ready-made feed URLs per language. All paths below are relative to that base.
Endpoints
| Route | Returns |
|---|---|
GET /posts | Published posts, newest first (summary fields only) |
GET /posts/{slug} | One published post, full contract (see below) |
GET /terms | Categories and tags with post counts |
Query parameters (/posts)
| Param | Description |
|---|---|
language | Posts in one language, as a bare language code (e.g. de; regional campaign variants such as de_AT collapse to it). Any of the twenty supported content languages. Each language is a separate blog — one campaign per language. |
category | Filter by category slug. Mutually exclusive with tag. |
tag | Filter by tag slug. Mutually exclusive with category. |
cursor | Next (older) page — pass the previous response’s nextCursor. |
before | Previous (newer) page — pass the response’s prevCursor. |
limit | Items per page; default and maximum 50. |
language stacks with category or tag:
/posts?language=de&category=news is the German blog’s “News”
archive. /terms?language=de scopes the taxonomy the same way.
Pagination
List responses are bidirectional: each returns nextCursor (toward
older posts) and prevCursor (toward newer). A null cursor means
that direction has no more pages — render each page purely from its
URL token, no cursor trail needed.
Single-post response contract
GET /posts/{slug} returns { post } with:
id,slug,title,excerpt— identity and summary text.html— the rendered, sanitized body. Root-relative links are absolutized against your site’s public URL. Safe to inject as-is: strict tag allowlist, no<script>/<style>/on*attributes.blocks— the structured block array, if you prefer rendering to injectinghtml.metaTitle,metaDescription,keyphrase,categories,tags.featuredImage,bodyImage—{ url, width, height, alt?, caption? }.og—{ title, description, image }, ready for Open Graph tags.author—{ name, bio, avatarUrl, profileUrl, sameAs }, the real person the site credits for the post, ornullwhen the site has no author set for the post’s persona. It is never the persona name.bio,avatarUrlandprofileUrlcan benull;sameAsis an array of the author’s other profile URLs and can be empty. You set the author per persona for each site in the customer portal, see What is a persona?.schema— an array of schema.org JSON-LD objects. The first entry is aBlogPostingfor the post:headline,description,image,@idandmainEntityOfPagefrom the post’s canonical URL,datePublished,dateModified, and the author as aPerson(name,description,image,url,sameAs). Empty fields are left out, and so isauthorwhen the post has none. There is nopublisher: your site owns its Organization node.HowToandFAQPageentries follow when the post contains those sections. Delivered out of band because the body sanitizer strips<script>— emit one<script type="application/ld+json">per entry into your page<head>for rich-result eligibility. The matching body sections carrystructura-howto/structura-faqclasses for styling.publishedAt,updatedAt— ISO 8601.updatedAtis your revalidation signal.
The list response returns the summary subset only — no html,
blocks, author or schema.
Article markup and the author byline
If you emit every schema entry, your post pages carry article and
author markup that search engines can use for rich results, with no
extra work. If your site already builds its own Article or
BlogPosting node, leave ours out, or each page reports two articles:
const jsonLd = post.schema.filter((entry) => entry["@type"] !== "BlogPosting");Then take the author for your own node from post.author.
For a visible byline, show post.author.name, the avatarUrl image
and the bio, and link the name to profileUrl when it is set. When
post.author is null, leave the byline out.
Consuming from a framework
Fetch at build or revalidation time and treat the API as your CMS:
// Next.js App Router — a blog index with ISR
const BASE = process.env.STRUCTURA_DELIVERY_BASE; // from the Delivery API page
export default async function BlogIndex() {
const res = await fetch(`${BASE}/posts?language=en`, {
next: { revalidate: 300 },
});
const { posts } = await res.json();
// render titles/excerpts; link to /blog/[slug]
}// One post page — getStaticPaths-friendly
const res = await fetch(`${BASE}/posts/${slug}`);
const { post } = await res.json();
// inject post.html, emit post.schema entries as JSON-LD in <head>
// (filter out the BlogPosting entry if you build your own Article node),
// render a byline from post.author when it is not nullResponses are CDN-cached; poll-based setups should key revalidation
off updatedAt rather than re-rendering unconditionally.
Related pages
- Create a headless space
- Plugin REST API — the WordPress-side equivalent
Last verified