FlightByte

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.

flightbyte on npm is the official Node.js and TypeScript 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. It ships its own types, has no dependencies, and runs wherever fetch and Web Crypto do: Node 20.19 and later, Bun, Deno, Cloudflare Workers and Vercel Edge.

Source: flightbyte-node on GitHub, MIT license. Package: flightbyte on npm.

Install

npm install flightbyte
Package managerCommand
npmnpm install flightbyte
pnpmpnpm add flightbyte
Yarnyarn add flightbyte
Bunbun add flightbyte
Denodeno add npm:flightbyte

The package is ES modules, and require() works on Node 20.19 and later. You need an API key: the free plan covers three watched flights a month, no card.

Look a flight up

import { FlightByte } from 'flightbyte';

const client = new FlightByte({ apiKey: process.env.FLIGHTBYTE_API_KEY! });

const flight = await client.flights.get({
  carrier: 'TK',
  flightNumber: '1848',
  date: '2026-09-08', // local departure date at the origin
});

console.log(flight.status.label);           // Scheduled
console.log(flight.departure.gate);         // A11
console.log(flight.departure.delayMinutes); // 15

Every field autocompletes. The date is the local departure date at the origin airport; FlightByte.formatDate(new Date(), 'Europe/Istanbul') gives it in the YYYY-MM-DD form the API expects. A number that flies more than one leg that day, such as TK1, lists every leg in alternates: pass origin: 'IST' to pick one. The full snapshot is under Flights.

Watch a flight

const created = await client.subscriptions.create(
  {
    carrier: 'TK',
    flightNumber: '1848',
    date: '2026-09-08',
    webhookUrl: 'https://api.example.com/hooks/flights',
    events: ['departure.gate.changed', 'delay.increased', 'flight.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.
const 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. 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, with for await), get, update, pause, resume, delete, deliveries, events, secret and rotateSecret. Every method takes a last argument with a signal to cancel it and a timeoutMs of its own.

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.fetchHandler does the first and answers for you: 401 for a bad signature, 400 for a body that is not an event, 500 when your function throws, so FlightByte retries, and 200 otherwise.

import { Webhook } from 'flightbyte';

const receive = Webhook.fetchHandler(process.env.FLIGHTBYTE_WEBHOOK_SECRET!, async (event) => {
  if (!(await markHandled(event.id))) return; // a retry carries the same id
  queue.push(event); // answer first, work afterwards
});

It takes a Request and returns a Response, so it mounts as it is in every runtime built on them:

// app/api/flightbyte/route.ts
export const POST = receive;

Express

Give the route the raw body with express.raw, not express.json. The signature covers the bytes as sent, and JSON parsed and serialised again no longer matches.

import express from 'express';
import { Webhook, WebhookSignatureError } from 'flightbyte';

app.post('/hooks/flightbyte', express.raw({ type: 'application/json' }), async (req, res) => {
  let event;
  try {
    event = await Webhook.constructEvent(req.body, req.get(Webhook.SIGNATURE_HEADER), signingSecret);
  } catch (err) {
    return res.sendStatus(err instanceof WebhookSignatureError ? 401 : 400);
  }
  res.sendStatus(200);

  switch (event.type) {
    case 'flight.updated':
      for (const change of event.changes) console.log(`${event.flightKey} ${change.type}: ${change.message}`);
      break;
    case 'subscription.completed':
      console.log(`${event.flightKey} finished`);
      break;
  }
});

An empty secret throws a TypeError at once, because anyone can sign with an empty key: an unset environment variable fails at startup instead of accepting forged events. The check is HMAC-SHA256 over "<t>." + raw body, compared in constant time with crypto.subtle.verify, 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 rejects with a subclass of FlightByteError, so you branch with instanceof.

ErrorStatusWhat to do
BadRequestError400Fix the input; messages has a line per field. For a number that flies twice, legs lists them: pass one's origin.
AuthenticationError401The key is missing, invalid, revoked or expired.
PaymentRequiredError402No watched flights left. code is watched_flight_limit, overage_cap_reached (see resetsAt) or credits_exhausted (see balance and expiresAt).
PermissionDeniedError403The account is suspended, or a token lacks the scope.
NotFoundError404The flight did not operate that day, or there is no such subscription.
RateLimitError429A minute, day or month quota is used up. Wait retryAfterSeconds.
UpstreamError502The flight data provider failed. Retry shortly.
ServerError, UnexpectedStatusErrorotherQuote requestId to support.
ConnectionErrornoneNo answer at all; timedOut when the timeout ran out.
import { BadRequestError, PaymentRequiredError, RateLimitError } from 'flightbyte';

try {
  await client.subscriptions.create(params);
} catch (err) {
  if (err instanceof BadRequestError && err.legs.length > 0) {
    console.log('Pick a leg:', err.legs.map((leg) => leg.origin).join(', '));
  } else if (err instanceof PaymentRequiredError) {
    console.log(`No watched flights left (${err.code}) until ${err.resetsAt}`);
  } else if (err instanceof RateLimitError) {
    await new Promise((resolve) => setTimeout(resolve, (err.retryAfterSeconds ?? 5) * 1000));
  } else {
    throw err;
  }
}

client.lastRateLimit holds the minute, day and month windows from the latest response. Every status and code is in Errors.

Configuration

OptionDefaultMeaning
apiKeyrequiredYour API key, sent as x-api-key.
baseUrlhttps://api.flightbyte.dev/v1Another API base URL, such as a local one.
fetchthe global fetchYour own, for proxies, logging or tests.
timeoutMs30000Bounds each request, body included. 0 leaves it to your signal.
userAgentnonePut in front of flightbyte-node/<version> in the user-agent header.

What the library guarantees

  • The key stays on your server. A key or signing secret inside a browser or mobile app can be read out of it. Call FlightByte from your backend; flag images, from countries.flagUrl, are the one thing that needs no key.
  • The key stays with the API. Only reads follow redirects, and only on the API's own origin.
  • New values do not break old code. A phase, status or change type added later is still a valid string. The tests check every method and type against the API's OpenAPI document.
  • Zero is not unknown. A delay of 0 and no delay reading are different values.

Next

On this page