FlightByte

Go SDK

The official FlightByte module for Go 1.23+. Look a flight up, watch it, and verify signed flight status webhooks with a ready http.Handler, on the standard library alone.

github.com/FlightByte-dev/flightbyte-go is the official Go 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 gives you a ready http.Handler that verifies each delivery. It uses only the standard library, needs Go 1.23 or later, and a *Client is safe to share between goroutines.

Source: flightbyte-go on GitHub, MIT license. Docs: pkg.go.dev.

Install

go get github.com/FlightByte-dev/flightbyte-go

The package name is flightbyte:

import flightbyte "github.com/FlightByte-dev/flightbyte-go"

You need an API key: the free plan covers three watched flights a month, no card.

Look a flight up

client, err := flightbyte.NewClient(os.Getenv("FLIGHTBYTE_API_KEY"))
if err != nil {
	log.Fatal(err)
}

flight, err := client.Flights.Get(context.Background(), flightbyte.FlightParams{
	Carrier:      "TK",
	FlightNumber: "1848",
	Date:         "2026-09-08", // local departure date at the origin
})
if err != nil {
	log.Fatal(err)
}

fmt.Println(flight.Status.Label)                     // Scheduled
fmt.Println(flight.Departure.Gate)                   // A11
fmt.Println(flight.Departure.Times.BestGate().Local) // 2026-09-08T12:25:00.000

Every method takes a context.Context first and honours its deadline and cancellation. Struct fields carry the API's names in Go form (FlightNumber, WebhookURL, IdempotencyKey), and the models carry every field of the API's JSON. flightbyte.FormatDate(t) gives the YYYY-MM-DD the API expects. A number that flies more than one leg that day lists every leg in flight.Alternates: set Origin: "IST" to pick one. The full snapshot is under Flights.

To try it from a terminal:

export FLIGHTBYTE_API_KEY=frk_live_...
go run github.com/FlightByte-dev/flightbyte-go/examples/lookup@latest TK 1 2026-11-11

Watch a flight

created, err := client.Subscriptions.Create(ctx, flightbyte.CreateSubscriptionParams{
	Carrier:      "TK",
	FlightNumber: "1848",
	Date:         "2026-09-08",
	WebhookURL:   "https://api.example.com/hooks/flights",
	Events: []flightbyte.FlightChangeType{
		flightbyte.ChangeDepartureGate,
		flightbyte.ChangeDelayIncreased,
		flightbyte.ChangeDeparted,
	},
	Metadata:       map[string]string{"bookingId": "BK-1042"},
	IdempotencyKey: "BK-1042", // a retry never counts twice
})
if err != nil {
	return err
}

// Shown in full only now: store it where your webhook receiver can read it.
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(ctx, id) sends a signed subscription.test whenever you want to see the path work.

The other calls: List, All (every subscription as an iter.Seq2 you range over), 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. flightbyte.WebhookHandler does the first and gives you an ordinary http.Handler:

mux := http.NewServeMux()
mux.Handle("POST /hooks/flights", flightbyte.WebhookHandler(signingSecret,
	func(ctx context.Context, event *flightbyte.WebhookEvent) error {
		if !markHandled(event.ID) { // a retry carries the same id
			return nil
		}
		jobs <- event // answer first, work afterwards
		return nil
	}))

It answers 401 for a bad signature, 400 for a body that is not an event, 500 when your function returns an error, so FlightByte retries, and 200 otherwise. Your function's errors go to the log package, or wherever flightbyte.WithErrorHandler(fn) sends them. Then, off the request path:

for event := range jobs {
	switch event.Type {
	case flightbyte.EventFlightUpdated:
		for _, change := range event.Changes {
			log.Printf("%s %s: %s", event.FlightKey, change.Type, change.Message)
		}
	case flightbyte.EventSubscriptionCompleted:
		log.Printf("%s finished", event.FlightKey)
	}
}

chi mounts the handler as it is, and Gin and Echo wrap it with gin.WrapH and echo.WrapHandler. Elsewhere, read the body yourself and call flightbyte.ConstructWebhookEvent(payload, r.Header.Get(flightbyte.SignatureHeader), secret).

WebhookHandler panics when the secret is empty, 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 with hmac.Equal, 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

A failed request returns an *APIError that matches a sentinel with errors.Is, so you branch on the status without comparing numbers, and read the details with errors.As.

SentinelStatusWhat to do
ErrBadRequest400Fix the input; Messages has a line per field. For a number that flies twice, Legs lists them: set one's Origin.
ErrUnauthorized401The key is missing, invalid, revoked or expired.
ErrPaymentRequired402No watched flights left. Code says why; ResetsAt says when the count resets.
ErrForbidden403The account is suspended, or a token lacks the scope.
ErrNotFound404The flight did not operate that day, or there is no such subscription.
ErrRateLimited429A minute, day or month quota is used up. Wait RetryAfter.
ErrUpstream502The flight data provider failed. Retry shortly.
ErrServer5xxAny server error. Quote RequestID to support.
*ConnectionErrornoneNo answer at all: network error, timeout or a cancelled context. It unwraps to the cause.
_, err := client.Subscriptions.Create(ctx, params)

var apiErr *flightbyte.APIError
switch {
case errors.Is(err, flightbyte.ErrBadRequest) && errors.As(err, &apiErr) && len(apiErr.Legs) > 0:
	for _, leg := range apiErr.Legs {
		fmt.Println("Pick a leg:", leg.Origin, "→", leg.Destination)
	}
case errors.Is(err, flightbyte.ErrPaymentRequired) && errors.As(err, &apiErr):
	fmt.Println("No watched flights left until", apiErr.ResetsAt)
case errors.Is(err, flightbyte.ErrRateLimited) && errors.As(err, &apiErr):
	time.Sleep(apiErr.RetryAfter)
case err != nil:
	return err
}

client.LastRateLimit() returns the minute, day and month windows from the latest response. Every status and code is in Errors.

Configuration

client, err := flightbyte.NewClient(apiKey,
	flightbyte.WithBaseURL("http://localhost:3000/v1"),
	flightbyte.WithHTTPClient(myHTTPClient),
	flightbyte.WithTimeout(10*time.Second),
	flightbyte.WithUserAgent("my-app/1.2"),
)
OptionDefaultMeaning
WithBaseURLhttps://api.flightbyte.dev/v1Another API base URL, such as a local one.
WithHTTPClienta new http.ClientYour own, for retries, proxies, logging or tests. It is copied, not changed.
WithTimeout30 secondsBounds each request, body included. 0 leaves it to the context.
WithUserAgentnonePut in front of flightbyte-go/<version> in the User-Agent header.

Built for Go services

  • One client, many goroutines. Create it once and share it.
  • No dependencies. Only net/http, encoding/json and crypto/hmac.
  • The key stays on your server, and with the API. Only a GET follows a redirect, and only on the API's own host and scheme, so a write is never replayed and the key never travels elsewhere.
  • New values do not break old binaries. Phases, statuses and change types are string types, so a value added later arrives as it is; Known() says whether this version has a constant for it.
  • Zero is not unknown. Delays and times the API may leave out are pointers, so a delay of 0 and a missing reading differ.

Next

On this page