Find the cheapest flight between two cities across an entire date range.
Most flight sites make you check one date at a time. flightsearch sweeps every day in the range you give it — "sometime in August" — and shows you the single cheapest departure, plus a price-per-day breakdown so you can see whether shifting your trip by a day saves you a hundred euros.
- Whole-range search — give a start and end date, get the cheapest day.
- One-way and round trip — for round trips you give a departure range and a return range, and get the cheapest combination of the two.
- Price per day — full sorted list, so near-misses are visible too.
- Filters — direct only, or at most 1 / 2 stops. For round trips both legs are checked, not just the outbound.
- Currency and passengers picked in the UI.
- Shareable links — every search is encoded in the URL, so a result can be bookmarked or sent to someone; opening the link re-runs the search.
- Response caching — repeating a search does not spend your API quota.
- Real aggregated data — prices come from the Travelpayouts / Aviasales aggregator, which continuously scans hundreds of agencies and airlines.
- 4 languages — English, German, Russian, Ukrainian. Switches the whole UI, city names, date formats, and plural forms.
- City autocomplete with IATA resolution — type "Vienna", "Wien" or "Вена",
get
VIE. Works without an API token. - Direct booking links to the actual ticket.
- Zero dependencies — Python standard library only.
- Demo mode — runs out of the box without any API key, so you can see the whole interface before signing up for anything.
Requires Python 3.10 or newer. No packages to install.
git clone https://github.com/ostrvcxztd/flightsearch.git
cd flightsearch
python app.pyOpen http://127.0.0.1:8000.
It starts in demo mode with generated placeholder prices. City search is already real. To get real prices, add a token — see below.
-
Register for free at travelpayouts.com and copy your API token.
-
Create your
.envfrom the template:cp .env.example .env # Windows: copy .env.example .env -
Put the token in
.env:TRAVELPAYOUTS_TOKEN=your_token_here -
Restart. The console will confirm real-data mode.
The token is read and used server-side only — it is never sent to the browser, so it cannot leak to users of your deployed instance.
All settings live in .env (or real environment variables, which take
precedence).
| Variable | Default | Purpose |
|---|---|---|
TRAVELPAYOUTS_TOKEN |
(empty) | API token. Empty → demo mode. |
TRAVELPAYOUTS_MARKER |
(empty) | Partner ID for affiliate booking links. |
FLIGHTSEARCH_CURRENCY |
eur |
Default currency; switchable in the UI. |
FLIGHTSEARCH_CACHE_TTL |
1800 |
Cache lifetime in seconds. 0 disables it. |
FLIGHTSEARCH_HOST |
127.0.0.1 |
Bind address. Use 0.0.0.0 to expose. |
FLIGHTSEARCH_PORT |
8000 |
Port. |
docker build -t flightsearch .
docker run -p 8000:8000 -e TRAVELPAYOUTS_TOKEN=your_token_here flightsearchThe token is passed at run time and is deliberately not baked into the image.
The frontend is a thin client over two JSON endpoints — useful if you want to build your own UI.
{ "cities": [ { "code": "VIE", "name": "Vienna", "country": "Austria" } ] }| Parameter | Required | Notes |
|---|---|---|
from, to |
yes | IATA city codes, e.g. VIE, BCN |
date_from, date_to |
yes | Departure range, YYYY-MM-DD |
trip |
no | oneway (default) or round |
return_from, return_to |
if round | Return range |
currency |
no | eur usd rub uah gbp pln |
passengers |
no | 1–9, default 1 |
max_transfers |
no | 0 = direct only; omit for any |
{
"demo": false,
"origin": "VIE",
"destination": "BCN",
"round_trip": true,
"currency": "EUR",
"passengers": 2,
"count": 13,
"cheapest": {
"date": "2026年08月19日",
"return_date": "2026年08月25日",
"price": 119,
"currency": "EUR",
"airline": "LW",
"flight_number": "1532",
"transfers": 1,
"return_transfers": 0,
"duration": 190,
"duration_back": 150,
"link": "https://www.aviasales.com/search/..."
},
"results": [ "... same shape, sorted by price ..." ]
}transfers/duration describe the outbound leg; return_transfers/
duration_back describe the return leg and are null for one-way searches.
Invalid input returns HTTP 400 with {"error": "...", "error_code": "..."}.
The error_code is stable and machine-readable (date_order,
range_too_big, too_many_months, return_before_departure, no_cities,
bad_dates, upstream) — the frontend uses it to show a translated message.
pip install -e ".[dev]" pytest tests/ -q # 47 tests, no network required ruff check .
Tests stub out all external calls, so the suite runs offline and in CI.
- Add a block to the
I18Nobject at the top ofstatic/app.js. - Add the language code to
SUPPORTED_LOCALESinapp.py. - Add a button to
#lang-switchinstatic/index.html.
Note that Russian and Ukrainian need three plural forms for "stops"; see the
existing transfers functions.
app.py HTTP server, search providers (real + demo), city lookup
static/index.html Page markup
static/style.css Styles
static/app.js i18n, autocomplete, search, rendering
tests/test_app.py Test suite
Dockerfile Container build
- Prices are per passenger. The aggregator quotes one adult; the passenger count is carried into the booking link rather than multiplied into the price, because fares are not always linear in group size. The UI labels the price accordingly.
- Cached prices. The aggregator serves recently-seen prices, so the final price is confirmed by the seller at checkout and may differ slightly.
- Free-tier rate limits. One API call per calendar month spanned by your range; for round trips it is one per (departure month ×ばつ return month) pair, capped at 16 per search. Responses are cached to keep this down.
- Development server.
app.pyuses Python's built-inhttp.server, which is fine for personal and local use but is not hardened for public traffic. For a public deployment put it behind a reverse proxy (nginx, Caddy) or port the handler to a WSGI/ASGI server.
Issues and pull requests are welcome. Please run pytest and ruff check .
before opening a PR.
MIT © Maksym Yarmolenko
Not affiliated with Travelpayouts, Aviasales, or any airline.