"Who flies from New York to London?" sounds like a question with a simple answer. In practice there are three slightly different questions hiding in it, and they need different calls:

  • Which airlines operate between these two airports, and how long does it take?
  • Where can I fly nonstop from this airport?
  • This aircraft is broadcasting the callsign BAW178. Where is it going?

This post covers all three with the route endpoints, then goes through the problems that show up once you use the answers in a real product.

Question one: who flies between two airports

/routes/pairs takes a departure and an arrival, in IATA or ICAO form:

curl "https://skylink-api.p.rapidapi.com/routes/pairs?departure=JFK&arrival=LHR" \
  -H "X-RapidAPI-Key: YOUR_KEY" \
  -H "X-RapidAPI-Host: skylink-api.p.rapidapi.com"
{
  "count": 1,
  "routes": [
    {
      "departure": "JFK",
      "arrival": "LHR",
      "airlines": ["American Airlines", "British Airways", "Delta Air Lines", "Virgin Atlantic"],
      "km": 5555,
      "duration_min": 430
    }
  ]
}

Four carriers, 5,555 km, and a typical block time of 430 minutes, or 7 hours 10 minutes gate to gate.

Two of those fields need careful reading. km is the great-circle distance between the airports, not the distance the aircraft actually flies. That's the shortest path over the globe; real tracks are longer because of airways, weather and, on the North Atlantic, the day's organised track system. And duration_min is a typical block time, which includes taxiing, not airborne time.

Ask in both directions

Don't assume the return trip looks the same. Jet streams blow west to east, so on transatlantic routes the westbound flight is often scheduled an hour or so longer than the eastbound one. The carrier list can differ too: an airline might fly a route only one way as part of a triangle routing.

So ask twice:

import requests
from functools import lru_cache

BASE = "https://skylink-api.p.rapidapi.com"
H = {"X-RapidAPI-Key": KEY, "X-RapidAPI-Host": "skylink-api.p.rapidapi.com"}

def get(path, **params):
    r = requests.get(f"{BASE}{path}", headers=H, params=params, timeout=15)
    r.raise_for_status()
    return r.json()

@lru_cache(maxsize=None)
def airport(code):
    """One airport record, by IATA (3 letters) or ICAO (4 letters)."""
    key = "icao" if len(code) == 4 else "iata"
    return get("/airports/search", **{key: code.upper()})

def to_icao(code):
    """Route rows can hold IATA or ICAO codes, so normalise before comparing."""
    return code.upper() if len(code) == 4 else airport(code)["icao_code"]

def who_flies(a, b):
    """Carriers, distance and typical block time, in both directions."""
    result = {}
    for dep, arr in ((a, b), (b, a)):
        rows = get("/routes/pairs", departure=dep, arrival=arr, limit=10)["routes"]
        match = [r for r in rows
                 if to_icao(r["departure"]) == to_icao(dep)
                 and to_icao(r["arrival"]) == to_icao(arr)]
        result[f"{dep}-{arr}"] = match[0] if match else None
    return result

The filter on the returned rows looks fussy, and it is deliberate. Results are sorted by number of airlines, and the docs mention partial matches, so the safe habit is to check you got back the pair you asked about rather than trusting the first row.

The trap: IATA and ICAO in the same list

That to_icao helper is there because of a note in the airport routes docs: route rows "may use IATA or ICAO depending on the source row". Heathrow can come back as LHR in one row and EGLL in another.

If you group by the raw code, Heathrow turns into two destinations, each with only some of its airlines. The count of destinations is inflated and each carrier list is incomplete. If you've read ICAO vs IATA codes, this is the same problem showing up in a different place. Normalise everything to one form before you group, count or join.

Question two: where can I fly nonstop from here?

/routes/airport/{code} lists the routes at an airport. With direction=dep it's a nonstop destination list:

def nonstop_destinations(code):
    data = get(f"/routes/airport/{code}", direction="dep", limit=500)
    if data["count"] >= 500:
        print("warning: hit the limit, list may be incomplete")
    dests = {}
    for row in data["routes"]:
        d = dests.setdefault(to_icao(row["arrival"]), {
            "airlines": set(), "km": row["km"], "duration_min": row["duration_min"]})
        d["airlines"].update(row["airlines"])
    return dests

Grouping on the normalised code merges the LHR and EGLL rows into a single Heathrow entry with the union of their airlines.

Two practical notes. The limit tops out at 500, and a large hub can come close to that, so check count. And normalising codes costs one airport lookup per distinct IATA code. The lru_cache handles that within a process, but for a whole network map, build the IATA-to-ICAO table once and store it. Airports don't change codes often.

An American Airlines Boeing 767 waiting to take off

Question three: where is this aircraft going?

An aircraft's ADS-B broadcast gives you a callsign, not a route. /routes/callsign/{callsign} fills the gap:

{
  "callsign": "BAW178",
  "callsign_prefix": "BAW",
  "airline_code": "BAW",
  "departure_icao": "KJFK",
  "arrival_icao": "EGLL",
  "airports": ["KJFK", "EGLL"],
  "confidence": "high",
  "source": "vrs"
}

When there's no exact match for the callsign, departure_icao and arrival_icao are null, confidence is low, and you get a routes list of that airline's routes instead. The docs are explicit about that case: show the top route as a suggestion, and don't assume it's the flight in front of you.

Even a high-confidence match can be out of date. Airlines reuse and reassign callsigns between seasons, and a callsign isn't the same thing as a flight number anyway; callsigns and flight numbers explains why. A cheap check is whether the aircraft is anywhere near the route it supposedly flies:

import math

def km_between(a, b):
    p1, p2 = math.radians(a["latitude_deg"]), math.radians(b["latitude_deg"])
    dl = math.radians(b["longitude_deg"] - a["longitude_deg"])
    c = math.sin(p1) * math.sin(p2) + math.cos(p1) * math.cos(p2) * math.cos(dl)
    return 6371 * math.acos(max(-1.0, min(1.0, c)))

def route_for_callsign(callsign, lat=None, lon=None):
    d = get(f"/routes/callsign/{callsign}")
    if d["confidence"] != "high":
        # Airline-level guesses only. Show them as suggestions, never as fact.
        return {"certain": False, "candidates": (d.get("routes") or [])[:3]}
    route = {"certain": True, "from": d["departure_icao"], "to": d["arrival_icao"]}
    if lat is not None:
        # Is the aircraft somewhere sensible for this route? A position on or
        # near the path keeps dep->pos->arr close to the direct distance.
        dep, arr = airport(route["from"]), airport(route["to"])
        pos = {"latitude_deg": lat, "longitude_deg": lon}
        detour = (km_between(dep, pos) + km_between(pos, arr)) / km_between(dep, arr)
        route["plausible"] = detour < 1.2
    return route

An aircraft calling itself BAW178 over the middle of the Atlantic passes. The same callsign over Miami fails, and your map should stop labelling it "New York to London". The 20% tolerance is loose on purpose, because North Atlantic tracks can sit a long way from the great circle on a given day.

A TAG Airlines route map of Central America

What this data is not

The route endpoints use SkyLink's own database of routes. It's a picture of the network, and it isn't a timetable. A seasonal route can be listed in February, a route an airline dropped last month can still be there, and nothing here tells you whether a flight operates on a specific date.

When that matters, check the schedules endpoint for the departure airport. It covers from five days back to one day ahead, so it can confirm what's actually flying around now, though it can't tell you about next summer. For a booking-style answer about a future date, you need ticket and fare data instead.

The airlines field holds carrier names, not codes, so joining it to the airlines endpoint means matching names. The docs also don't say whether codeshare marketing carriers are included alongside operating carriers, so don't present the list as one or the other.

Where it's useful

Route pages and SEO landing pages. "Flights from Dublin to Boston" pages with the carriers, distance and typical flight time are a classic use. Build them from route pairs, refresh weekly, and link each one to a live departures board.

Network maps. Draw a hub's destinations from nonstop_destinations, and colour the lines by airline.

Labelling live traffic. Turn a bare callsign on an ADS-B map into "London to New York", with the plausibility check stopping obvious mislabels.

Rough journey estimates. duration_min is a sensible default for "about 7 hours" copy. For a specific aircraft type, the ML flight-time model gives a more tailored estimate.

The route endpoints are included from the free trial upwards. Route data changes slowly, so cache it and a small quota covers a lot.