Build a Flight Price Tracker
Track real airline prices and visualize fare trends with a Python web app.
Introduction
30 Second Summary
You know that feeling when you check a flight price, close the tab, and then wonder a week later whether it went up or down? Without a record of past prices, you are guessing every time you decide to buy or wait.
In this project, you will build a personal flight price tracker that fetches real airline prices, stores them in a database, and displays everything on a web dashboard with a chart showing how fares move over time.
What You'll Build
You will have a dashboard running in your browser where you enter a route you care about, hit a button, and instantly see live prices from real airlines alongside a line chart revealing whether fares are trending up or down.
By the end of this project, you'll have:
- A working Flask web app that calls a real flight pricing API and displays live airline prices with carrier names, durations, and stop counts.
- A SQLite database that accumulates price snapshots every time you check, building a history you can query and compare.
- An interactive Chart.js line chart showing price trends over time so you can spot the cheapest moment to book.
- Secret Mission: Add a price-drop alert that highlights routes where the current fare drops below your target budget.
Are there any prerequisites?
None. This project walks you through installing everything you need, including Python, VS Code, and a free API key. No prior web development experience is required.
Before We Start
Before you can fetch live flight prices and display them in a browser, you need a few tools in place.
In this step, you'll create your project folder, install Flask and the requests library, grab a free API key from Ignav, and set up the file structure for your web dashboard.
In this step, get ready to:
- Install Flask and the requests library in a virtual environment.
- Get a free Ignav API key for fetching real flight prices.
- Create the project files and folders.
Create a virtual environment and install dependencies
A virtual environment keeps your project's packages separate from your system Python. This way, the libraries you install here won't interfere with anything else on your machine.
- Open your terminal. Press Cmd+Space (macOS) or the Windows key (Windows) to open your search bar.
- Type Terminal (macOS) or PowerShell (Windows) and press Enter to open it.
- Move to your Desktop by running this command:
cd ~/Desktop
- Create your project folder and move into it by running these commands:
mkdir flight-tracker
cd flight-tracker
Your terminal prompt now shows flight-tracker as the current directory.
From here use the following command to open up Vs Code. code .
- Create a Python virtual environment by running:
python3 -m venv venv
- Activate the virtual environment:
macOS
source venv/bin/activate
Windows
.\venv\Scripts\activate
You should see (venv) appear at the start of your terminal prompt. This confirms the virtual environment is active.
Don't see (venv) in your prompt?
Make sure you ran the activate command for your operating system. On Windows, use PowerShell (not Command Prompt).
help me activate my Python virtual environment
- Install Flask and requests by running:
pip install Flask==3.1.3 requests==2.34.2
Or use the following if receiveing "Could not find a version" errors
pip install Flask==3.1.3 requests==2.32.5
Pip prints a list of packages as it installs them. You should see a success message at the end.
What are these two packages?
Flask is a lightweight web framework that lets you serve HTML pages and handle browser requests with just a few lines of Python.
requests is a library for making HTTP calls to external APIs. You'll use it to talk to the Ignav flight pricing API.
- Before you run the next command, what version of Flask do you expect to see?
- Confirm Flask installed correctly by running:
python -m flask --version
You should see Flask 3.1.3 printed in the output.
Seeing "command not found" or a different version?
Make sure your virtual environment is still active. You should see (venv) in your prompt. If not, re-run the activate command from above.
help me fix my Flask installation
Get your free Ignav API key
Ignav is a flight pricing API that returns real, bookable fares from airlines. Their free tier includes 1,000 requests with no credit card required.
- Open your browser and go to https://ignav.com.
- Click Sign Up and create a free account.
- After signing in, copy your API key from the dashboard.
Where is my API key?
After you sign up and log in, your API key is displayed on the Ignav dashboard. It's a string of characters you'll use to authenticate your requests.
- Back in your terminal, create a .env file in your project folder by running:
touch .env
- Open VS Code in your project folder by running:
code .
code command not found?
If code . doesn't work, open VS Code manually. Press Cmd+Space (macOS) or the Windows key (Windows), type VS Code, and press Enter. Then click File, select Open Folder, and choose the flight-tracker folder on your Desktop.
help me open VS Code from the terminal
- In VS Code, click on the .env file in the Explorer sidebar to open it.
- Paste the following line into the file, replacing your-api-key-here with the API key you copied from Ignav:
IGNAV_API_KEY=your-api-key-here
- Save the file.
Why a .env file?
Storing your API key in a .env file keeps it separate from your code. This makes it easy to avoid accidentally sharing your key if you ever push your code to GitHub.
Create the project file structure
Your flight tracker needs two more files: a Python file for the application logic and an HTML file for the web page.
- Create the remaining project files and folders by running these commands in your terminal:
touch app.py
mkdir templates
touch templates/index.html
- Confirm your files exist by running:
ls
You should see app.py, .env, templates, and venv in the output.
- Also confirm the HTML file inside templates by running:
ls templates
You should see index.html listed.
What are these files for?
- app.py is where all your Python application logic will live (API calls, database, web routes).
- templates/index.html is the HTML page Flask will serve to your browser. Flask looks for templates in a folder named templates by default.
🙋♀️ Missing a file?
Check that you ran each command from inside the flight-tracker folder. Run pwd to confirm your current directory ends with flight-tracker.
help me find where my files were created
Your environment is ready. Next up, you'll write the code that actually fetches real flight prices from the Ignav API.
Fetch Live Flight Prices
Your environment is ready. Now it's time to prove the core value of this project: real, live flight prices from actual airlines.
Before building a dashboard or storing any data, you need to confirm that you can call the Ignav API and get back genuine, bookable fare data. In this step, you'll write the function that fetches prices and see real airline names and dollar amounts printed in your terminal.
In this step, get ready to:
- Write a function that calls the Ignav flight pricing API.
- Run the script and see real airline prices printed in the terminal.
Write the API call function
The Ignav API is a REST API that returns flight prices as structured JSON. You send it an origin airport, a destination airport, and a date. It returns a list of itineraries with prices, airline names, and flight durations.
- Open app.py in VS Code and add the following imports and configuration:
import os
import sqlite3
from datetime import datetime
import requests
from flask import Flask, render_template, request, redirect, url_for, jsonify
app = Flask(__name__)
# Read API key from environment, fall back to placeholder
IGNAV_API_KEY = os.environ.get("IGNAV_API_KEY", "your-api-key-here")
DATABASE = "flights.db"
def get_db():
# Connect to SQLite and enable dict-like row access
conn = sqlite3.connect(DATABASE)
conn.row_factory = sqlite3.Row
return conn
What does this code do?
- os.environ.get("IGNAV_API_KEY", "your-api-key-here") reads your API key from the terminal environment. If the key is not set, it falls back to the placeholder string.
- app = Flask(__name__) creates a Flask web application instance. You'll use this in later steps to serve your dashboard.
- get_db() is a helper that connects to a local SQLite database. You'll use this in the next step to store price history.
- Save app.py.
- Below get_db(), add the fetch_prices function that calls the Ignav API:
def fetch_prices(origin, destination, departure_date):
# Ignav one-way fares endpoint
url = "https://ignav.com/api/fares/one-way"
headers = {
"X-Api-Key": IGNAV_API_KEY,
"Content-Type": "application/json",
}
# The API expects origin, destination, and date in the JSON body
payload = {
"origin": origin,
"destination": destination,
"departure_date": departure_date,
}
response = requests.post(url, json=payload, headers=headers)
response.raise_for_status()
return response.json()
What does this code do?
- requests.post() sends an HTTP POST request to the Ignav API with your API key in the X-Api-Key header.
- The json=payload argument automatically converts the Python dictionary into a JSON request body.
- raise_for_status() throws an error if the API returns a failure code (like 401 for a bad API key). This way you'll immediately know if something went wrong.
- Save app.py.
Parse and display the response
The Ignav API returns an itineraries array where each item contains a price object and an outbound object with the airline name and duration. You need a quick test script to call your function and print the results.
First, load your API key into your terminal session so the script can read it from the environment.
- In your terminal, load your API key by running this command (replace your-api-key-here with the actual key from your .env file):
export IGNAV_API_KEY=your-api-key-here
Find your key quickly
Open .env in VS Code and copy the value after the = sign. Paste it in place of your-api-key-here in the export command above.
- Now add a test block at the bottom of app.py that calls fetch_prices and prints the results:
if __name__ == "__main__":
# Test: fetch prices for SFO to JFK and print each result
data = fetch_prices("SFO", "JFK", "2025-09-01")
for itinerary in data.get("itineraries", []):
carrier = itinerary["outbound"]["carrier"]
price = itinerary["price"]["amount"]
print(f"{carrier} SFO to JFK: ${price}")
What does this code do?
- if __name__ == "__main__": means this code only runs when you execute the file directly with python app.py. It won't run when Flask imports the file later.
- data.get("itineraries", []) safely retrieves the list of flight options. If no itineraries exist, it defaults to an empty list instead of crashing.
- For each itinerary, you pull out the airline name from outbound.carrier and the fare from price.amount.
- Save app.py.
Before you run this, what do you expect to see? A list of airline names with dollar amounts, or an error?
- Run the script by executing this command:
python app.py
You should see real airline names and prices printed in your terminal, something like:
American Airlines SFO to JFK: $299
United Airlines SFO to JFK: $315
Delta Air Lines SFO to JFK: $342
Seeing a 401 or connection error?
- Make sure you ran the export IGNAV_API_KEY=... command with your actual API key, not the placeholder text.
- Check that your API key matches what's shown in your Ignav dashboard at https://ignav.com.
- If you see a connection error, confirm your internet connection is working.
Help me debug my Ignav API connection
✔️ Awesome, I've got everything!
Great. Double check you've saved app.py and that real prices are printing in your terminal.
ⓧ I'd like to double check the full code
Here is the complete app.py so far. Compare yours against it:
import os
import sqlite3
from datetime import datetime
import requests
from flask import Flask, render_template, request, redirect, url_for, jsonify
app = Flask(__name__)
IGNAV_API_KEY = os.environ.get("IGNAV_API_KEY", "your-api-key-here")
DATABASE = "flights.db"
def get_db():
conn = sqlite3.connect(DATABASE)
conn.row_factory = sqlite3.Row
return conn
def fetch_prices(origin, destination, departure_date):
url = "https://ignav.com/api/fares/one-way"
headers = {
"X-Api-Key": IGNAV_API_KEY,
"Content-Type": "application/json",
}
payload = {
"origin": origin,
"destination": destination,
"departure_date": departure_date,
}
response = requests.post(url, json=payload, headers=headers)
response.raise_for_status()
return response.json()
if __name__ == "__main__":
data = fetch_prices("SFO", "JFK", "2025-09-01")
for itinerary in data.get("itineraries", []):
carrier = itinerary["outbound"]["carrier"]
price = itinerary["price"]["amount"]
print(f"{carrier} SFO to JFK: ${price}")
You just proved the most important thing: your app can reach real airline pricing data. Next up, you'll store these prices in a database so you can track how they change over time.
Store Prices in a Database
In the last step, you confirmed that real airline prices flow from the Ignav API into your terminal. That is great for a single check, but the moment you close the terminal, all that data vanishes.
The real power of a price tracker is watching prices change over time. In this step, you will add a SQLite database so every price check is saved permanently. The more you run the tracker, the more history you accumulate.
In this step, get ready to:
- Create a database schema to store price check results.
- Write a function that saves fetched prices into the database with a timestamp.
- Run the script twice and confirm your price history is accumulating.
Create the database schema
Before you can save any prices, you need a table structure in your database. SQLite stores everything in a single file, so there is no server to install or configure.
- In app.py, add the following init_db() function directly below the existing get_db() function:
def init_db():
# Connect to the database and create tables if they don't exist
conn = get_db()
conn.execute("""
CREATE TABLE IF NOT EXISTS tracked_routes (
id INTEGER PRIMARY KEY AUTOINCREMENT,
origin TEXT NOT NULL,
destination TEXT NOT NULL,
departure_date TEXT NOT NULL,
UNIQUE(origin, destination, departure_date)
)
""")
conn.execute("""
CREATE TABLE IF NOT EXISTS price_checks (
id INTEGER PRIMARY KEY AUTOINCREMENT,
origin TEXT NOT NULL,
destination TEXT NOT NULL,
departure_date TEXT NOT NULL,
carrier TEXT NOT NULL,
price REAL NOT NULL,
currency TEXT NOT NULL,
duration_minutes INTEGER,
checked_at TEXT NOT NULL
)
""")
conn.commit()
conn.close()
What does this code do?
This function sets up two tables in your SQLite database.
- tracked_routes will store which flight routes you want to monitor. You will use this in the next step when you build the web dashboard.
- price_checks stores every price result: route details, airline, price, duration, and a timestamp of when you checked.
- CREATE TABLE IF NOT EXISTS means it is safe to call this function every time the app starts. It only creates the tables the first time.
Now you need to call init_db() when the script runs so the database file gets created.
- Scroll down to the if __name__ == "__main__": block at the bottom of app.py.
- Find this line:
if __name__ == "__main__":
data = fetch_prices("SFO", "JFK", "2025-09-01")
- Add init_db() as the first line inside the block, so it now looks like this:
if __name__ == "__main__":
init_db()
data = fetch_prices("SFO", "JFK", "2025-09-01")
- Save app.py.
- Run the script to create the database file:
python app.py
- You should see flight prices printed just like before.
- Confirm the database file was created by running:
ls
You should see flights.db listed alongside app.py and your other project files.
Don't see flights.db?
- Make sure init_db() is indented inside the if __name__ == "__main__": block. It needs to be at the same indentation level as the data = fetch_prices(...) line.
- Check that init_db() is defined below get_db() in the file. Python reads top to bottom, so functions must be defined before they are called.
- help me debug why flights.db is not being created
Save each price check to the database
Now that the database exists, you need a function that takes the API response and inserts each flight result as a row. Each row includes a timestamp so you can track when each price was recorded.
- In app.py, add the following save_prices() function below the fetch_prices() function:
def save_prices(origin, destination, departure_date, itineraries):
# Open database connection and record the current time
conn = get_db()
checked_at = datetime.now().isoformat()
for itinerary in itineraries:
# Extract price, airline, and duration from each result
price = itinerary["price"]["amount"]
currency = itinerary["price"]["currency"]
carrier = itinerary["outbound"]["carrier"]
duration = itinerary["outbound"]["duration_minutes"]
conn.execute(
"""INSERT INTO price_checks
(origin, destination, departure_date, carrier, price, currency, duration_minutes, checked_at)
VALUES (?, ?, ?, ?, ?, ?, ?, ?)""",
(origin, destination, departure_date, carrier, price, currency, duration, checked_at),
)
conn.commit()
conn.close()
What does this code do?
- datetime.now().isoformat() captures the exact moment you checked prices, stored as a readable timestamp like 2025-09-01T14:30:22.
- The for loop walks through every flight itinerary the API returned and pulls out the fields you care about.
- The ? placeholders in the SQL query protect against injection attacks. SQLite fills them in safely with your actual values.
Now update the __main__ block to call save_prices() after fetching data.
- Find the current if __name__ == "__main__": block at the bottom of app.py:
if __name__ == "__main__":
init_db()
data = fetch_prices("SFO", "JFK", "2025-09-01")
for itinerary in data.get("itineraries", []):
carrier = itinerary["outbound"]["carrier"]
price = itinerary["price"]["amount"]
print(f"{carrier} SFO to JFK: ${price}")
- Replace the entire if __name__ block with this updated version:
if __name__ == "__main__":
init_db()
data = fetch_prices("SFO", "JFK", "2025-09-01")
itineraries = data.get("itineraries", [])
save_prices("SFO", "JFK", "2025-09-01", itineraries)
for itinerary in itineraries:
carrier = itinerary["outbound"]["carrier"]
price = itinerary["price"]["amount"]
print(f"{carrier} SFO to JFK: ${price}")
What changed?
- itineraries = data.get("itineraries", []) stores the list in a variable so you can pass it to both save_prices() and the print loop.
- save_prices("SFO", "JFK", "2025-09-01", itineraries) writes every returned flight into your database before printing.
- Save app.py.
Before you run the script, think about this: if you run it twice, how many rows do you expect in the database?
- Run the script twice to accumulate price history:
python app.py
python app.py
You should see flight prices printed both times. Now verify the data was actually saved.
- Check how many rows are stored in the database by running:
python -c "import sqlite3; conn = sqlite3.connect('flights.db'); print(conn.execute('SELECT COUNT(*) FROM price_checks').fetchone())"
You should see a tuple like (6,) or higher. The exact number depends on how many itineraries the API returned per call, but it must be 2 or more.
Why is the number higher than 2?
Each API call returns multiple itineraries (one per airline offering that route). If the API returned 3 airlines and you ran the script twice, you have 6 rows. Every run adds a new batch of rows with a fresh timestamp.
🙋♀️ Seeing (0,) or an error?
- If you see (0,), check that save_prices() is being called in your __main__ block. The function must appear after fetch_prices() and before the print loop.
- If you get no such table: price_checks, delete flights.db and run again. The old file may have been created without the table.
- help me figure out why my price data is not saving to the database
✔️ Awesome, I've got everything!
Great. Double check you have saved app.py and confirmed rows exist in your database.
ⓧ I'd like to double check the full code
Your app.py should contain these sections in order. Scroll up and confirm each one is present:
- Imports (os, sqlite3, datetime, requests, Flask imports).
- App setup (app = Flask(__name__), IGNAV_API_KEY, DATABASE).
- get_db() function connecting to flights.db with row_factory.
- init_db() function creating tracked_routes and price_checks tables.
- fetch_prices() function sending a POST request to the Ignav API.
- save_prices() function inserting itineraries into price_checks.
- __main__ block calling init_db(), fetch_prices(), save_prices(), then printing results.
Your price tracker now has a memory. Every time you run the script, it adds a new snapshot to the database. Next up, you will build a web dashboard so you can see all this data in a browser instead of scrolling through terminal output.
Build the Web Dashboard
Terminal output is fine for testing, but you want to see your tracked routes and prices at a glance in a browser.
In this step you will convert your Python script into a Flask web application that serves an HTML dashboard. You will add routes that query your database and render results in a clean page you can refresh any time.
In this step, get ready to:
- Create Flask routes that query your database and serve an HTML page.
- Build an HTML template that displays tracked routes with airline, price, and duration.
- Add a form so you can track new routes directly from the browser.
Create Flask routes
Your script currently runs once and prints to the terminal. To serve a web page instead, you need Flask route functions that handle browser requests and return HTML.
- In app.py, find the entire if __name__ == "__main__": block at the bottom of the file (everything from that line to the end).
- Select and delete that entire block. You will replace it with new code.
Here is the old code you are removing:
if __name__ == "__main__":
init_db()
data = fetch_prices("SFO", "JFK", "2025-09-01")
itineraries = data.get("itineraries", [])
save_prices("SFO", "JFK", "2025-09-01", itineraries)
for itinerary in itineraries:
carrier = itinerary["outbound"]["carrier"]
price = itinerary["price"]["amount"]
print(f"{carrier} SFO to JFK: ${price}")
- In its place, paste the following code that adds your homepage route and starts Flask as a web server:
@app.route("/")
def index():
# Query all tracked routes from the database
conn = get_db()
routes = conn.execute("SELECT * FROM tracked_routes").fetchall()
# Build a dictionary of latest prices for each route
latest_prices = {}
for route in routes:
key = f"{route['origin']}-{route['destination']}-{route['departure_date']}"
prices = conn.execute(
"""SELECT * FROM price_checks
WHERE origin = ? AND destination = ? AND departure_date = ?
ORDER BY checked_at DESC LIMIT 5""",
(route["origin"], route["destination"], route["departure_date"]),
).fetchall()
latest_prices[key] = prices
conn.close()
return render_template("index.html", routes=routes, latest_prices=latest_prices)
if __name__ == "__main__":
init_db()
app.run(debug=True)
What does this code do?
- @app.route("/") tells Flask to call index() whenever someone visits the root URL in their browser.
- The function queries tracked_routes for every route you are monitoring, then fetches the 5 most recent price checks for each one.
- render_template("index.html", ...) sends the data to an HTML template that Flask will render and return to the browser.
- app.run(debug=True) starts the Flask development server with auto-reload so changes appear instantly.
- Save app.py and start the server by running:
python app.py
You should see output including * Running on http://127.0.0.1:5000 and * Debug mode: on. Flask is now serving your app, but visiting the URL will show an error because the HTML template does not exist yet.
- Press Ctrl+C in the terminal to stop the server.
Server won't start?
- If you see OSError: Address already in use, another process is using port 5000. On macOS, disable AirPlay Receiver in System Settings or run on a different port.
- If you see a SyntaxError, check that you deleted the entire old if __name__ block and pasted the new code with correct indentation.
help me fix my Flask server startup error
- Now add the price-check route. In app.py, place the following function between index() and if __name__:
@app.route("/check")
def check_prices():
# Fetch fresh prices for every tracked route
conn = get_db()
routes = conn.execute("SELECT * FROM tracked_routes").fetchall()
conn.close()
for route in routes:
try:
data = fetch_prices(route["origin"], route["destination"], route["departure_date"])
itineraries = data.get("itineraries", [])
save_prices(route["origin"], route["destination"], route["departure_date"], itineraries)
except Exception as e:
print(f"Error fetching {route['origin']}->{route['destination']}: {e}")
return redirect(url_for("index"))
What does check_prices do?
When a user visits /check, this function loops through every tracked route, calls the Ignav API for fresh prices, saves them to the database, and then redirects back to the homepage so the user sees updated data immediately.
- Save app.py.
Build the HTML template
Flask looks for templates in a templates/ folder. You already created templates/index.html in Step 1. Now you will fill it with the page structure and styles.
- Open templates/index.html in VS Code and paste the page skeleton with styles:
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Flight Price Tracker</title>
<style>
body { font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif; max-width: 900px; margin: 0 auto; padding: 20px; background: #f5f5f5; }
h1 { color: #333; }
.card { background: white; border-radius: 8px; padding: 20px; margin-bottom: 20px; box-shadow: 0 2px 4px rgba(0,0,0,0.1); }
table { width: 100%; border-collapse: collapse; margin-top: 10px; }
th, td { padding: 10px; text-align: left; border-bottom: 1px solid #eee; }
th { background: #f8f9fa; font-weight: 600; }
.btn { display: inline-block; padding: 10px 20px; background: #2563eb; color: white; border: none; border-radius: 6px; cursor: pointer; text-decoration: none; font-size: 14px; }
.btn:hover { background: #1d4ed8; }
.form-row { display: flex; gap: 10px; align-items: end; flex-wrap: wrap; }
.form-group { display: flex; flex-direction: column; gap: 4px; }
.form-group label { font-size: 12px; font-weight: 600; color: #555; }
.form-group input { padding: 8px 12px; border: 1px solid #ddd; border-radius: 4px; font-size: 14px; }
.price { font-weight: bold; color: #16a34a; font-size: 1.1em; }
</style>
</head>
<body>
<h1>Flight Price Tracker</h1>
</body>
</html>
What does this set up?
This creates the outer HTML shell with CSS classes you will use throughout the dashboard. The .card class gives content sections a white rounded container. The .btn class styles links and buttons as blue action buttons. The .price class highlights dollar amounts in green.
- Now add the routes display card. In templates/index.html, place the following code between the <h1> line and the </body> tag:
<div class="card">
<div style="display: flex; justify-content: space-between; align-items: center;">
<h2>Your Tracked Routes</h2>
<a href="/check" class="btn">Check All Prices Now</a>
</div>
{% if routes %}
{% for route in routes %}
{% set key = route['origin'] ~ '-' ~ route['destination'] ~ '-' ~ route['departure_date'] %}
<h3>{{ route['origin'] }} to {{ route['destination'] }} on {{ route['departure_date'] }}</h3>
{% if latest_prices.get(key) %}
<table>
<thead><tr><th>Airline</th><th>Price</th><th>Duration</th><th>Checked At</th></tr></thead>
<tbody>
{% for price in latest_prices[key] %}
<tr>
<td>{{ price['carrier'] }}</td>
<td class="price">${{ price['price'] }} {{ price['currency'] }}</td>
<td>{{ (price['duration_minutes'] // 60) }}h {{ (price['duration_minutes'] % 60) }}m</td>
<td>{{ price['checked_at'][:16] }}</td>
</tr>
{% endfor %}
</tbody>
</table>
{% else %}<p>No price data yet. Click "Check All Prices Now" to fetch prices.</p>
{% endif %}
{% endfor %}
{% else %}
<p>No routes tracked yet. Add one above to get started.</p>
{% endif %}
</div>
What does this template do?
- {% if routes %} checks whether you have any tracked routes. If not, it shows a helpful message.
- The {% for route in routes %} loop creates a section for each tracked route with its own price table.
- Each table row displays the airline name, price in green, flight duration converted to hours and minutes, and when the price was last checked.
- The Check All Prices Now button links to your /check route, triggering a fresh API call for all routes.
Before you run this, what do you expect to see on the page given that your tracked_routes table is currently empty?
- Save templates/index.html and start the Flask server by running:
python app.py
- Open http://127.0.0.1:5000 in your browser.
You should see the heading "Flight Price Tracker", a blue Check All Prices Now button, and the message "No routes tracked yet. Add one above to get started."
Seeing a TemplateNotFound error?
- Make sure your file is at templates/index.html (not template/index.html or directly in the project root). Flask requires the folder to be named templates exactly.
- Confirm the file is saved. An unsaved file in VS Code shows a dot on the tab.
help me fix a Flask TemplateNotFound error
Add route configuration
The dashboard works, but you have no way to add routes from the browser yet. You need a form that lets you type in an origin, destination, and date, and a Flask route that handles the form submission.
- Stop the Flask server with Ctrl+C.
- In app.py, add this function between index() and check_prices():
@app.route("/add-route", methods=["POST"])
def add_route():
# Read form data and normalize airport codes to uppercase
origin = request.form["origin"].upper().strip()
destination = request.form["destination"].upper().strip()
departure_date = request.form["departure_date"].strip()
# Insert the route if it doesn't already exist
conn = get_db()
conn.execute(
"INSERT OR IGNORE INTO tracked_routes (origin, destination, departure_date) VALUES (?, ?, ?)",
(origin, destination, departure_date),
)
conn.commit()
conn.close()
return redirect(url_for("index"))
What does add_route do?
- methods=["POST"] means this route only accepts form submissions, not regular page visits.
- .upper().strip() normalizes airport codes so "sfo" and "SFO" are treated the same.
- INSERT OR IGNORE prevents duplicate routes thanks to the UNIQUE constraint you defined in init_db().
- Now add the form to your HTML. In templates/index.html, paste this card between the <h1> line and the routes card <div class="card"> you added earlier:
<div class="card">
<h2>Track a New Route</h2>
<form action="/add-route" method="POST">
<div class="form-row">
<div class="form-group">
<label for="origin">From (IATA code)</label>
<input type="text" id="origin" name="origin" placeholder="SFO" maxlength="3" required>
</div>
<div class="form-group">
<label for="destination">To (IATA code)</label>
<input type="text" id="destination" name="destination" placeholder="JFK" maxlength="3" required>
</div>
<div class="form-group">
<label for="departure_date">Departure Date</label>
<input type="date" id="departure_date" name="departure_date" required>
</div>
<button type="submit" class="btn">Add Route</button>
</div>
</form>
</div>
What does this form do?
The form sends a POST request to /add-route with the three fields your Flask function expects. The maxlength="3" attribute limits input to 3-letter IATA airport codes. The date picker ensures a valid format.
Think about what will happen when you add a route and then click Check All Prices Now. What do you expect to see?
- Save both app.py and templates/index.html.
- Start the server again by running:
python app.py
- Open http://127.0.0.1:5000 in your browser.
- In the Track a New Route form, enter SFO as the origin, JFK as the destination, and pick any future date.
- Click Add Route.
- Click Check All Prices Now.
You should see the page reload with a table showing real airline names, prices in green, flight durations, and timestamps. Your flight price dashboard is live.
Prices not showing after clicking Check All Prices Now?
- Check your terminal for error messages. If you see a 401 or 403 error, verify that your .env file has the correct Ignav API key and that your app reads it with os.environ.get("IGNAV_API_KEY", ...).
- If you see "No price data yet" under your route, the API call may have returned an empty itineraries list. Try a popular route like SFO to JFK with a date at least 7 days in the future.
- Make sure you added the route first (it appears under the heading) before clicking Check All Prices Now.
help me debug why my flight prices aren't showing up
✔️ Awesome, I've got everything!
Great. Double check you have saved both app.py and templates/index.html.
ⓧ I'd like to double check the full code
Scroll up and confirm your app.py contains each of these pieces in order:
- Imports and app = Flask(__name__) at the top.
- IGNAV_API_KEY and DATABASE constants.
- get_db(), init_db(), fetch_prices(), save_prices() helper functions.
- index() route at /.
- add_route() route at /add-route.
- check_prices() route at /check.
- if __name__ == "__main__": block calling init_db() then app.run(debug=True).
For templates/index.html, confirm it contains in order:
- The <head> section with all CSS styles.
- The Track a New Route form card.
- The Your Tracked Routes card with the Check All Prices Now button and the Jinja table loop.
Your dashboard is serving real flight prices from the browser. Next up, you will add a price history chart so you can visualize how fares move over time.
Add a Price History Chart
Your dashboard now shows live flight prices in a clean table. The data is there, but scanning rows of numbers to spot a price trend takes effort.
A visual chart makes price movements immediately obvious. In this step, you'll add an interactive line chart below each route's table using Chart.js, so you can see at a glance whether fares are rising or falling over time.
In this step, get ready to:
- Create a JSON API endpoint that serves price history data for a given route.
- Add an interactive Chart.js line chart to the dashboard showing price trends over time.
Create an API endpoint for chart data
The chart in your browser needs a source of data. You'll create a Flask route that returns JSON containing every historical price point for a given route, grouped by check time.
- In app.py, find the check_prices() function and scroll to the bottom of it (just above the if __name__ == "__main__": block).
- Add the following new route below the check_prices() function:
@app.route("/api/history/<origin>/<destination>/<departure_date>")
def price_history(origin, destination, departure_date):
conn = get_db()
rows = conn.execute(
"""SELECT checked_at, MIN(price) as price FROM price_checks
WHERE origin = ? AND destination = ? AND departure_date = ?
GROUP BY checked_at ORDER BY checked_at""",
(origin, destination, departure_date),
).fetchall()
conn.close()
history = [{"checked_at": row["checked_at"], "price": row["price"]} for row in rows]
return jsonify(history)
What does this code do?
This route takes three URL parameters (origin, destination, and departure date) and queries your database for all price checks matching that route.
- MIN(price) picks the cheapest fare from each check session, since multiple airlines may have been returned.
- GROUP BY checked_at collapses each check session into one data point for the chart.
- The result is a JSON array of {checked_at, price} objects that Chart.js can plot directly.
- Save app.py.
- Flask's debug mode auto-reloads the server when you save, so the new route is already live.
- Open your browser and visit a URL matching one of your tracked routes, for example:
http://127.0.0.1:5000/api/history/SFO/JFK/2026-09-01
Replace the airport codes and date with a route you actually tracked. You should see a JSON array of price data points in your browser.
Seeing a 404 or empty array?
- Make sure the origin, destination, and date in the URL exactly match a route you have already checked prices for (case matters for the airport codes in the URL).
- Confirm you saved app.py and that your terminal shows Flask reloading.
- If you see [] (empty array), you need to click Check All Prices Now on your dashboard first to populate data.
Why is my price history endpoint returning no data?
Add Chart.js to the template
Now that the API serves chart data, you need the front-end to fetch it and draw a line chart. Chart.js handles the rendering with just a few lines of JavaScript.
- In templates/index.html, find the <title>Flight Price Tracker</title> line in the <head> section.
- Add this script tag on the line directly below it:
<script src="https://cdn.jsdelivr.net/npm/chart.js"></script>
- Next, find the closing .price { font-weight: bold; color: #16a34a; font-size: 1.1em; } rule at the end of your <style> block.
- Add this CSS rule after it (before the closing </style> tag):
canvas { margin-top: 20px; }
- Now scroll down to the route loop. Find the closing </table> tag inside the {% if latest_prices.get(key) %} block.
- Add the following canvas and script block directly below </table>:
<canvas id="chart-{{ loop.index }}" height="150"></canvas>
<script>
(async function() {
const res = await fetch('/api/history/{{ route["origin"] }}/{{ route["destination"] }}/{{ route["departure_date"] }}');
const data = await res.json();
if (data.length > 0) {
new Chart(document.getElementById('chart-{{ loop.index }}'), {
type: 'line',
data: {
labels: data.map(d => d.checked_at.slice(0, 16)),
datasets: [{
label: 'Lowest Price ($)',
data: data.map(d => d.price),
borderColor: '#2563eb',
backgroundColor: 'rgba(37, 99, 235, 0.1)',
fill: true,
tension: 0.3,
}]
},
options: {
responsive: true,
scales: {
y: { beginAtZero: false },
x: { display: true, title: { display: true, text: 'Time' } }
}
}
});
}
})();
</script>
What does this code do?
- The <canvas> element gives Chart.js a drawing area. Each route gets its own canvas with a unique ID using Jinja's loop.index.
- The immediately-invoked async function fetches JSON from your /api/history/ endpoint for that specific route.
- Chart.js renders a line chart with timestamps on the x-axis and the lowest fare on the y-axis. The fill: true option shades the area under the line so trends are easier to read.
- Save templates/index.html.
Before you refresh, what do you expect to see below each route's price table?
- Refresh http://127.0.0.1:5000 in your browser.
You should see a line chart below each route's table showing price points over time, with dates on the x-axis and price on the y-axis. The line is blue with a shaded area underneath.
Don't see a chart?
- If the table shows but no chart appears, open your browser's DevTools console (Cmd+Option+J on macOS or Ctrl+Shift+J on Windows) and look for JavaScript errors. A typo in the fetch URL or a missing quote will show here.
- Make sure you placed the <script src="https://cdn.jsdelivr.net/npm/chart.js"></script> tag in the <head> section, not inside the body.
- If you see "No price data yet" instead of a table, you need at least one price check. Click Check All Prices Now to fetch data first.
Help me debug why my Chart.js chart isn't rendering.
✔️ Awesome, I've got everything!
Great. Double check you have saved both app.py and templates/index.html.
ⓧ I'd like to double check the full code
Your app.py should contain, in order:
- Imports (os, sqlite3, datetime, requests, flask).
- App setup (app = Flask(__name__), constants).
- Helper functions: get_db(), init_db(), fetch_prices(), save_prices().
- Routes: index() at /, add_route() at /add-route, check_prices() at /check, price_history() at /api/history/<origin>/<destination>/<departure_date>.
- The if __name__ == "__main__": block at the bottom.
Your templates/index.html should contain:
- The Chart.js CDN <script> tag in the <head>.
- The canvas { margin-top: 20px; } CSS rule.
- A <canvas> element and <script> block after the </table> tag inside the route loop.
Scroll up and confirm each chunk is present in order.
Your flight tracker now shows price trends visually. You can check prices multiple times and watch the chart grow with each data point. Next up, you'll take this further in the Secret Mission by adding price-drop alerts that highlight routes when fares fall below your target budget.
Secret mission
Add Price-Drop Alerts
Tracking prices is only half the battle. What if your dashboard could tell you the moment a fare drops below your budget? In this mission, you'll add a target price to each route and highlight it in green when the current price beats your goal.
Clean Up Your Resources
Clean Up Your Resources
This project runs entirely locally, so there are no ongoing costs. Decide whether to keep your resources running, pause them to come back later, or delete them entirely.
Resources you used:
- The flight-tracker project folder on your Desktop (includes your virtual environment, application code, and templates).
- The flights.db SQLite database file (inside your project folder, stores all tracked routes and price history).
- Your Ignav API key (free tier with 1,000 requests, no credit card attached, no recurring charges).
Keep everything running
No action needed. Choose this if you are still actively tracking flight prices and want your history to keep growing.
- Your Flask app runs locally with zero cost.
- Your flights.db database grows each time you check prices, building a richer history for your chart.
- Your Ignav API key remains active with no recurring charges.
Pause - I'll come back to this later
Shut down the Flask server to free up your terminal. Your database and code persist and pick up where you left off next time.
- Switch to the terminal where Flask is running and press Ctrl+C to stop the server.
- Your flights.db file keeps all your price history intact.
- To resume later, activate your virtual environment and run python app.py again.
Delete - I don't want to use this again
Remove all project files and start fresh. This deletes your price history, application code, and virtual environment.
- Switch to the terminal where Flask is running and press Ctrl+C to stop the server.
- Deactivate your virtual environment by running:
deactivate
- Delete the entire project folder by running:
cd ~/Desktop
rm -rf flight-tracker
On Windows, use this command instead:
cd ~\Desktop
Remove-Item -Recurse -Force flight-tracker
- Your Ignav API key is free and has no recurring charges. No action is needed to avoid costs.
Nice Work!
Nice Work!
Nice work! You just built a personal flight price tracker that fetches real airline prices, stores them in a database, and displays everything on a clean web dashboard with interactive charts.
You've learned how to:
- Build a Flask web application that connects to the Ignav API to fetch live, bookable flight prices from real airlines.
- Store price snapshots in a SQLite database so your price history persists between sessions and grows over time.
- Create an HTML dashboard with a Chart.js line chart that makes price trends immediately visible so you can spot the best time to buy.
- Secret Mission: Implemented price-drop alerts that highlight routes where the current fare drops below your target budget.
Ready to quiz yourself?