Skyscanner Flight Price Calendar API in Python: Find Cheaper Travel Dates

Published October 12, 2026

A traveler can move a departure by a few days, but checking every date individually is cumbersome. A Skyscanner flight price calendar API in Python helps shortlist lower quoted dates before searching for specific itineraries. The goal is the lowest usable quote among the returned dates, not a promise of the globally cheapest flight.

This workflow resolves London Heathrow and New York JFK, requests an eight-day future window, filters unavailable quotes and rechecks the selected day. The documented tested London–New York search included LHR–JFK itineraries; current suggestions and offers may differ.

Endpoints for the workflow

All requests are GET calls under https://api.opendata-api.com/api/project/skyscanner/<operationId>?key=YOUR_API_KEY.

OperationRequired inputsPurpose
flights-search-airportqueryResolve cities and airports
flights-get-price-calendaroriginSkyId, destinationSkyId, fromDateCompare departure-day quotes; add toDate for an explicit range
flights-search-flightsOrigin and destination skyId/entityId pairs, dateRecheck a shortlisted day for itineraries
flights-search-incompletesessionIdRequest further results from an incomplete search

Get an authorized platform key from your dashboard. YOUR_API_KEY is an honest placeholder. Install requests and supply OPENDATA_API_KEY through your environment or secret manager. Keys and search sessions travel in query URLs: redact request URLs, browser history, screenshots and logs, and never publish a captured token.

HTTP success and business success are separate. Check HTTP status, then the gateway's code, message and data: business success uses code=200. Inspect the operation payload as well; successful airport lookup can return places:[], and usable flight results can still be incomplete.

1. Resolve airports, not guessed identifiers

Start with the same safe London autocomplete example used in the project quick start:

curl --get 'https://api.opendata-api.com/api/project/skyscanner/flights-search-airport?key=YOUR_API_KEY' \
  --data-urlencode 'query=London' \
  --data-urlencode 'market=US' \
  --data-urlencode 'locale=en-US'

Choose Heathrow rather than the London city suggestion. The observed identifiers were LHR / 95565050 for Heathrow and LOND / 27544008 for London city. Resolve New York separately and select JFK. Keep each suggestion's paired identifiers; do not substitute an IATA code for entityId or manufacture a missing match.

2. Compare a future range in Python

Run this script deliberately; reading the guide or copying an example does not send a request. Dates are calculated at execution time, avoiding an expired travel example.

import math
import os
from datetime import date, timedelta

import requests

BASE = "https://api.opendata-api.com/api/project/skyscanner"
API_KEY = os.environ["OPENDATA_API_KEY"]


def get(operation, **params):
    try:
        response = requests.get(
            f"{BASE}/{operation}",
            params={"key": API_KEY, **params}, timeout=45,
        )
    except requests.RequestException:
        raise RuntimeError("Transport failure; inspect redacted logs.") from None
    if not response.ok:
        raise RuntimeError(f"HTTP failure: {response.status_code}")
    body = response.json()
    if not isinstance(body, dict) or body.get("code") != 200:
        raise RuntimeError("Business failure; inspect redacted code/message.")
    payload = body.get("data")
    if not isinstance(payload, dict):
        raise RuntimeError("Inspect the endpoint payload before parsing.")
    return payload


def airport(query, sky_id):
    places = get("flights-search-airport", query=query,
                 market="US", locale="en-US").get("places", [])
    for place in places:
        if (isinstance(place, dict) and place.get("skyId") == sky_id
                and place.get("placeType") == "AIRPORT"
                and place.get("entityId")):
            return place
    raise RuntimeError("No matching airport; review actual suggestions.")


origin = airport("London", "LHR")
destination = airport("New York", "JFK")
start = date.today() + timedelta(days=45)
end = start + timedelta(days=7)
calendar = get(
    "flights-get-price-calendar",
    originSkyId=origin["skyId"], destinationSkyId=destination["skyId"],
    fromDate=start.isoformat(), toDate=end.isoformat(),
    currency="USD", market="US", cabinClass="economy",
)
if calendar.get("currency") != "USD":
    raise RuntimeError("Check returned quote currency before comparison.")

quoted = []
for row in calendar.get("outboundDates", []):
    if not isinstance(row, dict) or row.get("hasQuote") is not True:
        continue
    try:
        day = date.fromisoformat(row.get("date", ""))
    except (TypeError, ValueError):
        continue
    price = row.get("price")
    if (start <= day <= end and type(price) in (int, float)
            and math.isfinite(price) and price >= 0):
        quoted.append((day, price))

if not quoted:
    raise SystemExit("No usable quoted dates; review the route or range.")
for day, price in sorted(quoted):
    print(day.isoformat(), price, calendar["currency"])
selected_day, selected_price = min(quoted, key=lambda item: item[1])
print("Lowest returned quote:", selected_day, selected_price)

The observed calendar contains currency and outboundDates records with date, numeric price, group and hasQuote. Missing days are unknown, not zero-priced flights. The script excludes malformed and unquoted entries rather than filling gaps. group labels such as low and medium are relative hints. Neither directPrice nor isDirect was observed, so the calendar does not establish non-stop availability.

3. Recheck the selected date

Continue the same script with a date-specific one-way search:

flights = get(
    "flights-search-flights",
    originSkyId=origin["skyId"], originEntityId=origin["entityId"],
    destinationSkyId=destination["skyId"],
    destinationEntityId=destination["entityId"],
    date=selected_day.isoformat(), adults=1, cabinClass="economy",
    currency="USD", market="US", countryCode="US",
)
print("Search status:", flights.get("status"))

Observed itineraries have id, price.amount/currency/formatted, legs and bookingUrl. Inspect actual airports, times, durationMinutes, stopCount and carriers in each leg. The rechecked amount can differ from the calendar quote. A booking link is a redirect, not a reservation.

RESULT_STATUS_INCOMPLETE can accompany usable itineraries, total and sessionToken. Treat the list as partial. For an explicit optional continuation, pass the returned token as sessionId:

# Execute separately only if you choose to request more results.
if flights.get("status") == "RESULT_STATUS_INCOMPLETE":
    token = flights.get("sessionToken")
    if not isinstance(token, str) or not token:
        raise RuntimeError("No usable search context; start a fresh search.")
    follow_up = get("flights-search-incomplete", sessionId=token,
                    currency="USD", countryCode="US")

Polling itself was not independently verified. Inspect follow-up structure before merging, deduplicate by itinerary ID where available, and set backoff, a request budget and a deadline. Stop on verified completion or your deadline; no session lifetime or final count is guaranteed. Do not print the token or assume follow-up fields match the initial search.

Practical uses

Build flexible-date trip planners, route-specific deal newsletters or scheduled fare watchlists. Save retrieval time, route, currency, dates and request context with observations. Your application supplies scheduling, price history and notifications; this API does not supply a historical fare archive or an alert subscription.

FAQ

How do I search Skyscanner flights with Python? Resolve both places, keep their skyId/entityId pairs, then call flights-search-flights with a future date. The example above performs that lookup after comparing calendar quotes.

How do skyId, entityId and IATA codes differ? Autocomplete supplies search identifiers. skyId selects a place; entityId is its associated identifier. iataCode is separate and can be blank. City suggestions can encompass multiple airports.

Why can HTTP 200 contain empty places or a blank IATA code? places:[] is a clean no-match result. A blank IATA code does not invalidate populated search identifiers. Refine the query instead of guessing a result.

How do I compare a date range or whole month? Use flights-get-price-calendar with future fromDate/toDate. flights-get-cheapest-oneway documents a month in YYYY-MM; its response schema and coverage need separate validation.

Are calendar prices guaranteed bookable fares? No. They are quoted observations. Recheck selected dates for itineraries, and confirm current availability and terms at the returned booking destination.

How do I retrieve additional incomplete results? Pass your search's sessionToken as sessionId to flights-search-incomplete, retaining currency/countryCode. Bound polling and validate its response before reconciliation.

How do I submit multi-city legs? flights-search-flights-multi-stops takes legs as a URL-encoded JSON array string in the GET query. Each documented leg has originSkyId, destinationSkyId and date; it is not a request body.

How do I search hotels for a stay? Resolve entityId using hotels-search-destination, then call hotels-search-hotels with future checkIn/checkOut, checkout later, and optional adults/rooms.

How do I get hotel prices, reviews and coordinates? Use search-derived hotelId for documented room-price/review follow-ups; prices also require destination entityId and stay dates. Hotel search exposes coordinates.lat/lng; follow-up schemas, review paging and price basis remain unverified.

Can this API book flights or hotels? No. It returns search data and available booking links, not ticket issuance, payment or hotel-order creation.

Does car search return rental quotes? cars-search-location returns pickup/drop-off location suggestions with IDs and coordinates, not vehicles, rates or reservations.

How do localization and exchange-rate dates affect comparisons? Market selects search context, locale controls supported language/formatting, and currency denominates quotes. Validate codes with documented config-get-locale; preserve the base and date from config-get-exchange-rates alongside converted estimates.

References and next steps

Skyscanner's flexible-date use cases and progressive-search overview provide conceptual background, not this interface's request schemas. Use the Skyscanner endpoint reference for its contract.

For comparable travel products such as KAYAK, Booking.com or Expedia, contact us about additional APIs or private source-code deployment.