diff --git a/.github/docs-requirements.txt b/.github/docs-requirements.txt
deleted file mode 100644
index 383ae2d..0000000
--- a/.github/docs-requirements.txt
+++ /dev/null
@@ -1,5 +0,0 @@
-# Pinned so a compromised release cannot be pulled in automatically: these
-# packages execute during `mkdocs build`, inside the job that publishes the
-# docs site. Bump deliberately, not implicitly.
-mkdocs-material==9.7.7
-mkdocs-git-committers-plugin-2==2.5.0
diff --git a/.github/workflows/deploy-docs.yml b/.github/workflows/deploy-docs.yml
deleted file mode 100644
index e44b23f..0000000
--- a/.github/workflows/deploy-docs.yml
+++ /dev/null
@@ -1,53 +0,0 @@
-name: Deploy MkDocs
-on:
- push:
- branches:
- - main
- paths:
- - 'mkdocs.yml'
- - 'docs/**'
- - '.github/docs-requirements.txt'
- - '.github/workflows/deploy-docs.yml'
-
-# Pages is served from the Actions artifact, so nothing here needs to push to
-# the repository: a compromised docs dependency gets no write access.
-permissions:
- contents: read
- pages: write
- id-token: write
-
-# One deploy at a time, and never cancel one midway.
-concurrency:
- group: pages
- cancel-in-progress: false
-
-jobs:
- build:
- runs-on: ubuntu-latest
- steps:
- - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
- - uses: actions/setup-python@5fda3b95a4ea91299a34e894583c3862153e4b97 # v7.0.0
- with:
- python-version: 3.x
- - run: echo "cache_id=$(date --utc '+%V')" >> $GITHUB_ENV
- - uses: actions/cache@55cc8345863c7cc4c66a329aec7e433d2d1c52a9 # v6.1.0
- with:
- key: mkdocs-material-${{ env.cache_id }}
- path: .cache
- restore-keys: |
- mkdocs-material-
- - run: pip install -r .github/docs-requirements.txt
- - run: mkdocs build
- - uses: actions/upload-pages-artifact@fc324d3547104276b827a68afc52ff2a11cc49c9 # v5.0.0
- with:
- path: site
-
- deploy:
- needs: build
- runs-on: ubuntu-latest
- environment:
- name: github-pages
- url: ${{ steps.deployment.outputs.page_url }}
- steps:
- - id: deployment
- uses: actions/deploy-pages@368f82528645a54fb793d4d04e342629a3f51346 # v5.0.1
diff --git a/.github/workflows/docs.yml b/.github/workflows/docs.yml
new file mode 100644
index 0000000..81fb1d0
--- /dev/null
+++ b/.github/workflows/docs.yml
@@ -0,0 +1,47 @@
+name: Docs
+
+# The documentation builds without warnings. Read the Docs publishes it; this
+# only checks that a change does not break the build.
+
+on:
+ push:
+ branches: [main]
+ paths:
+ - 'docs/**'
+ - '.readthedocs.yaml'
+ - 'CONTRIBUTING.md'
+ - 'LICENSE'
+ - 'python/FlightRadarAPI/__init__.py'
+ - '.github/workflows/docs.yml'
+ pull_request:
+ paths:
+ - 'docs/**'
+ - '.readthedocs.yaml'
+ - 'CONTRIBUTING.md'
+ - 'LICENSE'
+ - 'python/FlightRadarAPI/__init__.py'
+ - '.github/workflows/docs.yml'
+ workflow_dispatch:
+
+permissions:
+ contents: read
+
+concurrency:
+ group: ${{ github.workflow }}-${{ github.ref }}
+ cancel-in-progress: true
+
+jobs:
+ # Warnings (including broken cross-references, via -n) fail the build, the
+ # same as on Read the Docs.
+ docs:
+ runs-on: ubuntu-latest
+ timeout-minutes: 10
+ steps:
+ - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
+ - uses: actions/setup-python@5fda3b95a4ea91299a34e894583c3862153e4b97 # v7.0.0
+ with:
+ python-version: '3.11'
+ - name: Install docs dependencies
+ run: pip install -r docs/requirements.txt
+ - name: Build docs
+ run: python -m sphinx -n -W --keep-going -b html docs docs/_build/html
diff --git a/.github/workflows/labeler.yml b/.github/workflows/labeler.yml
index 3d554d4..cb1a6a8 100644
--- a/.github/workflows/labeler.yml
+++ b/.github/workflows/labeler.yml
@@ -84,7 +84,7 @@ jobs:
const isDocs = (f) =>
f.startsWith("docs/") ||
- f === "mkdocs.yml" ||
+ f === ".readthedocs.yaml" ||
/\.md$/i.test(f);
const isPython = (f) => f.startsWith("python/");
diff --git a/.gitignore b/.gitignore
index e7f5f61..86acefa 100644
--- a/.gitignore
+++ b/.gitignore
@@ -13,7 +13,6 @@ venv/
.mypy_cache/
.pytest_cache/
htmlcov/
-site/
.coverage
coverage.xml
@@ -34,6 +33,9 @@ coverage.out
.DS_Store
Thumbs.db
+# Docs build output
+docs/_build/
+
# Misc
.env
.cache
diff --git a/.readthedocs.yaml b/.readthedocs.yaml
new file mode 100644
index 0000000..8b6e793
--- /dev/null
+++ b/.readthedocs.yaml
@@ -0,0 +1,17 @@
+# Read the Docs configuration file for Sphinx + MyST.
+# See https://docs.readthedocs.io/en/stable/config-file/v2.html
+
+version: 2
+
+build:
+ os: ubuntu-22.04
+ tools:
+ python: "3.11"
+
+sphinx:
+ configuration: docs/conf.py
+ fail_on_warning: true
+
+python:
+ install:
+ - requirements: docs/requirements.txt
diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md
index c061eb2..ed6a8e6 100644
--- a/CONTRIBUTING.md
+++ b/CONTRIBUTING.md
@@ -9,30 +9,30 @@ touch every side.
### Python
```bash
cd python
-make dev-setup # creates venv, installs package + test extras + tooling
+make dev-setup # creates venv, installs package + test extras + tooling
source venv/bin/activate
-make test # runs offline + integration
-make lint # flake8
-make type-check # mypy
+make test # runs offline + integration
+make lint # flake8
+make type-check # mypy
```
### Node.js
```bash
cd nodejs
make install
-make test # mocha (all tiers)
-make lint # eslint
-make test-types # tsd
+make test # mocha (all tiers)
+make lint # eslint
+make test-types # tsd
```
### Go
```bash
cd go
make deps
-make test # offline suite (the PR gate)
-make test-integration # live FR24 suite
-make lint # gofmt + go vet
-make lint-strict # adds staticcheck
+make test # offline suite (the PR gate)
+make test-integration # live FR24 suite
+make lint # gofmt + go vet
+make lint-strict # adds staticcheck
```
## Keeping the SDKs aligned
@@ -47,7 +47,25 @@ is a documented reason not to. Common targets that must stay in sync:
- The public surface — `FlightRadar24API` methods (`Client` in Go), the
`Countries` enum (`Country` constants in Go), `FlightTrackerConfig` fields,
and the `Entity` / `Airport` / `Flight` attributes consumers depend on.
- `docs/go.md` documents where the Go surface deliberately differs.
+
+## Documentation
+
+The documentation lives in `docs/`, written in Markdown (MyST) and built with
+Sphinx. It is published on [Read the Docs](https://flightradarapi.readthedocs.io/)
+from `main`, and every pull request gets a preview build.
+
+```bash
+make venv # optional: creates .venv, which the targets below then use
+make install-docs # installs Sphinx and the theme
+make docs # builds into docs/_build/html
+make docs-serve # live-reload server at http://127.0.0.1:8000
+```
+
+Run these from the repository root. Warnings fail the build, here and in CI.
+
+Each page shows its examples in the three languages, in tabs. When a change
+reaches the public surface, update the guide that covers it and the reference
+pages (`docs/api.md`, `docs/flight.md`, `docs/airport.md`, `docs/countries.md`).
## Style
@@ -100,5 +118,5 @@ module was tagged by hand, does what you would expect and nothing else.
## Reporting bugs and asking questions
- Bugs: open a GitHub issue with the bug report template.
-- Security: see [`SECURITY.md`](SECURITY.md). **Do not** report via GitHub
- issues.
+- Security: see [`SECURITY.md`](https://github.com/JeanExtreme002/FlightRadarAPI/blob/main/SECURITY.md).
+ **Do not** report via GitHub issues.
diff --git a/Makefile b/Makefile
new file mode 100644
index 0000000..cec7ae7
--- /dev/null
+++ b/Makefile
@@ -0,0 +1,76 @@
+# Makefile for the FlightRadarAPI documentation.
+#
+# Each SDK has its own Makefile (python/, nodejs/, go/). This one covers what
+# belongs to the repository as a whole: the documentation.
+
+# Variables
+VENV_DIR = .venv
+DOCS_DIR = docs
+DOCS_BUILD_DIR = $(DOCS_DIR)/_build/html
+
+# Use the virtual environment when `make venv` has created it.
+ifneq ($(wildcard $(VENV_DIR)/bin/python),)
+PYTHON := $(VENV_DIR)/bin/python
+else
+PYTHON := python3
+endif
+PIP = $(PYTHON) -m pip
+
+# Colors for output
+GREEN = \033[0;32m
+YELLOW = \033[0;33m
+NC = \033[0m # No Color
+
+# Default target
+.PHONY: help
+help:
+ @echo "$(GREEN)FlightRadarAPI documentation Makefile$(NC)"
+ @echo ""
+ @echo "Setup:"
+ @echo " $(YELLOW)venv$(NC) - Create a virtual environment in $(VENV_DIR)"
+ @echo " $(YELLOW)install-docs$(NC) - Install the dependencies to build the documentation"
+ @echo ""
+ @echo "Documentation:"
+ @echo " $(YELLOW)docs$(NC) - Build the HTML documentation (output: $(DOCS_BUILD_DIR))"
+ @echo " $(YELLOW)docs-serve$(NC) - Live-reload docs server at http://127.0.0.1:8000"
+ @echo " $(YELLOW)docs-clean$(NC) - Remove the docs build directory"
+ @echo ""
+ @echo "The SDKs have their own Makefiles: python/, nodejs/ and go/."
+
+# Create virtual environment. `python3` literally: $(PYTHON) may already point
+# at the environment being created.
+.PHONY: venv
+venv:
+ @echo "$(GREEN)Creating virtual environment...$(NC)"
+ python3 -m venv $(VENV_DIR)
+ @echo "$(YELLOW)To activate: source $(VENV_DIR)/bin/activate$(NC)"
+
+# Install the documentation dependencies. The build imports nothing from the
+# SDKs, so none of them has to be installed.
+.PHONY: install-docs
+install-docs:
+ @echo "$(GREEN)Installing documentation dependencies...$(NC)"
+ $(PIP) install -r $(DOCS_DIR)/requirements.txt
+
+# Build the HTML documentation with the same flags as CI and Read the Docs:
+# warnings, including broken cross-references (-n), fail the build.
+.PHONY: docs
+docs:
+ @echo "$(GREEN)Building HTML documentation...$(NC)"
+ $(PYTHON) -m sphinx -n -W --keep-going -b html $(DOCS_DIR) $(DOCS_BUILD_DIR)
+ @echo "$(GREEN)Documentation generated at $(DOCS_BUILD_DIR)/index.html$(NC)"
+
+# Live-reload docs server. sphinx-autobuild is installed on demand. After
+# changing a toctree, restart it: it only rebuilds changed pages, so the
+# sidebar on the other pages goes stale.
+.PHONY: docs-serve
+docs-serve:
+ @echo "$(GREEN)Starting live-reload docs server at http://127.0.0.1:8000$(NC)"
+ @$(PYTHON) -c "import sphinx_autobuild" 2>/dev/null || $(PIP) install sphinx-autobuild
+ $(PYTHON) -m sphinx_autobuild $(DOCS_DIR) $(DOCS_BUILD_DIR) --open-browser
+
+# Wipe the built docs
+.PHONY: docs-clean
+docs-clean:
+ @echo "$(GREEN)Cleaning docs build directory...$(NC)"
+ rm -rf $(DOCS_DIR)/_build
diff --git a/README.md b/README.md
index 19a4351..b28a9a7 100644
--- a/README.md
+++ b/README.md
@@ -5,6 +5,7 @@ This SDK should only be used for your own educational purposes. If you are inter
**Official FR24 API**: https://fr24api.flightradar24.com/
+[](https://flightradarapi.readthedocs.io/?badge=latest)
[](https://github.com/JeanExtreme002/FlightRadarAPI/actions)
[](https://github.com/JeanExtreme002/FlightRadarAPI/actions)
[](https://github.com/JeanExtreme002/FlightRadarAPI/actions)
@@ -35,7 +36,7 @@ go get github.com/JeanExtreme002/FlightRadarAPI/go@latest
```
## Documentation
-Explore the docs of FlightRadarAPI package, for Python, NodeJS or Go, through [FlightRadarAPI Documentation](https://JeanExtreme002.github.io/FlightRadarAPI/) page.
+Explore the docs of FlightRadarAPI package, for Python, NodeJS or Go, through [FlightRadarAPI Documentation](https://flightradarapi.readthedocs.io/) page.
## Project resources
**Contributing**: [`CONTRIBUTING.md`](CONTRIBUTING.md)
diff --git a/docs/_static/custom.css b/docs/_static/custom.css
new file mode 100644
index 0000000..8b67817
--- /dev/null
+++ b/docs/_static/custom.css
@@ -0,0 +1,183 @@
+/* FlightRadarAPI docs: tweaks on top of the Read the Docs theme. */
+
+/* --- Repository link (injected in _templates/layout.html) --------------- */
+/* Scoped under `.wy-menu-vertical a` so it beats the theme's own link rule. */
+.wy-menu-vertical a.repo-link {
+ display: flex;
+ align-items: center;
+ gap: 0.65rem;
+ margin: 0.6rem 0.8rem 0.4rem;
+ padding: 0.55rem 0.7rem;
+ border-radius: 0.35rem;
+ color: #d9d9d9;
+ line-height: 1.25;
+ text-decoration: none;
+ transition: background 0.15s ease, color 0.15s ease;
+}
+
+.wy-menu-vertical a.repo-link:hover,
+.wy-menu-vertical a.repo-link:focus-visible {
+ background: rgba(255, 255, 255, 0.08);
+ color: #fff;
+}
+
+.repo-link__icon {
+ flex-shrink: 0;
+}
+
+.repo-link__text {
+ display: flex;
+ flex-direction: column;
+ min-width: 0;
+}
+
+.repo-link__name {
+ overflow: hidden;
+ font-size: 0.8rem;
+ font-weight: 600;
+ text-overflow: ellipsis;
+ white-space: nowrap;
+}
+
+.repo-link__stats {
+ display: flex;
+ gap: 0.75rem;
+ margin-top: 0.15rem;
+ font-size: 0.72rem;
+ opacity: 0.75;
+}
+
+.repo-link__stats[hidden] {
+ display: none;
+}
+
+.repo-link__stat {
+ display: inline-flex;
+ align-items: center;
+ gap: 0.25rem;
+}
+
+/* The theme's script injects an expand button into every sidebar link. */
+.repo-link button.toctree-expand {
+ display: none;
+}
+
+/* --- Badge table on the home page ---------------------------------------- */
+.badge-table {
+ margin-bottom: 1.5rem;
+ border-collapse: collapse;
+}
+
+.badge-table th,
+.badge-table td {
+ padding: 0.4rem 0.75rem;
+ border: 1px solid #e1e4e5;
+ vertical-align: middle;
+}
+
+.badge-table th {
+ background: #f3f6f6;
+ text-align: center;
+}
+
+.badge-table td a {
+ display: inline-block;
+ margin: 0.1rem 0.15rem;
+ line-height: 0;
+}
+
+/* --- Home page hero ------------------------------------------------------ */
+/* The hero stands for the page title, which stays for the browser tab and
+ the navigation but isn't shown. */
+section#flightradarapi > h1 {
+ position: absolute;
+ width: 1px;
+ height: 1px;
+ overflow: hidden;
+ clip: rect(0 0 0 0);
+}
+
+.rst-content .hero {
+ display: flex;
+ align-items: center;
+ justify-content: center;
+ gap: 1rem;
+ margin: 0.5rem 0 2rem;
+}
+
+.rst-content .hero img {
+ width: 84px;
+ height: 84px;
+}
+
+.rst-content .hero span {
+ font-family: "Roboto Slab", ff-tisa-web-pro, Georgia, Arial, sans-serif;
+ font-size: 2.6rem;
+ font-weight: 700;
+ line-height: 1;
+ letter-spacing: -0.02em;
+ color: #404040;
+}
+
+.rst-content .hero span em {
+ font-style: normal;
+ color: #2980b9;
+}
+
+@media (max-width: 480px) {
+ .rst-content .hero img {
+ width: 56px;
+ height: 56px;
+ }
+
+ .rst-content .hero span {
+ font-size: 1.8rem;
+ }
+}
+
+/* --- Language tabs (sphinx-design) --------------------------------------- */
+.rst-content .sd-tab-set {
+ margin-bottom: 1.5rem;
+}
+
+.rst-content .sd-tab-set > label {
+ padding: 0.6rem 1rem 0.5rem;
+ font-size: 0.9rem;
+}
+
+.rst-content .sd-tab-set > input:checked + label {
+ border-color: #2980b9;
+ color: #2980b9;
+}
+
+.rst-content .sd-tab-set > input:not(:checked) + label:hover {
+ color: #2980b9;
+}
+
+.rst-content .sd-tab-content {
+ padding: 0.9rem 0 0;
+ box-shadow: 0 -0.0625rem #e1e4e5;
+}
+
+.rst-content .sd-tab-content > div[class^="highlight"]:last-child,
+.rst-content .sd-tab-content > p:last-child {
+ margin-bottom: 0;
+}
+
+/* --- Wide reference tables ----------------------------------------------- */
+/* The theme never wraps table cells, which pushes three-column name tables
+ off the page. */
+.rst-content table.docutils td,
+.rst-content table.docutils th {
+ white-space: normal;
+}
+
+.rst-content table.docutils td code {
+ white-space: nowrap;
+}
+
+/* --- Content width -------------------------------------------------------- */
+/* Room for the reference tables; the theme's 800px is tight for them. */
+.wy-nav-content {
+ max-width: 960px;
+}
diff --git a/docs/assets/favicon.ico b/docs/_static/favicon.ico
similarity index 100%
rename from docs/assets/favicon.ico
rename to docs/_static/favicon.ico
diff --git a/docs/assets/logo.png b/docs/_static/logo.png
similarity index 100%
rename from docs/assets/logo.png
rename to docs/_static/logo.png
diff --git a/docs/_templates/layout.html b/docs/_templates/layout.html
new file mode 100644
index 0000000..abd9d3f
--- /dev/null
+++ b/docs/_templates/layout.html
@@ -0,0 +1,60 @@
+{#- Extend the Read the Docs theme with a link to the GitHub repository at the
+ top of the sidebar, on every page: the repo name plus its live star and fork
+ counts, instead of a "please star" button. The counts come from the public
+ GitHub API; if that fails (offline, rate limit) only the name is shown. -#}
+{% extends "!layout.html" %}
+
+{% block menu %}
+
+
+
+ {{ github_user }}/{{ github_repo }}
+
+
+
+
+
+
+
+
+
+
+
+
+
+ {{ super() }}
+{% endblock %}
diff --git a/docs/account.md b/docs/account.md
new file mode 100644
index 0000000..b205ba2
--- /dev/null
+++ b/docs/account.md
@@ -0,0 +1,203 @@
+# Login, Bookmarks and Flight History
+
+Everything else in this documentation works without an account. Logging in to
+your own FlightRadar24 account adds what belongs to it: your bookmarks and, with
+a premium subscription, the history of a flight.
+
+## Logging in
+
+::::{tab-set}
+:sync-group: lang
+
+:::{tab-item} Python
+:sync: python
+
+```python
+from FlightRadarAPI import FlightRadar24API
+
+fr_api = FlightRadar24API()
+fr_api.login("your@email.com", "your-password")
+```
+
+The credentials can also be given to the constructor, which then logs in right
+away:
+
+```python
+fr_api = FlightRadar24API(user="your@email.com", password="your-password")
+```
+:::
+
+:::{tab-item} Node.js
+:sync: node
+
+```javascript
+const { FlightRadar24API } = require("flightradarapi");
+
+const frApi = new FlightRadar24API();
+await frApi.login("your@email.com", "your-password");
+```
+:::
+
+:::{tab-item} Go
+:sync: go
+
+```go
+client := flightradarapi.New()
+
+if err := client.Login(ctx, "your@email.com", "your-password"); err != nil {
+ log.Fatal(err)
+}
+```
+:::
+::::
+
+A wrong email or password fails with `LoginError` (`ErrLogin` in Go), carrying
+the message FlightRadar24 answered with.
+
+Once logged in, the flight feed and the airport details are requested with your
+session too.
+
+## Session state
+
+::::{tab-set}
+:sync-group: lang
+
+:::{tab-item} Python
+:sync: python
+
+```python
+fr_api.is_logged_in() # True
+
+user = fr_api.get_login_data() # dict with the data of your account
+
+fr_api.logout() # True when the server confirmed it
+```
+:::
+
+:::{tab-item} Node.js
+:sync: node
+
+```javascript
+frApi.isLoggedIn(); // true
+
+const user = frApi.getLoginData(); // object with the data of your account
+
+await frApi.logout(); // true when the server confirmed it
+```
+:::
+
+:::{tab-item} Go
+:sync: go
+
+```go
+client.IsLoggedIn() // true
+
+user, err := client.GetLoginData() // map[string]any with the data of your account
+
+confirmed, err := client.Logout(ctx) // true when the server confirmed it
+```
+:::
+::::
+
+- `get_login_data` returns a copy of the account data FlightRadar24 sent at
+ login. Without a session it fails with `LoginError`.
+- `logout` always ends the local session and clears its cookies. It returns
+ whether the server confirmed the logout, and `true` when there was no session
+ to end.
+- Logging in again replaces the current session.
+
+## Bookmarks
+
+The bookmarks saved in your account, as FlightRadar24 sends them.
+
+::::{tab-set}
+:sync-group: lang
+
+:::{tab-item} Python
+:sync: python
+
+```python
+bookmarks = fr_api.get_bookmarks() # dict
+```
+:::
+
+:::{tab-item} Node.js
+:sync: node
+
+```javascript
+const bookmarks = await frApi.getBookmarks(); // object
+```
+:::
+
+:::{tab-item} Go
+:sync: go
+
+```go
+bookmarks, err := client.GetBookmarks(ctx) // map[string]any
+```
+:::
+::::
+
+## Flight history
+
+```{important}
+Downloading the history of a flight requires a FlightRadar24 premium
+subscription.
+```
+
+`get_history_data` downloads the track of a flight as a CSV or KML document.
+
+| Parameter | Meaning |
+|---|---|
+| flight | The flight to download, as a `Flight` from any call that returns flights. |
+| file type | `"CSV"` or `"KML"`, in any letter case. |
+| timestamp | The scheduled time of departure (STD) of the flight in UTC, as a Unix timestamp. |
+
+::::{tab-set}
+:sync-group: lang
+
+:::{tab-item} Python
+:sync: python
+
+```python
+history_data = fr_api.get_history_data(flight, file_type="CSV", timestamp=1706529600)
+
+with open("history_data.csv", "w") as file:
+ file.write(history_data)
+```
+:::
+
+:::{tab-item} Node.js
+:sync: node
+
+```javascript
+const fs = require("fs");
+
+const historyData = await frApi.getHistoryData(flight, "CSV", 1706529600);
+
+fs.writeFileSync("history_data.csv", Buffer.from(historyData));
+```
+:::
+
+:::{tab-item} Go
+:sync: go
+
+```go
+historyData, err := client.GetHistoryData(ctx, flight, "CSV", 1706529600)
+
+if err != nil {
+ log.Fatal(err)
+}
+
+err = os.WriteFile("history_data.csv", []byte(historyData), 0o644)
+```
+:::
+::::
+
+```{warning}
+If an invalid time is provided, a blank document is returned.
+```
+
+Calling it without a session fails with `LoginError`. Any file type other than
+CSV or KML is rejected before the request: with `ValueError` in Python,
+`TypeError` in Node.js and an error in Go.
diff --git a/docs/airlines.md b/docs/airlines.md
new file mode 100644
index 0000000..859e493
--- /dev/null
+++ b/docs/airlines.md
@@ -0,0 +1,181 @@
+# Airlines, Logos and Flags
+
+## Listing the airlines
+
+`get_airlines` returns every airline FlightRadar24 knows, with its name and
+codes.
+
+::::{tab-set}
+:sync-group: lang
+
+:::{tab-item} Python
+:sync: python
+
+```python
+airlines = fr_api.get_airlines()
+
+print(airlines[0])
+# e.g. {"Name": "TAP", "ICAO": "TAP", "IATA": "TP", "n_aircrafts": None}
+```
+
+Each airline is a dictionary with the keys `Name`, `ICAO`, `IATA` and
+`n_aircrafts`.
+:::
+
+:::{tab-item} Node.js
+:sync: node
+
+```javascript
+const airlines = await frApi.getAirlines();
+
+console.log(airlines[0]);
+// e.g. { Name: "TAP", ICAO: "TAP", IATA: "TP", n_aircrafts: null }
+```
+
+Each airline is an object with the keys `Name`, `ICAO`, `IATA` and
+`n_aircrafts`.
+:::
+
+:::{tab-item} Go
+:sync: go
+
+```go
+airlines, err := client.GetAirlines(ctx)
+
+if err != nil {
+ log.Fatal(err)
+}
+
+fmt.Println(airlines[0].Name, airlines[0].ICAO, airlines[0].IATA)
+```
+
+Each airline is an `Airline`, with the fields `Name`, `ICAO`, `IATA` and
+`NumAircrafts` (a `*int`). Marshalled to JSON, it uses the same keys as the
+other SDKs: `Name`, `ICAO`, `IATA` and `n_aircrafts`.
+:::
+::::
+
+| Key | Meaning |
+|---|---|
+| `Name` | Name of the airline |
+| `ICAO` | ICAO code (3 characters), when the airline has one |
+| `IATA` | IATA code (2 characters), when the airline has one |
+| `n_aircrafts` | Size of the fleet. FlightRadar24 only publishes it for the airlines with the largest fleets; it is empty for the others. |
+
+The ICAO code is what the [flight search](flights.md#filtering) takes as its
+airline filter.
+
+## Airline logos
+
+`get_airline_logo` downloads the logo of an airline, given its IATA and ICAO
+codes. The result is the image bytes and the file extension, or nothing when
+FlightRadar24 has no logo for that airline.
+
+::::{tab-set}
+:sync-group: lang
+
+:::{tab-item} Python
+:sync: python
+
+```python
+logo = fr_api.get_airline_logo("EK", "UAE")
+
+if logo is not None:
+ data, extension = logo
+
+ with open(f"emirates.{extension}", "wb") as file:
+ file.write(data)
+```
+:::
+
+:::{tab-item} Node.js
+:sync: node
+
+```javascript
+const fs = require("fs");
+
+const logo = await frApi.getAirlineLogo("EK", "UAE");
+
+if (logo !== null) {
+ const [data, extension] = logo;
+
+ fs.writeFileSync(`emirates.${extension}`, Buffer.from(data));
+}
+```
+:::
+
+:::{tab-item} Go
+:sync: go
+
+```go
+logo, err := client.GetAirlineLogo(ctx, "EK", "UAE")
+
+if err != nil {
+ log.Fatal(err)
+}
+
+if logo != nil {
+ err = os.WriteFile("emirates."+logo.Extension, logo.Data, 0o644)
+}
+```
+
+The result is an `*Image`, with the fields `Data` and `Extension`. It is `nil`,
+with a `nil` error, when there is no logo.
+:::
+::::
+
+## Country flags
+
+`get_country_flag` downloads the flag of a country, given its name. It returns
+the same shape as the logo, or nothing when FlightRadar24 has no such flag.
+
+::::{tab-set}
+:sync-group: lang
+
+:::{tab-item} Python
+:sync: python
+
+```python
+flag = fr_api.get_country_flag("Brazil")
+
+if flag is not None:
+ data, extension = flag
+
+ with open(f"brazil.{extension}", "wb") as file:
+ file.write(data)
+```
+:::
+
+:::{tab-item} Node.js
+:sync: node
+
+```javascript
+const flag = await frApi.getCountryFlag("Brazil");
+
+if (flag !== null) {
+ const [data, extension] = flag;
+
+ fs.writeFileSync(`brazil.${extension}`, Buffer.from(data));
+}
+```
+:::
+
+:::{tab-item} Go
+:sync: go
+
+```go
+flag, err := client.GetCountryFlag(ctx, "Brazil")
+
+if err != nil {
+ log.Fatal(err)
+}
+
+if flag != nil {
+ err = os.WriteFile("brazil."+flag.Extension, flag.Data, 0o644)
+}
+```
+:::
+::::
+
+The name is matched loosely: `"United States"`, `"united-states"` and the
+country of an airport (`airport.country`) all work.
diff --git a/docs/airport.md b/docs/airport.md
new file mode 100644
index 0000000..5e0402c
--- /dev/null
+++ b/docs/airport.md
@@ -0,0 +1,332 @@
+# Airport
+
+An `Airport` is an [`Entity`](distances.md), so it has a position and can
+measure its distance to a flight or to another airport.
+
+How much an airport carries depends on the call that produced it. In the tables
+below, a column per call says whether the attribute is filled in:
+
+- the [airports listing](airports.md#airports-of-a-country);
+- [one airport by code](airports.md#one-airport-by-code);
+- an airport [with details](airports.md#airport-details): fetched with the
+ details flag, or filled in from a details payload.
+
+## Attributes
+
+### Identity and location
+
+::::{tab-set}
+:sync-group: lang
+
+:::{tab-item} Python
+:sync: python
+
+| Attribute | Description | `get_airports()` | `get_airport()` | with details |
+|---|---|:-:|:-:|:-:|
+| `name` | Name of the airport | ✓ | ✓ | ✓ |
+| `icao` | ICAO code | ✓ | ✓ | ✓ |
+| `iata` | IATA code | ✓ | ✓ | ✓ |
+| `country` | Name of the country | ✓ | ✓ | ✓ |
+| `latitude` | Latitude, in degrees | empty | ✓ | ✓ |
+| `longitude` | Longitude, in degrees | empty | ✓ | ✓ |
+| `altitude` | Elevation of the airport, in feet | empty | ✓ | ✓ |
+| `country_code` | Code of the country | | ✓ | ✓ |
+| `city` | City of the airport | | ✓ | ✓ |
+| `country_id` | FlightRadar24 identifier of the country | | | ✓ |
+:::
+
+:::{tab-item} Node.js
+:sync: node
+
+| Property | Description | `getAirports()` | `getAirport()` | with details |
+|---|---|:-:|:-:|:-:|
+| `name` | Name of the airport | ✓ | ✓ | ✓ |
+| `icao` | ICAO code | ✓ | ✓ | ✓ |
+| `iata` | IATA code | ✓ | ✓ | ✓ |
+| `country` | Name of the country | ✓ | ✓ | ✓ |
+| `latitude` | Latitude, in degrees | empty | ✓ | ✓ |
+| `longitude` | Longitude, in degrees | empty | ✓ | ✓ |
+| `altitude` | Elevation of the airport, in feet | empty | ✓ | ✓ |
+| `countryCode` | Code of the country | | ✓ | ✓ |
+| `city` | City of the airport | | ✓ | ✓ |
+| `countryId` | FlightRadar24 identifier of the country | | | ✓ |
+:::
+
+:::{tab-item} Go
+:sync: go
+
+| Field | Description | `GetAirports()` | `GetAirport()` | with details |
+|---|---|:-:|:-:|:-:|
+| `Name` | Name of the airport | ✓ | ✓ | ✓ |
+| `ICAO` | ICAO code | ✓ | ✓ | ✓ |
+| `IATA` | IATA code | ✓ | ✓ | ✓ |
+| `Country` | Name of the country | ✓ | ✓ | ✓ |
+| `Latitude` | Latitude, in degrees | empty | ✓ | ✓ |
+| `Longitude` | Longitude, in degrees | empty | ✓ | ✓ |
+| `Altitude` | Elevation of the airport, in feet | empty | ✓ | ✓ |
+| `CountryCode` | Code of the country | | ✓ | ✓ |
+| `City` | City of the airport | | ✓ | ✓ |
+| `CountryID` | FlightRadar24 identifier of the country | | | ✓ |
+:::
+
+::::
+
+### Timezone
+
+::::{tab-set}
+:sync-group: lang
+
+:::{tab-item} Python
+:sync: python
+
+| Attribute | Description | `get_airports()` | `get_airport()` | with details |
+|---|---|:-:|:-:|:-:|
+| `timezone_name` | Name of the timezone, such as `"Asia/Kathmandu"` | | ✓ | ✓ |
+| `timezone_abbr` | Abbreviation of the timezone | | ✓ | ✓ |
+| `timezone_abbr_name` | Name the abbreviation stands for | | ✓ | ✓ |
+| `timezone_offset` | Offset from UTC, in seconds | | ✓ | ✓ |
+| `timezone_offset_hours` | Offset from UTC, as text in hours | | ✓ | ✓ |
+:::
+
+:::{tab-item} Node.js
+:sync: node
+
+| Property | Description | `getAirports()` | `getAirport()` | with details |
+|---|---|:-:|:-:|:-:|
+| `timezoneName` | Name of the timezone, such as `"Asia/Kathmandu"` | | ✓ | ✓ |
+| `timezoneAbbr` | Abbreviation of the timezone | | ✓ | ✓ |
+| `timezoneAbbrName` | Name the abbreviation stands for | | ✓ | ✓ |
+| `timezoneOffset` | Offset from UTC, in seconds | | ✓ | ✓ |
+| `timezoneOffsetHours` | Offset from UTC, as text in hours | | ✓ | ✓ |
+:::
+
+:::{tab-item} Go
+:sync: go
+
+| Field | Description | `GetAirports()` | `GetAirport()` | with details |
+|---|---|:-:|:-:|:-:|
+| `TimezoneName` | Name of the timezone, such as `"Asia/Kathmandu"` | | ✓ | ✓ |
+| `TimezoneAbbr` | Abbreviation of the timezone | | ✓ | ✓ |
+| `TimezoneAbbrName` | Name the abbreviation stands for | | ✓ | ✓ |
+| `TimezoneOffset` | Offset from UTC, in seconds | | ✓ | ✓ |
+| `TimezoneOffsetHours` | Offset from UTC, as text in hours | | ✓ | ✓ |
+:::
+
+::::
+
+### Links
+
+::::{tab-set}
+:sync-group: lang
+
+:::{tab-item} Python
+:sync: python
+
+| Attribute | Description | `get_airports()` | `get_airport()` | with details |
+|---|---|:-:|:-:|:-:|
+| `website` | Website of the airport | | ✓ | ✓ |
+| `visible` | The `visible` flag FlightRadar24 sends for the airport | | ✓ | ✓ |
+| `wikipedia` | Wikipedia page of the airport | | | ✓ |
+| `images` | Photos of the airport, as FlightRadar24 sends them | | | ✓ |
+:::
+
+:::{tab-item} Node.js
+:sync: node
+
+| Property | Description | `getAirports()` | `getAirport()` | with details |
+|---|---|:-:|:-:|:-:|
+| `website` | Website of the airport | | ✓ | ✓ |
+| `visible` | The `visible` flag FlightRadar24 sends for the airport | | ✓ | ✓ |
+| `wikipedia` | Wikipedia page of the airport | | | ✓ |
+| `images` | Photos of the airport, as FlightRadar24 sends them | | | ✓ |
+:::
+
+:::{tab-item} Go
+:sync: go
+
+| Field | Description | `GetAirports()` | `GetAirport()` | with details |
+|---|---|:-:|:-:|:-:|
+| `Website` | Website of the airport | | ✓ | ✓ |
+| `Visible` | The `visible` flag FlightRadar24 sends for the airport | | ✓ | ✓ |
+| `Wikipedia` | Wikipedia page of the airport | | | ✓ |
+| `Images` | Photos of the airport, as FlightRadar24 sends them | | | ✓ |
+:::
+
+::::
+
+### Operations
+
+::::{tab-set}
+:sync-group: lang
+
+:::{tab-item} Python
+:sync: python
+
+| Attribute | Description | `get_airports()` | `get_airport()` | with details |
+|---|---|:-:|:-:|:-:|
+| `runways` | Runways of the airport | | | ✓ |
+| `weather` | Current weather at the airport | | | ✓ |
+| `arrivals` | Arrivals of the airport schedule | | | ✓ |
+| `departures` | Departures of the airport schedule | | | ✓ |
+| `aircraft_on_ground` | Number of aircraft on the ground | | | ✓ |
+| `aircraft_visible_on_ground` | Number of those FlightRadar24 can see | | | ✓ |
+:::
+
+:::{tab-item} Node.js
+:sync: node
+
+| Property | Description | `getAirports()` | `getAirport()` | with details |
+|---|---|:-:|:-:|:-:|
+| `runways` | Runways of the airport | | | ✓ |
+| `weather` | Current weather at the airport | | | ✓ |
+| `arrivals` | Arrivals of the airport schedule | | | ✓ |
+| `departures` | Departures of the airport schedule | | | ✓ |
+| `aircraftOnGround` | Number of aircraft on the ground | | | ✓ |
+| `aircraftVisibleOnGround` | Number of those FlightRadar24 can see | | | ✓ |
+:::
+
+:::{tab-item} Go
+:sync: go
+
+| Field | Description | `GetAirports()` | `GetAirport()` | with details |
+|---|---|:-:|:-:|:-:|
+| `Runways` | Runways of the airport | | | ✓ |
+| `Weather` | Current weather at the airport | | | ✓ |
+| `Arrivals` | Arrivals of the airport schedule | | | ✓ |
+| `Departures` | Departures of the airport schedule | | | ✓ |
+| `AircraftOnGround` | Number of aircraft on the ground | | | ✓ |
+| `AircraftVisibleOnGround` | Number of those FlightRadar24 can see | | | ✓ |
+:::
+
+::::
+
+### Reviews
+
+::::{tab-set}
+:sync-group: lang
+
+:::{tab-item} Python
+:sync: python
+
+| Attribute | Description | `get_airports()` | `get_airport()` | with details |
+|---|---|:-:|:-:|:-:|
+| `reviews_url` | Address of the reviews page on FlightRadar24 | | | ✓ |
+| `reviews` | Number of reviews | | | ✓ |
+| `evaluation` | Evaluation of the airport | | | ✓ |
+| `average_rating` | Average rating | | | ✓ |
+| `total_rating` | Number of ratings | | | ✓ |
+:::
+
+:::{tab-item} Node.js
+:sync: node
+
+| Property | Description | `getAirports()` | `getAirport()` | with details |
+|---|---|:-:|:-:|:-:|
+| `reviewsUrl` | Address of the reviews page on FlightRadar24 | | | ✓ |
+| `reviews` | Number of reviews | | | ✓ |
+| `evaluation` | Evaluation of the airport | | | ✓ |
+| `averageRating` | Average rating | | | ✓ |
+| `totalRating` | Number of ratings | | | ✓ |
+:::
+
+:::{tab-item} Go
+:sync: go
+
+| Field | Description | `GetAirports()` | `GetAirport()` | with details |
+|---|---|:-:|:-:|:-:|
+| `ReviewsURL` | Address of the reviews page on FlightRadar24 | | | ✓ |
+| `Reviews` | Number of reviews | | | ✓ |
+| `Evaluation` | Evaluation of the airport | | | ✓ |
+| `AverageRating` | Average rating | | | ✓ |
+| `TotalRating` | Number of ratings | | | ✓ |
+:::
+
+::::
+
+`runways`, `weather`, `arrivals`, `departures` and `images` are kept as
+FlightRadar24 sends them. How many flights the schedule lists is set by the
+flight limit and the page of
+[`get_airport_details`](airports.md#the-raw-details-payload).
+
+## Missing values
+
+::::{tab-set}
+:sync-group: lang
+
+:::{tab-item} Python
+:sync: python
+
+An attribute whose column is blank **does not exist** on that airport: reading
+it raises `AttributeError`. Use `getattr(airport, "city", None)` when the
+airport may come from more than one call.
+
+On an airport from the listing, `latitude`, `longitude` and `altitude` are `None`.
+
+On the other two, a value FlightRadar24 left out is `None` or the string
+`"N/A"`, depending on the attribute and on the call. The collections are empty
+instead: `runways` is `[]`, and `weather`, `arrivals`,
+`departures` and `images` are `{}`.
+:::
+
+:::{tab-item} Node.js
+:sync: node
+
+A property whose column is blank is `undefined` on that airport.
+
+On an airport from the listing, `latitude`, `longitude` and `altitude` are `null`.
+
+On the other two, a value FlightRadar24 left out is `null` or the string
+`"N/A"`, depending on the property and on the call. The collections are empty
+instead: `runways` is `[]`, and `weather`, `arrivals`,
+`departures` and `images` are `{}`.
+:::
+
+:::{tab-item} Go
+:sync: go
+
+Every field exists on every airport. One that the call did not fill in, or that
+FlightRadar24 left out, holds its zero value: `""` for text, `nil` for the
+numbers and for `Visible`, which are pointers, and empty for the maps and
+slices.
+
+The numbers are `*float64`, `Visible` is `*bool`, `Runways` is `[]any`, and
+`Weather`, `Arrivals`, `Departures` and `Images` are `map[string]any`.
+
+`RawDetails` holds the payload the airport was built from, for what the struct
+does not name. It is `nil` on a listed airport.
+:::
+::::
+
+## Methods
+
+::::{tab-set}
+:sync-group: lang
+
+:::{tab-item} Python
+:sync: python
+
+| Method | Description |
+|---|---|
+| `set_airport_details(details)` | Fill the airport in from a details payload |
+| `get_distance_from(entity)` | Distance to another entity, in kilometers. See [Distances](distances.md). |
+:::
+
+:::{tab-item} Node.js
+:sync: node
+
+| Method | Description |
+|---|---|
+| `setAirportDetails(details)` | Fill the airport in from a details payload |
+| `getDistanceFrom(entity)` | Distance to another entity, in kilometers. See [Distances](distances.md). |
+:::
+
+:::{tab-item} Go
+:sync: go
+
+| Method | Description |
+|---|---|
+| `SetAirportDetails(details)` | Fill the airport in from a details payload |
+| `GetDistanceFrom(other)` | Distance to another entity, in kilometers. See [Distances](distances.md). |
+:::
+
+::::
diff --git a/docs/airports.md b/docs/airports.md
new file mode 100644
index 0000000..8b301f9
--- /dev/null
+++ b/docs/airports.md
@@ -0,0 +1,277 @@
+# Airports
+
+There are three ways to reach airports, and each returns a different amount of
+information:
+
+| Call | Requests | What the airport carries |
+|---|---|---|
+| [One airport by code](#one-airport-by-code) | 1 | Name, codes, position, altitude, country, city, timezone |
+| [One airport, with details](#airport-details) | 1 | All of the above, plus runways, weather, schedule, ratings and more |
+| [Airports of a country](#airports-of-a-country) | 1 or more per country | Name, codes and country only |
+
+The [Airport reference](airport.md) lists every attribute and where it comes
+from.
+
+## One airport by code
+
+Airports are looked up by their IATA code (3 letters) or ICAO code (4 letters).
+
+::::{tab-set}
+:sync-group: lang
+
+:::{tab-item} Python
+:sync: python
+
+```python
+airport = fr_api.get_airport("VNLK")
+
+print(airport.name, airport.city, airport.country)
+print(airport.latitude, airport.longitude, airport.altitude)
+```
+:::
+
+:::{tab-item} Node.js
+:sync: node
+
+```javascript
+const airport = await frApi.getAirport("VNLK");
+
+console.log(airport.name, airport.city, airport.country);
+console.log(airport.latitude, airport.longitude, airport.altitude);
+```
+:::
+
+:::{tab-item} Go
+:sync: go
+
+```go
+airport, err := client.GetAirport(ctx, "VNLK", false)
+
+if errors.Is(err, flightradarapi.ErrAirportNotFound) {
+ fmt.Println("no such airport")
+ return
+}
+if err != nil {
+ log.Fatal(err)
+}
+
+fmt.Println(airport.Name, airport.City, airport.Country)
+fmt.Println(*airport.Latitude, *airport.Longitude, *airport.Altitude)
+```
+
+`Latitude`, `Longitude` and `Altitude` are pointers, `nil` when FlightRadar24
+sent no value.
+:::
+::::
+
+A code that no airport answers to fails with `AirportNotFoundError`
+(`ErrAirportNotFound` in Go). A code that is not 3 or 4 characters long is
+rejected before any request is made. See [Errors and retries](errors.md).
+
+## Airport details
+
+Ask for the details to get the whole picture of an airport in one object.
+
+::::{tab-set}
+:sync-group: lang
+
+:::{tab-item} Python
+:sync: python
+
+```python
+airport = fr_api.get_airport("VNLK", details=True)
+
+print(airport.timezone_name, airport.website)
+print(len(airport.runways), "runways")
+print(airport.weather)
+```
+:::
+
+:::{tab-item} Node.js
+:sync: node
+
+```javascript
+const airport = await frApi.getAirport("VNLK", true);
+
+console.log(airport.timezoneName, airport.website);
+console.log(airport.runways.length, "runways");
+console.log(airport.weather);
+```
+:::
+
+:::{tab-item} Go
+:sync: go
+
+```go
+airport, err := client.GetAirport(ctx, "VNLK", true)
+
+if err != nil {
+ log.Fatal(err)
+}
+
+fmt.Println(airport.TimezoneName, airport.Website)
+fmt.Println(len(airport.Runways), "runways")
+fmt.Println(airport.Weather)
+```
+:::
+::::
+
+### The raw details payload
+
+`get_airport_details` returns the payload itself, as FlightRadar24 sends it. It
+takes two more parameters, which control the arrivals and departures of the
+airport schedule:
+
+| Parameter | Default | Meaning |
+|---|---|---|
+| flight limit | 100 | Number of flights listed in the schedule. The maximum is 100. |
+| page | 1 | Page of the schedule, to go past the first 100 flights. |
+
+::::{tab-set}
+:sync-group: lang
+
+:::{tab-item} Python
+:sync: python
+
+```python
+details = fr_api.get_airport_details("KJFK", flight_limit=50, page=2)
+
+arrivals = details["airport"]["pluginData"]["schedule"]["arrivals"]
+```
+
+The payload can be turned into an `Airport` at any time:
+
+```python
+from FlightRadarAPI import Airport
+
+airport = Airport.from_details(details)
+
+# Or, on an airport you already have
+airport.set_airport_details(details)
+```
+:::
+
+:::{tab-item} Node.js
+:sync: node
+
+```javascript
+const details = await frApi.getAirportDetails("KJFK", 50, 2);
+
+const arrivals = details["airport"]["pluginData"]["schedule"]["arrivals"];
+```
+
+The payload can be turned into an `Airport` at any time:
+
+```javascript
+const { Airport } = require("flightradarapi");
+
+const airport = Airport.fromDetails(details);
+
+// Or, on an airport you already have
+airport.setAirportDetails(details);
+```
+:::
+
+:::{tab-item} Go
+:sync: go
+
+```go
+details, err := client.GetAirportDetails(ctx, "KJFK", 50, 2)
+```
+
+Zero means the default: `GetAirportDetails(ctx, "KJFK", 0, 0)` asks for 100
+flights of the first page.
+
+The payload can be turned into an `Airport` at any time:
+
+```go
+airport := flightradarapi.NewAirportFromDetails(details)
+
+// Or, on an airport you already have
+airport.SetAirportDetails(details)
+```
+
+`airport.RawDetails` keeps the payload the airport was built from, for the
+fields the struct does not name.
+:::
+::::
+
+A flight limit above 100 is refused by FlightRadar24, and the SDK reports its
+message: as a `ValueError` in Python, a `RangeError` in Node.js and an error in
+Go.
+
+## Airports of a country
+
+`get_airports` lists the airports of one or more countries, or of the whole
+world.
+
+::::{tab-set}
+:sync-group: lang
+
+:::{tab-item} Python
+:sync: python
+
+```python
+from FlightRadarAPI import Countries
+
+airports = fr_api.get_airports([Countries.BRAZIL, Countries.UNITED_STATES])
+
+# A single country needs no list
+airports = fr_api.get_airports(Countries.BRAZIL)
+
+# Every airport
+all_airports = fr_api.get_airports()
+```
+
+Countries can also be given by name or by slug: `"United States"` and
+`"united-states"` are the same as `Countries.UNITED_STATES`. Any iterable is
+accepted.
+:::
+
+:::{tab-item} Node.js
+:sync: node
+
+```javascript
+const { Countries } = require("flightradarapi");
+
+let airports = await frApi.getAirports([Countries.BRAZIL, Countries.UNITED_STATES]);
+
+// A single country needs no array
+airports = await frApi.getAirports(Countries.BRAZIL);
+
+// Every airport
+const allAirports = await frApi.getAirports();
+```
+
+Countries can also be given by name or by slug: `"United States"` and
+`"united-states"` are the same as `Countries.UNITED_STATES`. Any iterable is
+accepted.
+:::
+
+:::{tab-item} Go
+:sync: go
+
+```go
+airports, err := client.GetAirports(ctx, []flightradarapi.Country{
+ flightradarapi.CountryBrazil,
+ flightradarapi.CountryUnitedStates,
+})
+
+// Every airport
+allAirports, err := client.GetAirports(ctx, nil)
+```
+
+Countries can also be given by name or by slug:
+`flightradarapi.Country("United States")` and
+`flightradarapi.Country("united-states")` are the same as `CountryUnitedStates`.
+`AllCountries()` enumerates the constants.
+:::
+::::
+
+See [Countries](countries.md) for how countries are named.
+
+## See also
+
+- [Airport reference](airport.md): every attribute of an airport.
+- [Airport disruptions](live-data.md): delays and cancellations by airport.
+- [Distances](distances.md): how far an airport is from a flight, or from another airport.
diff --git a/docs/api.md b/docs/api.md
new file mode 100644
index 0000000..7b204b2
--- /dev/null
+++ b/docs/api.md
@@ -0,0 +1,246 @@
+# Client Methods
+
+The client is the entry point of each SDK: `FlightRadar24API` in Python and
+Node.js, `Client` in Go. Its methods are the same in the three languages, apart
+from the naming convention of each.
+
+This page lists every one of them. The linked guides explain them with examples.
+The Go package is also documented, from its source, on
+[pkg.go.dev](https://pkg.go.dev/github.com/JeanExtreme002/FlightRadarAPI/go/flightradarapi).
+
+## Flights
+
+::::{tab-set}
+:sync-group: lang
+
+:::{tab-item} Python
+:sync: python
+
+| Method | Description |
+|---|---|
+| `get_flights` | Flights of the live feed, optionally filtered. See [Live flights](flights.md). |
+| `get_flight_details` | Details payload of one flight. See [Flight details](flights.md#flight-details). |
+| `get_flight_tracker_config` | Copy of the current feed settings. See [Flight tracker settings](tracker-config.md). |
+| `set_flight_tracker_config` | Change the feed settings. See [Flight tracker settings](tracker-config.md). |
+:::
+
+:::{tab-item} Node.js
+:sync: node
+
+| Method | Description |
+|---|---|
+| `getFlights` | Flights of the live feed, optionally filtered. See [Live flights](flights.md). |
+| `getFlightDetails` | Details payload of one flight. See [Flight details](flights.md#flight-details). |
+| `getFlightTrackerConfig` | Copy of the current feed settings. See [Flight tracker settings](tracker-config.md). |
+| `setFlightTrackerConfig` | Change the feed settings. See [Flight tracker settings](tracker-config.md). |
+:::
+
+:::{tab-item} Go
+:sync: go
+
+| Method | Description |
+|---|---|
+| `GetFlights` | Flights of the live feed, optionally filtered. See [Live flights](flights.md). |
+| `GetFlightDetails` | Details payload of one flight. See [Flight details](flights.md#flight-details). |
+| `GetFlightTrackerConfig` | Copy of the current feed settings. See [Flight tracker settings](tracker-config.md). |
+| `SetFlightTrackerConfig` | Change the feed settings. See [Flight tracker settings](tracker-config.md). |
+:::
+
+::::
+
+## Zones and bounds
+
+::::{tab-set}
+:sync-group: lang
+
+:::{tab-item} Python
+:sync: python
+
+| Method | Description |
+|---|---|
+| `get_zones` | Major zones of the globe. See [Zones](zones.md#zones). |
+| `get_bounds` | Bounds string of a zone. See [Zones](zones.md#zones). |
+| `get_bounds_by_point` | Bounds string of the square around a point. See [Around a point](zones.md#around-a-point). |
+:::
+
+:::{tab-item} Node.js
+:sync: node
+
+| Method | Description |
+|---|---|
+| `getZones` | Major zones of the globe. See [Zones](zones.md#zones). |
+| `getBounds` | Bounds string of a zone. See [Zones](zones.md#zones). |
+| `getBoundsByPoint` | Bounds string of the square around a point. See [Around a point](zones.md#around-a-point). |
+:::
+
+:::{tab-item} Go
+:sync: go
+
+| Method | Description |
+|---|---|
+| `GetZones` | Major zones of the globe. See [Zones](zones.md#zones). |
+| `GetBounds` | Bounds string of a zone. See [Zones](zones.md#zones). |
+| `GetBoundsByPoint` | Bounds string of the square around a point. See [Around a point](zones.md#around-a-point). |
+:::
+
+::::
+
+## Airports
+
+::::{tab-set}
+:sync-group: lang
+
+:::{tab-item} Python
+:sync: python
+
+| Method | Description |
+|---|---|
+| `get_airport` | One airport, by its IATA or ICAO code. See [Airports](airports.md#one-airport-by-code). |
+| `get_airport_details` | Details payload of one airport. See [Airport details](airports.md#the-raw-details-payload). |
+| `get_airports` | Airports of some countries, or all of them. See [Airports of a country](airports.md#airports-of-a-country). |
+| `get_airport_disruptions` | Current airport disruptions. See [Airport disruptions](live-data.md). |
+:::
+
+:::{tab-item} Node.js
+:sync: node
+
+| Method | Description |
+|---|---|
+| `getAirport` | One airport, by its IATA or ICAO code. See [Airports](airports.md#one-airport-by-code). |
+| `getAirportDetails` | Details payload of one airport. See [Airport details](airports.md#the-raw-details-payload). |
+| `getAirports` | Airports of some countries, or all of them. See [Airports of a country](airports.md#airports-of-a-country). |
+| `getAirportDisruptions` | Current airport disruptions. See [Airport disruptions](live-data.md). |
+:::
+
+:::{tab-item} Go
+:sync: go
+
+| Method | Description |
+|---|---|
+| `GetAirport` | One airport, by its IATA or ICAO code. See [Airports](airports.md#one-airport-by-code). |
+| `GetAirportDetails` | Details payload of one airport. See [Airport details](airports.md#the-raw-details-payload). |
+| `GetAirports` | Airports of some countries, or all of them. See [Airports of a country](airports.md#airports-of-a-country). |
+| `GetAirportDisruptions` | Current airport disruptions. See [Airport disruptions](live-data.md). |
+:::
+
+::::
+
+## Airlines and images
+
+::::{tab-set}
+:sync-group: lang
+
+:::{tab-item} Python
+:sync: python
+
+| Method | Description |
+|---|---|
+| `get_airlines` | Every airline. See [Airlines](airlines.md#listing-the-airlines). |
+| `get_airline_logo` | Logo of an airline. See [Airline logos](airlines.md#airline-logos). |
+| `get_country_flag` | Flag of a country. See [Country flags](airlines.md#country-flags). |
+:::
+
+:::{tab-item} Node.js
+:sync: node
+
+| Method | Description |
+|---|---|
+| `getAirlines` | Every airline. See [Airlines](airlines.md#listing-the-airlines). |
+| `getAirlineLogo` | Logo of an airline. See [Airline logos](airlines.md#airline-logos). |
+| `getCountryFlag` | Flag of a country. See [Country flags](airlines.md#country-flags). |
+:::
+
+:::{tab-item} Go
+:sync: go
+
+| Method | Description |
+|---|---|
+| `GetAirlines` | Every airline. See [Airlines](airlines.md#listing-the-airlines). |
+| `GetAirlineLogo` | Logo of an airline. See [Airline logos](airlines.md#airline-logos). |
+| `GetCountryFlag` | Flag of a country. See [Country flags](airlines.md#country-flags). |
+:::
+
+::::
+
+## More data
+
+::::{tab-set}
+:sync-group: lang
+
+:::{tab-item} Python
+:sync: python
+
+| Method | Description |
+|---|---|
+| `search` | Search results, grouped by category. See [Search](searching.md). |
+| `get_most_tracked` | Most tracked flights. See [Most tracked flights](live-data.md). |
+| `get_volcanic_eruptions` | Volcanic eruptions and ash clouds. See [Volcanic eruptions](live-data.md). |
+:::
+
+:::{tab-item} Node.js
+:sync: node
+
+| Method | Description |
+|---|---|
+| `search` | Search results, grouped by category. See [Search](searching.md). |
+| `getMostTracked` | Most tracked flights. See [Most tracked flights](live-data.md). |
+| `getVolcanicEruptions` | Volcanic eruptions and ash clouds. See [Volcanic eruptions](live-data.md). |
+:::
+
+:::{tab-item} Go
+:sync: go
+
+| Method | Description |
+|---|---|
+| `Search` | Search results, grouped by category. See [Search](searching.md). |
+| `GetMostTracked` | Most tracked flights. See [Most tracked flights](live-data.md). |
+| `GetVolcanicEruptions` | Volcanic eruptions and ash clouds. See [Volcanic eruptions](live-data.md). |
+:::
+
+::::
+
+## Account
+
+::::{tab-set}
+:sync-group: lang
+
+:::{tab-item} Python
+:sync: python
+
+| Method | Description |
+|---|---|
+| `login` | Log in to a FlightRadar24 account. See [Logging in](account.md#logging-in). |
+| `logout` | End the session. See [Session state](account.md#session-state). |
+| `is_logged_in` | Whether the client holds a session. |
+| `get_login_data` | Data of the logged-in account. |
+| `get_bookmarks` | Bookmarks of the account. See [Bookmarks](account.md#bookmarks). |
+| `get_history_data` | Track of a flight, as CSV or KML. See [Flight history](account.md#flight-history). |
+:::
+
+:::{tab-item} Node.js
+:sync: node
+
+| Method | Description |
+|---|---|
+| `login` | Log in to a FlightRadar24 account. See [Logging in](account.md#logging-in). |
+| `logout` | End the session. See [Session state](account.md#session-state). |
+| `isLoggedIn` | Whether the client holds a session. |
+| `getLoginData` | Data of the logged-in account. |
+| `getBookmarks` | Bookmarks of the account. See [Bookmarks](account.md#bookmarks). |
+| `getHistoryData` | Track of a flight, as CSV or KML. See [Flight history](account.md#flight-history). |
+:::
+
+:::{tab-item} Go
+:sync: go
+
+| Method | Description |
+|---|---|
+| `Login` | Log in to a FlightRadar24 account. See [Logging in](account.md#logging-in). |
+| `Logout` | End the session. See [Session state](account.md#session-state). |
+| `IsLoggedIn` | Whether the client holds a session. |
+| `GetLoginData` | Data of the logged-in account. |
+| `GetBookmarks` | Bookmarks of the account. See [Bookmarks](account.md#bookmarks). |
+| `GetHistoryData` | Track of a flight, as CSV or KML. See [Flight history](account.md#flight-history). |
+:::
+
+::::
diff --git a/docs/client.md b/docs/client.md
new file mode 100644
index 0000000..ff9e6e6
--- /dev/null
+++ b/docs/client.md
@@ -0,0 +1,279 @@
+# Configuration
+
+The client works with no configuration. A few options are there for when the
+defaults do not fit.
+
+::::{tab-set}
+:sync-group: lang
+
+:::{tab-item} Python
+:sync: python
+
+| Option | Name | Default |
+|---|---|---|
+| [Timeout](#timeout) | `timeout` (seconds) | 30 seconds |
+| [Concurrency](#concurrency) | `max_workers` | 8 |
+| [TLS impersonation](#tls-impersonation) | `impersonate` | Chrome 136 |
+| [Retry](errors.md#retrying) | `retry` | No retry |
+:::
+
+:::{tab-item} Node.js
+:sync: node
+
+| Option | Name | Default |
+|---|---|---|
+| [Timeout](#timeout) | `timeout` (milliseconds) | 30 seconds |
+| [Concurrency](#concurrency) | `maxWorkers` | 8 |
+| [TLS impersonation](#tls-impersonation) | `impersonate` | Chrome 136 |
+| [Retry](errors.md#retrying) | `retry` | No retry |
+:::
+
+:::{tab-item} Go
+:sync: go
+
+| Option | Name | Default |
+|---|---|---|
+| [Timeout](#timeout) | `Options.Timeout` | 30 seconds |
+| [Concurrency](#concurrency) | `Options.MaxWorkers` | 8 |
+| [TLS impersonation](#tls-impersonation) | `Options.TLSProfile`, `Options.HTTPClient` | Chrome 136 |
+| [Retry](errors.md#retrying) | `Options.Retry` | No retry |
+:::
+
+::::
+
+::::{tab-set}
+:sync-group: lang
+
+:::{tab-item} Python
+:sync: python
+
+```python
+from FlightRadarAPI import FlightRadar24API, RetryPolicy
+
+fr_api = FlightRadar24API(
+ timeout=10, # seconds
+ max_workers=4,
+ retry=RetryPolicy(max_attempts=3),
+)
+```
+
+The constructor also takes `user` and `password`, to
+[log in](account.md#logging-in) right away.
+:::
+
+:::{tab-item} Node.js
+:sync: node
+
+```javascript
+const { FlightRadar24API, RetryPolicy } = require("flightradarapi");
+
+const frApi = new FlightRadar24API({
+ timeout: 10000, // milliseconds
+ maxWorkers: 4,
+ retry: new RetryPolicy({ maxAttempts: 3 }),
+});
+```
+:::
+
+:::{tab-item} Go
+:sync: go
+
+```go
+retry, err := flightradarapi.NewRetryPolicy(3)
+
+if err != nil {
+ log.Fatal(err)
+}
+
+client := flightradarapi.New(flightradarapi.Options{
+ Timeout: 10 * time.Second,
+ MaxWorkers: 4,
+ Retry: retry,
+})
+```
+
+Every field of `Options` defaults from its zero value, so set only what you
+need. `New` takes at most one `Options`.
+:::
+::::
+
+## Timeout
+
+The timeout bounds a single request, not a whole call: a call that makes several
+requests, such as a flight search with details, may take longer.
+
+It can be changed after the client is created.
+
+::::{tab-set}
+:sync-group: lang
+
+:::{tab-item} Python
+:sync: python
+
+```python
+fr_api.timeout = 60 # seconds
+```
+:::
+
+:::{tab-item} Node.js
+:sync: node
+
+```javascript
+frApi.timeout = 60000; // milliseconds
+```
+:::
+
+:::{tab-item} Go
+:sync: go
+
+```go
+client.Timeout = 60 * time.Second
+```
+
+Change it before making calls, not while one is in flight. For a deadline over
+a whole call, cancel the `context.Context` you pass to it:
+
+```go
+ctx, cancel := context.WithTimeout(context.Background(), 2*time.Minute)
+defer cancel()
+
+airports, err := client.GetAirports(ctx, nil)
+```
+:::
+::::
+
+## Concurrency
+
+Two calls fan out into many requests:
+
+- a [flight search with details](flights.md#details-for-every-flight-at-once),
+ which makes one request per flight found;
+- the [airports listing](airports.md#airports-of-a-country), which makes one
+ request per country and state page.
+
+The number of workers is how many of those requests run at the same time. It can
+be changed after the client is created, through `max_workers` in Python,
+`maxWorkers` in Node.js and `MaxWorkers` in Go.
+
+A higher number is faster, and more likely to be answered with a
+[Cloudflare block](errors.md#cloudflare-blocks).
+
+## TLS impersonation
+
+FlightRadar24 sits behind Cloudflare, which tells browsers from scripts by the
+way they open a TLS connection. The SDKs present the handshake of Chrome 136 by
+default. Override it when FlightRadar24 updates its bot mitigation faster than
+this library releases.
+
+::::{tab-set}
+:sync-group: lang
+
+:::{tab-item} Python
+:sync: python
+
+`impersonate` takes the name of a
+[curl_cffi](https://github.com/lexiforest/curl_cffi) browser profile. The
+default is `"chrome136"`.
+
+```python
+fr_api = FlightRadar24API(impersonate="chrome")
+```
+
+The profiles available depend on the version of `curl_cffi` installed.
+:::
+
+:::{tab-item} Node.js
+:sync: node
+
+`impersonate` takes an object with any of `ciphers`, `sigalgs` and `ecdhCurve`.
+What you leave out keeps the value of the bundled Chrome 136 profile.
+
+```javascript
+const frApi = new FlightRadar24API({
+ impersonate: {
+ ciphers: ["TLS_AES_128_GCM_SHA256", "TLS_AES_256_GCM_SHA384", "TLS_CHACHA20_POLY1305_SHA256"],
+ ecdhCurve: "X25519:P-256:P-384",
+ },
+});
+```
+
+`ciphers` and `sigalgs` are arrays of OpenSSL names, and `ecdhCurve` is a
+colon-separated string.
+:::
+
+:::{tab-item} Go
+:sync: go
+
+`Options.TLSProfile` narrows the cipher suites, the curve order and the TLS
+versions the client offers. Start from the default and change what you need:
+
+```go
+profile := flightradarapi.Chrome136Profile()
+profile.MinVersion = tls.VersionTLS13
+
+client := flightradarapi.New(flightradarapi.Options{TLSProfile: &profile})
+```
+
+Go fixes its own cipher ordering, so a profile is an approximation of a browser
+rather than a byte-exact fingerprint. For full impersonation, pass a client
+built on [utls](https://github.com/refraction-networking/utls) or
+[tls-client](https://github.com/bogdanfinn/tls-client):
+
+```go
+client := flightradarapi.New(flightradarapi.Options{
+ HTTPClient: &http.Client{Transport: myImpersonatingTransport},
+})
+```
+
+When you replace the HTTP client:
+
+- set `DisableCompression` on its transport, so the SDK decodes the responses
+ and can enforce its [size limit](errors.md#response-size-limit);
+- leave `CheckRedirect` unset, or the cookies FlightRadar24 hands out on a
+ redirect are lost;
+- any `Jar` is ignored: the SDK manages the cookies itself.
+:::
+::::
+
+## Logging
+
+The SDKs log a warning when FlightRadar24 answers with something they did not
+expect, such as a page whose layout changed or a country with no airports. A
+warning never interrupts the call.
+
+::::{tab-set}
+:sync-group: lang
+
+:::{tab-item} Python
+:sync: python
+
+Warnings go to the standard `logging` module, under the logger `FlightRadarAPI`.
+
+```python
+import logging
+
+logging.basicConfig() # show the warnings
+logging.getLogger("FlightRadarAPI").setLevel(logging.ERROR) # or silence them
+```
+:::
+
+:::{tab-item} Node.js
+:sync: node
+
+The warnings are written with `console.warn`, to the standard error stream.
+:::
+
+:::{tab-item} Go
+:sync: go
+
+Warnings go to `log/slog`, through `slog.Default()` unless `SetLogger` is called.
+
+```go
+logger := slog.New(slog.NewJSONHandler(os.Stderr, nil))
+
+flightradarapi.SetLogger(logger) // nil restores slog.Default()
+```
+
+The logger is set for the whole process, not per client.
+:::
+::::
diff --git a/docs/conf.py b/docs/conf.py
new file mode 100644
index 0000000..b655bfe
--- /dev/null
+++ b/docs/conf.py
@@ -0,0 +1,84 @@
+# -*- coding: utf-8 -*-
+"""Sphinx configuration for the FlightRadarAPI documentation.
+
+Pages are written in Markdown (MyST) and rendered with the Read the Docs theme.
+Nothing is imported from the SDKs, so the build needs none of their dependencies.
+"""
+
+import os
+import re
+from datetime import datetime
+from pathlib import Path
+
+# -- Project information -----------------------------------------------------
+
+project = "FlightRadarAPI"
+author = "Jean Loui Bernard Silva de Jesus"
+copyright = f"{datetime.now().year}, {author}"
+
+# The three SDKs share one version; the Python package is where it is read from.
+_init = Path(__file__).parent.parent / "python" / "FlightRadarAPI" / "__init__.py"
+release = re.search(r'__version__ = "([^"]+)"', _init.read_text(encoding="utf-8")).group(1)
+version = ".".join(release.split(".")[:2])
+
+# -- General configuration ---------------------------------------------------
+
+extensions = [
+ "myst_parser",
+ "sphinx_copybutton",
+ "sphinx_design",
+ "sphinxext.opengraph",
+]
+
+source_suffix = {".md": "markdown"}
+
+master_doc = "index"
+templates_path = ["_templates"]
+exclude_patterns = ["_build", "Thumbs.db", ".DS_Store"]
+
+myst_enable_extensions = ["colon_fence", "deflist", "substitution"]
+myst_heading_anchors = 3
+myst_substitutions = {"release": release}
+
+# -- HTML output -------------------------------------------------------------
+
+html_theme = "sphinx_rtd_theme"
+html_title = "FlightRadarAPI"
+html_favicon = "_static/favicon.ico"
+
+html_theme_options = {
+ "collapse_navigation": False,
+ "sticky_navigation": True,
+ "navigation_depth": 3,
+ "prev_next_buttons_location": "bottom",
+ "style_external_links": True,
+ # The site is published as a single version: no switchers in the sidebar.
+ "version_selector": False,
+ "language_selector": False,
+}
+
+# "Edit on GitHub" link, plus the data the sidebar repository card reads.
+html_context = {
+ "display_github": True,
+ "github_user": "JeanExtreme002",
+ "github_repo": "FlightRadarAPI",
+ "github_version": "main",
+ "conf_py_path": "/docs/",
+}
+
+html_static_path = ["_static"]
+html_css_files = ["custom.css"]
+
+# -- Extensions --------------------------------------------------------------
+
+# Read the Docs exports the address the build is served from.
+html_baseurl = os.environ.get("READTHEDOCS_CANONICAL_URL", "https://flightradarapi.readthedocs.io/")
+
+ogp_site_url = html_baseurl
+ogp_site_name = "FlightRadarAPI"
+ogp_image = "_static/logo.png"
+ogp_image_alt = "FlightRadarAPI: unofficial SDK for FlightRadar24"
+ogp_social_cards = {"enable": False}
+
+copybutton_prompt_text = r">>> |\.\.\. |\$ "
+copybutton_prompt_is_regexp = True
diff --git a/docs/contributing.md b/docs/contributing.md
new file mode 100644
index 0000000..fd36271
--- /dev/null
+++ b/docs/contributing.md
@@ -0,0 +1,5 @@
+# Contributing
+
+```{include} ../CONTRIBUTING.md
+:start-line: 1
+```
diff --git a/docs/countries.md b/docs/countries.md
new file mode 100644
index 0000000..d203b59
--- /dev/null
+++ b/docs/countries.md
@@ -0,0 +1,75 @@
+# Countries
+
+The [airports listing](airports.md#airports-of-a-country) is narrowed by
+country. Each SDK names the countries FlightRadar24 lists, so you do not
+have to spell them.
+
+::::{tab-set}
+:sync-group: lang
+
+:::{tab-item} Python
+:sync: python
+
+`Countries` is an `Enum`. The value of each member is the slug of the country.
+
+```python
+from FlightRadarAPI import Countries
+
+Countries.BRAZIL.value # "brazil"
+
+airports = fr_api.get_airports([Countries.BRAZIL, Countries.UNITED_STATES])
+
+for country in Countries: # every country
+ print(country.name, country.value)
+```
+:::
+
+:::{tab-item} Node.js
+:sync: node
+
+`Countries` is a frozen object. Each property holds the slug of the country.
+
+```javascript
+const { Countries } = require("flightradarapi");
+
+Countries.BRAZIL; // "brazil"
+
+const airports = await frApi.getAirports([Countries.BRAZIL, Countries.UNITED_STATES]);
+
+for (const [name, slug] of Object.entries(Countries)) { // every country
+ console.log(name, slug);
+}
+```
+:::
+
+:::{tab-item} Go
+:sync: go
+
+`Country` is a string type, with one constant per country. Each constant holds
+the slug of the country.
+
+```go
+flightradarapi.CountryBrazil // "brazil"
+
+airports, err := client.GetAirports(ctx, []flightradarapi.Country{
+ flightradarapi.CountryBrazil,
+ flightradarapi.CountryUnitedStates,
+})
+
+for _, country := range flightradarapi.AllCountries() { // every country
+ fmt.Println(country)
+}
+```
+:::
+::::
+
+## Names and slugs
+
+A slug is the name of a country the way FlightRadar24 spells it in its URLs:
+lower case, with hyphens. The constants are a convenience, not a requirement.
+Wherever a country is expected, a slug or a plain name works too, and is
+normalized the same way: `"United States"`, `"united-states"` and
+`Countries.UNITED_STATES` select the same country.
+
+This also means a country FlightRadar24 adds before the SDK names it can be
+reached by its name.
diff --git a/docs/distances.md b/docs/distances.md
new file mode 100644
index 0000000..9ba0a87
--- /dev/null
+++ b/docs/distances.md
@@ -0,0 +1,129 @@
+# Distances
+
+`Flight` and `Airport` are both an `Entity`: something with a latitude and a
+longitude. Any two entities can measure the distance between them, in
+kilometers, along the surface of the Earth.
+
+::::{tab-set}
+:sync-group: lang
+
+:::{tab-item} Python
+:sync: python
+
+```python
+airport = fr_api.get_airport("KJFK")
+distance = flight.get_distance_from(airport)
+
+print(f"The flight is {distance:.1f} km away from the airport.")
+```
+:::
+
+:::{tab-item} Node.js
+:sync: node
+
+```javascript
+const airport = await frApi.getAirport("KJFK");
+const distance = flight.getDistanceFrom(airport);
+
+console.log(`The flight is ${distance.toFixed(1)} km away from the airport.`);
+```
+:::
+
+:::{tab-item} Go
+:sync: go
+
+```go
+airport, err := client.GetAirport(ctx, "KJFK", false)
+
+if err != nil {
+ log.Fatal(err)
+}
+
+distance, err := flight.GetDistanceFrom(airport)
+
+if err != nil {
+ log.Fatal(err)
+}
+
+fmt.Printf("The flight is %.1f km away from the airport.\n", distance)
+```
+
+`GetDistanceFrom` takes anything that implements `Positioned`, which both
+`*Flight` and `*Airport` do.
+:::
+::::
+
+It works in every combination: flight to airport, airport to airport, flight to
+flight.
+
+::::{tab-set}
+:sync-group: lang
+
+:::{tab-item} Python
+:sync: python
+
+```python
+jfk = fr_api.get_airport("JFK")
+heathrow = fr_api.get_airport("LHR")
+
+print(jfk.get_distance_from(heathrow)) # about 5540 km
+```
+:::
+
+:::{tab-item} Node.js
+:sync: node
+
+```javascript
+const jfk = await frApi.getAirport("JFK");
+const heathrow = await frApi.getAirport("LHR");
+
+console.log(jfk.getDistanceFrom(heathrow)); // about 5540 km
+```
+:::
+
+:::{tab-item} Go
+:sync: go
+
+```go
+jfk, err := client.GetAirport(ctx, "JFK", false)
+heathrow, err := client.GetAirport(ctx, "LHR", false)
+
+distance, err := jfk.GetDistanceFrom(heathrow) // about 5540 km
+```
+:::
+::::
+
+## Entities without a position
+
+A distance needs both positions. The airports returned by the
+[airports listing](airports.md#airports-of-a-country) have none, and a flight
+may be missing its own.
+
+::::{tab-set}
+:sync-group: lang
+
+:::{tab-item} Python
+:sync: python
+
+An airport without a position holds `None`, and the call raises `ValueError`.
+A flight without one holds the string `"N/A"`, and the call raises `TypeError`.
+:::
+
+:::{tab-item} Node.js
+:sync: node
+
+An airport without a position holds `null`, and the call throws `TypeError`.
+A flight without one holds the string `"N/A"`, and the call returns `NaN`
+instead of throwing: check the result, or the coordinates, before using it.
+:::
+
+:::{tab-item} Go
+:sync: go
+
+A missing position is `nil`, and the call returns an error wrapping
+`ErrFlightRadar`.
+:::
+::::
+
+To measure from a listed airport, fetch it by its code first: that one carries
+the coordinates.
diff --git a/docs/errors.md b/docs/errors.md
new file mode 100644
index 0000000..2142ba7
--- /dev/null
+++ b/docs/errors.md
@@ -0,0 +1,341 @@
+# Errors and Retries
+
+## The errors of the SDK
+
+Every error the SDK defines shares one base, so a single handler can catch them
+all.
+
+::::{tab-set}
+:sync-group: lang
+
+:::{tab-item} Python
+:sync: python
+
+| Error | Raised when |
+|---|---|
+| `FlightRadarError` | Base of all the errors below |
+| `AirportNotFoundError` | No airport answers to the code given |
+| `CloudflareError` | Cloudflare blocked the request |
+| `LoginError` | The login failed, or the call needs a session and there is none |
+| `DecompressionLimitError` | A response is larger than the [size limit](#response-size-limit) |
+:::
+
+:::{tab-item} Node.js
+:sync: node
+
+| Error | Raised when |
+|---|---|
+| `FlightRadarError` | Base of all the errors below |
+| `AirportNotFoundError` | No airport answers to the code given |
+| `CloudflareError` | Cloudflare blocked the request |
+| `LoginError` | The login failed, or the call needs a session and there is none |
+| `DecompressionLimitError` | A response is larger than the [size limit](#response-size-limit) |
+:::
+
+:::{tab-item} Go
+:sync: go
+
+| Sentinel | Type | Raised when |
+|---|---|---|
+| `ErrFlightRadar` | | Base of all the errors below |
+| `ErrAirportNotFound` | `*AirportNotFoundError` | No airport answers to the code given |
+| `ErrCloudflare` | `*CloudflareError` | Cloudflare blocked the request |
+| `ErrLogin` | `*LoginError` | The login failed, or the call needs a session and there is none |
+| `ErrDecompressionLimit` | `*DecompressionLimitError` | A response is larger than the [size limit](#response-size-limit) |
+:::
+::::
+
+::::{tab-set}
+:sync-group: lang
+
+:::{tab-item} Python
+:sync: python
+
+```python
+from FlightRadarAPI import (
+ AirportNotFoundError, CloudflareError, FlightRadarError, LoginError,
+)
+
+try:
+ airport = fr_api.get_airport("XXXX")
+except AirportNotFoundError:
+ print("No such airport")
+except CloudflareError as error:
+ print("Blocked by Cloudflare:", error.response.status_code)
+except FlightRadarError as error:
+ print("FlightRadarAPI failed:", error)
+```
+:::
+
+:::{tab-item} Node.js
+:sync: node
+
+```javascript
+const {
+ AirportNotFoundError, CloudflareError, FlightRadarError,
+} = require("flightradarapi");
+
+try {
+ const airport = await frApi.getAirport("XXXX");
+}
+catch (error) {
+ if (error instanceof AirportNotFoundError) {
+ console.log("No such airport");
+ }
+ else if (error instanceof CloudflareError) {
+ console.log("Blocked by Cloudflare:", error.response.status);
+ }
+ else if (error instanceof FlightRadarError) {
+ console.log("FlightRadarAPI failed:", error.message);
+ }
+ else {
+ throw error;
+ }
+}
+```
+:::
+
+:::{tab-item} Go
+:sync: go
+
+Every error wraps a sentinel, so both `errors.Is` and `errors.As` work:
+
+```go
+airport, err := client.GetAirport(ctx, "XXXX", false)
+
+switch {
+case errors.Is(err, flightradarapi.ErrAirportNotFound):
+ // no such airport
+case errors.Is(err, flightradarapi.ErrCloudflare):
+ // blocked by Cloudflare
+case errors.Is(err, flightradarapi.ErrLogin):
+ // the call needs an account
+case errors.Is(err, flightradarapi.ErrFlightRadar):
+ // anything else from this package
+}
+```
+
+Use `errors.As` to read what an error carries:
+
+```go
+var blocked *flightradarapi.CloudflareError
+
+if errors.As(err, &blocked) {
+ fmt.Println(blocked.Response.StatusCode, len(blocked.Body))
+}
+```
+:::
+::::
+
+## Cloudflare blocks
+
+FlightRadar24 sits behind Cloudflare, which may block a client that makes too
+many calls or that does not look like a browser. The SDK reports it as a
+`CloudflareError` when the response is one of:
+
+- an HTTP 520;
+- an HTTP 403 carrying the `cf-mitigated` header, which Cloudflare sets when it
+ was Cloudflare, and not FlightRadar24, that refused the request.
+
+The error carries the blocked response, for inspection:
+
+::::{tab-set}
+:sync-group: lang
+
+:::{tab-item} Python
+:sync: python
+
+`error.response` is the response object, with the challenge page as its content.
+:::
+
+:::{tab-item} Node.js
+:sync: node
+
+`error.response` is the response object, and `error.body` is the challenge page,
+as text.
+:::
+
+:::{tab-item} Go
+:sync: go
+
+In a `*CloudflareError`, `Response` is the `*http.Response` and `Body` is the
+challenge page, as bytes.
+:::
+::::
+
+When blocks keep happening:
+
+1. Slow down. Lower the [number of workers](client.md#concurrency) and space
+ your calls.
+2. Add a [retry policy](#retrying), which backs off between attempts.
+3. Update the package, or override the
+ [TLS impersonation](client.md#tls-impersonation).
+
+## Retrying
+
+By default a failed request is not retried. A `RetryPolicy` retries the
+failures that may go away on their own, such as a Cloudflare block or a timeout,
+with an exponential backoff.
+
+::::{tab-set}
+:sync-group: lang
+
+:::{tab-item} Python
+:sync: python
+
+```python
+from FlightRadarAPI import FlightRadar24API, RetryPolicy
+
+fr_api = FlightRadar24API(retry=RetryPolicy(max_attempts=3))
+```
+
+| Parameter | Default | Meaning |
+|---|---|---|
+| `max_attempts` | `1` | Total number of attempts, including the first |
+| `base_delay` | `1.0` | Seconds to wait before the second attempt |
+| `max_delay` | `30.0` | Longest wait between attempts, in seconds |
+| `jitter` | `0.5` | Upper bound of the random seconds added to each wait |
+
+A `max_attempts` below 1 or a negative delay raises `ValueError`.
+
+Retried: a `CloudflareError` and every `curl_cffi` request error. That includes
+timeouts and network errors, and also an unexpected status code, such as a 404
+or a 500, which is retried like the rest.
+:::
+
+:::{tab-item} Node.js
+:sync: node
+
+```javascript
+const { FlightRadar24API, RetryPolicy } = require("flightradarapi");
+
+const frApi = new FlightRadar24API({ retry: new RetryPolicy({ maxAttempts: 3 }) });
+```
+
+| Option | Default | Meaning |
+|---|---|---|
+| `maxAttempts` | `1` | Total number of attempts, including the first |
+| `baseDelayMs` | `1000` | Milliseconds to wait before the second attempt |
+| `maxDelayMs` | `30000` | Longest wait between attempts, in milliseconds |
+| `jitterMs` | `500` | Upper bound of the random milliseconds added to each wait |
+
+A `maxAttempts` below 1 or a negative delay throws an `Error`.
+
+Retried: a `CloudflareError`, a timeout and a connection reset. An unexpected
+status code is not.
+:::
+
+:::{tab-item} Go
+:sync: go
+
+```go
+retry, err := flightradarapi.NewRetryPolicy(3)
+
+if err != nil {
+ log.Fatal(err)
+}
+
+client := flightradarapi.New(flightradarapi.Options{Retry: retry})
+```
+
+`NewRetryPolicy` takes the number of attempts and uses the defaults for the
+rest. For other values, write the struct:
+
+```go
+retry := &flightradarapi.RetryPolicy{
+ MaxAttempts: 5,
+ BaseDelay: 2 * time.Second,
+ MaxDelay: time.Minute,
+ Jitter: 250 * time.Millisecond,
+}
+```
+
+| Field | `NewRetryPolicy` | Meaning |
+|---|---|---|
+| `MaxAttempts` | the argument | Total number of attempts, including the first |
+| `BaseDelay` | `1s` | Wait before the second attempt. Zero means `1s`. |
+| `MaxDelay` | `30s` | Longest wait between attempts. Zero means `30s`. |
+| `Jitter` | `500ms` | Upper bound of the random time added to each wait. Zero means none. |
+
+`NewRetryPolicy` returns an error for a number of attempts below 1.
+
+Retried: a Cloudflare block, a timeout and a network error. An unexpected
+status code and a cancelled context are not.
+:::
+::::
+
+The wait doubles after each attempt, up to the maximum: with the defaults, about
+1, 2, 4, 8 seconds and so on, each plus the jitter. When every attempt fails,
+the last error is the one you get.
+
+An airport that does not exist, a failed login or an oversized response is
+never retried.
+
+## Response size limit
+
+A response body is limited to 64 MiB once decompressed. A body past the limit
+fails with `DecompressionLimitError` (`ErrDecompressionLimit` in Go) instead of
+exhausting memory. Nothing FlightRadar24 legitimately sends comes close to it.
+
+Python and Go also apply the limit to the compressed body as it arrives. Node.js
+counts the decompressed bytes only.
+
+## Other failures
+
+Not every failure is one of the errors above. Invalid arguments are rejected
+with the error the language uses for them, and HTTP and network failures come
+from the transport.
+
+::::{tab-set}
+:sync-group: lang
+
+:::{tab-item} Python
+:sync: python
+
+| Situation | Error |
+|---|---|
+| Airport code that is not 3 or 4 characters long | `ValueError` |
+| Flight limit of the airport details out of range | `ValueError` |
+| File type of the flight history other than CSV or KML | `ValueError` |
+| Unknown flight tracker option | `KeyError` |
+| Flight tracker value that is not a number | `TypeError` |
+| Distance between entities without a position | `ValueError`, or `TypeError` (see [Distances](distances.md#entities-without-a-position)) |
+| Countries that are neither a string nor an iterable | `TypeError` |
+| Unexpected status code | `curl_cffi` `HTTPError` |
+| Timeout | `curl_cffi` `Timeout` |
+| Network error | `curl_cffi` `RequestsError` |
+
+The last three are subclasses of `curl_cffi.requests.errors.RequestsError`.
+:::
+
+:::{tab-item} Node.js
+:sync: node
+
+| Situation | Error |
+|---|---|
+| Airport code that is not 3 or 4 characters long | `TypeError` |
+| Flight limit of the airport details out of range | `RangeError` |
+| File type of the flight history other than CSV or KML | `TypeError` |
+| Unknown flight tracker option | `RangeError` |
+| Flight tracker value that is not a number | `TypeError` |
+| Distance between entities without a position | `TypeError`, or no error and `NaN` (see [Distances](distances.md#entities-without-a-position)) |
+| Countries that are neither a string nor an iterable | `TypeError` |
+| Unexpected status code | `Error`, with the status in its message |
+| Timeout | `Error` named `TimeoutError` |
+| Network error | the error of `fetch` |
+:::
+
+:::{tab-item} Go
+:sync: go
+
+| Situation | Error |
+|---|---|
+| Invalid argument | plain error |
+| Unexpected status code | `*StatusError` |
+| Timeout | error matching `context.DeadlineExceeded` |
+| Network error | error wrapping a `*url.Error` |
+
+All of them wrap `ErrFlightRadar`. A `*StatusError` carries the `StatusCode`,
+`Status`, `URL` and `Body` of the response.
+:::
+::::
diff --git a/docs/flight.md b/docs/flight.md
new file mode 100644
index 0000000..ac01dd0
--- /dev/null
+++ b/docs/flight.md
@@ -0,0 +1,414 @@
+# Flight
+
+A `Flight` is one aircraft of the live feed. It is an [`Entity`](distances.md),
+so it has a position and can measure its distance to an airport or to another
+flight.
+
+Its attributes come in two groups: the ones the feed sends for every flight, and
+the ones added by the [flight details](flights.md#flight-details).
+
+## Attributes from the feed
+
+Present on every flight.
+
+::::{tab-set}
+:sync-group: lang
+
+:::{tab-item} Python
+:sync: python
+
+| Attribute | Description |
+|---|---|
+| `id` | Identifier of the flight in FlightRadar24 |
+| `latitude` | Latitude, in degrees |
+| `longitude` | Longitude, in degrees |
+| `altitude` | Altitude, in feet |
+| `heading` | Heading, in degrees |
+| `ground_speed` | Ground speed, in knots |
+| `vertical_speed` | Vertical speed, in feet per minute |
+| `on_ground` | `1` when the aircraft is on the ground, `0` otherwise |
+| `time` | Time of the position, as a Unix timestamp |
+| `callsign` | Callsign of the flight |
+| `number` | Flight number, such as `"EK202"` |
+| `squawk` | Transponder code |
+| `icao_24bit` | ICAO 24-bit address of the aircraft, in hexadecimal |
+| `registration` | Registration of the aircraft |
+| `aircraft_code` | Model code of the aircraft, such as `"B77W"` |
+| `airline_icao` | ICAO code of the airline |
+| `airline_iata` | IATA code of the airline, taken from the first two characters of the flight number |
+| `origin_airport_iata` | IATA code of the origin airport |
+| `destination_airport_iata` | IATA code of the destination airport |
+:::
+
+:::{tab-item} Node.js
+:sync: node
+
+| Property | Description |
+|---|---|
+| `id` | Identifier of the flight in FlightRadar24 |
+| `latitude` | Latitude, in degrees |
+| `longitude` | Longitude, in degrees |
+| `altitude` | Altitude, in feet |
+| `heading` | Heading, in degrees |
+| `groundSpeed` | Ground speed, in knots |
+| `verticalSpeed` | Vertical speed, in feet per minute |
+| `onGround` | `1` when the aircraft is on the ground, `0` otherwise |
+| `time` | Time of the position, as a Unix timestamp |
+| `callsign` | Callsign of the flight |
+| `number` | Flight number, such as `"EK202"` |
+| `squawk` | Transponder code |
+| `icao24bit` | ICAO 24-bit address of the aircraft, in hexadecimal |
+| `registration` | Registration of the aircraft |
+| `aircraftCode` | Model code of the aircraft, such as `"B77W"` |
+| `airlineIcao` | ICAO code of the airline |
+| `airlineIata` | IATA code of the airline, taken from the first two characters of the flight number |
+| `originAirportIata` | IATA code of the origin airport |
+| `destinationAirportIata` | IATA code of the destination airport |
+:::
+
+:::{tab-item} Go
+:sync: go
+
+| Field | Description |
+|---|---|
+| `ID` | Identifier of the flight in FlightRadar24 |
+| `Latitude` | Latitude, in degrees |
+| `Longitude` | Longitude, in degrees |
+| `Altitude` | Altitude, in feet |
+| `Heading` | Heading, in degrees |
+| `GroundSpeed` | Ground speed, in knots |
+| `VerticalSpeed` | Vertical speed, in feet per minute |
+| `OnGround` | `1` when the aircraft is on the ground, `0` otherwise |
+| `Time` | Time of the position, as a Unix timestamp |
+| `Callsign` | Callsign of the flight |
+| `Number` | Flight number, such as `"EK202"` |
+| `Squawk` | Transponder code |
+| `ICAO24Bit` | ICAO 24-bit address of the aircraft, in hexadecimal |
+| `Registration` | Registration of the aircraft |
+| `AircraftCode` | Model code of the aircraft, such as `"B77W"` |
+| `AirlineICAO` | ICAO code of the airline |
+| `AirlineIATA` | IATA code of the airline, taken from the first two characters of the flight number |
+| `OriginAirportIATA` | IATA code of the origin airport |
+| `DestinationAirportIATA` | IATA code of the destination airport |
+:::
+
+::::
+
+## Attributes from the details
+
+Present after `set_flight_details` is called, or when the flights were searched
+with details.
+
+::::{tab-set}
+:sync-group: lang
+
+:::{tab-item} Python
+:sync: python
+
+The attributes below are set on the flight itself, next to the ones from the
+feed. They do not exist before `set_flight_details` is called.
+:::
+
+:::{tab-item} Node.js
+:sync: node
+
+The properties below are set on the flight itself, next to the ones from the
+feed. They are `undefined` before `setFlightDetails` is called.
+:::
+
+:::{tab-item} Go
+:sync: go
+
+The fields below belong to `FlightDetails`, reached through `flight.Details`:
+`flight.Details.AirlineName`. `flight.Details` is `nil` before
+`SetFlightDetails` is called.
+
+`FlightDetails` also has `Raw`, the whole details payload, for what the struct
+does not name.
+:::
+::::
+
+### Aircraft and airline
+
+::::{tab-set}
+:sync-group: lang
+
+:::{tab-item} Python
+:sync: python
+
+| Attribute | Description |
+|---|---|
+| `aircraft_model` | Model of the aircraft, as text |
+| `aircraft_age` | Age of the aircraft |
+| `aircraft_country_id` | FlightRadar24 identifier of the country of the aircraft |
+| `aircraft_images` | Photos of the aircraft, as FlightRadar24 sends them |
+| `aircraft_history` | Previous flights of the aircraft |
+| `airline_name` | Name of the airline |
+| `airline_short_name` | Short name of the airline |
+:::
+
+:::{tab-item} Node.js
+:sync: node
+
+| Property | Description |
+|---|---|
+| `aircraftModel` | Model of the aircraft, as text |
+| `aircraftAge` | Age of the aircraft |
+| `aircraftCountryId` | FlightRadar24 identifier of the country of the aircraft |
+| `aircraftImages` | Photos of the aircraft, as FlightRadar24 sends them |
+| `aircraftHistory` | Previous flights of the aircraft |
+| `airlineName` | Name of the airline |
+| `airlineShortName` | Short name of the airline |
+:::
+
+:::{tab-item} Go
+:sync: go
+
+| Field | Description |
+|---|---|
+| `AircraftModel` | Model of the aircraft, as text |
+| `AircraftAge` | Age of the aircraft |
+| `AircraftCountryID` | FlightRadar24 identifier of the country of the aircraft |
+| `AircraftImages` | Photos of the aircraft, as FlightRadar24 sends them |
+| `AircraftHistory` | Previous flights of the aircraft |
+| `AirlineName` | Name of the airline |
+| `AirlineShortName` | Short name of the airline |
+:::
+
+::::
+
+### Destination and origin airports
+
+Each attribute below exists twice: once for the destination and once for the
+origin. Replace `destination` with `origin` for the second set:
+`origin_airport_name`, `originAirportName`, `OriginAirportName`.
+
+::::{tab-set}
+:sync-group: lang
+
+:::{tab-item} Python
+:sync: python
+
+| Attribute | Description |
+|---|---|
+| `destination_airport_name` | Name of the airport |
+| `destination_airport_icao` | ICAO code |
+| `destination_airport_latitude` | Latitude, in degrees |
+| `destination_airport_longitude` | Longitude, in degrees |
+| `destination_airport_altitude` | Altitude of the airport |
+| `destination_airport_country_name` | Name of the country |
+| `destination_airport_country_code` | Code of the country |
+| `destination_airport_terminal` | Terminal of the flight |
+| `destination_airport_gate` | Gate of the flight |
+| `destination_airport_baggage` | Baggage claim of the flight |
+| `destination_airport_timezone_name` | Name of the timezone, such as `"America/New_York"` |
+| `destination_airport_timezone_abbr` | Abbreviation of the timezone |
+| `destination_airport_timezone_abbr_name` | Name the abbreviation stands for |
+| `destination_airport_timezone_offset` | Offset from UTC, in seconds |
+| `destination_airport_timezone_offset_hours` | Offset from UTC, as text in hours |
+| `destination_airport_visible` | The `visible` flag FlightRadar24 sends for the airport |
+| `destination_airport_website` | Website of the airport |
+:::
+
+:::{tab-item} Node.js
+:sync: node
+
+| Property | Description |
+|---|---|
+| `destinationAirportName` | Name of the airport |
+| `destinationAirportIcao` | ICAO code |
+| `destinationAirportLatitude` | Latitude, in degrees |
+| `destinationAirportLongitude` | Longitude, in degrees |
+| `destinationAirportAltitude` | Altitude of the airport |
+| `destinationAirportCountryName` | Name of the country |
+| `destinationAirportCountryCode` | Code of the country |
+| `destinationAirportTerminal` | Terminal of the flight |
+| `destinationAirportGate` | Gate of the flight |
+| `destinationAirportBaggage` | Baggage claim of the flight |
+| `destinationAirportTimezoneName` | Name of the timezone, such as `"America/New_York"` |
+| `destinationAirportTimezoneAbbr` | Abbreviation of the timezone |
+| `destinationAirportTimezoneAbbrName` | Name the abbreviation stands for |
+| `destinationAirportTimezoneOffset` | Offset from UTC, in seconds |
+| `destinationAirportTimezoneOffsetHours` | Offset from UTC, as text in hours |
+| `destinationAirportVisible` | The `visible` flag FlightRadar24 sends for the airport |
+| `destinationAirportWebsite` | Website of the airport |
+:::
+
+:::{tab-item} Go
+:sync: go
+
+| Field | Description |
+|---|---|
+| `DestinationAirportName` | Name of the airport |
+| `DestinationAirportICAO` | ICAO code |
+| `DestinationAirportLatitude` | Latitude, in degrees |
+| `DestinationAirportLongitude` | Longitude, in degrees |
+| `DestinationAirportAltitude` | Altitude of the airport |
+| `DestinationAirportCountryName` | Name of the country |
+| `DestinationAirportCountryCode` | Code of the country |
+| `DestinationAirportTerminal` | Terminal of the flight |
+| `DestinationAirportGate` | Gate of the flight |
+| `DestinationAirportBaggage` | Baggage claim of the flight |
+| `DestinationAirportTimezoneName` | Name of the timezone, such as `"America/New_York"` |
+| `DestinationAirportTimezoneAbbr` | Abbreviation of the timezone |
+| `DestinationAirportTimezoneAbbrName` | Name the abbreviation stands for |
+| `DestinationAirportTimezoneOffset` | Offset from UTC, in seconds |
+| `DestinationAirportTimezoneOffsetHours` | Offset from UTC, as text in hours |
+| `DestinationAirportVisible` | The `visible` flag FlightRadar24 sends for the airport |
+| `DestinationAirportWebsite` | Website of the airport |
+:::
+
+::::
+
+The IATA codes of both airports come with the feed: see
+`origin_airport_iata` and `destination_airport_iata` above.
+
+### Status, times and trail
+
+::::{tab-set}
+:sync-group: lang
+
+:::{tab-item} Python
+:sync: python
+
+| Attribute | Description |
+|---|---|
+| `status_text` | Status of the flight, as text |
+| `status_icon` | Name of the status icon FlightRadar24 shows |
+| `time_details` | Scheduled, estimated and real times of the flight, as FlightRadar24 sends them |
+| `trail` | Positions the flight has been through |
+:::
+
+:::{tab-item} Node.js
+:sync: node
+
+| Property | Description |
+|---|---|
+| `statusText` | Status of the flight, as text |
+| `statusIcon` | Name of the status icon FlightRadar24 shows |
+| `timeDetails` | Scheduled, estimated and real times of the flight, as FlightRadar24 sends them |
+| `trail` | Positions the flight has been through |
+:::
+
+:::{tab-item} Go
+:sync: go
+
+| Field | Description |
+|---|---|
+| `StatusText` | Status of the flight, as text |
+| `StatusIcon` | Name of the status icon FlightRadar24 shows |
+| `TimeDetails` | Scheduled, estimated and real times of the flight, as FlightRadar24 sends them |
+| `Trail` | Positions the flight has been through |
+:::
+
+::::
+
+## Missing values
+
+FlightRadar24 does not send every value for every flight.
+
+::::{tab-set}
+:sync-group: lang
+
+:::{tab-item} Python
+:sync: python
+
+A value FlightRadar24 left out is the string `"N/A"`. The collections are empty
+instead: `aircraft_history`, `aircraft_images` and `trail` are `[]`, and
+`time_details` is `{}`.
+
+Because of that, a numeric attribute is either a number or `"N/A"`. Check before
+doing arithmetic:
+
+```python
+if isinstance(flight.altitude, (int, float)):
+ print(flight.altitude * 0.3048, "m")
+```
+:::
+
+:::{tab-item} Node.js
+:sync: node
+
+A value FlightRadar24 left out is the string `"N/A"`. The collections are empty
+instead: `aircraftHistory`, `aircraftImages` and `trail` are `[]`, and
+`timeDetails` is `{}`.
+
+Because of that, a numeric property is either a number or `"N/A"`. Check before
+doing arithmetic:
+
+```javascript
+if (typeof flight.altitude === "number") {
+ console.log(flight.altitude * 0.3048, "m");
+}
+```
+:::
+
+:::{tab-item} Go
+:sync: go
+
+A value FlightRadar24 left out is the zero value of its field: `""` for text
+and `nil` for the numbers and booleans, which are pointers.
+
+```go
+if flight.Altitude != nil {
+ fmt.Println(*flight.Altitude*0.3048, "m")
+}
+```
+
+The numbers are `*float64`, except `Time`, which is `*int64`. The `...Visible`
+fields are `*bool`. `DefaultText` holds the `"N/A"` the formatting methods
+return.
+:::
+::::
+
+## Methods
+
+::::{tab-set}
+:sync-group: lang
+
+:::{tab-item} Python
+:sync: python
+
+| Method | Description |
+|---|---|
+| `get_altitude()` | Altitude with its unit. See [Formatted values](flights.md#formatted-values). |
+| `get_flight_level()` | Flight level with its unit |
+| `get_ground_speed()` | Ground speed with its unit |
+| `get_heading()` | Heading with its unit |
+| `get_vertical_speed()` | Vertical speed with its unit |
+| `check_info(**info)` | Whether the flight matches some values. See [Checking a flight](flights.md#checking-a-flight-against-criteria). |
+| `set_flight_details(details)` | Attach a details payload to the flight |
+| `get_distance_from(entity)` | Distance to another entity, in kilometers. See [Distances](distances.md). |
+:::
+
+:::{tab-item} Node.js
+:sync: node
+
+| Method | Description |
+|---|---|
+| `getAltitude()` | Altitude with its unit. See [Formatted values](flights.md#formatted-values). |
+| `getFlightLevel()` | Flight level with its unit |
+| `getGroundSpeed()` | Ground speed with its unit |
+| `getHeading()` | Heading with its unit |
+| `getVerticalSpeed()` | Vertical speed with its unit |
+| `checkInfo(info)` | Whether the flight matches some values. See [Checking a flight](flights.md#checking-a-flight-against-criteria). |
+| `setFlightDetails(details)` | Attach a details payload to the flight |
+| `getDistanceFrom(entity)` | Distance to another entity, in kilometers. See [Distances](distances.md). |
+:::
+
+:::{tab-item} Go
+:sync: go
+
+| Method | Description |
+|---|---|
+| `GetAltitude()` | Altitude with its unit. See [Formatted values](flights.md#formatted-values). |
+| `GetFlightLevel()` | Flight level with its unit |
+| `GetGroundSpeed()` | Ground speed with its unit |
+| `GetHeading()` | Heading with its unit |
+| `GetVerticalSpeed()` | Vertical speed with its unit |
+| `CheckInfo(criteria)` | Whether the flight matches some values. See [Checking a flight](flights.md#checking-a-flight-against-criteria). |
+| `SetFlightDetails(details)` | Attach a details payload to the flight |
+| `GetDistanceFrom(other)` | Distance to another entity, in kilometers. See [Distances](distances.md). |
+:::
+
+::::
diff --git a/docs/flights.md b/docs/flights.md
new file mode 100644
index 0000000..0689087
--- /dev/null
+++ b/docs/flights.md
@@ -0,0 +1,332 @@
+# Live Flights
+
+The Real Time Flight Tracker is the feed behind the FlightRadar24 map. Each
+entry becomes a `Flight`, with its position, altitude, speed and codes.
+
+## Getting flights
+
+Without arguments, the feed returns the flights of the whole globe, up to the
+`limit` of the [flight tracker settings](tracker-config.md) (5000 by default).
+
+::::{tab-set}
+:sync-group: lang
+
+:::{tab-item} Python
+:sync: python
+
+```python
+flights = fr_api.get_flights()
+
+for flight in flights[:10]:
+ print(flight) # <(B77W) A6-EBM - Altitude: 35000 - Ground Speed: 480 - Heading: 90>
+```
+:::
+
+:::{tab-item} Node.js
+:sync: node
+
+```javascript
+const flights = await frApi.getFlights();
+
+for (const flight of flights.slice(0, 10)) {
+ console.log(flight.callsign, flight.aircraftCode, flight.registration);
+}
+```
+:::
+
+:::{tab-item} Go
+:sync: go
+
+```go
+flights, err := client.GetFlights(ctx, flightradarapi.FlightSearch{})
+
+if err != nil {
+ log.Fatal(err)
+}
+
+for _, flight := range flights[:min(10, len(flights))] {
+ fmt.Println(flight) // <(B77W) A6-EBM - Altitude: 35000 - Ground Speed: 480 - Heading: 90>
+}
+```
+:::
+::::
+
+## Filtering
+
+Four filters narrow the search, and they can be combined:
+
+| Filter | Meaning | Example |
+|---|---|---|
+| airline | ICAO code of the airline | `"UAE"` |
+| bounds | Area to look at, as a `"y1,y2,x1,x2"` string (see [Zones and bounds](zones.md)) | `"73,-12,-156,38"` |
+| registration | Aircraft registration | `"A6-EBM"` |
+| aircraft type | Aircraft model code | `"B77W"` |
+
+::::{tab-set}
+:sync-group: lang
+
+:::{tab-item} Python
+:sync: python
+
+```python
+zone = fr_api.get_zones()["northamerica"]
+bounds = fr_api.get_bounds(zone)
+
+emirates_flights = fr_api.get_flights(
+ airline="UAE",
+ aircraft_type="B77W",
+ bounds=bounds,
+)
+```
+
+The signature is
+`get_flights(airline=None, bounds=None, registration=None, aircraft_type=None, *, details=False)`.
+:::
+
+:::{tab-item} Node.js
+:sync: node
+
+```javascript
+const zone = frApi.getZones()["northamerica"];
+const bounds = frApi.getBounds(zone);
+
+// getFlights(airline, bounds, registration, aircraftType, details)
+const emiratesFlights = await frApi.getFlights("UAE", bounds, null, "B77W");
+```
+
+The arguments are positional: pass `null` for the filters you do not use.
+:::
+
+:::{tab-item} Go
+:sync: go
+
+```go
+bounds := client.GetBounds(client.GetZones()["northamerica"])
+
+emiratesFlights, err := client.GetFlights(ctx, flightradarapi.FlightSearch{
+ Airline: "UAE",
+ AircraftType: "B77W",
+ Bounds: bounds,
+})
+```
+
+`FlightSearch` has the fields `Airline`, `Bounds`, `Registration`,
+`AircraftType` and `Details`. Leave out the ones you do not use.
+:::
+::::
+
+More options, such as which data sources to include or how many flights to
+return, live in the [flight tracker settings](tracker-config.md).
+
+## Flight details
+
+The feed carries the essentials only. The details of a flight (airline and
+aircraft names, origin and destination airports, status, times, trail) come from
+a second request, and are then attached to the flight.
+
+::::{tab-set}
+:sync-group: lang
+
+:::{tab-item} Python
+:sync: python
+
+```python
+flight_details = fr_api.get_flight_details(flight) # dict, as FR24 sends it
+flight.set_flight_details(flight_details)
+
+print("Flying to", flight.destination_airport_name)
+```
+:::
+
+:::{tab-item} Node.js
+:sync: node
+
+```javascript
+const flightDetails = await frApi.getFlightDetails(flight); // object, as FR24 sends it
+flight.setFlightDetails(flightDetails);
+
+console.log("Flying to", flight.destinationAirportName);
+```
+:::
+
+:::{tab-item} Go
+:sync: go
+
+```go
+details, err := client.GetFlightDetails(ctx, flight) // map[string]any, as FR24 sends it
+
+if err != nil {
+ log.Fatal(err)
+}
+
+flight.SetFlightDetails(details)
+
+fmt.Println("Flying to", flight.Details.DestinationAirportName)
+```
+
+In Go the detail values live in `flight.Details`, which is `nil` until
+`SetFlightDetails` is called. `flight.Details.Raw` keeps the whole payload.
+:::
+::::
+
+### Details for every flight at once
+
+Ask for the details in the search itself and they are fetched for every flight
+found, several requests at a time (8 by default, see
+[Configuration](client.md#concurrency)).
+
+::::{tab-set}
+:sync-group: lang
+
+:::{tab-item} Python
+:sync: python
+
+```python
+flights = fr_api.get_flights(airline="GLO", details=True)
+
+for flight in flights:
+ print(flight.callsign, "→", flight.destination_airport_name)
+```
+:::
+
+:::{tab-item} Node.js
+:sync: node
+
+```javascript
+const flights = await frApi.getFlights("GLO", null, null, null, true);
+
+for (const flight of flights) {
+ console.log(flight.callsign, "→", flight.destinationAirportName);
+}
+```
+:::
+
+:::{tab-item} Go
+:sync: go
+
+```go
+flights, err := client.GetFlights(ctx, flightradarapi.FlightSearch{
+ Airline: "GLO",
+ Details: true,
+})
+
+if err != nil {
+ log.Fatal(err)
+}
+
+for _, flight := range flights {
+ fmt.Println(flight.Callsign, "→", flight.Details.DestinationAirportName)
+}
+```
+:::
+::::
+
+```{note}
+This makes one extra request per flight. Combine it with a filter: asking for
+the details of thousands of flights is slow and likely to get you blocked.
+```
+
+## Formatted values
+
+A flight can render its numbers with their units.
+
+::::{tab-set}
+:sync-group: lang
+
+:::{tab-item} Python
+:sync: python
+
+| Method | Example |
+|---|---|
+| `get_altitude()` | `"35000 ft"` |
+| `get_flight_level()` | `"350 FL"` |
+| `get_ground_speed()` | `"480 kts"` |
+| `get_heading()` | `"90°"` |
+| `get_vertical_speed()` | `"-1280 fpm"` |
+:::
+
+:::{tab-item} Node.js
+:sync: node
+
+| Method | Example |
+|---|---|
+| `getAltitude()` | `"35000 ft"` |
+| `getFlightLevel()` | `"350 FL"` |
+| `getGroundSpeed()` | `"480 kts"` |
+| `getHeading()` | `"90°"` |
+| `getVerticalSpeed()` | `"-1280 fpm"` |
+:::
+
+:::{tab-item} Go
+:sync: go
+
+| Method | Example |
+|---|---|
+| `GetAltitude()` | `"35000 ft"` |
+| `GetFlightLevel()` | `"350 FL"` |
+| `GetGroundSpeed()` | `"480 kts"` |
+| `GetHeading()` | `"90°"` |
+| `GetVerticalSpeed()` | `"-1280 fpm"` |
+:::
+
+::::
+
+The flight level is shown from 10000 ft up; below that, `get_flight_level()`
+returns the same as `get_altitude()`. Each of these returns `"N/A"` when the
+feed did not send the value.
+
+## Checking a flight against criteria
+
+`check_info` tells whether a flight matches one or more values. A plain name
+compares for equality; with the `min` or `max` prefix it compares numerically.
+
+::::{tab-set}
+:sync-group: lang
+
+:::{tab-item} Python
+:sync: python
+
+```python
+flight.check_info(min_altitude=6700, max_altitude=13000, airline_icao="THY") # True or False
+```
+
+The prefixes are `min_` and `max_`. A name the flight does not have is ignored.
+
+A `min_` or `max_` comparison raises `TypeError` when the flight has no value
+for that attribute, which is then the string `"N/A"`.
+:::
+
+:::{tab-item} Node.js
+:sync: node
+
+```javascript
+flight.checkInfo({ minAltitude: 6700, maxAltitude: 13000, airlineIcao: "THY" }); // true or false
+```
+
+The prefixes are `min` and `max`, followed by the attribute name in camelCase.
+A name the flight does not have is ignored, and a `min` or `max` comparison is
+`false` when the flight has no value for that attribute.
+:::
+
+:::{tab-item} Go
+:sync: go
+
+```go
+matched, err := flight.CheckInfo(map[string]any{
+ "min_altitude": 6700,
+ "max_altitude": 13000,
+ "airline_icao": "THY",
+})
+```
+
+The keys are the snake_case names the Python SDK uses, with the prefixes `min_`
+and `max_`. A name that exists nowhere is an error, instead of being ignored,
+and a `min_` or `max_` comparison is `false` when the flight has no value for
+that field.
+:::
+::::
+
+## See also
+
+- [Flight reference](flight.md): every attribute of a flight.
+- [Distances](distances.md): how far a flight is from an airport, or from another flight.
diff --git a/docs/go.md b/docs/go.md
deleted file mode 100644
index ce09a61..0000000
--- a/docs/go.md
+++ /dev/null
@@ -1,286 +0,0 @@
----
-title: Go
-description: API Documentation for Go
----
-
-## Installation
-
-To install the FlightRadarAPI for Go, use the following command:
-
-```bash
-go get github.com/JeanExtreme002/FlightRadarAPI/go@latest
-```
-
-## Basic Usage
-
-Import the package and create a client:
-
-```go
-import (
- "time"
-
- "github.com/JeanExtreme002/FlightRadarAPI/go/flightradarapi"
-)
-
-client := flightradarapi.New()
-
-// Every field's zero value means the default, so set only what you need.
-client = flightradarapi.New(flightradarapi.Options{
- Timeout: 10 * time.Second, // default: 30s
- MaxWorkers: 4, // default: 8
-})
-```
-
-Construction cannot fail, so it needs no error handling. Every method that talks
-to FlightRadar24 takes a `context.Context` and returns an `error` alongside its
-result.
-
-### Fetching Data
-
-You can fetch various types of data using the following methods:
-
-- **Flights list:**
-
- ```go
- flights, err := client.GetFlights(ctx, flightradarapi.FlightSearch{}) // Returns []*Flight
- ```
-
-- **Airports list:**
-
- ```go
- // Airports of specific countries
- airports, err := client.GetAirports(ctx, []flightradarapi.Country{
- flightradarapi.CountryBrazil, flightradarapi.CountryUnitedStates,
- })
-
- // Pass nil to get every airport
- allAirports, err := client.GetAirports(ctx, nil)
- ```
-
- FR24 lists airports one page per country (and per state for the largest
- countries), so this makes one request per page: a handful for a few
- countries, a few hundred for every airport, `MaxWorkers` at a time. The
- listing carries the name, codes and country only; use `GetAirport(ctx, code)`
- for the position and altitude of an airport.
-
-- **Airlines list:**
-
- ```go
- airlines, err := client.GetAirlines(ctx)
- ```
-
-- **Zones list:**
-
- ```go
- zones := client.GetZones()
- ```
-
-### Fetching Detailed Information
-
-Fetch more information about a specific flight or airport using the following methods:
-
-- **Flight details:**
-
- ```go
- details, err := client.GetFlightDetails(ctx, flight)
- flight.SetFlightDetails(details)
-
- fmt.Println("Flying to", flight.Details.DestinationAirportName)
- ```
-
- Ask for every flight's details in one call with `FlightSearch{Details: true}`.
- Requests run `MaxWorkers` at a time (8 by default).
-
-- **Airport details:**
-
- ```go
- details, err := client.GetAirportDetails(ctx, icao, 100, 1)
- ```
-
- !!! note
- Arrivals and departures can have a limit `flightLimit` (max value is 100) to display. When you need to reach more than 100 flights you can use the `page` parameter to view other pages.
-
-## Advanced Usage
-
-### Fetching Flights Above a Specific Position
-
-Use the `GetBoundsByPoint(...)` method to fetch flights above a specific position. This method takes `latitude` and `longitude` for your position and `radius` for the distance in meters from your position to designate a tracking area.
-
-```go
-// Your point is 52°34'04.7"N 13°16'57.5"E from Google Maps and radius 2km
-bounds := client.GetBoundsByPoint(52.567967, 13.282644, 2000)
-
-flights, err := client.GetFlights(ctx, flightradarapi.FlightSearch{Bounds: bounds})
-```
-
-### Filtering Flights and Airports
-
-Use the `GetFlights(...)` method to search for flights by airline, bounds (customized coordinates or obtained by the `GetZones()` method), aircraft registration or aircraft type.
-
-```go
-// You may also set a custom region, such as: bounds := "73,-12,-156,38"
-bounds := client.GetBounds(client.GetZones()["northamerica"])
-
-emiratesFlights, err := client.GetFlights(ctx, flightradarapi.FlightSearch{
- Airline: "UAE",
- AircraftType: "B77W",
- Bounds: bounds,
-})
-```
-
-A single flight can be checked against several values at once, with the optional
-`min_`/`max_` prefixes for numeric comparisons:
-
-```go
-matched, err := flight.CheckInfo(map[string]any{
- "min_altitude": 6700, "max_altitude": 13000, "airline_icao": "THY",
-})
-```
-
-### Building Entities From a Payload You Already Have
-
-The constructors the Python and Node.js ports expose have counterparts here:
-
-```go
-airport := flightradarapi.NewAirportFromBasicInfo(row) // one airports-feed row
-airport = flightradarapi.NewAirportFromInfo(info) // the "details" block
-airport = flightradarapi.NewAirportFromDetails(payload) // a GetAirportDetails payload
-flight := flightradarapi.NewFlight("2e0f1a2", feedRow) // one live-feed row
-```
-
-### Fetching Airport by ICAO or IATA
-
-```go
-luklaAirport, err := client.GetAirport(ctx, "VNLK", true)
-```
-
-### Calculating Distance Between Flights and Airports
-
-`Flight` and `Airport` both embed `Entity`, which provides the `GetDistanceFrom(...)` method. It returns the distance between the two entities in kilometers, and an error when either of them carries no position.
-
-```go
-airport, err := client.GetAirport(ctx, "KJFK", false)
-distance, err := flight.GetDistanceFrom(airport)
-
-fmt.Printf("The flight is %.1f km away from the airport.\n", distance)
-```
-
-### Downloading Flight Data :material-information-outline:{ title="This requires a premium subscription" }
-
-```go
-if err := client.Login(ctx, "email", "password"); err != nil {
- log.Fatal(err)
-}
-
-historyData, err := client.GetHistoryData(ctx, flight, "CSV", 1706529600)
-err = os.WriteFile("history_data.csv", []byte(historyData), 0o644)
-```
-
-!!! warning inline end
- If an invalid time is provided, a blank document will be returned.
-
-| Parameter | Description |
-| ------------- | ------------- |
-| `flight` | The flight to download. This can be obtained from any other function that returns flights. |
-| `fileType` | The format of the file to download. This can be either "CSV" or "KML". |
-| `timestamp` | The scheduled time of departure (STD) of the flight in UTC, as a Unix timestamp. |
-
-### Setting and Getting Real-time Flight Tracker Parameters
-
-Set them with the `SetFlightTrackerConfig(...)` method. It takes a `*FlightTrackerConfig` and a map of single values; either may be `nil`. Unknown options and non-numeric values are rejected, and a rejected update leaves the current config untouched.
-
-Get the current configuration with `GetFlightTrackerConfig()`, which returns a copy. Note: `NewFlightTrackerConfig()` means resetting all parameters to default.
-
-```go
-config := client.GetFlightTrackerConfig()
-config.Limit = "10"
-
-err := client.SetFlightTrackerConfig(&config, nil)
-
-flights, err := client.GetFlights(ctx, flightradarapi.FlightSearch{}) // Returns only 10 flights
-```
-
-### Handling Errors
-
-Every error wraps a sentinel, so both `errors.Is` and `errors.As` work:
-
-```go
-airport, err := client.GetAirport(ctx, "XXX", false)
-
-switch {
-case errors.Is(err, flightradarapi.ErrAirportNotFound):
- // no such airport
-case errors.Is(err, flightradarapi.ErrCloudflare):
- // blocked by Cloudflare
-case errors.Is(err, flightradarapi.ErrLogin):
- // the endpoint needs an account
-}
-```
-
-Transient failures — a Cloudflare block, a timeout, a network error — can be
-retried with exponential backoff:
-
-```go
-retry, err := flightradarapi.NewRetryPolicy(3)
-client := flightradarapi.New(flightradarapi.Options{Retry: retry})
-```
-
-### Configuring the Client
-
-Every field of `Options` defaults from its zero value, so set only what you need:
-
-```go
-retry, err := flightradarapi.NewRetryPolicy(3) // 1s base, 30s cap, 500ms jitter
-
-client := flightradarapi.New(flightradarapi.Options{
- Timeout: 10 * time.Second, // default: 30s
- MaxWorkers: 4, // default: 8
- Retry: retry, // default: no retry
-})
-```
-
-### TLS Impersonation
-
-FlightRadar24 fingerprints TLS handshakes through Cloudflare. The default client
-narrows Go's offered cipher suites and curve order to Chrome's, which is enough
-today. Since Go fixes its own cipher ordering, that is an approximation rather
-than a byte-exact fingerprint; for full impersonation, pass a client built on
-[utls](https://github.com/refraction-networking/utls) or
-[tls-client](https://github.com/bogdanfinn/tls-client):
-
-```go
-client := flightradarapi.New(flightradarapi.Options{
- HTTPClient: &http.Client{Transport: myImpersonatingTransport},
-})
-```
-
-## Differences from the Python and Node.js Packages
-
-The behavior is the same; the surface is Go-shaped.
-
-The features are the same; the names differ only where Go's conventions do. The
-package name is part of every identifier, so the client is `Client` rather than
-`FlightRadar24API` — the way it is `http.Client` and not `http.HTTPClient`.
-
-| Python / Node.js | Go |
-| --- | --- |
-| `FlightRadar24API` | `Client`, built with `New` |
-| `new FlightRadar24API({timeout, maxWorkers})` | `New(Options{Timeout: ..., MaxWorkers: ...})` |
-| `FlightRadarError` (base class) | `ErrFlightRadar`, wrapped by every error here |
-| `Countries.BRAZIL` | `CountryBrazil`, with `AllCountries()` to enumerate |
-| `FlightRadar24API(user, password)` logs in (Python only) | `client.Login(ctx, user, password)` |
-| `get_flights(airline, bounds, registration, aircraft_type, details)` | `GetFlights(ctx, FlightSearch{...})` |
-| `check_info(min_altitude=6700)` | `CheckInfo(map[string]any{"min_altitude": 6700})` |
-| `Airport.from_details(payload)` | `NewAirportFromDetails(payload)` |
-| `(bytes, extension)` tuple | `*Image` |
-| `airline["n_aircrafts"]` | `Airline.NumAircrafts` |
-| `flight.destination_airport_name` | `flight.Details.DestinationAirportName` |
-| default arguments (`flight_limit=100`, `limit=50`) | zero means the same default |
-| `get_airports(None)` for every airport | `GetAirports(ctx, nil)` |
-| exceptions | `error` values wrapping `ErrFlightRadar` |
-| missing value is the string `"N/A"` | zero value: `""` or a nil pointer |
-| `parsers` module (internal) | unexported functions |
-| blocking calls | `context.Context` on every request |
-
-Every method of `FlightRadar24API` has a counterpart here, with the same
-parameters, kept that way by a test that reads the Python source.
diff --git a/docs/index.md b/docs/index.md
index 65225ff..fee252d 100644
--- a/docs/index.md
+++ b/docs/index.md
@@ -1,72 +1,219 @@
-# FlightRadarAPI Documentation
+---
+og:description: Unofficial SDK for FlightRadar24, for Python, Node.js and Go. Live flights, airports, airlines, zones and more, with the same features in every language.
+myst:
+ html_meta:
+ description: "Unofficial SDK for FlightRadar24, for Python, Node.js and Go. Live flights, airports, airlines, zones and more, with the same features in every language."
+---
+
+# FlightRadarAPI
+
+
FlightRadarAPI
| tests | +
+ |
+
|---|---|
| python | +
+ |
+
| node.js | +
+ |
+
| go | +
+ |
+
| license | +
+ |
+