{
  "openapi": "3.0.1",
  "info": {
    "title": "Skyscanner Flight Scraper",
    "description": "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.",
    "version": "0.1",
    "x-build-id": "UvhgrLMaDYacY1B9B"
  },
  "servers": [
    {
      "url": "https://api.apify.com/v2"
    }
  ],
  "paths": {
    "/acts/exotic_harmonium~skyscanner-flight/run-sync-get-dataset-items": {
      "post": {
        "operationId": "run-sync-get-dataset-items-exotic_harmonium-skyscanner-flight",
        "x-openai-isConsequential": false,
        "summary": "Executes an Actor, waits for its completion, and returns Actor's dataset items in response.",
        "tags": [
          "Run Actor"
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/inputSchema"
              }
            }
          }
        },
        "parameters": [
          {
            "name": "token",
            "in": "query",
            "required": true,
            "schema": {
              "type": "string"
            },
            "description": "Enter your Apify token here"
          }
        ],
        "responses": {
          "200": {
            "description": "OK"
          }
        }
      }
    },
    "/acts/exotic_harmonium~skyscanner-flight/runs": {
      "post": {
        "operationId": "runs-sync-exotic_harmonium-skyscanner-flight",
        "x-openai-isConsequential": false,
        "summary": "Executes an Actor and returns information about the initiated run in response.",
        "tags": [
          "Run Actor"
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/inputSchema"
              }
            }
          }
        },
        "parameters": [
          {
            "name": "token",
            "in": "query",
            "required": true,
            "schema": {
              "type": "string"
            },
            "description": "Enter your Apify token here"
          }
        ],
        "responses": {
          "200": {
            "description": "OK",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/runsResponseSchema"
                }
              }
            }
          }
        }
      }
    },
    "/acts/exotic_harmonium~skyscanner-flight/run-sync": {
      "post": {
        "operationId": "run-sync-exotic_harmonium-skyscanner-flight",
        "x-openai-isConsequential": false,
        "summary": "Executes an Actor, waits for completion, and returns the OUTPUT from Key-value store in response.",
        "tags": [
          "Run Actor"
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/inputSchema"
              }
            }
          }
        },
        "parameters": [
          {
            "name": "token",
            "in": "query",
            "required": true,
            "schema": {
              "type": "string"
            },
            "description": "Enter your Apify token here"
          }
        ],
        "responses": {
          "200": {
            "description": "OK"
          }
        }
      }
    }
  },
  "components": {
    "schemas": {
      "inputSchema": {
        "type": "object",
        "required": [
          "origin.0",
          "target.0",
          "depart.0"
        ],
        "properties": {
          "origin.0": {
            "title": "Origin #0",
            "type": "string",
            "description": "Origin city or airport (IATA code, e.g. TLV, or a city name)."
          },
          "target.0": {
            "title": "Target #0",
            "type": "string",
            "description": "Destination city or airport (IATA code, e.g. HER, or a city name)."
          },
          "depart.0": {
            "title": "Depart #0",
            "type": "string",
            "description": "Departure date (YYYY-MM-DD)."
          },
          "origin.1": {
            "title": "Origin #1",
            "type": "string",
            "description": "Second leg origin (for round trip, set = target.0)."
          },
          "target.1": {
            "title": "Target #1",
            "type": "string",
            "description": "Second leg destination (for round trip, set = origin.0)."
          },
          "depart.1": {
            "title": "Depart #1",
            "type": "string",
            "description": "Second leg / return date (YYYY-MM-DD)."
          },
          "origin.2": {
            "title": "Origin #2",
            "type": "string",
            "description": "Leg 2 origin."
          },
          "target.2": {
            "title": "Target #2",
            "type": "string",
            "description": "Leg 2 destination."
          },
          "depart.2": {
            "title": "Depart #2",
            "type": "string",
            "description": "Leg 2 date (YYYY-MM-DD)."
          },
          "origin.3": {
            "title": "Origin #3",
            "type": "string",
            "description": "Leg 3 origin."
          },
          "target.3": {
            "title": "Target #3",
            "type": "string",
            "description": "Leg 3 destination."
          },
          "depart.3": {
            "title": "Depart #3",
            "type": "string",
            "description": "Leg 3 date (YYYY-MM-DD)."
          },
          "origin.4": {
            "title": "Origin #4",
            "type": "string",
            "description": "Leg 4 origin."
          },
          "target.4": {
            "title": "Target #4",
            "type": "string",
            "description": "Leg 4 destination."
          },
          "depart.4": {
            "title": "Depart #4",
            "type": "string",
            "description": "Leg 4 date (YYYY-MM-DD)."
          },
          "origin.5": {
            "title": "Origin #5",
            "type": "string",
            "description": "Leg 5 origin."
          },
          "target.5": {
            "title": "Target #5",
            "type": "string",
            "description": "Leg 5 destination."
          },
          "depart.5": {
            "title": "Depart #5",
            "type": "string",
            "description": "Leg 5 date (YYYY-MM-DD)."
          },
          "market": {
            "title": "Market",
            "type": "string",
            "description": "Country you're searching from — gives local deals. ISO country code.",
            "default": "US"
          },
          "currency": {
            "title": "Currency",
            "type": "string",
            "description": "Pricing currency (ISO code, e.g. USD, ILS, EUR).",
            "default": "USD"
          },
          "sort": {
            "title": "Sort",
            "enum": [
              "score",
              "cheapest",
              "fastest",
              "departure"
            ],
            "type": "string",
            "description": "How to sort results.",
            "default": "score"
          },
          "limit": {
            "title": "Limit",
            "minimum": 1,
            "type": "integer",
            "description": "Max number of results to push per search (slice, after sort). Leave empty for all."
          },
          "trips": {
            "title": "Multi-search (price calendar)",
            "type": "array",
            "description": "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."
          },
          "poll_budget_s": {
            "title": "Poll budget (seconds)",
            "minimum": 15,
            "type": "integer",
            "description": "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.",
            "default": 120
          },
          "poll_stall_s": {
            "title": "Stop after N seconds with no new results",
            "minimum": 0,
            "type": "integer",
            "description": "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).",
            "default": 45
          },
          "freeze_stall_s": {
            "title": "Stall timeout for a never-growing search (seconds)",
            "minimum": 0,
            "type": "integer",
            "description": "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.",
            "default": 20
          },
          "poll_interval_s": {
            "title": "Poll interval (seconds)",
            "minimum": 1,
            "maximum": 15,
            "type": "integer",
            "description": "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.",
            "default": 2
          },
          "passive_poll": {
            "title": "Passive poll (let the site drive)",
            "type": "boolean",
            "description": "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.",
            "default": false
          },
          "renav_on_freeze": {
            "title": "Re-navigate on a frozen search",
            "type": "boolean",
            "description": "When a search shows the freeze signature (count stuck at the first snapshot, no poll continuing the same session), reload the results page once instead of waiting out the stall timeout. Costs ~10s and lets the site create a fresh search, versus ~45s of waiting that ends with 10 records.",
            "default": false
          },
          "block_third_party": {
            "title": "Block ad/analytics hosts",
            "type": "boolean",
            "description": "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.",
            "default": false
          },
          "search_concurrency": {
            "title": "Parallel dates (multi-search)",
            "minimum": 1,
            "maximum": 6,
            "type": "integer",
            "description": "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.",
            "default": 1
          },
          "early_stop": {
            "title": "Early stop (faster)",
            "type": "boolean",
            "description": "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`.",
            "default": false
          },
          "race_ips": {
            "title": "Race N residential IPs",
            "minimum": 1,
            "maximum": 4,
            "type": "integer",
            "description": "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).",
            "default": 2
          },
          "cabin_class": {
            "title": "Cabin class",
            "enum": [
              "economy",
              "premiumeconomy",
              "business",
              "first"
            ],
            "type": "string",
            "description": "Cabin class.",
            "default": "economy"
          },
          "alternate_origin": {
            "title": "Alternate origin airports",
            "type": "boolean",
            "description": "Include nearby origin airports.",
            "default": false
          },
          "alternate_target": {
            "title": "Alternate target airports",
            "type": "boolean",
            "description": "Include nearby destination airports.",
            "default": false
          },
          "non_stop": {
            "title": "Direct only",
            "type": "boolean",
            "description": "Keep only direct (non-stop) flights.",
            "default": false
          },
          "one_stop": {
            "title": "1 stop",
            "type": "boolean",
            "description": "Keep 1-stop flights.",
            "default": false
          },
          "two_stop": {
            "title": "2+ stops",
            "type": "boolean",
            "description": "Keep 2+ stop flights.",
            "default": false
          },
          "adults": {
            "title": "Adults",
            "minimum": 1,
            "maximum": 8,
            "type": "integer",
            "description": "Number of adults (max 8).",
            "default": 1
          },
          "children": {
            "title": "Children",
            "minimum": 0,
            "maximum": 8,
            "type": "integer",
            "description": "Number of children (max 8).",
            "default": 0
          },
          "infants": {
            "title": "Infants",
            "minimum": 0,
            "maximum": 8,
            "type": "integer",
            "description": "Number of infants (max 8).",
            "default": 0
          },
          "dev_proxy_config": {
            "title": "Proxy configuration",
            "type": "object",
            "description": "Proxy networking. Defaults to Apify Residential proxy (required to pass PerimeterX). Sticky sessions are used internally so the cookie mint and every fire exit the same IP.",
            "default": {
              "useApifyProxy": true,
              "apifyProxyGroups": [
                "BUYPROXIES94952"
              ]
            }
          },
          "dev_custom_headers": {
            "title": "Custom HTTP headers",
            "type": "array",
            "description": "Additional HTTP headers as {name, value} objects, merged into every request."
          },
          "dev_custom_cookies": {
            "title": "Custom HTTP cookies",
            "type": "array",
            "description": "Additional cookies as {name, value} objects, merged into the cookie jar. Use to inject a pre-minted _px3 if you have one (see reuse_session_id)."
          },
          "reuse_session_id": {
            "title": "Reuse PX session id",
            "type": "string",
            "description": "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."
          },
          "dev_transform_fields": {
            "title": "Output fields",
            "type": "array",
            "description": "Restrict pushed output to these field names (in order). Empty = full record."
          },
          "dev_dataset_name": {
            "title": "Dataset name",
            "type": "string",
            "description": "Named dataset to push to. Supports {ACTOR}, {DATE}, {TIME} tokens. Empty = default dataset."
          },
          "dev_dataset_clear": {
            "title": "Clear dataset first",
            "type": "boolean",
            "description": "Wipe the target dataset before pushing.",
            "default": false
          },
          "dev_no_strip": {
            "title": "Keep empty values",
            "type": "boolean",
            "description": "Keep null/empty fields in output instead of stripping them.",
            "default": false
          },
          "dev_discover_config": {
            "title": "Discover config endpoint (debug)",
            "type": "boolean",
            "description": "One-off: navigate the /config/<id> 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'.",
            "default": false
          },
          "fetch_details": {
            "title": "Fetch per-agent pricing options (optional)",
            "type": "boolean",
            "description": "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.",
            "default": false
          },
          "details_limit": {
            "title": "Details limit",
            "minimum": 1,
            "type": "integer",
            "description": "How many top itineraries (after sort) to enrich with pricing_options when 'Fetch per-agent pricing options' is on.",
            "default": 10
          },
          "details_only": {
            "title": "Only enriched results",
            "type": "boolean",
            "description": "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.",
            "default": false
          },
          "replay_dates": {
            "title": "Replay later dates instead of navigating",
            "type": "boolean",
            "description": "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.",
            "default": true
          },
          "px_block_fire_s": {
            "title": "Extra fire wait after a PerimeterX challenge is detected (seconds)",
            "minimum": 0,
            "maximum": 30,
            "type": "integer",
            "description": "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.",
            "default": 0
          },
          "parallel_replays": {
            "title": "Fire every date's search in one round-trip (experimental)",
            "type": "boolean",
            "description": "EXPERIMENTAL, off by default. For 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. Each date otherwise costs 12-27s of pure server latency and they add up; this makes the total roughly the slowest single call. 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.",
            "default": false
          },
          "partial_enrich_top": {
            "title": "Recover the agent list on partial searches (top N)",
            "minimum": 0,
            "maximum": 10,
            "type": "integer",
            "description": "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.",
            "default": 4
          }
        }
      },
      "runsResponseSchema": {
        "type": "object",
        "properties": {
          "data": {
            "type": "object",
            "properties": {
              "id": {
                "type": "string"
              },
              "actId": {
                "type": "string"
              },
              "userId": {
                "type": "string"
              },
              "startedAt": {
                "type": "string",
                "format": "date-time",
                "example": "2025-01-08T00:00:00.000Z"
              },
              "finishedAt": {
                "type": "string",
                "format": "date-time",
                "example": "2025-01-08T00:00:00.000Z"
              },
              "status": {
                "type": "string",
                "example": "READY"
              },
              "meta": {
                "type": "object",
                "properties": {
                  "origin": {
                    "type": "string",
                    "example": "API"
                  },
                  "userAgent": {
                    "type": "string"
                  }
                }
              },
              "stats": {
                "type": "object",
                "properties": {
                  "inputBodyLen": {
                    "type": "integer",
                    "example": 2000
                  },
                  "rebootCount": {
                    "type": "integer",
                    "example": 0
                  },
                  "restartCount": {
                    "type": "integer",
                    "example": 0
                  },
                  "resurrectCount": {
                    "type": "integer",
                    "example": 0
                  },
                  "computeUnits": {
                    "type": "integer",
                    "example": 0
                  }
                }
              },
              "options": {
                "type": "object",
                "properties": {
                  "build": {
                    "type": "string",
                    "example": "latest"
                  },
                  "timeoutSecs": {
                    "type": "integer",
                    "example": 300
                  },
                  "memoryMbytes": {
                    "type": "integer",
                    "example": 1024
                  },
                  "diskMbytes": {
                    "type": "integer",
                    "example": 2048
                  }
                }
              },
              "buildId": {
                "type": "string"
              },
              "defaultKeyValueStoreId": {
                "type": "string"
              },
              "defaultDatasetId": {
                "type": "string"
              },
              "defaultRequestQueueId": {
                "type": "string"
              },
              "buildNumber": {
                "type": "string",
                "example": "1.0.0"
              },
              "containerUrl": {
                "type": "string"
              },
              "usage": {
                "type": "object",
                "properties": {
                  "ACTOR_COMPUTE_UNITS": {
                    "type": "integer",
                    "example": 0
                  },
                  "DATASET_READS": {
                    "type": "integer",
                    "example": 0
                  },
                  "DATASET_WRITES": {
                    "type": "integer",
                    "example": 0
                  },
                  "KEY_VALUE_STORE_READS": {
                    "type": "integer",
                    "example": 0
                  },
                  "KEY_VALUE_STORE_WRITES": {
                    "type": "integer",
                    "example": 1
                  },
                  "KEY_VALUE_STORE_LISTS": {
                    "type": "integer",
                    "example": 0
                  },
                  "REQUEST_QUEUE_READS": {
                    "type": "integer",
                    "example": 0
                  },
                  "REQUEST_QUEUE_WRITES": {
                    "type": "integer",
                    "example": 0
                  },
                  "DATA_TRANSFER_INTERNAL_GBYTES": {
                    "type": "integer",
                    "example": 0
                  },
                  "DATA_TRANSFER_EXTERNAL_GBYTES": {
                    "type": "integer",
                    "example": 0
                  },
                  "PROXY_RESIDENTIAL_TRANSFER_GBYTES": {
                    "type": "integer",
                    "example": 0
                  },
                  "PROXY_SERPS": {
                    "type": "integer",
                    "example": 0
                  }
                }
              },
              "usageTotalUsd": {
                "type": "number",
                "example": 0.00005
              },
              "usageUsd": {
                "type": "object",
                "properties": {
                  "ACTOR_COMPUTE_UNITS": {
                    "type": "integer",
                    "example": 0
                  },
                  "DATASET_READS": {
                    "type": "integer",
                    "example": 0
                  },
                  "DATASET_WRITES": {
                    "type": "integer",
                    "example": 0
                  },
                  "KEY_VALUE_STORE_READS": {
                    "type": "integer",
                    "example": 0
                  },
                  "KEY_VALUE_STORE_WRITES": {
                    "type": "number",
                    "example": 0.00005
                  },
                  "KEY_VALUE_STORE_LISTS": {
                    "type": "integer",
                    "example": 0
                  },
                  "REQUEST_QUEUE_READS": {
                    "type": "integer",
                    "example": 0
                  },
                  "REQUEST_QUEUE_WRITES": {
                    "type": "integer",
                    "example": 0
                  },
                  "DATA_TRANSFER_INTERNAL_GBYTES": {
                    "type": "integer",
                    "example": 0
                  },
                  "DATA_TRANSFER_EXTERNAL_GBYTES": {
                    "type": "integer",
                    "example": 0
                  },
                  "PROXY_RESIDENTIAL_TRANSFER_GBYTES": {
                    "type": "integer",
                    "example": 0
                  },
                  "PROXY_SERPS": {
                    "type": "integer",
                    "example": 0
                  }
                }
              }
            }
          }
        }
      }
    }
  }
}