πŸ–ΌοΈ Facebook Posts Media & OCR Content Harvester avatar

πŸ–ΌοΈ Facebook Posts Media & OCR Content Harvester

Pricing

$24.99/month + usage

Go to Apify Store
πŸ–ΌοΈ Facebook Posts Media & OCR Content Harvester

πŸ–ΌοΈ Facebook Posts Media & OCR Content Harvester

This Facebook Posts Scraper extracts text posts, media, reactions, shares, comments, and metadata from public pages and profiles. Ideal for trend tracking, social listening, competitor analysis, and building large-scale Facebook intelligence.

Pricing

$24.99/month + usage

Rating

5.0

(2)

Developer

Scrapier

Scrapier

Maintained by Community

Actor stats

2

Bookmarked

97

Total users

2

Monthly active users

13 days ago

Last modified

Share

Facebook Posts Scraper β€” Extract Post Images, OCR Text and Entities

Facebook Posts Media & OCR Content Harvester turns a Facebook Page's already-scraped post timeline into a structured creative dataset: a per-post image-asset manifest (full-resolution URL, width, height, id), the OCR image-text pulled from Facebook's own accessibility captions, and real entities β€” links, emails, phone numbers, mentions, hashtags, prices, and call-to-action phrases β€” found in the caption and image text. Every response is structured JSON, ready to pass directly to an LLM, index into a vector store, or feed a monitoring pipeline. No extra Facebook requests are made to get there: the feature layer is pure reshaping of data the timeline scrape already fetched.

What is Facebook Posts Media & OCR Content Harvester?

It is a Facebook Page post scraper that reshapes each scraped post into a namespaced creative block on top of the pristine base post fields β€” the per-post image manifest, the OCR image-text, and the extracted entities β€” plus an optional AI enrichment pass. No Facebook account or login is required: src/html_loader.py bootstraps each target from the live, publicly loaded page HTML (entity id, GraphQL doc_id, and x-fb-friendly-name), and the timeline is fetched over Facebook's own GraphQL endpoint with cursor-based pagination.

  • Per-post image manifest β€” creative.images[], one entry per photo asset with url, width, height, id, an auto-generated suggestedFilename, and ocrText when present.
  • OCR image-text β€” creative.imageText and hasTextInImage, read from Facebook's own accessibility caption on each image.
  • Real entity extraction β€” creative.entities (urls, emails, phones, mentions, hashtags, prices, ctaMatches), pulled as literal substrings of the post caption plus the OCR text β€” only non-empty types are attached.
  • Optional AI enrichment β€” aiEnhancement.{sentiment, topics, brandSafety, language}, run over the same creative free-text when aiEnhancement is on.
  • Query controls that actually filter the run β€” resultsLimit (per target page), onlyPostsNewerThan / onlyPostsOlderThan (absolute or relative dates), ocrKeywords (keeps only posts whose caption or OCR text matches), and captionText to blank the caption while still running OCR/entities against image text.
  • Three output shapes β€” the default wrapped row, metricsOnly (identity + creative only), and rawData (byte-identical passthrough of the base post fields, no creative block).

What data can you get with Facebook Posts Media & OCR Content Harvester?

Every dataset row starts from the pristine base post record and, unless rawData is on, adds a creative block plus a handful of surfaced view fields.

Result typeExtracted fieldsPrimary use case
Base post recordfacebookUrl, postId, pageName, url, time, timestamp, user.{id,name,profileUrl,profilePic}, collaborators, text, likes, comments, shares, topReactionsCount, media[], feedbackId, reactionLikeCount/reactionLoveCount/reactionHahaCount/reactionCareCount/reactionSadCount/reactionWowCount/reactionAngryCount, topLevelUrl, facebookId, pageAdLibrary.id, inputUrlTimeline monitoring, engagement analytics
Image-asset manifestcreative.images[].{url, width, height, id, suggestedFilename, ocrText}, creative.imageCountBulk creative download, ad-asset audits
OCR image-textcreative.imageText, creative.hasTextInImage, top-level imageText / hasTextInImageText-in-image compliance checks, creative copy mining
Entitiescreative.entities.{urls, emails, phones, mentions, hashtags, prices, ctaMatches}, top-level entitySummaryLead extraction, CTA and hashtag tracking
AI enrichment (opt-in)aiEnhancement.{sentiment, topics, brandSafety, language}Brand-safety triage, topic/sentiment classification

OCR image-text and the image-asset manifest

This is what a standard Facebook Posts scraper does not surface: as observed on the Apify Store on 2026-07-26, apify/facebook-posts-scraper's documented output covers post text, engagement metrics, media URLs, thumbnails and video transcripts, but does not document accessibility-caption OCR text or a structured per-image manifest with dimensions and id. This Actor reads media[].photo_image.{uri,width,height} and media[].id for the manifest, and media[].accessibility_caption for the OCR text β€” all values Facebook already returned in the same timeline response, so building the manifest costs no extra request. A caption only becomes ocrText when Facebook's own caption contains quoted text (e.g. May be an image of fireworks and text "50% OFF TODAY"); generic captions like "May be an image of fireworks" are recognized as containing no real text and are not surfaced as OCR:

"creative": {
"imageCount": 1,
"images": [
{
"url": "https://scontent.xx.fbcdn.net/.../618970043_n.jpg",
"width": 960,
"height": 540,
"id": "1315686073934452",
"suggestedFilename": "1315686150601111_0.jpg",
"ocrText": "50% OFF TODAY"
}
],
"imageText": "50% OFF TODAY",
"hasTextInImage": true
}

Entity extraction

creative.entities is built from real regex matches over the post caption plus the OCR image-text combined β€” URLs, emails, phone numbers, @mentions, #hashtags, prices ($, €, Β£), and a fixed vocabulary of call-to-action phrases (shop now, sign up, link in bio, buy now, and 14 others) matched as literal substrings. Only entity types that actually matched are attached to the object, so a post with no links has no entities.urls key at all rather than an empty array.

Why not build this yourself?

Facebook does not expose a public API for pulling an arbitrary Page's post timeline the way this Actor does β€” the Graph API's page-content endpoints require a page-admin access token, not a URL. Reproducing this scrape means reverse-engineering Facebook's internal GraphQL timeline query: discovering the current persisted-query doc_id and the __relay_internal__pv__* provider-variable set live from each page's HTML (both change without notice, and a stale set returns zero posts), impersonating a real browser's TLS/HTTP fingerprint to avoid the anti-bot layer, and handling 403/429/999 responses with backoff. This Actor's src/html_loader.py and src/main.py already do all of that β€” live doc_id/provider-variable discovery per page, a known-good fallback snapshot if live extraction returns nothing, browser impersonation via impit with a requests fallback, and residential-proxy-first networking. Building and maintaining that pipeline yourself means re-discovering it every time Facebook rotates the bundle; running it here means one input schema.

How to scrape Facebook Page posts with Facebook Posts Media & OCR Content Harvester?

  1. Open Facebook Posts Media & OCR Content Harvester on the Apify Store and click Try for free (or find it under your existing Apify Actors).
  2. Enter one or more targets in startUrls β€” full page URLs (https://www.facebook.com/nytimes/) or bare usernames (nytimes, Cristiano), which are normalized to full URLs automatically.
  3. Set resultsLimit for the number of posts to collect per page, and optionally onlyPostsNewerThan / onlyPostsOlderThan to bound the date range.
  4. Click Start and let the run finish (or stream results as they land in the dataset).
  5. Download the dataset as JSON or CSV from the Output tab, or pull it via the Apify API / apify-client.

How to scrape multiple Facebook Pages in one run

Add every target page as a separate entry in the startUrls array. Each target is scraped in sequence, not concurrently β€” the schema documents this explicitly ("Each target is scraped in sequence to reduce rate limits") β€” so a run against five pages with resultsLimit: 20 collects up to 100 posts total, one page at a time, with a 2-second pause between targets.

⬇️ Input

All fields below are reproduced exactly from .actor/actor.json.

ParameterRequiredTypeDefault / constraintsDescription
startUrlsYesarrayβ€”One or more Facebook page targets β€” full URL or bare username, e.g. nytimes or https://www.facebook.com/Cristiano/.
resultsLimitNointegerdefault 20; min 1; max 1000000Maximum posts to collect per target page. Scraping stops once this count is reached for each page.
captionTextNobooleandefault trueON includes the post's text and runs OCR/entity extraction over it; OFF blanks text (OCR from images still runs).
includeMediaNobooleandefault trueBuilds creative.images[] from each post's scraped media.
extractEntitiesNobooleandefault trueExtracts URLs, emails, phones, mentions, hashtags, prices and CTA matches into creative.entities.
ocrKeywordsNoarray of stringsdefault []Case-insensitive substring filter β€” keeps only posts whose caption or OCR image-text contains any of these keywords. Empty keeps every post.
mediaManifestNobooleandefault trueAlso saves a flat run-level image-asset manifest to the key-value store under MEDIA_MANIFEST. Requires includeMedia.
metricsOnlyNobooleandefault falseDrops the heavy base post fields from each row, keeping only identity fields plus the creative block.
rawDataNobooleandefault falseReturns the plain base post objects β€” byte-identical to the base Facebook Posts Scraper. No creative block, no ocrKeywords filtering, no AI enrichment.
onlyPostsNewerThanNostring (datepicker)β€”Absolute date (2024-01-01) or relative (7 days, 2 months, 1 year), counted back from today.
onlyPostsOlderThanNostring (datepicker)β€”Absolute (2024-12-31) or relative end date.
aiEnhancementNobooleandefault falseRuns an LLM pass over each post's creative free-text (caption + OCR text) to add aiEnhancement.{sentiment, topics, brandSafety, language}. No LLM calls or key needed while off.
aiModelNostring (enum)default claude-haiku-4-5Provider is auto-detected from the model name prefix. See the full enum below.
aiApiKeyNostring (secret)β€”API key for the selected provider. Falls back to the provider's environment variable (ANTHROPIC_API_KEY, OPENAI_API_KEY, GEMINI_API_KEY, XAI_API_KEY/GROK_API_KEY, DEEPSEEK_API_KEY, PERPLEXITY_API_KEY/PPLX_API_KEY, MISTRAL_API_KEY) when left empty.
proxyConfigurationNoobject (proxy editor)prefill {"useApifyProxy": false}Apify Residential proxy is tried first, then Datacenter, then any custom proxyUrls supplied here.

aiModel enum values: claude-haiku-4-5 (cheapest, default), claude-sonnet-5, claude-opus-4-8, claude-fable-5, gpt-4o-mini, gpt-4o, gpt-4.1-mini, gpt-4.1, o3-mini, o1, gemini-2.0-flash-lite, gemini-2.0-flash, gemini-1.5-pro, gemini-2.5-flash, gemini-2.5-pro, grok-2-latest, grok-3-mini, grok-3, grok-beta, deepseek-chat, deepseek-reasoner, sonar, sonar-pro, sonar-reasoning, mistral-small-latest, mistral-large-latest. Provider is detected from the prefix: claude-* β†’ Anthropic, gpt-*/o1/o3-* β†’ OpenAI, gemini-* β†’ Google, grok-* β†’ xAI, deepseek-* β†’ DeepSeek, sonar* β†’ Perplexity, mistral-* β†’ Mistral.

Example input:

{
"startUrls": ["nytimes", "https://www.facebook.com/Cristiano/"],
"resultsLimit": 30,
"captionText": true,
"includeMedia": true,
"extractEntities": true,
"ocrKeywords": ["sale", "discount"],
"mediaManifest": true,
"metricsOnly": false,
"rawData": false,
"onlyPostsNewerThan": "30 days",
"aiEnhancement": true,
"aiModel": "claude-haiku-4-5",
"proxyConfiguration": { "useApifyProxy": true }
}

Common pitfall: rawData: true short-circuits before any of ocrKeywords, metricsOnly, includeMedia, extractEntities, mediaManifest, or aiEnhancement run β€” it pushes the plain base post objects for every scraped post that passes the date filters, unfiltered. If you need OCR text, the image manifest, entities, or AI enrichment, leave rawData off.

⬆️ Output

Results are typed, normalized JSON with a consistent schema across runs, downloadable as JSON, JSONL, CSV, Excel, HTML table or XML from the Output tab. Run-level aggregates (MEDIA_INDEX, and MEDIA_MANIFEST when mediaManifest and includeMedia are both on) are written to the run's key-value store rather than the dataset.

Scraped results

Default mode (rawData: false, metricsOnly: false) β€” one row per post, base fields plus the creative block:

[
{
"facebookUrl": "https://www.facebook.com/nytimes/",
"postId": "1315686150601111",
"pageName": "nytimes",
"url": "https://www.facebook.com/nytimes/posts/pfbid02tCuZQNXY4oeg...",
"time": "2026-07-20T13:00:05.000Z",
"timestamp": 1784646005,
"user": {
"id": "100064792144187",
"name": "The New York Times",
"profileUrl": "https://www.facebook.com/100064792144187",
"profilePic": "https://scontent.xx.fbcdn.net/.../348216459_n.jpg"
},
"collaborators": [],
"text": "Summer heat is breaking records again β€” here's what it means for your city. Shop our climate coverage: nytimes.com/climate",
"likes": 146,
"comments": 3,
"shares": 27,
"topReactionsCount": 4,
"media": [
{
"thumbnail": "https://scontent.xx.fbcdn.net/.../618970043_n.jpg",
"__typename": "Photo",
"__isMedia": "Photo",
"accent_color": "FF1A0534",
"photo_product_tags": [],
"photo_image": { "uri": "https://scontent.xx.fbcdn.net/.../618970043_n.jpg", "height": 540, "width": 960 },
"url": "https://www.facebook.com/photo/?fbid=1315686073934452",
"id": "1315686073934452",
"feedback": { "can_viewer_comment": false, "id": "ZmVlZGJhY2s6MTMxNTY4NjE1MDYwMTExMQ==" },
"ocrText": "Record heat this week"
}
],
"feedbackId": "ZmVlZGJhY2s6MTMxNTY4NjE1MDYwMTExMQ==",
"reactionLikeCount": 135,
"reactionLoveCount": 8,
"reactionHahaCount": 0,
"reactionCareCount": 2,
"reactionSadCount": 0,
"reactionWowCount": 1,
"reactionAngryCount": 0,
"topLevelUrl": "https://www.facebook.com/100064792144187/posts/1315686150601111",
"facebookId": "100064792144187",
"pageAdLibrary": { "id": "100064792144187" },
"inputUrl": "https://www.facebook.com/nytimes/",
"type": "photo",
"imageCount": 1,
"imageText": "Record heat this week",
"hasTextInImage": true,
"entitySummary": "1 link Β· 1 hashtag",
"creative": {
"textLength": 118,
"wordCount": 18,
"imageCount": 1,
"images": [
{
"url": "https://scontent.xx.fbcdn.net/.../618970043_n.jpg",
"width": 960,
"height": 540,
"id": "1315686073934452",
"suggestedFilename": "1315686150601111_0.jpg",
"ocrText": "Record heat this week"
}
],
"imageText": "Record heat this week",
"hasTextInImage": true,
"entities": {
"urls": ["nytimes.com/climate"],
"ctaMatches": ["shop now"]
}
},
"aiEnhancement": {
"sentiment": "neutral",
"topics": ["climate", "weather", "news"],
"brandSafety": "safe",
"language": "en"
},
"scrapedAt": "2026-07-26T09:14:02.000Z"
},
{
"facebookUrl": "https://www.facebook.com/Cristiano/",
"postId": "1441693187314472",
"pageName": "Cristiano",
"url": "https://www.facebook.com/reel/895509256298494/",
"time": "2026-07-25T14:00:46.000Z",
"timestamp": 1784973646,
"user": {
"id": "100044214140223",
"name": "Cristiano Ronaldo",
"profileUrl": "https://www.facebook.com/100044214140223",
"profilePic": "https://scontent.xx.fbcdn.net/.../378863511_n.jpg"
},
"collaborators": [],
"text": "Back on the pitch.",
"likes": 147,
"comments": 2,
"shares": 3,
"topReactionsCount": 5,
"media": [
{ "__typename": "Video", "__isMedia": "Video", "videoId": "895509256298494", "thumbnail": "https://scontent.xx.fbcdn.net/.../thumb.jpg", "url": "https://www.facebook.com/reel/895509256298494/", "id": "895509256298494" }
],
"feedbackId": "ZmVlZGJhY2s6MTQ0MTY5MzE4NzMxNDQ3Mg==",
"reactionLikeCount": 112,
"reactionLoveCount": 29,
"reactionHahaCount": 2,
"reactionCareCount": 3,
"reactionSadCount": 0,
"reactionWowCount": 1,
"reactionAngryCount": 0,
"topLevelUrl": "https://www.facebook.com/100044214140223/posts/1441693187314472",
"facebookId": "100044214140223",
"pageAdLibrary": { "id": "100044214140223" },
"inputUrl": "https://www.facebook.com/Cristiano/",
"type": "video",
"imageCount": 0,
"creative": { "textLength": 19, "wordCount": 3, "imageCount": 0 },
"scrapedAt": "2026-07-26T09:14:07.000Z"
}
]

With metricsOnly: true, each row is reduced to postId, type, url, time, timestamp, pageName, facebookUrl, imageCount, imageText, hasTextInImage, entitySummary, creative, aiEnhancement, scrapedAt β€” whichever of those keys are present. With rawData: true, each row is exactly the base post object shown above without type, imageCount, imageText, hasTextInImage, entitySummary, creative, aiEnhancement, or scrapedAt.

How can I use the data extracted with Facebook Posts Media & OCR Content Harvester?

  • πŸ“Š Brand and creative-compliance teams: filter creative.hasTextInImage and run aiEnhancement.brandSafety to flag creative assets carrying risky or off-brand image text before a campaign goes live.
  • 🎯 Marketing and ad-intelligence teams: pull creative.entities.ctaMatches and creative.entities.prices across a competitor's page to track which calls-to-action and price points they're running.
  • πŸ€– AI engineers and LLM developers: an agent issues a run against a page, receives the structured creative block as JSON, and passes creative.imageText and creative.entities directly to the model as grounded context β€” no separate OCR pipeline required.
  • πŸ”¬ Researchers and analysts: aggregate creative.entities.hashtags and creative.entities.mentions over a page's history to map campaign themes and collaborator networks.

How do you monitor creative content over time?

Creative monitoring is watching what a Page posts as images and OCR text change from run to run, not just how engagement numbers move. Because this Actor re-derives the creative block on every run from whatever the timeline currently returns, scheduling repeated runs against the same startUrls turns a single scrape into a trend line: new postId values mark new posts, and comparing creative.imageText, creative.entities.hashtags, and creative.entities.ctaMatches between runs shows when a page switches its creative messaging or promotional language.

The fields worth diffing are creative.imageText (new text-in-image creative), creative.entities.ctaMatches (a new call-to-action phrase entering rotation), creative.entities.prices (a price change baked into an image), and the run-level MEDIA_INDEX.topOcrKeywords / MEDIA_INDEX.linkDomains saved to the key-value store, which already aggregate OCR keyword frequency and outbound link domains across the run. A practical loop: schedule the Actor to run daily against a fixed set of pages via an Apify Console Schedule, store each run's MEDIA_INDEX alongside the dataset, and alert when a keyword in topOcrKeywords or a domain in linkDomains appears that wasn't present in the previous run. The Actor itself has no built-in diffing or scheduling β€” this loop runs on top of Apify's platform-level Schedule feature and your own comparison logic.

Integrate Facebook Posts Media & OCR Content Harvester and automate your workflow

Facebook Posts Media & OCR Content Harvester works with any language or tool that can send an HTTP request through the Apify API.

REST API with Python

from apify_client import ApifyClient
client = ApifyClient("<YOUR_APIFY_TOKEN>")
run = client.actor("scrapier/facebook-posts-media-ocr-harvester").call(run_input={
"startUrls": ["nytimes"],
"resultsLimit": 20,
"extractEntities": True,
"aiEnhancement": False,
})
for item in client.dataset(run["defaultDatasetId"]).iterate_items():
print(item["postId"], item.get("creative", {}).get("imageText"))

Scheduled monitoring and delivery

The Actor has no built-in webhook or scheduling logic of its own; use Apify Console's Schedule feature to trigger recurring runs on a cron-style cadence, and Apify's platform-level webhooks (configured per Actor, not per run) to notify an external endpoint when a run finishes, then pull the dataset via the API.

Scraping publicly accessible Facebook posts is generally lawful β€” courts have held that accessing data a website makes available to any visitor, without circumventing a login wall, does not violate the U.S. Computer Fraud and Abuse Act (hiQ Labs v. LinkedIn, 9th Cir. 2019). This Actor scrapes only pages loaded without authentication and does not access private content. Because the output includes identifying fields for individual page owners and profile-style targets (user.name, user.id, profileUrl) alongside business-page content, GDPR and CCPA can attach to the personal-data portions of a run β€” treat the profile/name fields with the same care you'd apply to any personal data, and confirm your legal basis before storing them in bulk. Business-page content additionally remains subject to Facebook's own Terms of Service, independent of data-protection law. Scraping for AI training and scraping for operational monitoring carry different risk profiles. Consult your legal team for commercial use cases involving bulk data storage.

Frequently asked questions

How does Facebook Posts Media & OCR Content Harvester handle Facebook's anti-bot measures?

It impersonates a real Chrome browser's TLS/HTTP fingerprint via impit (falling back to requests if unavailable), extracts the current GraphQL doc_id and __relay_internal__pv__* provider variables live from each target page's HTML rather than hardcoding them, and backs off with exponential delay plus jitter on 403/429/999 responses, aborting a target after 5 consecutive empty/failed batches.

Does Facebook Posts Media & OCR Content Harvester extract OCR image-text?

Yes β€” creative.imageText and creative.images[].ocrText, sourced from Facebook's own auto-generated accessibility caption on each photo (not a separate vision model run by this Actor). It's only populated when Facebook's caption itself contains quoted, human-readable text; a generic caption like "May be an image of fireworks" is recognized as containing no real text and produces no ocrText.

How many posts does Facebook Posts Media & OCR Content Harvester return per run?

Up to resultsLimit posts per target page (default 20, minimum 1, maximum 1,000,000) β€” two pages at resultsLimit: 20 can return up to 40 posts total. In practice each target is also bounded by an internal timeline-request budget (60–160 GraphQL requests depending on resultsLimit) and a per-target timeout window of 6–90 minutes (max(360, min(300 + resultsLimit * 1.5, 5400)) seconds), so a target with a very high resultsLimit against a sparsely-posting page can stop before reaching the requested count.

Can I filter posts by date?

Yes β€” onlyPostsNewerThan and onlyPostsOlderThan each accept an absolute date (2024-01-01) or a relative expression (7 days, 2 months, 1 year) counted back from today. Both filters apply before a post is emitted, in every output mode including rawData.

How do I use Facebook Posts Media & OCR Content Harvester to monitor a page's creative over time?

Schedule recurring runs against the same startUrls via an Apify Console Schedule, and on each run diff the new postId values plus creative.imageText, creative.entities.ctaMatches, and the key-value store's MEDIA_INDEX.topOcrKeywords / linkDomains against the previous run's output to catch new creative or messaging.

Does Facebook Posts Media & OCR Content Harvester work with Claude, ChatGPT, and AI agent frameworks?

It's callable as an HTTP endpoint through the Apify API by any agent framework, which enables grounded agents that retrieve fresh creative data before answering. Separately, the built-in aiEnhancement option calls an LLM directly inside the run (Claude, GPT, Gemini, Grok, DeepSeek, Sonar or Mistral models, auto-detected from aiModel) to classify each post's sentiment, topics, brand safety and language β€” no external agent required for that step. No MCP server is documented for this Actor.

How does Facebook Posts Media & OCR Content Harvester compare to other Facebook scrapers?

As observed on the Apify Store on 2026-07-26: apify/facebook-posts-scraper (PAY_PER_EVENT pricing, video transcript support) documents post text, engagement metrics, media URLs and thumbnails, but not accessibility-caption OCR text, a structured image manifest, or entity extraction. scrapier/facebook-posts-scraper β€” the base, un-enriched sibling Actor on this account β€” returns the same underlying post fields (this Actor's rawData: true output is byte-identical to it) but without the creative block, OCR text, entities or AI enrichment this Actor adds. This Actor's OCR image-text and per-post image-asset manifest are not documented on either listing as of that date.

Can I use Facebook Posts Media & OCR Content Harvester without managing proxies or Facebook credentials?

Yes. No Facebook login or session is required β€” every target is loaded as a public, logged-out page. Proxying is handled automatically: Apify Residential proxy is tried first, then Datacenter proxy, with an optional custom proxyUrls list in proxyConfiguration as a final fallback if you supply one.

πŸ’¬ Your feedback

Found a bug, or a field that doesn't match what Facebook actually returns? Open an issue on this Actor's Issues tab in Apify Console β€” the Scrapier team reads and triages reports there.