Public Blog API
API reference for fetching published blog posts onto static sites, custom frontends, and third-party integrations using your project's read-only pk_live key.
The Public Blog API lets you fetch your published blog content from theStacc and display it on any website — including static sites hosted on Cloudflare Pages, Vercel, Netlify, or any other platform. No CMS required.
When to use this#
Use the Public Blog API when:
- Your site runs on Cloudflare Pages, Vercel, Netlify, or GitHub Pages
- You don't use a CMS like WordPress, Ghost, or Webflow
- Your site is built with a static site generator (Astro, Next.js, Nuxt, Hugo, Gatsby, 11ty, etc.)
- You want full control over how blog content is rendered on your site
If you use WordPress, Ghost, Webflow, or Zepio, you don't need this — use the direct publishing integrations instead, which push posts straight into your CMS.
This is the pull side of theStacc's integrations: your site reads content from us. For an end-to-end walkthrough of wiring it into a specific framework, follow Static Site Integration.
How it works#
theStacc generates and stores your blog content. The Public Blog API exposes the published posts for one project as JSON. Your static site fetches them at build time and generates static HTML pages.
theStacc generates blog → stored in theStacc database
↓
Your site rebuilds → fetches blogs from Public Blog API
↓
Static HTML pages generated → deployed to your CDN
↓
Google crawls → sees full static HTML → indexes and ranks normallyBecause content is fetched at build time (not in the browser), Google sees fully rendered HTML pages. SEO is identical to any other static site — Google has no idea the content came from an API.
To rebuild automatically whenever you publish or update a post, pair this with a Deploy Hook.
Authentication#
Every project gets one Public API Key for read-only public access. Find it in your project under Settings > Publishing > API Access. Click Generate API Key if you don't have one yet; Reveal and Copy to read it.
The key always starts with pk_live_. It is project-scoped: it only ever returns the published blogs that belong to that one project.
Include the key in every request — either as a query parameter or as a Bearer header:
# Query parameter
GET https://api.thestacc.com/blog/api/v1/public/blogs?api_key=YOUR_API_KEY
# Or as a header
GET https://api.thestacc.com/blog/api/v1/public/blogs
Authorization: Bearer YOUR_API_KEYIf both are supplied, the Authorization: Bearer header wins.
This is a project key, not an agent key#
The pk_live_ Public API Key is read-only. It can list and read your published blogs — nothing more. It cannot create, edit, publish, or delete content, and it can't see drafts or unpublished posts. That's why it's safe to use in a static-site build.
It is a completely different credential from the agent keys (which start with sk_agent_) used by the Model Context Protocol (MCP) server. Agent keys are secret, write-capable (they can generate, edit, and publish blogs), are stored hashed, and live in the Agent access section right beside API Access. Never use one in place of the other — and never embed an sk_agent_ key in a public build.
Keep your key safe#
Though the Public API Key is read-only, treat it as a credential. Store it in an environment variable in your build process and never commit it to your repository.
If a key is ever exposed, open Settings > Publishing > API Access and click Rotate Key to issue a new one (the old key stops working immediately), or Revoke to disable public access entirely. Any site still using the old key will stop fetching the moment you rotate or revoke, so update your build environment variable at the same time.
Endpoints#
All endpoints share the base URL https://api.thestacc.com/blog/api/v1/public.
List published blogs#
GET /blog/api/v1/public/blogs?api_key=YOUR_API_KEYReturns blogs with status: published, sorted by published_at (newest first). By default it returns metadata only (no body content) for a fast, lightweight listing. Pass include_content=true to get the full HTML body for every post in the page.
Query parameters:
api_key(required) — your project'spk_live_key (or pass it via the Authorization header instead)limit(optional) — number of blogs to return. Default50, minimum1, maximum100offset(optional) — pagination offset. Default0, minimum0category(optional) — return only blogs that include this category nameinclude_content(optional) — set totrueto include the full HTML body of each blog. Defaultfalse
Use limit and offset together to page through more than 100 posts. The total field in the response is the full count of published blogs (after any category filter), so you can compute how many pages remain.
Response (default — without content):
{
"blogs": [
{
"id": "abc-123",
"title": "10 SEO Tips for Small Businesses",
"slug": "seo-tips-small-businesses",
"excerpt": "Discover actionable SEO strategies that...",
"meta_title": "10 SEO Tips for Small Businesses | Your Brand",
"meta_description": "Discover 10 actionable SEO strategies...",
"featured_image_url": "https://storage.thestacc.com/images/seo-tips.webp",
"categories": ["SEO", "Marketing"],
"tags": ["seo", "small-business", "organic-traffic"],
"published_at": "2026-03-28T10:00:00Z"
}
],
"total": 42,
"limit": 50,
"offset": 0
}When include_content=true is passed, each blog also includes a content field with the full HTML body.
Get a single blog by slug#
GET /blog/api/v1/public/blogs/:slug?api_key=YOUR_API_KEYReturns the full blog for one slug (content is always included). This is the call your blog post template makes to render an individual page.
The single-blog response includes two fields the list response doesn't: updated_at and word_count. word_count is computed on the fly by stripping the HTML tags from the body and counting the remaining words, so it always reflects the current content.
Response:
{
"id": "abc-123",
"title": "10 SEO Tips for Small Businesses",
"slug": "seo-tips-small-businesses",
"excerpt": "Discover actionable SEO strategies that...",
"content": "<h2>Why SEO matters</h2><p>Search engine optimization...</p>",
"meta_title": "10 SEO Tips for Small Businesses | Your Brand",
"meta_description": "Discover 10 actionable SEO strategies...",
"featured_image_url": "https://storage.thestacc.com/images/seo-tips.webp",
"categories": ["SEO", "Marketing"],
"tags": ["seo", "small-business", "organic-traffic"],
"published_at": "2026-03-28T10:00:00Z",
"updated_at": "2026-03-29T14:30:00Z",
"word_count": 1850
}Returns 404 if the slug doesn't exist in this project or the matching blog isn't published.
Get blog sitemap data#
GET /blog/api/v1/public/blogs/sitemap?api_key=YOUR_API_KEYReturns a lightweight list of all published blogs for generating your XML sitemap. Each entry includes only slug, published_at, and updated_at — just enough to build <loc> and <lastmod> entries.
This endpoint returns up to 5,000 blogs in a single response (newest first), with no pagination. If a project ever publishes more than 5,000 posts, the sitemap call returns the most recent 5,000. The total field is the number of blogs actually returned.
Response:
{
"blogs": [
{
"slug": "seo-tips-small-businesses",
"published_at": "2026-03-28T10:00:00Z",
"updated_at": "2026-03-29T14:30:00Z"
}
],
"total": 42
}Content format#
The content field is HTML — the same body HTML theStacc generates for your post. It includes:
- Headings (
h2,h3,h4) - Paragraphs, lists, and blockquotes
- Links (internal and external)
- Images with alt text
- Inline styles for formatting
You can render it directly with set:html (Astro), dangerouslySetInnerHTML (React/Next.js), or | safe (Hugo). Apply your own CSS to match your site's design.
The featured image is separate from the body#
featured_image_url is a standalone field, not part of the content HTML. theStacc treats your post's hero image (or, if there's no dedicated hero, the first generated image) as a structured field so you can render it however you like — as a hero banner above the article, as a card thumbnail in your listing, or in your social/OG tags.
The body content is the article itself and is not prefixed with the featured image, so the hero won't appear twice. In your template, render featured_image_url as the hero and render content as the body. Any other images that belong inside the article stay inside content as normal <img> tags.
Cross-post links are resolved for you#
When theStacc writes a post that links to another one of your posts, it stores that link as an internal placeholder ([INTERNAL:other-post-slug]) rather than a hard URL, so the link survives no matter where you publish. The Public API resolves these placeholders for you on every read whenever it returns body content (the single-blog endpoint, and the list endpoint when include_content=true):
- If the referenced slug matches another published post in the same project, the placeholder becomes a real link to that post on your site, using the path convention
your-site.com/blog/<slug>/. The base URL comes from the website connected to your project; if no site URL is set, the link is rendered as a root-relative/blog/<slug>/path. - If the referenced post isn't published yet (or the slug doesn't match anything), the link is dropped but the anchor text is kept. You'll never receive a broken link or a raw
[INTERNAL:...]placeholder.
Because resolution happens at read time, fixing or publishing the target post is reflected on your next fetch — you don't need to regenerate or republish the post that links to it.
This matches the your-site.com/blog/<slug>/ URL pattern used throughout the Static Site Integration guide. If your blog lives at a different path, you'll want to keep that path as /blog/ (or adjust your routing accordingly) so resolved internal links point to the right place.
Self-hosting images#
Images referenced in content and featured_image_url are served from theStacc storage. For best performance and to avoid depending on our storage, many teams download these images during their build and rewrite the URLs to their own CDN. This is optional — the URLs work as-is.
Rate limits#
The Public Blog API is designed for build-time fetching, not real-time client requests. A typical static site build makes a handful of requests (a list or sitemap call, plus one per post). Fetch the content during your build, generate static pages, and serve those — don't call the API from client-side browser code on every page view.
If you have a very large catalog, page through the list endpoint with limit/offset rather than requesting everything at once, and use the lightweight sitemap endpoint (which returns just slugs and dates) when you only need URLs.
Error handling#
All errors return a JSON object with a detail field describing what went wrong:
{
"detail": "Invalid API key. Check that your key is correct and hasn't been revoked. You can find your API key in Settings > Publishing > API Access."
}Status codes you may see:
401— Missing or invalid API key. The key wasn't supplied, or it doesn't match any project (often because it was rotated or revoked). Check the value in your build environment.404— Blog not found or not published. The slug doesn't exist in this project, or the matching post isn't published. (Only the single-blog endpoint returns this.)500— Server error. Transient — retry after a few seconds.
In your fetch helper, treat a non-200 response as a failure: throw on the list/sitemap calls so a broken build is obvious, and return null on the single-blog call so you can render a 404 page for a missing slug.
Next steps#
- Follow Static Site Integration for framework-specific setup (Astro, Next.js, Hugo, and more)
- Set up Deploy Hooks so your site rebuilds automatically when new content is published
- Want an AI assistant or automation to create and publish posts on your behalf? That's a different, write-capable credential — see Agent Keys & MCP