flightbyte 1.0.1
flightbyte: ^1.0.1 copied to clipboard
Official Dart client for the FlightByte flight status API. Look flights up, subscribe to changes and verify signed webhooks.
FlightByte for Dart and Flutter
The official Dart client for the FlightByte flight status API.
Look flights up, watch them with signed webhooks and verify every delivery,
from a Dart server, a CLI or the backend behind your Flutter app.
Docs · Get a free key · API reference · Webhook docs · Support
Quick start #
-
Get a free API key at app.flightbyte.dev/signup.
-
Add the package:
Command Dart dart pub add flightbyteFlutter flutter pub add flightbyte -
Look a flight up:
import 'package:flightbyte/flightbyte.dart'; final client = FlightByteClient(apiKey: 'frk_live_...'); final flight = await client.flights.get( carrier: 'TK', flightNumber: '1848', date: '2026-09-08', // local departure date at the origin ); print(flight.status.label); // Scheduled print(flight.departure.gate); // A11 print(flight.departure.times.bestGate?.local); // 2026-09-08T12:25:00.000 print(flight.departure.delayMinutes); // 15 client.close();
FlightByteClient.formatDate(DateTime) turns a date into the YYYY-MM-DD the API expects. To try it from a terminal, run the bundled example:
export FLIGHTBYTE_API_KEY=frk_live_...
dart run example/main.dart TK 1 2026-11-10
Turkish Airlines TK1 on 2026-11-10
IST → JFK Departed (en_route)
departs 2026-11-10T15:17:00.000 from gate F7
arrives 2026-11-10T18:14:00.000
quota: 28 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 BadRequestException 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.constructEvent(...) checks 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 idempotencyKey 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.listAll |
Every matching subscription as a Stream, fetched page by page.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.sendTest |
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.rotateSecret |
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.flagUrl |
The URL of a country's SVG flag, 4:3 or square. Needs no key, so it can go straight into an image widget.GET /v1/flags/{code}.svg |
Parameters carry the API's own names (carrier, flightNumber, date, origin, webhookUrl, Idempotency-Key, …), and the models carry every field of the API's JSON. The full snapshot is documented under Flights.
Watch a flight #
final created = await client.subscriptions.create(
carrier: 'TK',
flightNumber: '1848',
date: '2026-09-08',
webhookUrl: Uri.parse('https://api.example.com/hooks/flights'),
events: [
FlightChangeType.departureGateChanged,
FlightChangeType.delayIncreased,
FlightChangeType.departed,
],
metadata: {'bookingId': 'BK-1042'},
idempotencyKey: 'BK-1042', // safe to retry
);
// Shown in full only now: store it where your webhook receiver can read it.
final signingSecret = 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.sendTest(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. With shelf:
import 'dart:async';
import 'package:flightbyte/flightbyte.dart';
import 'package:shelf/shelf.dart';
Future<Response> flightHook(Request request) async {
final payload = await request.readAsString(); // raw body, before any parsing
final WebhookEvent event;
try {
event = Webhook.constructEvent(
payload: payload,
signature: request.headers[Webhook.signatureHeader],
secret: signingSecret,
);
} on WebhookSignatureException {
return Response(401, body: 'bad signature');
}
// A retry carries the same id: answer it, but don't handle it twice.
if (!await markHandled(event.id)) return Response(204);
// Answer first: FlightByte treats a delivery as failed after 10 seconds.
unawaited(handle(event));
return Response(204);
}
Future<void> handle(WebhookEvent event) async {
switch (event.type) {
case WebhookEventType.flightUpdated:
for (final change in event.changes) {
print('${event.flightKey} ${change.type.value}: ${change.message}');
}
case WebhookEventType.subscriptionCompleted:
print('${event.flightKey} finished');
default:
break;
}
}
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 body you received, not over JSON you parsed and serialised again, compares it in constant time, accepts any of several v1 values, and rejects a t more than five minutes from now. An empty secret throws an ArgumentError instead of being used as a key, since anyone can sign with an empty key. Webhook.sign produces the same header, for testing your receiver. The test suite checks both against signatures made by the API's own signer.
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 throws a subclass of the sealed FlightByteException, so a switch over it is checked for completeness.
| Exception | Status | When, and what to do |
|---|---|---|
BadRequestException |
400 |
Invalid input; messages has one line per field. For a number that flies twice, legs lists them: pass one's origin. |
AuthenticationException |
401 |
Missing, invalid, revoked or expired API key. |
PaymentRequiredException |
402 |
No watched flights left this period. code says why (watched_flight_limit or overage_cap_reached); resetsAt says when the count resets. |
PermissionDeniedException |
403 |
The account is suspended, or an OAuth token lacks the scope (code). |
NotFoundException |
404 |
The flight did not operate that day, or no such subscription. |
RateLimitException |
429 |
A minute, day or month quota is used up. Wait retryAfter. |
UpstreamException |
502 |
The flight data provider failed. Retry shortly. |
ServerException, UnexpectedStatusException |
other | Anything else. requestId is what support asks for. |
FlightByteConnectionException |
none | No response at all: network error, TLS failure or timeout. cause holds the underlying error. |
try {
await client.subscriptions.create(/* ... */);
} on BadRequestException catch (e) {
if (e.legs.isEmpty) rethrow;
print('Pick a leg: ${e.legs.map((l) => l.origin).join(', ')}');
} on PaymentRequiredException catch (e) {
print('No watched flights left until ${e.resetsAt}');
} on RateLimitException catch (e) {
await Future<void>.delayed(e.retryAfter ?? const Duration(seconds: 5));
}
Every status is explained in the errors docs.
Built for Flutter and Dart servers #
- Dart servers, CLIs and the backend behind your Flutter app. It runs everywhere Dart does: iOS, Android, web, macOS, Windows and Linux.
- Two dependencies.
httpandcrypto, both from the Dart team. - Your key stays on your server. An API key or a signing secret shipped inside a mobile or web app can be pulled out of it by anyone who has the app. In Flutter, call your own backend and let the backend use this package. Flag images are the exception:
countries.flagUrlneeds 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 apps. A phase, status, change type or event type added after this release decodes to an
unknownmember instead of throwing, and the raw string is kept (typeValue,statusValue,phaseValue). - Zero stays apart from unknown. Values the API may leave out are nullable, so a delay of
0and no delay reading are different things. - Errors say what to do next. A number that flies twice comes back with its legs, a
429with how long to wait, a402with the reason and when the allowance resets. - Quotas are visible.
client.lastRateLimitholds the minute, day and month windows from the latest response'sX-RateLimit-*headers.
Configuration #
final client = FlightByteClient(
apiKey: 'frk_live_...',
baseUrl: Uri.parse('http://localhost:3000/v1'),
httpClient: myClient,
timeout: const Duration(seconds: 10),
);
| Parameter | Default | Meaning |
|---|---|---|
apiKey |
required | Your API key, sent as x-api-key. |
baseUrl |
https://api.flightbyte.dev/v1 |
API base URL, e.g. http://localhost:3000/v1 for a local API. |
httpClient |
a new http.Client |
Bring your own for retries, logging or tests. A client you pass is not closed by close(). |
timeout |
30 seconds | Bounds each request, response body included. When it runs out, the request is aborted, not left running. |
Development #
dart pub get
dart test # everything, against a mocked HTTP client
dart analyze --fatal-infos
dart format --set-exit-if-changed .
dart pub publish --dry-run
The tests never touch the real API and need no key. They cover every method's request mapping, the error mapping for each status, the models against fixtures shaped like the API's JSON, and webhook verification against signatures made with Node's createHmac exactly as the API signs them. CI runs the same commands on every push.
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
lib/
flightbyte.dart public exports
src/client.dart FlightByteClient
src/transport.dart requests, headers, timeouts, error mapping
src/resources/ flights, subscriptions, countries
src/models/ snapshots, subscriptions, deliveries, events, enums
src/errors.dart the sealed FlightByteException hierarchy
src/webhook.dart Webhook.verify, constructEvent and sign
src/rate_limit.dart X-RateLimit-* headers
example/main.dart looks a flight up from the terminal
test/fixtures/ API-shaped JSON and signed webhook vectors
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
- Bump
versioninpubspec.yamlandpackageVersioninlib/src/version.dart; a test fails if they differ. - Give the version its own
## <version>section inCHANGELOG.md. - Push a tag such as
v1.0.1.
The publish workflow checks that the tag, pubspec.yaml and lib/src/version.dart agree and that the changelog has the section, publishes to pub.dev unless that version is already there, and creates the GitHub release from the changelog section. Run it by hand from the Actions tab to rehearse without publishing. Publishing from a tag needs automated publishing enabled for this repository on the package's pub.dev admin page; the first release is published by hand with dart pub publish.
License #
FlightByte · Flight intelligence, byte by byte · flightbyte.dev