WordPress Content Scraper avatar

WordPress Content Scraper

Pricing

from $2.10 / 1,000 results

Go to Apify Store
WordPress Content Scraper

WordPress Content Scraper

Scrapes any self-hosted WordPress site via its public REST API — posts, pages, media, categories, tags, comments, users, even custom post types. No login. Raw JSON passthrough, honest pagination, and a clear error row for sites that disable their REST API.

Pricing

from $2.10 / 1,000 results

Rating

0.0

(0)

Developer

Ibnu Adzim

Ibnu Adzim

Maintained by Community

Actor stats

0

Bookmarked

2

Total users

1

Monthly active users

5 days ago

Last modified

Categories

Share

Scrapes any self-hosted WordPress site through its public wp-json/wp/v2 REST API — the same API WordPress core exposes, unauthenticated, on every install by default. No login, no cookies, no browser.

Point it at one or more site URLs and pick which content types to pull — posts, pages, media, comments, categories, tags, users, or even custom post types your theme/plugins registered. Every pick shares one client, so fetching several types from the same site costs no extra setup.

Why one actor, not one per content type

Every WordPress collection endpoint — built-in or custom — answers the identical shape:

GET {site}/wp-json/wp/v2/{resource}?page=N&per_page=M
-> 200, a JSON array, with X-WP-Total / X-WP-TotalPages response headers

posts, categories, comments, a WooCommerce product — they are not different modes of one linear chain, they are independent resources a user may reasonably want several of from the same site in the same run. Splitting that into N actors would re-pay the per-site setup N times for no benefit; the resources input is a checklist, not a mode switch.

What you get

Three record types share one dataset, told apart by recordType.

POST / PAGE / MEDIA / CATEGORY / TAG / COMMENT / USER / CUSTOM_<slug>

Upstream's JSON object, passed through verbatim — every field WordPress returns, unrenamed. With "Embed related objects" on (the default), each row also carries an _embedded object with the author, featured image and taxonomy terms already inlined, so you are not making a second request per row to resolve them.

SEARCH_SUMMARY — one row per (site, resource type)

WordPress's own true total (X-WP-Total) and page count (X-WP-TotalPages), how many items this run actually pulled, the filters that were sent, and whether the per-resource item cap was hit.

ERROR — one row per (site, resource) that failed, so nothing vanishes silently

Every failure is classified by why, not just that it failed:

_errorMeaning
rest_api_disabledThis install turned off unauthenticated REST access site-wide (HTTP 401). A site-owner setting, not a block.
forbiddenWordPress's own policy answer for this specific resource (HTTP 403 with a rest_* code) — e.g. comments turned off. Also not a block; retrying would not help.
not_foundThe resource route doesn't exist on this install (wrong resource/custom-type slug, or the URL isn't a WordPress site at all).
bad_requestThe query itself was malformed (shouldn't happen in normal use — the actor stops paging before this can occur).
fetch_failedGenuine transient failure (DNS, timeout, connection reset, rate limiting) that persisted across every retry.
unexpected_shapeUpstream returned 200 but not a JSON array — the API may have changed.
invalid_inputThe run's own input failed validation (e.g. perPage out of range) before any request was made.

Input

FieldWhat it does
WordPress site URLsOne or more base URLs, e.g. https://example.com
Content types to fetchAny combination of posts / pages / media / comments / categories / tags / users
Custom post type REST base slugsExtra show_in_rest post types your theme/plugins registered
Search queryWordPress's own full-text search param, applied to every selected resource
Published after / beforeISO 8601 date range (posts/pages/media/comments/custom types only — categories/tags/users have no date field)
Embed related objects_embed=true — inlines author/featured-image/terms. On by default.
Items per requestper_page, capped at 100 by the API
Max items per (site, resource) pair0 = unlimited (walks every page WordPress reports)
Max concurrent requestsAcross every site and resource this run touches
Min seconds between request starts, per sitePoliteness pacing applied separately to each site — see "Notes on reliability"

Example

{
"startUrls": ["https://wptavern.com"],
"resources": ["posts", "categories", "tags"],
"search": "gutenberg",
"maxItemsPerResource": 100,
"perPage": 100
}

Notes on reliability

  • No WAF observed anywhere tested — including sites sitting behind Cloudflare. The retry/rotation ladder exists for transport flakiness and real (if occasional) rate-limiting, not bot mitigation — see CRAWLING_METHOD.md.
  • WordPress's own 401/403 policy answers are told apart from blocks. A site that disabled its REST API, or turned off comments, gets one clear diagnostic row instead of four wasted retries and a generic failure.
  • Pagination is honest on every site tested: X-WP-TotalPages is trustworthy, per_page/page out-of-range answers are a clean 400 (not a silent clamp or a re-served page 1), and a bogus filter value returns an honest zero rather than the unfiltered baseline.
  • Per-site politeness pacing, not global. Inputs here are arbitrary user-supplied sites of unknown scale — a small blog on shared hosting next to a large outlet on enterprise infrastructure — so the pacing floor applies separately to each site rather than being shared across unrelated domains.
  • Public content only, by construction. An unauthenticated request can never see drafts, private posts, or unapproved comments — there is nothing to accidentally over-collect.

Known limits

  • No per-site taxonomy filtering by name. WordPress's categories/ tags filters take numeric IDs, which differ on every install, so a generic multi-site actor can't ship a fixed slug list the way a single-publisher scraper can. Use Search query or the Published after/before date range instead, or fetch the categories/tags resource first to look up a site's own IDs.
  • A site can turn its REST API off entirely, or disable specific resources (comments is the common one). That surfaces as a clear ERROR row (rest_api_disabled / forbidden), not a silent empty result — but there is no bypass, by design: this is a deliberate site-owner setting, not a technical restriction to route around.
  • Custom post type slugs must actually be registered with show_in_rest. A guessed slug that isn't gets a clean not_found row for that resource — check the site's own /wp-json/ route index if unsure which custom types it exposes.
  • You are responsible for using this actor consistent with each target site's terms of use and robots.txt — it queries whatever URLs you provide via WordPress's own public API and applies no site-specific policy of its own.