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| Tool | Command |
|---|---|
| pip | pip install flightbyte |
| uv | uv add flightbyte |
| Poetry | poetry add flightbyte |
| pipenv | pipenv 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")) # 15Arguments 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", 200Then, 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.
| Error | Status | What to do |
|---|---|---|
BadRequestError | 400 | Fix the input; messages has a line per field. For a number that flies twice, legs lists them: pass one's origin. |
AuthenticationError | 401 | The key is missing, invalid, revoked or expired. |
PaymentRequiredError | 402 | No watched flights left. code is watched_flight_limit, overage_cap_reached (see resets_at) or credits_exhausted (see balance and expires_at). |
PermissionDeniedError | 403 | The account is suspended, or a token lacks the scope. |
NotFoundError | 404 | The flight did not operate that day, or there is no such subscription. |
RateLimitError | 429 | A minute, day or month quota is used up. Wait retry_after seconds. |
UpstreamError | 502 | The flight data provider failed. Retry shortly. |
ServerError, UnexpectedStatusError | other | Quote request_id to support. |
APIConnectionError | none | No 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
| Option | Default | Meaning |
|---|---|---|
api_key | required | Your API key, sent as x-api-key. |
base_url | https://api.flightbyte.dev/v1 | Another API base URL, such as a local one. |
timeout | 30.0 | Seconds each request may take, body included. None turns it off. |
user_agent | none | Put 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
defendpoint or throughasyncio.to_thread. - No dependencies. Only
urllib.request,json,hmacandhashlib. - 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
0and a missing delay reading are different values.
Next
- Webhooks: payload, change types and the retry schedule.
- Building a flight delay notification system.
- From a booking email to a watched flight.
- The same calls in Node.js, Go, Ruby and Dart, or all five side by side.
Node.js SDK
The official FlightByte library for Node.js and TypeScript. Look a flight up, watch it, and verify signed flight status webhooks on Node, Bun, Deno, Cloudflare Workers, Next.js and Express.
Go SDK
The official FlightByte module for Go 1.23+. Look a flight up, watch it, and verify signed flight status webhooks with a ready http.Handler, on the standard library alone.