# Changelog of Facebook Pages Scraper (`vortex_data/facebook-pages`) Actor

- **URL**: https://apify.com/vortex\_data/facebook-pages/changelog.md
- **Full Actor documentation**: https://apify.com/vortex\_data/facebook-pages.md

## Changelog

All notable changes to this Actor will be documented in this file.

The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/).

***

### \[0.1.105] - 2026-07-12

#### Changed

- **Traffic diagnostics now support pricing decisions** - `OUTPUT.trafficStats`
  reports request counts, response-body bytes, and network errors by Facebook
  route and by datacenter/residential transport. The values intentionally do
  not pretend to be billing bytes; exact proxy charges remain available in the
  Apify run usage report.
- **Pricing is one predictable Page-result event** - effective July 27, 2026,
  one complete Page row costs $0.0055, regardless of how many selected nested
  posts, comments, media, reviews, or events it contains. The only additional
  event is Apify's minimum $0.00005 synthetic Actor start.

### \[0.1.104] - 2026-07-12

#### Changed

- **Anonymous Page details are residential-first** - the critical `/about/`
  request no longer waits for repeated datacenter login redirects before using
  the transport that recovered every tested Page. Supplemental Page metadata
  and all content sections remain on datacenter proxy; datacenter becomes the
  final same-endpoint fallback only if residential is exhausted. Cookie-mode is
  unchanged.
- **Page details now use the browser request profile** - `facebookexternalhit`
  remains limited to content listing routes where it expands embedded edges.
  The Page `/about/` route uses the Chrome-shaped request profile to avoid the
  volume-dependent login redirects seen from crawler-UA About requests.
- **Residential proxy is an adaptive Page-details recovery only** - ordinary
  traffic remains on datacenter proxy. When anonymous Page details exhaust the
  normal route because Facebook keeps redirecting it to login, only the
  critical `/about/` request moves to a residential exit. Transparency, long
  About, profile-photo, cookie-mode, and all content sections stay on the
  datacenter transport; the normal parser then merges their fields.
- **Default Page concurrency is now 8** - a 100-page A/B produced the same
  completeness as concurrency 12 in slightly less wall time and without the
  cascading login-response behavior seen when retries increased sustained
  request pressure. API callers can still explicitly choose 1-16.
- **Numeric-ID events are no longer duplicated across future and past** - the
  shared `profile.php?id=...&sk=events` response is now classified by actual
  event start time or Facebook's explicit past/upcoming marker. Unknown-time
  records default to past instead of being falsely advertised as upcoming.
- **Failed numeric-ID primary bundles get one combined recovery attempt** -
  only after a real login/unparseable/network failure on the shared numeric
  route, the Actor retries the same combined Facebook response with a fresh
  proxy exit before falling back to separate Posts and Photos requests. Handle
  URLs do not add this retry because a 100-page A/B showed cascading login
  responses under sustained load. Valid empty feeds are never retried.
- **Bulk-run diagnostics now show where time and recovery go** - OUTPUT
  separates the core `/about/`, Page Transparency, root-profile, and long
  About routes, including successful attempts, failures, elapsed time, and
  how many missing fields each route contributed. `recoveryStats` aggregates
  primary-bundle outcomes and section retry reasons without storing URLs,
  cookies, or scraped content.
- **Current Facebook Page reviews are parsed again** - the reviews tab now
  recognizes Facebook's newer recommendation-shaped `Story` records, including
  positive/negative recommendation, review text, author, URL, timestamp,
  engagement counts, and recommendation tags. The legacy recommendation shape
  remains supported.
- **Auto-detected section URLs are routed per Page** - mixed `/posts/`,
  `/reviews/`, `/videos/`, `/reels/`, `/photos/`, and `/events/` inputs no
  longer merge into one global section set that is fetched for every Page.
- **First-run input UX is more task-oriented** - the Apify Input tab now starts
  with Page URLs, "Scraping scenario", and the main item cap. Advanced filters,
  per-section limits, comments, performance, and cookies are grouped into
  optional sections.
- **Export format was removed from the Console form** - users no longer have to
  choose an output mode. The Actor writes the simple one-row-per-Page dataset by
  default, while the old `outputMode` parameter remains accepted for existing
  API integrations.
- **User-facing mode names are clearer** - the form now says things like
  "All-in-one Page scraper", "Page info / lead list", "Comments from Page
  posts" instead of asking users to understand internal package names.
- **Input is now framed as concrete scraping scenarios** - the main selector is
  "Scraping scenario" with actor-style choices such as All-in-one Page scraper,
  Page info / lead list, Posts scraper, Comments from Page posts, Media scraper,
  Reviews scraper, and Events scraper. API callers can use `scenario` or `task`
  as aliases for `dataPackage`.
- **Safer first-run defaults** - default content cap changed from `50` to
  `20` items per section, and public comments now inspect `2` post permalinks
  with up to `5` comments each by default. Users can still raise all limits for
  heavier runs.
- **High-traffic Apify actor compatibility expanded** - `resultsLimit` now
  works as an alias for `maxResults`, matching Apify's official Facebook
  Posts / Comments / Reels input style.
- **Official Posts date aliases are supported** - `onlyPostsNewerThan` and
  `onlyPostsOlderThan` now map to the Actor's posts date filter, and date
  inputs accept `YYYY-MM-DD`, ISO timestamps, or relative values like
  `7 days`.
- **Flat item rows now include migration-friendly aliases** - post/comment/
  media/review/event rows include familiar fields such as `postId`,
  `commentId`, `time`, `text`, `user`, `likesCount`, and `commentsCount`
  alongside the Actor's native snake\_case fields.
- **URL section routing is now available via `dataPackage=auto`** - inputs
  like `/posts/`, `/reviews/`, `/videos/`, `/reels/`, and `/events/` now
  resolve to the matching focused Page scenario instead of fetching every
  section unnecessarily. Plain page URLs still resolve to full-content mode.
- **Focused actor compatibility improved** - API callers can use `type` as an
  alias for `sections`, matching the common pattern used by posts/reviews/
  events-style Facebook actors.
- **Posts can now be date-filtered** - `startDate` and `endDate` keep only
  posts within an inclusive `YYYY-MM-DD` range, and the comments pass now opens
  permalinks only for posts that remain after that filter.
- **Default runs now use the all-in-one full-content scenario** - new inputs
  default to `scrapeMode=fullContent`, combining page details, posts, photos,
  videos, reels, reviews, and events in one Actor instead of pushing users
  through separate competitor-style Actors. `pageDetailsOnly` remains available
  for high-volume metadata/contact enrichment.
- **API callers can now select exact sections** - `sections` /
  `contentTypes` accepts values like `posts`, `reviews`, `photos`, `videos`,
  `reels`, or `events`; page details are always included so rows keep stable
  identity and contact metadata.
- **Task-oriented jobs replace raw section thinking in the UI** - users can now
  choose human-readable jobs backed by `fullContent`, `contentMonitoring`, `posts`,
  `media`, `reputation`, `events`, or `leadEnrichment`. Older `scrapeMode`
  inputs still work as aliases.
- **Full-content root bundling no longer replaces rich page details** - the
  root page bundle still seeds posts/photos, but `/about/` remains the source
  of contact/category/rating fields so speedups do not silently drop metadata.
- **Page details now merge competitor parity fields** - Page Transparency and
  About routes can now fill `confirmed_owner`, `CONFIRMED_OWNER_LABEL`,
  `about_me`, `whatsapp`, `whatsappUrl`, and
  `pageAdLibrary.is_business_page_active` when Facebook exposes them.
- **Page-details concurrency is higher by default** - metadata-only runs now
  default to `pageConcurrency=10`; full-content runs keep the safer internal
  default of `6`.
- **Authenticated runs now preserve input identity more carefully** - when
  logged-in Facebook HTML contains unrelated Page/User nodes, the Actor no
  longer lets those embedded IDs overwrite the requested numeric profile/page
  ID or handle. Unverified handle IDs are omitted and marked with
  `pageIdentityUnverified=true`.
- **Authenticated requests are paced per page** - cookie mode now limits each
  page to a small number of simultaneous section requests, reducing timeout
  storms from sending one Facebook account through too many proxy exits at
  once.
- **Anonymous section fetches are paced too** - each page now fetches up to 3
  sections at once instead of all sections simultaneously, reducing proxy
  timeout storms on batches with `pageConcurrency=6`.
- **Core Facebook requests get a little more time to finish** - the default
  per-attempt request timeout increased from 15 to 20 seconds, while optional
  empty-prone sections still use their shorter budget.
- **Core section network failures get one targeted recovery pass** - if page
  details, posts, photos, or videos exhaust the normal network budget before
  emitting data, the Actor retries that section once with a fresh proxy
  session instead of immediately downgrading the page to a partial result.
- **Network retries no longer stay pinned to one slow proxy exit** - initial
  Facebook page fetches now let each retry rotate to a fresh Apify Proxy
  session, so a bad exit IP is skipped sooner instead of consuming the whole
  retry budget.
- **False deleted-handle skips are reduced with tokenless oEmbed** - when the
  anonymous Graph object probe returns 400 for a handle, the Actor now checks
  Meta's public `oembed_page` endpoint before skipping. If oEmbed confirms the
  URL exists, the Actor continues with the normal public HTML scrape instead
  of incorrectly returning `not_available`.
- **Rows with public content but missing page details are marked partial** -
  when Facebook gates the `/about/` page but public listing sections still
  expose posts/videos/reels, the row now uses `resultStatus=partial_public`
  and `dataQuality=Partial` instead of looking like a full page-details scrape.
- **Bulk default concurrency increased to 6** - controlled Apify bulk tests on
  real inputs completed cleanly at six pages in parallel, so new runs finish
  faster without users needing to tune `pageConcurrency` manually.
- **Empty public optional sections use a faster network budget** - reels,
  reviews, future events, and past events still get fetched, but slow proxy
  retries no longer let mostly-empty tabs hold up an otherwise healthy page.
  Network timeouts in those optional tabs are treated as an empty public
  section instead of turning an otherwise complete page into an OUTPUT failure.
- **Proxy tunnel failures get one clean section retry** - if Apify Proxy returns
  `CONNECT tunnel failed, response 594` after the normal HTTP retry budget, the
  Actor retries that section once with a fresh proxy session before surfacing a
  section warning.
- **Bulk runs now process pages with bounded concurrency** - the Actor scrapes
  up to 6 input pages at a time by default, configurable with
  `pageConcurrency` from 1 to 8. Each page still scrapes all supported sections
  concurrently, so large batches spend much less wall-clock time waiting on
  slow Facebook/proxy responses.
- **Unavailable pages no longer trigger partial fallback recovery** - when
  Facebook reports a missing handle or returns no renderable page data, the
  Actor now emits a `not_available` row plus `OUTPUT.skippedPages` diagnostics
  instead of spending extra proxy traffic on Ad Library, Graph picture recovery,
  Wayback, Page Plugin, or other incomplete fallback sources.
- **False deleted-handle skips get one cheap public-details rescue** - when
  Graph and oEmbed both say a handle is gone, but Facebook's own public
  `/about/` route still renders page details, the Actor now keeps that row and
  continues scraping the remaining sections. The rescue is single-attempt and
  short-budgeted, so truly dead handles still return cheap diagnostics.
- **Slow proxy exits are rotated away sooner on core sections** - page details,
  posts, photos, and videos now use an 18-second per-attempt network budget
  with one in-request transport retry. If a core section still fails, the
  existing section-level recovery gets a fresh proxy session. This caps the
  damage from one bad exit IP while preserving recovery for high-value data.
- **Optional empty-prone sections use a tighter first budget** - reels,
  reviews, future events, and past events keep one retry but now use a
  10-second per-attempt budget, reducing long tails on sections that commonly
  return no public data.
- **Anonymous root HTML is reused as a primary bundle** - the root page route
  already carries page details, posts, and a rich photo set, so anonymous runs
  now parse those from the first page document instead of fetching separate
  `/about/` and `/photos/` routes when the bundle is complete.
- **Numeric page events are fetched once** - `profile.php?id=...` pages use the
  same Facebook `sk=events` route for upcoming and past event data, so the
  Actor now parses both arrays from one request instead of loading that URL
  twice.

#### Added

- **Legacy Facebook URL normalization** - older public links such as
  `/p/<name>-<id>/`, `/pages/<name>/<id>/`, `/people/<name>/<id>/`, and
  slug-with-ID URLs are now treated as numeric Page IDs instead of being
  skipped or misread as the handle `people`.
- **Timeout guard and progress checkpoints** - OUTPUT is now written during
  long runs, not only at the end. When the Apify platform timeout is near, the
  Actor stops starting new pages with a safety buffer, saves partial results,
  and lists the remaining inputs in `OUTPUT.notStartedPages` instead of being
  killed without a useful summary.
- **Optional authenticated cookie mode** - users can now paste cookies from
  their own logged-in Facebook browser session into `facebookCookies` to
  scrape pages that account is allowed to view. The Actor accepts Cookie header
  strings, JSON cookie-extension exports, and Netscape cookie-file rows; it
  validates that `c_user` and `xs` are present before starting. Raw cookie
  values are never written to logs, dataset rows, or OUTPUT.
- **Readable result summaries** - every dataset row now starts with
  `resultStatus`, `dataQuality`, `resultSummary`, `sourceSummary`,
  `contentCounts`, and `nextAction` so Apify Overview immediately explains
  whether the row is full public data, partial public data, authenticated data,
  or unavailable anonymously.
- **Friendly OUTPUT key** - the run summary now includes `status`, `message`,
  `resultBreakdown`, and a compact `pages[]` list with one readable summary
  per input URL.
- **Typed social-media arrays**: `youtube`, `tiktok`, `twitter`, `linkedin`
  (in addition to the existing `instagram`) — each shaped as
  `[{"username", "url"}]`. Parsed from every off-Facebook link surfaced by
  the info-cards (not just the legacy `screenname` slot), so pages that link
  multiple social networks now expose every one of them as a typed entry.
- **`alternativeSocialMedia`** now picks the most prominent off-Facebook
  social presence by preference order (Instagram → TikTok → YouTube →
  Twitter/X → LinkedIn) instead of the bare Instagram URL.
- **Stub rows for unavailable pages** — when Facebook refuses to render a
  page anonymously (renamed handle, audience-restricted with no ads, etc.),
  the Actor now emits a compact stub row matching the popular community
  scraper's format: `{"url", "error": "not_available", "errorDescription"}`.
  Drop-in compatibility with downstream pipelines that expect one row per
  input URL. The verbose diagnostic for each stub still goes to
  `OUTPUT.skippedPages` for operators who want to know exactly why.
- **Dataset schema fields**: `resultStatus`, `dataQuality`,
  `accessLevel`, `resultSummary`, `sourceSummary`, `dataSources`,
  `contentCounts`, `nextAction`, `authenticatedSessionUsed`,
  `recoveredViaAuthenticatedSession`, `youtube`, `tiktok`, `twitter`,
  `linkedin`, `url`, `error`, `errorDescription`.

#### Changed

- **README and Actor schemas** now describe the actual input/output behavior
  more precisely: optional cookie mode, anonymous pagination limits,
  profile.php numeric-ID handling, unavailable rows, and authenticated
  unavailable rows.
- **User-supplied Facebook cookies** are scoped to Facebook hosts only and raw
  cookie values are never written to logs, dataset rows, or OUTPUT.
- **`intro`** is now correctly extracted from the trailing prose after
  `"<N> talking about this. <intro text>"` in `og:description`. Pages
  without a physical location (creators, online brands) used to lose this
  field; now they keep it.
- **`websites[]`** is built from EVERY info-card's outbound link, not
  just the three legacy slots (`address`, `screenname`, `website`),
  PLUS a full-HTML sweep of every `l.facebook.com/l.php?u=<encoded>`
  redirect (the format Facebook uses for the page-header social-icons
  strip). Previously the Instagram / YouTube / TikTok URLs there were
  invisible to the parser because they live as raw redirect strings
  inside inline JSON blocks, not as typed card slots.
- **JSON-Unicode unescape** in `_unwrap_redirect` now resolves *every*
  `\\u00XX` sequence (previously only `\\u00253A` and `\\u00252F`
  were handled, so YouTube / TikTok handles after `\\u002540` = `@`
  were silently dropped — `youtube.com/@casinocarisa` came out as
  `youtube.com/`).

#### Changed

- Skip reason for gated pages now distinguishes the useful current cases:
  - **handle not found (renamed or deleted)** — Graph/oEmbed cannot confirm the
    handle.
  - **page requires login (age-restricted, country-locked, or audience-limited)** —
    Graph or oEmbed confirms the page exists, but the main public HTML route
    does not expose renderable data.
  - **Facebook returned no public data** — Facebook refused to render content
    and the Actor could not classify the reason further.
- Final run summary lists every skipped page with its short reason.

#### Fixed

- **Unavailable-page runs no longer fail** — if every input page is
  audience-restricted / renamed / otherwise hidden from anonymous viewers, the
  Actor now exits successfully after writing the expected `not_available`
  stub rows and `OUTPUT.skippedPages` diagnostics. These are handled data
  outcomes, not infrastructure failures.
- **Schema validation** — every dataset field declared with strict types now
  accepts `null` alongside its primary type, matching what Facebook actually
  ships (many optional fields are `null` for any given page).
- **Log noise** — graph.facebook.com probe responses (the 400 / 403 we use to
  classify pages) no longer surface as user-visible ERROR lines; they're DEBUG.
- **Slow-proxy WARN threshold** raised from 10 s to 25 s — Apify Datacenter US
  through Facebook routinely sits at 8–20 s end-to-end on busy pages, so the
  old threshold flooded the log with non-actionable warnings.
- **curl\_cffi error strings** trimmed in WARN / ERROR lines — the verbose
  `"Failed to perform, curl: (28) Operation timed out after 15000 milliseconds
  with 381283 bytes received. See https://curl.se/libcurl/c/libcurl-errors.html
  first for more details."` style is reduced to the actionable middle phrase.
- **Per-section WARN deferral** — section-level fetch / parse failures are now
  surfaced only when the page actually produces a dataset row. Pages that get
  skipped no longer pile up red WARN lines for sections that were never going
  to succeed; the skip reason itself is enough context.
- **Section-list 404s** (e.g. `/reviews/` on a page that has no reviews tab)
  are now silent — they were always handled gracefully by the section-skipping
  logic; the underlying `ERROR HTTP 404` line was just visual noise.
- **Section "not publicly available" INFO** demoted to DEBUG — the per-page
  summary line already reports the section as "0 reviews" / "0 events"; the
  per-section info line repeating the same fact for every gated page was
  log-spam.

#### Removed

- **Input `proxyConfiguration`** field — the proxy is now hard-coded to
  Apify Datacenter US so users have one less thing to misconfigure. The
  Actor continues to use Apify Proxy internally as before.
- **Geo-rotation fallback** (DE / BR / GB exits) — empirically verified
  not to help on the kinds of pages users run into (audience-restricted
  pages return the same gate from every exit country we tested). Removed
  to save the ~26 s it added to gated-page runs.

#### Performance

- **Pre-flight Graph API probe** for handle-based inputs — when Graph
  returns `400 "Object with ID ... does not exist"` we skip the full HTML
  scrape entirely. Saves ~25 s per renamed-/deleted-handle in the input.

#### Internal

- `HttpClientError` now carries a `status_code` attribute, replacing the
  fragile "search for the HTTP code in the message string" pattern that
  previously broke when the message phrasing varied between retry-budget
  exhaustion and direct 4xx returns.
- New `quiet=True` parameter on `FacebookHttpClient.request()` demotes
  retry / failure log lines to DEBUG for known-best-effort fetches (Page
  Transparency, profile-photo viewer, Graph API/oEmbed probes, section list
  URLs).

***

### \[0.1.x] — Initial public release

Public Facebook Pages scraper. No login, no cookies — fetches anonymously
via curl\_cffi with Chrome TLS impersonation and the
`facebookexternalhit/1.1` user agent.

Per input URL emits one rich dataset row with:

- Page details (title, page ID, handle, categories, intro, address, phone,
  email, website, hours, rating, follower counts, profile photo, cover
  photo, page creation date, linked Instagram).
- Posts, photos, videos, reels (with timestamps, reactions, comments,
  shares).
- Public reviews.
- Upcoming and past events.

Uses Apify Datacenter US Proxy with per-request session rotation. Soft-fails
on 404 / login-redirect for missing sections. Handles HTML entity decoding
for non-ASCII content. Eight section types fetched concurrently per page;
pages processed sequentially.
