Skip to main content

FlightByte

FlightByte for Python

The official Python client for the FlightByte flight status API.
Look flights up, watch them with signed webhooks and verify every delivery,
from a web service, a worker or a script.

PyPI version Python 3.10+ No dependencies MIT license

Docs · Get a free key · API reference · Webhook docs · Support


Quick start

  1. Get a free API key at app.flightbyte.dev/signup.

  2. Add the package:

    Command
    pip pip install flightbyte
    uv uv add flightbyte
    Poetry poetry add flightbyte
    pipenv pipenv install flightbyte
  3. 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
    

Responses are plain dicts typed with TypedDict, so mypy, Pyright and your editor know every key. FlightByte.format_date(datetime.now(timezone.utc), "Europe/Istanbul") gives the YYYY-MM-DD the API expects, in the origin airport's time zone. To try it from a terminal, run the example in this repository:

pip install -e .
FLIGHTBYTE_API_KEY=frk_live_... python examples/lookup.py TK 1 2026-11-15
TK1:2026-11-15@IST-JFK  IST → JFK
Status     Scheduled (scheduled)
Departure  2026-11-15T15:20:00.000 +03, gate D15
Arrival    2026-11-15T18:20:00.000 EST, baggage -
Quota      23 of 30 left this minute

What you can do

You want to You call
Show a passenger their gate and delay client.flights.get(...) returns the live snapshot: phase, gate, terminal, baggage belt, scheduled, estimated and actual times, delay, aircraft and codeshares.
Look up TK1 tomorrow, which flies two legs Pass origin="IST". Without it the snapshot lists every leg in alternates, and a subscription refuses to guess: the BadRequestError carries the legs to choose from.
Be told when anything changes client.subscriptions.create(...) starts the watch and returns the signing secret, shown once.
Find out why webhooks stopped arriving client.subscriptions.get(id) and deliveries(id) show the status and the HTTP code and error of every recent attempt.
Trust a webhook on your server Webhook.construct_event(body, signature, secret) verifies the signature against the raw body and decodes the event.

API

Flights: read-only.

Method What it does
flights.get Live status of one flight leg, and every leg the number flies that day.
GET /v1/flights/{carrier}/{flightNumber}/{date}
flights.timeline The airline's own change history for a leg, newest first.
GET /v1/flights/{carrier}/{flightNumber}/{date}/timeline

Subscriptions

Method What it does Access
subscriptions.create Watch a flight and get a signed webhook for every change. Counts as a watched flight on your plan; pass idempotency_key so a retry never counts twice.
POST /v1/subscriptions
writes
subscriptions.list One page of your subscriptions, filtered by status, carrier, number or date.
GET /v1/subscriptions
read-only
subscriptions.list_all Every matching subscription, fetched page by page as you iterate over it with for.
GET /v1/subscriptions
read-only
subscriptions.get One subscription with its status and delivery counters.
GET /v1/subscriptions/{id}
read-only
subscriptions.update Change the webhook URL, event filter, delay threshold, headers or metadata.
PATCH /v1/subscriptions/{id}
writes
subscriptions.pause Stop polling and deliveries without losing anything.
POST /v1/subscriptions/{id}/pause
writes
subscriptions.resume Resume a paused or auto-suspended subscription.
POST /v1/subscriptions/{id}/resume
writes
subscriptions.delete Delete a subscription for good.
DELETE /v1/subscriptions/{id}
destructive
subscriptions.send_test Send a signed subscription.test webhook to the subscription's URL.
POST /v1/subscriptions/{id}/test
writes
subscriptions.deliveries Recent delivery attempts with HTTP status and errors.
GET /v1/subscriptions/{id}/deliveries
read-only
subscriptions.events Recent events with the exact payloads that were sent.
GET /v1/subscriptions/{id}/events
read-only
subscriptions.secret Reveal the signing secret again.
GET /v1/subscriptions/{id}/secret
read-only
subscriptions.rotate_secret Generate a new signing secret. Previous signatures stop validating.
POST /v1/subscriptions/{id}/secret/rotate
writes

Countries: read-only.

Method What it does
countries.list Every country with its name in any language, emoji and flag URLs.
GET /v1/countries
countries.get One country by its ISO 3166-1 alpha-2 code.
GET /v1/countries/{code}
countries.flag_url The URL of a country's SVG flag, 4:3 or square. Needs no key, so it can go straight into an <img>.
GET /v1/flags/{code}.svg

Every method takes a keyword-only timeout of its own, in seconds. Arguments carry the API's own names in Python form (carrier, flight_number, date, origin, webhook_url, idempotency_key, …), and responses come back exactly as the API sent them: dicts with the API's own keys, timestamps as ISO 8601 strings, and no field renamed or dropped. The full snapshot is documented 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",  # safe to retry
)

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

FlightByte only delivers to public HTTPS addresses. To receive on your machine, expose the port with a tunnel (cloudflared tunnel --url http://localhost:8080 or ngrok http 8080) and use that URL. subscriptions.send_test(id) sends a signed subscription.test whenever you want to see the path work.

Receive webhooks

A receiver has to do four things, in this order: verify the signature against the raw body, deduplicate on the event id, answer 2xx before doing the work, and branch on the event type.

Webhook.construct_event does the first: it checks the signature against the body and decodes the event. It raises WebhookSignatureError for a bad signature (answer 401) and FlightByteError for a signed body that is not an event (answer 400). An empty secret raises a ValueError right away, because anyone can sign with an empty key: an unset environment variable fails loudly instead of accepting forged events. Give it the body as bytes, before any JSON parsing. With Flask, that 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(),  # the raw body
            request.headers.get(Webhook.SIGNATURE_HEADER),
            signing_secret,
        )
    except WebhookSignatureError:
        return "invalid signature", 401
    except FlightByteError:
        return "invalid event", 400

    # A retry carries the same id: answer it, but don't handle it twice.
    if mark_handled(event["id"]):
        # Answer first: FlightByte treats a delivery as failed after 10 seconds.
        jobs.put(event)
    return "ok", 200
Django and FastAPI
# Django: the raw body is request.body. The signature is the check, so no CSRF token.
from django.http import HttpResponse
from django.views.decorators.csrf import csrf_exempt
from django.views.decorators.http import require_POST


@csrf_exempt
@require_POST
def flightbyte_hook(request):
    try:
        event = Webhook.construct_event(
            request.body, request.headers.get(Webhook.SIGNATURE_HEADER), signing_secret
        )
    except WebhookSignatureError:
        return HttpResponse("invalid signature", status=401)
    except FlightByteError:
        return HttpResponse("invalid event", status=400)
    jobs.put(event)
    return HttpResponse("ok")


# FastAPI and Starlette: read the body yourself, not through a Pydantic model.
from fastapi import FastAPI, Request, Response

app = FastAPI()


@app.post("/hooks/flightbyte")
async def flightbyte_hook(request: Request) -> Response:
    try:
        event = Webhook.construct_event(
            await request.body(), request.headers.get(Webhook.SIGNATURE_HEADER), signing_secret
        )
    except WebhookSignatureError:
        return Response("invalid signature", status_code=401)
    except FlightByteError:
        return Response("invalid event", status_code=400)
    jobs.put_nowait(event)
    return Response("ok")

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")

A receiver on the standard library's http.server, with no framework, is in examples/webhook_server.py.

How the signature works
x-flightbyte-signature: t=1789305971,v1=5f1c9a...e3b2
v1 = HMAC-SHA256(secret, "<t>." + <raw body>)

Webhook.verify computes the HMAC over the bytes you received, not over JSON you parsed and serialised again, compares it in constant time with hmac.compare_digest, accepts any of several v1 values, and rejects a t more than five minutes from now. Webhook.sign produces the same header, for testing your receiver. The test suite checks both against signatures made by the API's own signer, and throws thousands of random headers at the parser.

The reasons behind each step are in Verifying webhook signatures, and reference receivers in Node.js, Python and Go are in flightbyte-webhook-examples.

Errors

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

Error Status When, and what to do
BadRequestError 400 Invalid input; messages has one line per field. For a number that flies twice, legs lists them: pass one's origin.
AuthenticationError 401 Missing, invalid, revoked or expired API key.
PaymentRequiredError 402 No watched flights left. code says why: 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 (code).
NotFoundError 404 The flight did not operate that day, or 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. It is also a ServerError.
ServerError, UnexpectedStatusError other Anything else. request_id is what support asks for.
APIConnectionError none No response 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)

An APIError keeps the whole answer in status, messages, body, raw_body and headers, and every error pickles cleanly, so it crosses a process pool or a task queue intact. Every status is explained in the errors docs.

Built for Python services

  • Web services, workers and scripts. Flask, Django, FastAPI, Celery, cron jobs and notebooks, on Python 3.10 to 3.14, and CI runs it on every one of them. One client is safe to share across threads and keeps no connection open, so there is nothing to close. The client is synchronous: in an async app, call it from a plain def endpoint or through asyncio.to_thread. Proxies set in HTTPS_PROXY and NO_PROXY are honoured.
  • No dependencies. Only the standard library: urllib.request, json, hmac and hashlib. Nothing to audit but this package.
  • Your key stays on your server. An API key or a signing secret shipped inside a browser or mobile app can be pulled out of it by anyone who has the app. Flag images are the exception: countries.flag_url needs no key.
  • Your key stays with the API. The client follows a redirect only for a read, and only on the API's own origin, so a misdirected answer cannot carry the key elsewhere and a write is never sent twice.
  • New API values don't break old code. A phase, status or change type added after this release is still a valid str; FLIGHT_PHASES, FLIGHT_CHANGE_TYPES and the other tuples list the ones this version knows. The tests check every method and every type against the API's OpenAPI document, so a field the API adds shows up as a failing test here first, not as a missing key in your code.
  • Zero stays apart from unknown. Keys the API may leave out are not required by the types, and values it may send as null include None, so a delay of 0 and no delay reading are different things.
  • Errors say what to do next. A number that flies twice comes back with its legs, a 429 with how long to wait, a 402 with the reason and when the allowance resets.
  • Quotas are visible. client.last_rate_limit holds the minute, day and month windows from the latest response's X-RateLimit-* headers.

Configuration

client = FlightByte(
    api_key=os.environ["FLIGHTBYTE_API_KEY"],
    base_url="http://localhost:3000/v1",
    timeout=10.0,
    user_agent="my-app/1.2",
)
Option Default Meaning
api_key required Your API key, sent as x-api-key.
base_url https://api.flightbyte.dev/v1 API base URL, e.g. http://localhost:3000/v1 for a local API.
timeout 30.0 Bounds each request, response body included, in seconds. None turns it off. Every method takes a timeout of its own too.
user_agent none Put in front of flightbyte-python/<version> in the user-agent header.

Development

python -m venv .venv && source .venv/bin/activate
pip install -e . -r requirements-dev.txt
python -m unittest discover -s tests -t .   # every test, against local servers
ruff check . && ruff format --check .
mypy --strict src tests
python -m build && twine check --strict dist/*
python scripts/sync_openapi.py               # refresh spec/openapi.json, then run the tests against it
node tool/webhook_vectors.mjs

The tests never touch the real API and need no key: they connect only to servers they start on 127.0.0.1, and fail if anything tries to go elsewhere. They cover every method's request mapping, the error class for each status, redirects and timeouts against real local servers, the types against fixtures shaped like the API's JSON and against the API's OpenAPI document, and webhook verification against signatures made with Node's createHmac exactly as the API signs them. CI runs them on Python 3.10, 3.11, 3.12, 3.13 and 3.14, and again against the built wheel installed in a clean environment.

How to add a field, an enum value or an endpoint is in CONTRIBUTING.md. Report security issues by email, as SECURITY.md explains, never in a public issue.

Project layout
src/flightbyte/
  __init__.py              public exports
  _client.py               FlightByte and its options
  _transport.py            requests, headers, timeouts, redirects, error mapping
  _flights.py              flights
  _subscriptions.py        subscriptions, list_all and mask_secret
  _countries.py            countries and flag URLs
  _models.py               the API's TypedDicts and value lists
  _errors.py               FlightByteError and its subclasses
  _webhook.py              Webhook.verify, is_valid, construct_event and sign
  _rate_limit.py           X-RateLimit-* headers
  _util.py                 unwrapping {"data": [...]} answers
  py.typed                 marks the package as typed
spec/openapi.json          the API document the tests check against
tests/fixtures/            API-shaped JSON and signed webhook vectors
examples/                  a lookup from the terminal and a receiver on http.server
scripts/sync_openapi.py    refreshes spec/openapi.json
tool/webhook_vectors.mjs   regenerates the signed vectors with Node, as the API signs
tool/release_notes.sh      a version's CHANGELOG.md section, for the GitHub release
Releasing
  1. Set version in pyproject.toml and __version__ in src/flightbyte/_version.py; a test fails if they differ.
  2. Give the version its own ## <version> section in CHANGELOG.md.
  3. Push a tag such as v1.0.1.

The release workflow checks that the tag, pyproject.toml and _version.py agree and that the changelog has the section, runs every check, builds the sdist and wheel once, publishes them to PyPI with trusted publishing unless that version is already there, and creates the GitHub release from the changelog section with the files attached. Run it by hand from the Actions tab to rehearse: it builds and checks everything but publishes nothing.

License

MIT


FlightByte
FlightByte · Flight intelligence, byte by byte · flightbyte.dev

Metadata

Release files for flightbyte 1.0.0

For a detailed explanation of source distributions (sdists) and built distributions (wheels), please see the package formats documentation.

Source distribution (sdist)

Source distribution for flightbyte 1.0.0
File Size Uploaded Provenance Actions
flightbyte-1.0.0.tar.gz 58.7 kB With provenance Details for flightbyte-1.0.0.tar.gz

Built distribution (wheel)

Table of built distributions (wheels) for flightbyte 1.0.0
File Interpreter ABI Platform Actions
flightbyte-1.0.0-py3-none-any.whl Python 3 none any Details for flightbyte-1.0.0-py3-none-any.whl

Total release size: 96.7 kB

Release files / flightbyte-1.0.0.tar.gz

Download URL flightbyte-1.0.0.tar.gz
Size 58.7 kB
Tags Source
SHA-256 checksum
How to use checksums
92b3d21d1059a07f795f134e5a2a8bfe14d6433f2e6de9ab81e4ba2f7ccb854c
BLAKE2b-256 checksum
How to use checksums
4409b7d0f5097e8a7fd508939716e5bb2398b30dacbe32d31227c85a0460b361
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
Yes
Uploaded via twine/7.0.0 CPython/3.13.16

Provenance

Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.

PyPI Publish Attestation

PyPI verified that this artifact, at this checksum, originated from the publisher listed below.

Signed by GitHub Actions, verified by PyPI on Nov 15, 2026.

Transparency log

Release files / flightbyte-1.0.0-py3-none-any.whl

Download URL flightbyte-1.0.0-py3-none-any.whl
Size 38.0 kB
Tags Python 3
SHA-256 checksum
How to use checksums
d8a7068ddd45eccbc0773328c26f770b1ccc2b46a383f49862c75d612d3a47c6
BLAKE2b-256 checksum
How to use checksums
1d956a16c0a2533d78bb72113db18efdc33adc4e6e67d18fa34d49935a944474
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
Yes
Uploaded via twine/7.0.0 CPython/3.13.16

Provenance

Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.

PyPI Publish Attestation

PyPI verified that this artifact, at this checksum, originated from the publisher listed below.

Signed by GitHub Actions, verified by PyPI on Nov 15, 2026.

Transparency log

Security for flightbyte 1.0.0

Provenance

Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.

Full provenance

Both files in this release have been uploaded with provenance metadata.

Select from 2 files

Attestation for flightbyte-1.0.0.tar.gz

PyPI Publish Attestation

PyPI verified that this artifact, at this checksum, originated from the publisher listed below.

Signed by GitHub Actions, verified by PyPI on Nov 15, 2026.

Transparency log

A note on package security

PyPI is a repository for distributing packages, not a service for auditing them. While third-party security researchers and our staff do their best to detect and remove malicious packages, PyPI cannot guarantee the quality, security or suitability of any given package.

To learn more about protecting yourself from malicious actors, visit our package security documentation.

Release history Release notifications | RSS feed

This release

1.0.0 This release

2 release files Full provenance

Anthropic, PBC Visionary sponsor Bloomberg Visionary sponsor Hudson River Trading Visionary sponsor Meta Visionary sponsor NVIDIA Visionary sponsor Microsoft Sustainability sponsor Depot Continuous Integration AWS Cloud computing and Security Sponsor Datadog Monitoring Fastly CDN Google Download Analytics Sentry Error logging StatusPage Status page