Skip to Content
ReferenceHeadless delivery API

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

RouteReturns
GET /postsPublished posts, newest first (summary fields only)
GET /posts/{slug}One published post, full contract (see below)
GET /termsCategories and tags with post counts

Query parameters (/posts)

ParamDescription
languagePosts in one language (e.g. de). Each language is a separate blog — one campaign per language.
categoryFilter by category slug. Mutually exclusive with tag.
tagFilter by tag slug. Mutually exclusive with category.
cursorNext (older) page — pass the previous response’s nextCursor.
beforePrevious (newer) page — pass the response’s prevCursor.
limitItems 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 injecting html.
  • 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 carry structura-howto / structura-faq classes for styling.
  • publishedAt, updatedAt — ISO 8601. updatedAt is 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.

Last updated on