Read API
An SEO content API over every post you planned.
The API answers the question a report asks: what did we publish, where, in which language, and what does the article say. One endpoint per need, one fixed shape, nothing to install β and no way for an integration to write back into your content.
Read the API referenceJSON over HTTPSBearer key or x-api-keyRead-only, always
What can you ask the content API for?
Two endpoints, five parameters, one envelope. That is the whole surface β which is why there is no client library to keep up to date.
The short version
- Two endpoints: a list of posts, and a single post by numeric id or by its slug: prefix and slug.
- Filters are website, status, language, limit (default 100, maximum 200) and offset.
- Every post returns the same 13 top-level keys; a value that has not been generated yet is an empty string rather than missing.
- Auth is one account-level key, sent as a Bearer token or an x-api-key header; the limit is 120 requests per minute.
- It is read-only. Nothing in the API can change a post, a date or a site.
- GET /api/v2/posts
- Every post on the account, newest first, with pagination metadata. The page size defaults to 100 and accepts up to 200; offset moves through the rest.
- GET /api/v2/posts/{id}
- One post by its numeric id, or by slug as `slug:your-post-slug`, so a CMS can look a post up by the URL it already knows.
- ?website=<domain>
- Narrow to one site β the primary domain or the blog domain, case-insensitive. The filter is applied before the count, so pagination stays correct.
- ?status=published
- Status is matched exactly: draft, pending_approval, published, unpublished, in_progress, failed_review. Unfiltered, you get them all β including what is still waiting.
- ?language=de
- One editorial line. Useful when the same topic exists in several markets and only one of them belongs in this report.
- 120 requests / minute
- Per account, with a Retry-After header when you exceed it. Enough for a nightly sync and a live internal search; not a scraping endpoint.
Two paths, two shapes
/api/v2/posts is the current path, and /api/posts is an alias of it. /api/v1/posts still serves the original flat shape, frozen, so an existing integration keeps working; new clients should start on v2. Only the post object differs β authentication, filters, status codes and pagination are identical.
The full reference
Every parameter, every field in the payload and every error code β with examples you can copy β is in the reference below. This page is the tour; that one is the contract. posts API reference
What a post looks like on the wire.
Thirteen top-level keys, every one of them always present. Three of them group the fields that belong together, and the article travels with its metadata, so a consumer never has to make a second call to find out whether a post is live.
/api/v2/posts?status=published&limit=1Authorization: Bearer xt4b_β’β’β’β’β’β’β’β’β’β’β’β’{
"data": [
{
"id": "57",
"status": "published",
"slug": "seo-content-cost-per-article",
"title": "How much does SEO content cost per article?",
"meta": {
"title": "How much does SEO content cost per article?",
"description": "Three pricing models and what each one hides."
},
"searchIntent": "commercial",
"keywords": {
"primary": "seo content cost",
"secondary": ["seo content pricing"]
},
"images": { "cover": "https://cdn.xt4b.com/β¦/cover.svg" },
"language": "en",
"website": "xt4b.com",
"publishedAt": "2026-09-11T09:00:04.118Z"
}
],
"pagination": { "limit": 1, "offset": 0, "total": 7 }
}The response is an envelope with data and pagination. A single post returns the same object in data, so one parser handles both calls.
Fields the pipeline has not produced yet are empty strings rather than absent: a planned post has a title, metadata, an intent and keywords, and no markdown, no cover and no published date. A client can render that row without a single conditional for missing keys.
The shape is a contract, not a convenience. It is what Xt4bβs own blog and its Search Console panels read, so a new column in the database cannot quietly become a new field in your integration.
The article, and the record around it.
Half of the payload is the piece a reader sees; half is the provenance an editor or a report needs.
The article
- title β the headline the article is published under
- meta.title and meta.description β written for the SERP, not lifted from the body
- markdown β the article as its source, which is what you edit
- html β the same article rendered by the publisher, heading anchors included
The record
- id and slug β stable string keys, and the URL the post lives at
- status β where the post is in the pipeline, not just live-or-not
- website and language β which site, which market
- publishedAt β ISO timestamp, empty while it is still planned
- keywords.primary, keywords.secondary and searchIntent β the brief it was written against
- images.cover β the generated cover
From zero to your first row.
Four steps, and the fourth is a curl line you can paste into a terminal or a cron job.
- 01
Create an account and connect a site
The API is a window on your own posts, so it needs a site and a plan behind it. The free scan shows you the queries a site is missing before you connect anything.
- 02
Copy the key from Settings
One key per account, shown on the settings page. Regenerating it revokes the previous key immediately, which is the whole rotation story β there is no client id, secret or callback URL.
- 03
Send it as a header
`Authorization: Bearer <key>` or `x-api-key: <key>`. Either works on every endpoint, so a script and a browser tool can use whichever is simpler.
- 04
Ask for what you need
Start with `?status=published&limit=50`, then narrow with `website` and `language` once you know the shape. Page with `offset` rather than requesting posts one at a time.
Three calls, copy and paste.
The first returns a page of live posts, the second looks one up by the slug in its URL, the third asks what is still waiting for approval.
List what is live, one site at a time
curl "https://xt4b.com/api/v2/posts?website=xt4b.com\ &status=published&limit=50" \ -H "Authorization: Bearer $XT4B_API_KEY"
One post, by the slug in its URL
curl "https://xt4b.com/api/v2/posts/\ slug:seo-content-cost-per-article" \ -H "x-api-key: $XT4B_API_KEY"
What is waiting for a decision
curl "https://xt4b.com/api/v2/posts?status=draft\ &language=de&limit=100&offset=0" \ -H "Authorization: Bearer $XT4B_API_KEY"
The error envelope
{
"error": {
"code": "rate_limited",
"message": "Too many requests β retry after 12s."
}
}What people build with it.
The four integrations that come up first, in the order they usually get built.
- 01
A report that builds itself
Fetch posts with status=published once a night and write the rows into your dashboard. No scraping your own blog, no spreadsheet transcription.
- 02
Your CMS as the destination
If the blog lives somewhere else, take the markdown (or the rendered HTML), the metadata and the images, and publish it yourself. The API is read-only, so nothing here can overwrite what your editors wrote.
- 03
Internal search you own
Filter by site and language to build a search box or a related-posts block over your own back catalogue β including the posts still in draft.
- 04
A QA queue
Ask for status=draft or failed_review and review what the pipeline produced before it goes live, using your own tooling instead of ours.
What the API is not.
Worth stating plainly, because a read API is easy to over-promise.
- Not a generation endpoint
- There is no POST that asks for an article. Content is planned and written by the pipeline against a plan, and the API reports the result.
- Not a publishing endpoint
- Nothing you send can change a post, a date or a site. If a client wants to publish through their own CMS, they fetch and publish there β the failure mode of a bad integration is a missing post, never a wrong one.
- Not per-site keys
- One key per account, with ?website= as the filter. Simpler to rotate, and it means an agency integration is configured once rather than per client.
- Not a promise about rankings
- It reports what was published, and what a connected Search Console shows. It cannot report a position that does not exist yet, and none of it guarantees traffic or revenue β ranking, traffic and revenue are not guaranteed by this or any other API.
More on content
- Multilingual SEO content
A language is an editorial line, not a translation of your English pages.
- Hosted blog
A blog on your own domain behind one DNS record, maintained for you.
- For multilingual markets
Which market to open next, and what it costs.
- llms.txt
The summary file for language models, and what ours contains.
Questions developers ask.
Including the two about what it refuses to do.
Is there an endpoint that writes or publishes?
No. The API is read-only by design: it exposes what the pipeline has planned and published, and nothing about it can change a post, a date or a site. Publishing happens in the dashboard, where a person is making the decision.
What does authentication look like?
One key per account, shown in your settings, sent either as `Authorization: Bearer <key>` or as an `x-api-key` header. Regenerating the key revokes the old one immediately, and there is no OAuth dance, no client secret and nothing to install.
Do I need an SDK?
No. It is JSON over HTTPS with one filter set and one envelope β a fetch call in any language, or a curl line in a cron. The response shape is fixed: every key is present for every post, and fields that have not been generated yet come back as null rather than missing.
Why are markdown and rendered HTML both returned?
Because most consumers need one of them. Markdown is what you edit and store; the HTML is the article as the publisher renders it, with heading anchors and hardened links, so a site can serve a post without parsing markdown itself. Neither is derived at request time β both are read from the stored row.
Does the key see every site on the account?
Yes β keys are per account, and sites are filtered with ?website=. For an agency that means one integration for every client, with the site filter as the only thing that has to change per report.
What happens when I hit the rate limit?
You get a 429 with a Retry-After header, and the counter is per account in a fixed one-minute window. Page through with limit and offset rather than firing requests per post: limit accepts up to 200.
How do I get an API key?
It is generated for you: create an account, and the key appears on the settings page, ready to copy. There is no application form, no OAuth client to register and no per-site key to distribute.
Can I filter by date range?
No β the filters are website, status, language, limit and offset. Posts arrive newest first, so a date window is a page-through: take 200 at a time and stop when publishedAt is older than the window you care about. That is deliberate: a half-supported date filter would be worse than an honest page-through.
Does the API return posts that are not published yet?
Yes, unless you filter them out. An unfiltered read returns every post with its status, including drafts, posts waiting for approval and ones that failed review β which is what makes a QA queue possible. Most reporting integrations start with ?status=published.
Is there a sandbox or test mode?
No. There is one API and one key. Because the API is read-only, pointing it at a real account is harmless β the worst a mistake can do is return rows you did not need.
What do errors look like?
The same envelope as the data: an `error` object with a machine-readable `code` and a message β invalid_api_key (401), invalid_post_id (400), post_not_found (404), rate_limited (429). Nothing returns a bare string, so a client can branch on the code rather than parse prose.
Can I get the data without using the hosted blog?
That is the point of publishing the API. The blog is the default destination because it removes the integration work entirely, but the same posts are available as data, so an existing CMS, a static site build or a data warehouse can be the destination instead.
One key, every site
Publish on your own domain, read it as data.
Create an account and connect a site: the pipeline produces the posts, the blog serves them, and the API hands you the rows to prove it.
Markdown and rendered HTML120 requests a minute3 days free, no card