Skyscanner Flight Scraper
Pricing
Pay per usage
Skyscanner Flight Scraper
Scrapes Skyscanner flights via the internal web-unified-search JSON API — no browser needed (pure HTTP over residential proxy). Mirrors the jupri/skyscanner-flight technique.
Skyscanner Flight Scraper
Pricing
Pay per usage
Scrapes Skyscanner flights via the internal web-unified-search JSON API — no browser needed (pure HTTP over residential proxy). Mirrors the jupri/skyscanner-flight technique.
Origin city or airport (IATA code, e.g. LHR, or a city name).
Destination city or airport (IATA code, e.g. JFK, or a city name).
Departure date (YYYY-MM-DD). NOTE: the prefill below is what Apify's automated quality test runs the Actor with, so it must stay in the FUTURE — a past date makes the test fail and the Actor gets flagged 'Under maintenance'. Refresh it well before 2026-12-08.
Second leg / return date (YYYY-MM-DD). Kept in the future for the same reason as Depart #0.
Country you're searching from — gives local deals. ISO country code.
Max number of results to push per search (slice, after sort). Leave empty for all.
Run MANY searches in ONE browser session — PerimeterX is passed once and amortized across all of them. Each item: {"depart":"YYYY-MM-DD","return":"YYYY-MM-DD"} (return optional; origin/target default to origin.0/target.0, or override per item with "origin"/"target"). When set, the single origin.0/depart.0 search is ignored. Records are tagged with search_depart/search_return.
[ { "depart": "2026-12-08", "return": "2026-12-15" }, { "depart": "2026-12-15", "return": "2026-12-22" }]Max seconds to poll each search toward status:complete. Higher = more agents populate per itinerary; lower = faster but fewer agents on some itineraries. Default 120.
Stop polling a search once its result count has not grown for this many seconds. Skyscanner currently freezes these searches (never reports 'complete'), so without this every search runs the full poll budget for nothing. 0 = disabled (poll the whole budget).
Shorter stall window applied only when the result count never grew past the first snapshot — the frozen shape. A search that grew at least once keeps the full poll_stall_s window. 0 = use poll_stall_s for both.
Seconds between polls of a running search. Each poll replays the CREATE request, which starts a NEW search server-side, so polling fast can keep killing a search before it matures. Raise this if searches freeze at 10 results. Default 2.
Send no poll requests of our own — only capture the web-unified-search responses the site's own JS fires. Avoids the 'every poll starts a new search' problem entirely. Falls back to active polling if the site fires only once in 20s.
Abort requests to third-party ad and analytics domains (doubleclick, GTM, hotjar, ...). Nothing on a skyscanner domain and nothing PerimeterX-related is blocked. Cuts page load work; measure the 403 rate before leaving it on.
How many of the trips dates may run at once. MEASURED: leave this at 1. Skyscanner degrades concurrent searches from the same session — same 2 dates took 76s sequentially (1 snapshot each, 15 records each) versus 165s in parallel tabs, where every poll took 20-35s and searches stall-stopped at 10 results. 3 was worse still and lost a whole date. Raise only with fresh measurements.
Stop polling once the cheapest limit itineraries have stopped changing for a few consecutive snapshots, instead of waiting for the full result set to complete. Much faster; small risk of missing a cheaper flight that surfaces late. Best combined with a set limit.
Launch a browser on this many residential IPs in parallel and take the first that passes PerimeterX; the rest are closed. 2 hides the ~20s otherwise wasted when the first IP hits an interstitial. 1 disables the race. Higher = more RAM (use 4GB if >2).
Cabin class.
Include nearby origin airports.
Include nearby destination airports.
Override the proxy networking entirely. LEAVE EMPTY unless you know you need it: when set, it takes precedence over proxy_groups, which pins one proxy group and disables the automatic failover away from a group PerimeterX has flagged. This field used to carry a DEFAULT of BUYPROXIES94952, which silently pinned every run to that group — including after PerimeterX flagged it. Sticky sessions are used internally either way, so the cookie mint and every fire exit the same IP.
Additional HTTP headers as {name, value} objects, merged into every request.
Reuse a previously-minted PerimeterX session to SKIP the browser. Set this to the session_id from a prior run's KV 'px_session', and pass that run's cookies in dev_custom_cookies. Phase 1 (pure HTTP) then exits the same residential IP the _px3 is bound to. Works only while the _px3 + sticky IP are still alive (minutes). On failure it falls back to the browser.
Restrict pushed output to these field names (in order). Empty = full record.
Named dataset to push to. Supports {ACTOR}, {DATE}, {TIME} tokens. Empty = default dataset.
Wipe the target dataset before pushing.
Keep null/empty fields in output instead of stripping them.
One-off: navigate the /config/ page for the first itinerary and log every XHR that carries pricing_options, so the details endpoint can be captured. Saves the raw hit to KV key 'config_capture'.
LEGACY / optional. Every run now already returns the full agent list inline in each record's agents (via poll-to-complete + snapshot merge), so this is only needed for the extra jupri-shaped pricing_options with agent names/ratings from the /config details endpoint. Costs one browser session + one page visit per itinerary — leave OFF unless you need the richer per-agent metadata.
How many top itineraries (after sort) to enrich with pricing_options when 'Fetch per-agent pricing options' is on.
With 'Fetch per-agent pricing options' on: output ONLY the top-N enriched itineraries and finish immediately, skipping all other itineraries. Fastest/cheapest when you just want the top results in full.
For multi-date (trips) runs, derive each later date's search by swapping the dates in the CREATE request captured on the first date, issued from the same PerimeterX-warm tab. Measured: the navigation itself is 1-2s but waiting for the site's own JS to fire its search costs 16-26s per extra date. Falls back to a normal navigation whenever the route differs or the replay does not return results. Only affects trips runs.
MEASURED CORRECTION: a PerimeterX block does NOT return 403 on the results document. The document loads normally and the site JS simply never fires a search, so both raced IPs sit out the full 30s deadline (~36s per failed attempt, up to 3 attempts per run). The actor therefore probes the PAGE for the PX challenge (captcha element / challenge copy) while waiting, and always LOGS when it sees one and whether that page went on to fire anyway. Set this to how many extra seconds to allow after the signature has been seen twice. 0 = off (keep the full wait) and is the default, because a page mid-challenge can still self-solve. Set it from the "after PX signature: fired=... at Xs" log lines rather than by guessing. The probe runs on every search path — multi-date, single-date, and the Standby session mint (where it is read from the PX_BLOCK_FIRE_S env var, since a standby container has no run input).
PerimeterX blocks are only knowable by the search never firing — neither the document status nor the page DOM shows one (both measured and refuted), so the only lever left is making a blocked attempt cheaper. Off, the actor retries in rounds: every IP of a round must die before the next round launches, so the round costs its slowest member and then pays a cold browser launch with nothing else in flight. On, a dead IP is replaced the instant it dies and the replacement's launch overlaps its siblings' waits. Same total IP budget either way (race_ips x 3). Default off pending an A/B in a blocking window.
A PerimeterX block is only knowable by the search never firing, so this value alone decides when an IP is declared dead — the largest lever measured on this actor, and it used to be set far too low. Three paired A/Bs, 8 runs per arm launched together in one blocking window (2026-08-05): 15s failed SEVEN of eight runs while 25s failed two, and all six of the 25s arm's wins fired at 16.5-25.2s (i.e. from IPs a 15s cutoff discarded); 25s vs 40s both reached zero failures, but 40s won on its first pair of IPs in all eight runs, median mint 56.1s to 23.9s; 40s vs 60s was indistinguishable. Most of what this actor recorded as PerimeterX blocks was in fact this timeout. Raising the cap is nearly free — the wait ends the instant the search fires, so a healthy IP never reaches it. Applies to the single-search, multi-date and Standby-mint paths alike.
How many browsers the rolling pool may launch in total before giving up. Only used when rolling_ips is on. 0 (default) keeps the historical coupling, race_ips x 3. Set it explicitly to vary pool DEPTH (race_ips) without also varying the budget — raising race_ips alone would change both at once.
For multi-date (trips) runs, ask for every remaining date's search concurrently from the first date's PerimeterX-warm tab in a single in-page call, instead of one tab + navigation + blocking replay per date. VALIDATED on 4 dates, 5 runs per arm on a healthy window: identical data in every run (451 records, same per-date counts) with wall median 174s -> 114s and p90 256s -> 141s. Unlike search_concurrency (measured worse) it opens no extra tabs and runs no competing site JS. Any date whose replay does not come back usable falls back to a normal navigation. Turn off to restore the strictly sequential behaviour.
When a search does not reach "complete", its flights and prices are correct but each itinerary carries only ONE agent, because the inline agent list fills in over poll snapshots that never arrive. /itinerary/details does not depend on the search completing — measured on a frozen date it returned 18-19 priced agents with names, ratings and booking links for itineraries that had 1. This enriches the cheapest N of any PARTIAL date so the rows people actually read keep a full seller list. Costs a few seconds, and only on dates that came back partial. 0 = off. Dates that complete normally are unaffected; fetch_details still overrides for all dates. Sizing note: the enrichment replays fire 4 per round, so 4 costs the same as 3 (one round) and 5-8 all cost one extra round. 4 is the free maximum.
PerimeterX flags whole Apify proxy GROUPS, not individual IPs, and which group is clean has already flipped once: BUYPROXIES94952 used to pass and RESIDENTIAL was flagged, then on 2026-08-13 18 of 18 BUYPROXIES94952 IPs were blocked while RESIDENTIAL passed 4 of 4. The Actor therefore tries these in order and moves to the next one whenever every IP in the current group comes back blocked — rotating IPs inside a flagged group is pointless. Leave empty for the default order.