WordPress Posts & Pages API — Any WP Site, No Login avatar

WordPress Posts & Pages API — Any WP Site, No Login

Pricing

from $0.48 / 1,000 posts

Go to Apify Store
WordPress Posts & Pages API — Any WP Site, No Login

WordPress Posts & Pages API — Any WP Site, No Login

Every WordPress site has a public JSON API; this reads it. Posts and pages from any WP site as clean rows: full text or Markdown, categories and tags by name, featured image, dates in UTC, word count. Free rows for sites that are not WordPress or have the API off. No login, no browser.

Pricing

from $0.48 / 1,000 posts

Rating

0.0

(0)

Developer

Insight Solutions

Insight Solutions

Maintained by Community

Actor stats

0

Bookmarked

2

Total users

1

Monthly active users

9 days ago

Last modified

Share

Every WordPress site has a public JSON API; this reads it — no browser, no key, full text as Markdown. Give it any WordPress site and get its posts (and pages) back as clean rows: title, URL, publish and modified dates in UTC, excerpt, the whole body as plain text, Markdown or HTML, word count, categories and tags by name, featured image, format and sticky flag. Newest first, filtered by search, category, tag or date window, with a cap per site.

About four in ten websites run WordPress, and unless the owner has switched it off, each one publishes its content at /wp-json/wp/v2/. This Actor reads that API directly — the same data the site's own editor and apps use — instead of scraping the HTML theme around it.

Why not scrape the pages? A theme wraps every post in navigation, sidebars, share buttons and comment forms that differ on every site. The REST API sends the post itself: wordpress.org/news answered ten whole posts as 336 KB of JSON in 0.8 seconds, already split into title, dates, body, categories and image.

At a glance

Input — this is the Store prefill; paste it and run:

{ "sites": ["https://wordpress.org/news"], "contentTypes": ["posts"], "textFormat": "text", "maxItemsPerSite": 20 }

The twenty newest posts from the WordPress news blog, as plain text, plus one free row describing the site.

Output — one row per post or page; the fields you will use most are title, url, publishedAt, text (or markdown), categories and imageUrl (full list under Output reference). Every site that could not be read — not WordPress, REST API switched off, blocked, rate-limited — comes back as a free diagnostic row (rowType: "diagnostic", errorType, error) instead of a charge.

Price — $0.80 per 1,000 posts on the FREE tier (+ $0.001 per run, charged only once a run has returned a post or page). Site rows and diagnostic rows are free, and a run that returns nothing bills nothing at all. Pay per event, no API key, no browser, limited permissions — works over the Apify MCP server (mcp.apify.com) and with agentic (x402) payments.

From code — client.actor("insight.solutions/wordpress-posts-api").call(run_input={…}) with apify-client, or

POST https://api.apify.com/v2/acts/insight.solutions~wordpress-posts-api/run-sync-get-dataset-items
.


Who it is for

  • Content audits — every post on a site with its word count, categories, tags, publish and modified dates, in one table: find the thin posts, the stale ones and the categories nobody writes in any more.
  • RAG corpora and AI training sets — whole bodies as Markdown, headings, lists, links and code blocks kept, one row per post with a stable URL and id.
  • Competitor and industry blogs — what a set of companies published this quarter (publishedAfter), in which categories, at what length.
  • Migrations and backups — posts and pages with their HTML exactly as WordPress rendered it (textFormat: "html"), slugs, page tree (parentId, menuOrder) and featured images.

What you get

A post (abridged — the null columns and the end of the long strings are cut here; from wordpress.org/news):

{
"ok": true,
"rowType": "post",
"input": "https://wordpress.org/news",
"source": "wordpress.org",
"sourceUrl": "https://wordpress.org/news/wp-json/wp/v2/posts/21709",
"postId": 21709,
"type": "post",
"title": "WordPress Takes Its Turn Leading the Open Website Alliance",
"url": "https://wordpress.org/news/2026/09/owa-president/",
"slug": "owa-president",
"publishedAt": "2026-09-21T14:28:41Z",
"modifiedAt": "2026-09-21T14:28:41Z",
"excerpt": "I’m happy to announce that I am now serving as president of the Open Website Alliance (OWA), representing the WordPress Foundation. …",
"text": "I’m happy to announce that I am now serving as president of the Open Website Alliance (OWA), representing the WordPress Foundation. The Alliance brings together…",
"wordCount": 239,
"textTruncated": false,
"contentProtected": false,
"format": "standard",
"sticky": false,
"categoryIds": [10],
"tagIds": [648, 647],
"featuredMediaId": 21712,
"imageUrl": "https://i0.wp.com/wordpress.org/news/files/2026/09/image-3.png?fit=1600%2C900&ssl=1",
"commentStatus": "closed",
"author": null,
"rank": 2
}

Categories and tags come back by name as well as by id. From TechCrunch:

{
"title": "OpenAI will start watermarking ChatGPT’s text in the EU",
"categories": ["AI", "Government & Policy"],
"categorySlugs": ["artificial-intelligence", "government-policy"],
"categoryIds": [577047203, 577065682, 17396]
}

17396 was not in the categories answer this sample was built from, so it stays in categoryIds and is left out of the names rather than guessed at.

With textFormat: "markdown" the body keeps its structure:

This security release features a fix for a critical severity security vulnerability.
**Because this is a security release, it is recommended that you update your sites immediately.**
You can [download WordPress 7.1.2 from WordPress.org](https://wordpress.org/download/), or visit your WordPress Dashboard, click “Updates”, and then click “Update Now”. …
## Security update included in this release

And one free site row per WordPress site (from TechCrunch):

{
"rowType": "site",
"siteName": "TechCrunch",
"siteUrl": "https://techcrunch.com",
"description": "Startup and Technology News",
"timezone": "America/Los_Angeles",
"wordpress": true,
"restApi": "open",
"totalPosts": 262648,
"returned": 20,
"filteredOut": 0
}

Quick start

  1. Put one or more sites in WordPress sites — https://example.com, example.com/blog, or any post URL from the site.
  2. Pick posts, pages or both, and a body format: plain text, Markdown, or the HTML as WordPress rendered it.
  3. Optionally narrow it: Search, Categories, Tags, Published after / before.
  4. Set Max items per site and run. Rows arrive newest first.

Input

FieldTypeDefaultWhat it does
sitesstring[]prefill ["https://wordpress.org/news"]Site URLs, scheme optional. A subdirectory install (example.com/blog) is tried at its own path first, then at the host root. A pasted post, feed or /wp-json/ URL is read as the site it belongs to. Duplicates are dropped.
contentTypesstring[]["posts"]posts, pages, or both (in the order listed).
searchstring""WordPress's own search (title, excerpt, content), sent to the list endpoint. Results stay newest first.
categories / tagsstring[][]Ids, slugs (artificial-intelligence) or archive URLs (…/category/venture/). A slug is looked up once per site; a slug a site does not have is a free row. Pages have no categories or tags, so pages are skipped when one is set.
publishedAfter / publishedBeforestring—2026-09-01 or 2026-09-01T12:00:00. Sent as WordPress's after / before. A bare date is midnight at the start of that day.
textFormatselecttexttext fills text; markdown fills markdown; html fills html with the body as WordPress rendered it.
maxTextCharsinteger50000Longer bodies are cut and textTruncated is set. wordCount always counts the whole post.
includeFeaturedImagebooleantrueFills imageUrl and imageAlt: one extra request per page of up to 100 posts.
maxItemsPerSiteinteger50 (prefill 20)1–10,000. The most posts and pages, together, read from each site.
maxConcurrencyinteger3Sites in flight at once (1–10).
maxRunSecsinteger24030–3,600. Rows already returned are kept when it runs out.
proxyConfigurationobject{ "useApifyProxy": true }Apify datacenter proxy by default; switch to residential for sites that refuse datacenter addresses.

A run with no usable site — nothing in sites, or nothing that parses as a URL — or a date filter that is not a date finishes FAILED before a single request, with a free row saying which entry was the problem. A date typo is refused rather than ignored, so it can never turn a filtered run into an unfiltered (and billed) one.


Output reference

Every row has the same columns, null where a column does not apply, so the dataset exports as one table.

On every row: ok, rowType (post · page · site · diagnostic), input, error, errorType, scrapedAt, source (the site's host), sourceUrl (the REST URL behind the row).

Posts and pages:

ColumnWhat it is
postId, type, slug, urlWordPress's id, type, slug and public URL.
titlePlain text, entities decoded (Lucid Motors’ becomes Lucid Motors’).
publishedAt, modifiedAtUTC with a Z, from WordPress's date_gmt / modified_gmt.
excerptOne paragraph of plain text. WordPress's […] and a theme's "Continue reading →" link both end in a single ….
text / markdown / htmlThe body, in the format you chose. Scripts, video players, embeds, lightbox buttons and icons are removed from text and markdown; html is untouched.
wordCount, textTruncatedWords in the whole body; whether maxTextChars cut it.
contentProtectedtrue for a password-protected post: the body and excerpt are then null (WordPress does not send them).
format, stickyPost format (standard, aside, video…) and whether the post is pinned. Posts only.
categories, categorySlugs, categoryIdsNames, slugs and ids. An id the site does not return is kept in the ids and left out of the names.
tags, tagSlugs, tagIdsThe same for tags.
featuredMediaId, imageUrl, imageAltThe featured image.
commentStatusopen or closed. Comments themselves are never read.
parentId, menuOrderPages only: the page tree.
authorAlways null, by design — see below.
siteName, rankThe site's name; 1 for its newest item, then 2, 3…

Site rows (free, one per WordPress site): siteUrl, siteName, description, timezone, wordpress (true), restApi (open · disabled · missing), totalPosts (the site's own post count), totalWpPages (its page count, when pages were asked for), returned (rows this run delivered from it) and filteredOut (how many of its posts your filters excluded — one extra tiny request, only when a filter is set).

Diagnostic rows (free):

errorTypeMeaning
invalid-inputAn entry that is not a URL, a category or tag slug the site does not have, a filter WordPress refused, or pages asked for with a category filter.
not-wordpressNo REST API index at /wp-json/ or ?rest_route=/, or one without the core wp/v2 API.
rest-disabledWordPress answered but refuses its API to visitors (401/403, rest_login_required, rest_forbidden), or has removed the posts route (rest_no_route). Also a 403 page that WordPress itself served.
blockedA 403 or a bot-protection page, twice, from two exit IPs.
rate-limitedHTTP 429, twice, from two exit IPs.
http, timeoutAny other failure, and a site that did not answer in time.
deadlineThe run's time budget ran out while a site was being read, or before it started. Rows already returned are kept.
no-postsThe site publishes none, or none match your filters.
internalThis Actor's own error. The run is then FAILED.

How a site is read

For each site, at most five kinds of request, never one per post:

  1. GET /wp-json/?_fields=name,description,…,namespaces — is this WordPress, and is its wp/v2 API on. Asked with _fields, because the full index is 798 KB on TechCrunch (636 routes).
  2. GET /wp/v2/posts?per_page=…&orderby=date&order=desc&_fields=… — the rows, up to 100 per request, paged with x-wp-total, x-wp-totalpages and Link: rel="next".
  3. GET /wp/v2/media?include=<ids> — featured images, one request per page of posts. When a site does not return an attachment there, the post's own Jetpack image URL is used.
  4. GET /wp/v2/categories?include=<ids> and /wp/v2/tags?include=<ids> — names, cached for the site, so an id is looked up once per run.
  5. Only with a filter: one per_page=1 request for the site's unfiltered total.

_embed is never used: on TechCrunch it turned ten posts into 13 MB. Each site gets at most two requests in flight, 300–600 ms apart, through its own proxy session, however many sites run at once. Every request carries a User-Agent naming this Actor, so a site owner can see what read their API.

Bylines are not included, by design. WordPress author names are personal data. This Actor never requests the author field, never calls /wp/v2/users and never reads comments, so no row can carry a byline or a commenter. The author column exists so you can see that, and it is null on every row.

Reading 1,000 posts of a news site with text and images is about 10 list requests, 10 media requests and a handful of term requests (only ids not seen on an earlier page are looked up).


What you are never charged for

  • Every site row.
  • Every diagnostic row: sites that are not WordPress, REST APIs that are switched off, blocked or rate-limited requests, timeouts, slugs that do not exist, filters that match nothing.
  • Posts your filters exclude — WordPress applies them before anything is downloaded.
  • A post that moves onto the next page while the walk is under way (it is dropped before it can be counted twice).
  • A second sites entry that turns out to be the same install as an earlier one — two post URLs from one blog, say. It is read once, and the second entry gets a free row saying so.
  • A run that returns nothing at all: it finishes SUCCEEDED with zero results and a "0 results … Nothing was charged." status message, and bills nothing, start fee included.

Password-protected posts are still post rows (title, URL, dates, categories) and are billed as posts; their body is null because WordPress does not send it.


Pricing

EventWhat it isFREEBRONZESILVERGOLD
actor-startCharged once per run, only after the run has returned a post or page$0.001$0.001$0.001$0.001
postOne post row$0.0008$0.0008$0.00064$0.00048
pageOne page row$0.0008$0.0008$0.00064$0.00048

$0.80 per 1,000 posts.

RunCost
The default input — 20 posts from one site$0.017
The last 1,000 posts of a news site, with text and images$0.801
The same 1,000 posts at GOLD$0.481
50 competitor blogs, 20 posts each$0.801
A list of sites none of which is WordPress$0.00

Charging is charge-after-push: rows are in your dataset before the event is recorded. ACTOR_MAX_TOTAL_CHARGE_USD is respected — the run stops fetching when it is reached and finishes SUCCEEDED saying so.


Proxy

The default is { "useApifyProxy": true } — Apify's datacenter pool. The capture probe read wordpress.org, TechCrunch and ma.tt through it. Some sites put their API behind a firewall that refuses datacenter addresses; for those, switch proxyConfiguration to residential. A 403 or 429 is treated as a block: that site's proxy session is rotated once, and a second refusal becomes a free blocked or rate-limited row rather than a retry loop. A 403 that WordPress itself served (a JSON rest_forbidden, or the site's own themed error page) is reported as rest-disabled — asking again from another address would not change the answer.


Use it from an AI agent, or from code

One JSON object in, one flat array out — the shape agent runtimes want. The Actor runs with limited permissions, uses pay-per-event pricing and never enters Standby, so it works over the Apify MCP server and with x402 agentic payments. The Integrations tab pushes results to Slack, a webhook, Zapier, Make, Google Sheets, Snowflake or BigQuery.

curl -X POST "https://api.apify.com/v2/acts/insight.solutions~wordpress-posts-api/run-sync-get-dataset-items?token=$APIFY_TOKEN" \
-H "Content-Type: application/json" \
-d '{"sites":["https://wordpress.org/news"],"textFormat":"markdown","publishedAfter":"2026-09-01","maxItemsPerSite":100}'
# pip install apify-client
from apify_client import ApifyClient
client = ApifyClient("<APIFY_TOKEN>")
run = client.actor("insight.solutions/wordpress-posts-api").call(run_input={
"sites": ["https://wordpress.org/news", "https://ma.tt"],
"textFormat": "markdown",
"maxItemsPerSite": 200,
})
for row in client.dataset(run["defaultDatasetId"]).iterate_items():
if row["rowType"] == "post":
print(row["publishedAt"], row["siteName"], row["title"], row["wordCount"], row["url"], sep=" | ")
elif row["rowType"] == "diagnostic":
print("could not read", row["input"], row["errorType"], row["error"])

FAQ

Do I need an account on the site, or an API key? No. Only what each site publishes to anonymous visitors is read.

What if a site has switched its REST API off? You get one free rest-disabled row for it and nothing is billed. A plugin that locks the API answers with a 401 or 403 and a rest_… error code; some sites answer with their own themed 403 page instead — both are recognised.

How do I know a site is not WordPress? It costs two or three tiny requests and returns one free not-wordpress row naming every address that was tried.

Why are some category names missing when the ids are there? The site did not return that term from its categories endpoint (it may be hidden or deleted). The id is kept in categoryIds so nothing is silently lost.

Can I get custom post types (products, events, case studies)? Not yet: this Actor reads posts and pages. Custom types live at their own routes and differ on every site.

Does publishedAfter use UTC? WordPress compares the value with each post's publish time in the site's own timezone, so a bare date means midnight on the site's clock. publishedAt on every row is UTC.

How many posts can I get from one site? Up to 10,000 per run (maxItemsPerSite), read 100 at a time.


Limitations

  • Sites that disabled the REST API cannot be read. That is the owner's choice; you get a free row saying so. This Actor never falls back to scraping the HTML theme.
  • Bylines are not included, by design (see How a site is read).
  • Only posts and pages; no custom post types, comments or users.
  • Featured-image alt text comes from the media endpoint; when a site does not return an attachment there, the image URL comes from the post's Jetpack field and imageAlt is null.
  • publishedAfter / publishedBefore follow WordPress's own comparison in the site's timezone.
  • A site's plugins can change what its API returns. The upstream format may change: this Actor reads WordPress core's documented fields and leaves a column null when a site does not send it, rather than failing the row.
  • Not affiliated with WordPress, the WordPress Foundation or Automattic. "WordPress" is used only to say which software's public API is read.

Related: for any site, WordPress or not, read from its HTML, use Website to Markdown — Content Extractor for LLMs & RAG. To get only the new posts of a blog on a schedule, use RSS & Atom Feed Monitor — every WordPress site also publishes a feed.

Our other Actors

Every Insight Solutions Actor is pay-per-result with no browser, no login and no API key, and every one of them returns free diagnostic rows instead of billing for failures. Prices are per 1,000 results.

Video, audio & social

News, documents & the web

Business, finance & jobs

Apps & games