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.
Scrapes flight itineraries from Skyscanner — prices, times, carriers, stops, and the full list of agents (sellers) with their ratings and booking links — into an Apify dataset. Search many dates in one run for a price-calendar view.
What you get
One record per itinerary, each carrying every agent that sells it:
{"id": "13554-2612081040--32753-0-12712-2612081335|12712-2612151810--32753-0-13554-2612160620","price": 429,"currency": "USD","carriers": "British Airways","stops": 0,"origin": "LHR","destination": "JFK","departure": "2026-12-08T10:40","arrival": "2026-12-08T13:35","durationMinutes": 475,"search_depart": "2026-12-08","search_return": "2026-12-15","search_complete": true,"legs": [ … ],"agents": [{"name": "skyp","agent_name": "Kiwi.com","rating": 3.96,"feedback_count": 8136,"is_carrier": false,"price": 429,"url": "https://www.skyscanner.co.il/transport_deeplink/…"}]}
Agent names and ratings come from the search feed itself, so every itinerary carries them — no extra per-itinerary requests, no partial coverage.
Input
Minimum: origin.0, target.0, depart.0.
| Field | Meaning |
|---|---|
origin.0..5 / target.0..5 / depart.0..5 | Per-leg origin, destination and YYYY-MM-DD date. Round trip: leg 0 outbound, leg 1 the return. |
trips | Multi-date search: [{depart, return?, origin?, target?}]. One browser session serves every date, so N dates cost far less than N runs. |
market / currency | ISO country + currency, e.g. IL / USD. |
sort | score | cheapest | fastest | departure |
cabin_class | economy | premiumeconomy | business | first |
adults / children / infants | Passenger counts. |
non_stop / one_stop / two_stop | Stop filters, any combination. |
limit | Keep only the first N itineraries per date, after sorting. |
partial_enrich_top | See "Partial searches" below. Default 4. |
fetch_details / details_limit | Also attach jupri-shaped pricing_options (fare brands) for the top N. Optional — agent names and ratings already arrive without it. |
Single round trip:
{"origin.0": "LHR", "target.0": "JFK", "depart.0": "2026-12-08","origin.1": "JFK", "target.1": "LHR", "depart.1": "2026-12-15","market": "IL", "currency": "USD","adults": 1, "non_stop": true, "sort": "cheapest"}
Price calendar over several dates:
{"origin.0": "LHR", "target.0": "JFK", "depart.0": "2026-12-08","trips": [{ "depart": "2026-08-16", "return": "2026-08-20" },{ "depart": "2026-08-23", "return": "2026-08-27" }],"market": "IL", "currency": "USD", "sort": "cheapest"}
Partial searches — read this
Skyscanner's search is create-then-poll: the first response holds ~10 itineraries and the
rest arrive as the search matures server-side. Sometimes it never matures. The search
stops growing at ~10 itineraries and never reports complete. This is a server-side
condition — the same thing happens to Skyscanner's own website — and it comes and goes in
windows lasting hours.
The actor does not hide this:
- Every record carries
search_complete. When it isfalse, the date is a partial picture, not a genuinely thin route. Surface it in your UI rather than reporting "no availability". - A partial search still returns the right flights — correct cheapest-first ordering,
prices and carriers — but its inline
agents[]is starved to one seller per itinerary. So the actor automatically enriches the cheapestpartial_enrich_topitineraries (default 4) through Skyscanner's itinerary-details endpoint, which does not depend on the search having completed. Those rows come back with their full seller list — typically 15–19 agents with names, ratings and booking links — inpricing_options. - Set
partial_enrich_top: 0to switch that off. Dates that complete normally never trigger it and cost nothing extra.
Re-running a partial date often returns a complete picture.
Performance and cost
Measured on a short-haul round trip, 2048 MB, direct flights only:
| Time | Records | |
|---|---|---|
| 2 dates, healthy | 70–90 s | ~209 |
| 4 dates, healthy | 180–240 s | ~427 |
| Any date, partial | — | ~10, top 4 fully enriched |
Cost is roughly $0.07–0.09 per run, dominated by residential proxy transfer ($8/GB), not by compute. It was $0.01–0.03 on the custom BUYPROXIES94952 group, which is included in the plan rather than metered per GB — but PerimeterX has flagged that group, so RESIDENTIAL is the working default. If you have a clean custom residential group, put it first in proxy_groups and the cost drops back. 2048 MB is the sweet spot; the run is bound by
Skyscanner's own search latency (~15–25 s per date), not by local compute.
Extra dates are asked for in one burst from the first date's warm tab (parallel_replays,
on by default). Measured on 4 dates, 5 runs per arm: identical data in every run, wall median
174 s → 114 s and p90 256 s → 141 s.
That is not the same thing as search_concurrency, which opens a tab per date and lets the
site's own JS run in each. That one was measured worse — slower and prone to losing a
whole date — and stays off. The difference is the extra tabs, not the concurrency.
Standby mode — a warm endpoint instead of a run
A normal run pays for a container boot and a PerimeterX pass every time. Standby keeps both alive and answers over HTTP, so a warm request is 9–12 s instead of 67–179 s. The first request after a cold start still pays the mint (~150 s).
GET https://<user>--skyscanner-flight.apify.actor/?origin=LHR&target=JFK&depart=2026-12-08&return=2026-12-15Authorization: Bearer <APIFY_TOKEN>
Several dates in one request — pass one return per depart, or none for one-way.
Repeated params and comma lists are equivalent, so use whichever your client produces:
?origin=LHR&target=JFK&depart=2026-12-08&depart=2026-12-15&return=2026-12-15&return=2026-12-22?origin=LHR&target=JFK&depart=2026-12-08,2026-12-15&return=2026-12-15,2026-12-22
Every date runs on the one warm session, so a price calendar costs one round trip rather than N. Max 10 dates per request; all dates share the route and the filters.
Response — records is the flat list across all dates, and every record already carries
search_depart / search_return, so it is groupable without reading trips:
{"records": [ /* … */ ],"count": 185,"search_complete": false, // true only if EVERY date completed"seconds": 11.6,"warm": true,"trips": [ // absent fields never change meaning for a 1-date request{ "depart": "2026-08-16", "return": "2026-08-20", "count": 92, "search_complete": true },{ "depart": "2026-08-23", "return": "2026-08-27", "count": 93, "search_complete": false }]}
Same query params as the input schema for non_stop / one_stop / two_stop, sort,
limit, currency, adults, children, infants, cabin, partial_enrich_top.
GET /healthz reports whether a warm session is held. A date that fails on its own returns
an empty entry rather than failing the request; if every date fails the session is
re-minted once and the request retried.
Notes
- Booking URLs are rewritten to the local market host, so links stay on the same site the prices were quoted on.
- Records are pushed as they land, so aborting a run keeps whatever was collected.
- A live
progressrecord in the run's key-value store reports the current phase and date-by-date progress, for driving a progress bar.