FlightByte

Python SDK

The official FlightByte library for Python 3.10+. Look a flight up, watch it, and verify signed flight status webhooks in Flask, Django and FastAPI, on the standard library alone.

flightbyte on PyPI is the official Python client for the FlightByte flight status API. It looks flights up, watches them so every gate change, delay, departure and landing arrives as a signed webhook, and verifies each delivery in one call. Responses are dicts typed with TypedDict, so mypy, Pyright and your editor know every key. It uses only the standard library and runs on Python 3.10 to 3.14.

Source: flightbyte-python on GitHub, MIT license. Package: flightbyte on PyPI.

Install

pip install flightbyte
ToolCommand
pippip install flightbyte
uvuv add flightbyte
Poetrypoetry add flightbyte
pipenvpipenv install flightbyte

You need an API key: the free plan covers three watched flights a month, no card.

Look a flight up

import os

from flightbyte import FlightByte

client = FlightByte(api_key=os.environ["FLIGHTBYTE_API_KEY"])

flight = client.flights.get(
    carrier="TK",
    flight_number="1848",
    date="2026-09-08",  # local departure date at the origin
)

print(flight["status"]["label"])                # Scheduled
print(flight["departure"].get("gate"))          # A11
print(flight["departure"].get("delayMinutes"))  # 15

Arguments are the API's names in Python form (flight_number, webhook_url, idempotency_key); responses keep the API's own keys, with timestamps as ISO 8601 strings. FlightByte.format_date(datetime.now(timezone.utc), "Europe/Istanbul") gives the local departure date the API expects. A number that flies more than one leg that day lists every leg in alternates: pass origin="IST" to pick one. The full snapshot is under Flights.

Watch a flight

created = client.subscriptions.create(
    carrier="TK",
    flight_number="1848",
    date="2026-09-08",
    webhook_url="https://api.example.com/hooks/flights",
    events=["departure.gate.changed", "delay.increased", "flight.departed"],
    metadata={"bookingId": "BK-1042"},
    idempotency_key="BK-1042",  # a retry never counts twice
)

# Shown in full only now: store it where your webhook receiver can read it.
signing_secret = created["secret"]

A subscription is one watched flight on your plan, however many changes it sends. Ten travellers on the same flight are one watched flight when you subscribe once and pass each change on to all ten. FlightByte delivers to public HTTPS addresses only: on your machine, expose the port with cloudflared tunnel --url http://localhost:8080 or ngrok http 8080 and use that URL. client.subscriptions.send_test(id) sends a signed subscription.test whenever you want to see the path work.

The other calls: list, list_all (every page, as you iterate with for), get, update, pause, resume, delete, deliveries, events, secret and rotate_secret. Every method takes a keyword-only timeout in seconds.

Receive webhooks

A receiver verifies the signature against the raw body, skips an event id it has seen, answers 2xx within ten seconds and does the work afterwards. Webhook.construct_event checks the signature and decodes the event. It raises WebhookSignatureError for a bad signature, so answer 401, and FlightByteError for a signed body that is not an event, so answer 400. Give it the body as bytes, before any JSON parsing.

The raw body is request.get_data(), not request.json.

import os

from flask import Flask, request
from flightbyte import FlightByteError, Webhook, WebhookSignatureError

app = Flask(__name__)
signing_secret = os.environ["FLIGHTBYTE_WEBHOOK_SECRET"]


@app.post("/hooks/flightbyte")
def flightbyte_hook():
    try:
        event = Webhook.construct_event(
            request.get_data(),
            request.headers.get(Webhook.SIGNATURE_HEADER),
            signing_secret,
        )
    except WebhookSignatureError:
        return "invalid signature", 401
    except FlightByteError:
        return "invalid event", 400

    if mark_handled(event["id"]):  # a retry carries the same id
        jobs.put(event)            # answer first, work afterwards
    return "ok", 200

Then, off the request path:

match event["type"]:
    case "flight.updated":
        for change in event["changes"]:
            print(f"{event['flightKey']} {change['type']}: {change['message']}")
    case "subscription.completed":
        print(f"{event['flightKey']} finished")

An empty secret raises a ValueError at once, because anyone can sign with an empty key: an unset environment variable fails loudly instead of accepting forged events. The check is HMAC-SHA256 over "<t>." + raw body, compared with hmac.compare_digest, with a timestamp more than five minutes off rejected. Verifying webhook signatures explains each step, and Webhooks has the payload and the 23 change types.

Errors

Every failed request raises a subclass of FlightByteError, so you catch the ones you handle.

ErrorStatusWhat to do
BadRequestError400Fix the input; messages has a line per field. For a number that flies twice, legs lists them: pass one's origin.
AuthenticationError401The key is missing, invalid, revoked or expired.
PaymentRequiredError402No watched flights left. code is watched_flight_limit, overage_cap_reached (see resets_at) or credits_exhausted (see balance and expires_at).
PermissionDeniedError403The account is suspended, or a token lacks the scope.
NotFoundError404The flight did not operate that day, or there is no such subscription.
RateLimitError429A minute, day or month quota is used up. Wait retry_after seconds.
UpstreamError502The flight data provider failed. Retry shortly.
ServerError, UnexpectedStatusErrorotherQuote request_id to support.
APIConnectionErrornoneNo answer at all: network or TLS error, or timed_out when the timeout ran out.
import time

from flightbyte import BadRequestError, PaymentRequiredError, RateLimitError

try:
    client.subscriptions.create(**params)
except BadRequestError as err:
    if not err.legs:
        raise
    print("Pick a leg:", ", ".join(leg["origin"] for leg in err.legs))
except PaymentRequiredError as err:
    print(f"No watched flights left ({err.code}) until {err.resets_at}")
except RateLimitError as err:
    time.sleep(err.retry_after or 5)

Every error pickles cleanly, so it crosses a process pool or a Celery queue intact. client.last_rate_limit holds the minute, day and month windows from the latest response. Every status and code is in Errors.

Configuration

OptionDefaultMeaning
api_keyrequiredYour API key, sent as x-api-key.
base_urlhttps://api.flightbyte.dev/v1Another API base URL, such as a local one.
timeout30.0Seconds each request may take, body included. None turns it off.
user_agentnonePut in front of flightbyte-python/<version> in the user-agent header.

HTTPS_PROXY and NO_PROXY are honoured.

Built for Python services

  • Web apps, workers and scripts. Flask, Django, FastAPI, Celery, cron jobs and notebooks. One client is safe to share across threads and holds no open connection, so there is nothing to close.
  • Synchronous. In an async app, call it from a plain def endpoint or through asyncio.to_thread.
  • No dependencies. Only urllib.request, json, hmac and hashlib.
  • The key stays on your server. A key or signing secret inside a browser or mobile app can be read out of it. Flag images, from countries.flag_url, are the one thing that needs no key.
  • New values do not break old code. A phase, status or change type added later is still a valid str. The tests check every method and type against the API's OpenAPI document.
  • Zero is not unknown. A delay of 0 and a missing delay reading are different values.

Next

On this page