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 (e.g. de). 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.schema— an array of schema.org JSON-LD objects (HowTo,FAQPage) when the post contains those sections;[]otherwise. 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, or schema.
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>Responses 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