FlightByte

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 flightbyte

In 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-10

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', // 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.

ExceptionStatusWhat to do
BadRequestException400Fix the input; messages has a line per field. For a number that flies twice, legs lists them: pass one's origin.
AuthenticationException401The key is missing, invalid, revoked or expired.
PaymentRequiredException402No watched flights left. code says why; resetsAt says when the count resets.
PermissionDeniedException403The account is suspended, or a token lacks the scope.
NotFoundException404The flight did not operate that day, or there is no such subscription.
RateLimitException429A minute, day or month quota is used up. Wait retryAfter.
UpstreamException502The flight data provider failed. Retry shortly.
ServerException, UnexpectedStatusExceptionotherQuote requestId to support.
FlightByteConnectionExceptionnoneNo 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

ParameterDefaultMeaning
apiKeyrequiredYour API key, sent as x-api-key.
baseUrlhttps://api.flightbyte.dev/v1Another API base URL, such as a local one.
httpClienta new http.ClientYour own, for retries, logging or tests. close() leaves a client you passed open.
timeout30 secondsBounds 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. http and crypto, 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 unknown member instead of throwing, and the raw string is kept in typeValue, statusValue and phaseValue.
  • Zero is not unknown. Values the API may leave out are nullable, so a delay of 0 and a missing reading differ.

Next

On this page