Dart SDK
The official FlightByte package for Dart 3.5+. Look a flight up, watch it, and verify signed flight status webhooks on a Dart server, in a CLI, or in the backend behind your Flutter app.
flightbyte on pub.dev is the official Dart 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. Responses are typed models, errors are a sealed class you can switch over, and it depends only on http and crypto from the Dart team. It needs Dart 3.5 or later.
Source: flightbyte-dart on GitHub, MIT license. Package: flightbyte on pub.dev.
Building a Flutter app?
Keep the API key on a server you run. A key inside an app you ship can be read out of it by anyone who has the app. The Flutter app calls your backend, and the backend uses this package, or the Node.js, Python, Go or Ruby one. Flag images, from countries.flagUrl, are the one thing an app can load with no key.
Install
dart pub add flightbyteIn a Flutter project, flutter pub add flightbyte. You need an API key: the free plan covers three watched flights a month, no card.
Look a flight up
import 'dart:io';
import 'package:flightbyte/flightbyte.dart';
final client = FlightByteClient(apiKey: Platform.environment['FLIGHTBYTE_API_KEY']!);
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();Parameters carry the API's own names, and the models carry every field of the API's JSON. FlightByteClient.formatDate(DateTime) gives the YYYY-MM-DD 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.
To try it from a terminal, in a clone of the repository:
export FLIGHTBYTE_API_KEY=frk_live_...
dart run example/main.dart TK 1 2026-11-10Watch 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', // a retry never counts twice
);
// Shown in full only now: store it where your webhook receiver can read it.
final signingSecret = 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, for example as push notifications to each traveller's device. 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.sendTest(id) sends a signed subscription.test whenever you want to see the path work.
The other calls: list, listAll (every page), get, update, pause, resume, delete, deliveries, events, secret and rotateSecret.
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. 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');
}
if (!await markHandled(event.id)) return Response(204); // a retry carries the same id
unawaited(handle(event)); // answer first, work afterwards
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;
}
}An empty secret throws an ArgumentError instead of being used as a key, because anyone can sign with an empty key. The check is HMAC-SHA256 over "<t>." + raw body, compared in constant time, 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 throws a subclass of the sealed FlightByteException, so a switch over it is checked for completeness.
| Exception | Status | What to do |
|---|---|---|
BadRequestException | 400 | Fix the input; messages has a line per field. For a number that flies twice, legs lists them: pass one's origin. |
AuthenticationException | 401 | The key is missing, invalid, revoked or expired. |
PaymentRequiredException | 402 | No watched flights left. code says why; resetsAt says when the count resets. |
PermissionDeniedException | 403 | The account is suspended, or a token lacks the scope. |
NotFoundException | 404 | The flight did not operate that day, or there is 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 | Quote requestId to support. |
FlightByteConnectionException | none | No answer at all: network error, TLS failure or timeout. cause holds the error underneath. |
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));
}client.lastRateLimit holds the minute, day and month windows from the latest response. Every status and code is in Errors.
Configuration
| Parameter | Default | Meaning |
|---|---|---|
apiKey | required | Your API key, sent as x-api-key. |
baseUrl | https://api.flightbyte.dev/v1 | Another API base URL, such as a local one. |
httpClient | a new http.Client | Your own, for retries, logging or tests. close() leaves a client you passed open. |
timeout | 30 seconds | Bounds each request, body included. A request that runs out is aborted, not left running. |
Built for Dart servers
- Dart servers, CLIs and the backend behind your Flutter app. It runs everywhere Dart does.
- Two dependencies.
httpandcrypto, both from the Dart team. - The key stays with the API. Only reads follow redirects, and only on the API's own origin.
- New values do not break old apps. A phase, status or change type added later decodes to an
unknownmember instead of throwing, and the raw string is kept intypeValue,statusValueandphaseValue. - Zero is not unknown. Values the API may leave out are nullable, so a delay of
0and a missing reading differ.
Next
- Flight status push notifications: one webhook, every device on the flight.
- Webhooks: payload, change types and the retry schedule.
- The same calls in Node.js, Python, Go and Ruby, or all five side by side.
Ruby SDK
The official FlightByte gem for Ruby 3.1+. Look a flight up, watch it, and verify signed flight status webhooks with a ready Rack receiver for Rails, Sinatra and Roda, on the standard library alone.
Authentication and quotas
The API key header, the quota windows on every key, watched-flight limits, and what a 429 or a 402 looks like.