Content API
The Content API is how your website reads its published posts. It's plain HTTPS and JSON, so it works with any language or framework.
Basics
- Base URL:
https://blogsforseo.com/api/v1/sites/{SITE_KEY} - Authentication: none. The site key is public, and the API only ever returns published posts.
- Methods:
GETonly. CORS is open, so browsers can call it too. Render posts on your server anyway, so crawlers can read them. - Caching: responses send
Cache-Control: public, max-age=60, s-maxage=300, stale-while-revalidate=86400. Cache on your side too. - Indexing: responses carry
X-Robots-Tag: noindex, so the API itself never shows up in search. Your pages do.
Endpoints
| Endpoint | Returns |
|---|---|
GET / |
Site details: { name, url, blogUrlPattern } |
GET /posts?page=1&pageSize=12 |
{ posts: PostSummary[], page, totalPages, total }, newest first. pageSize 1–50 |
GET /posts/{slug} |
One Post, or 404 |
GET /sitemap |
[{ url, lastmod }] for every live post |
GET /llms.txt |
Your posts as a plain-text llms.txt index |
GET /sitemap.xml |
The same, as a sitemap file, for a Sitemap: line in your robots.txt |
GET /html/posts?page=1 |
{ body, page, totalPages, total }: a page of the blog index as finished HTML |
GET /html/posts/{slug} |
{ head, body, meta }: one post as finished HTML (see below), or 404 |
curl https://blogsforseo.com/api/v1/sites/{SITE_KEY}/posts
curl https://blogsforseo.com/api/v1/sites/{SITE_KEY}/posts/what-is-llms-txt
HTML endpoints
The /html endpoints power the simple way. They return unstyled HTML with bfs- class names:
head: meta tags for<head>: title, description, canonical and Open Graph.body: the whole article, ending with itsapplication/ld+jsonstructured data.meta: the same values ashead, as data:{ title, description, canonical, openGraph, twitter }.
Every value is escaped, and links are http(s) only.
PostSummary
| Field | Type | Notes |
|---|---|---|
slug |
string | Lowercase words joined by hyphens |
title |
string | |
metaDescription |
string | |
shortAnswer |
string | A direct answer of 320 characters or fewer, written to be quoted |
canonicalUrl |
string or null | Built from your post URL pattern; null until you set the pattern |
heroImage |
{ url, alt, width, height } or null |
|
tags |
string[] | |
publishedAt |
ISO 8601 | |
updatedAt |
ISO 8601 | Changes only when what readers see changes |
readingMinutes |
number |
Post
Everything in PostSummary, plus:
| Field | Type | Notes |
|---|---|---|
metaTitle |
string | For <title> |
keyTakeaways |
string[] | |
bodyHtml |
string | Sanitised HTML, with heading ids and up to 5 links to your other posts |
bodyMarkdown |
string | The same body, as Markdown |
toc |
{ id, text, level }[] |
level is 2 or 3; link to #id |
faqs |
{ question, answer }[] |
|
sources |
{ label, url, asOf }[] |
Where each fact came from, and when it was true |
related |
{ slug, title, metaDescription, canonicalUrl }[] |
Up to 3 |
author |
{ name, type, jobTitle?, bio?, avatarUrl?, url? } |
type is Person or Organization |
jsonLd |
object | A ready schema.org @graph: BlogPosting, FAQPage and BreadcrumbList |
version |
number | Goes up on every published change |
When you put jsonLd in a <script type="application/ld+json">, replace every < with \u003c.
Errors
Every error looks like this:
{ "error": { "code": "not_found", "message": "No published post with that slug." } }
| Status | Code | When |
|---|---|---|
| 400 | bad_request |
A bad query value, such as pageSize=500 |
| 404 | not_found |
Unknown site key or slug, or 30 days after the site's subscription ended |
| 429 | rate_limited |
Over 600 requests a minute for one site from one caller; wait for Retry-After |
| 500 | internal |
Our fault; retry shortly |