Check a List of Cars for Safety Recalls in Python (and Flag the Do-Not-Drive Ones)
Paste in VINs, get every US and Canadian recall in a CSV, with the urgent ones called out. About 60 lines of Python.
Disclosure: I built the Vehicle Data API used in this tutorial. It has a free plan, and the script works with it.
If you sell, buy, insure or manage cars, sooner or later you need to answer one question about a whole list of them: which of these vehicles have safety recalls, and which ones shouldn't be driven at all?
The official data is public. NHTSA publishes US recalls and Transport Canada publishes Canadian ones. But checking it by hand means pasting VINs into a website one at a time, and the government APIs give you each country separately, in different formats.
This tutorial builds a small Python script that takes a list of VINs and:
- finds every US and Canadian recall for each vehicle,
- flags the urgent ones (do not drive and park outside),
- catches mistyped VINs and suggests the fix,
- writes everything to a CSV you can open in Excel or Google Sheets.
What you'll build
$ python recall_check.py vins.txt
1FTYR10U66PA41234 2006 FORD Ranger: 19 recalls, 4 urgent
!! DO NOT DRIVE 18V022000 (US) AIR BAGS:FRONTAL:PASSENGER SIDE:INFLATOR MODULE
!! DO NOT DRIVE 18V023000 (US) AIR BAGS:FRONTAL:DRIVER SIDE:INFLATOR MODULE
!! DO NOT DRIVE 17V787000 (US) AIR BAGS:FRONTAL:DRIVER SIDE:INFLATOR MODULE
!! DO NOT DRIVE 17V788000 (US) AIR BAGS:FRONTAL:PASSENGER SIDE:INFLATOR MODULE
KNDPB3ACXG7812345 2016 KIA Sportage: 6 recalls, 1 urgent
!! PARK OUTSIDE 22V051000 (US) SERVICE BRAKES, HYDRAULIC:ANTILOCK/TRACTION CONTROL/ELECTRONIC LIMITED SLIP:CONTROL UNIT/MODULE
1HGCM82633A004352 2003 HONDA Accord: 24 recalls, 0 urgent
1HGCM82633AOO4352 ERROR: VINs never contain O (often a typo for 1 or 0). Did you mean 1HGCM82633A004352?
49 recalls for 4 VINs written to /home/you/recalls.csv
That's real output from the script below. The 2006 Ranger's urgent recalls are for airbag inflators that can explode in a crash and throw sharp metal fragments, so Ford asks owners not to drive the truck until it's repaired. The last line is the same Honda VIN with two zeros typed as the letter O.
Where the data comes from
The Vehicle Data API keeps local copies of the official databases and refreshes recalls daily:
- NHTSA vPIC to decode the VIN into year, make and model,
- NHTSA recalls for the US,
- Transport Canada's Vehicle Recalls Database for Canada.
One call to /recalls?vin=...&country=us,ca decodes the VIN, matches both countries' recalls to the vehicle and returns them in one format. Every recall carries the flags this script needs:
| Field | Meaning |
|---|---|
urgent |
true when the recall says not to drive the vehicle or to park it outside |
doNotDrive |
the manufacturer or NHTSA says not to drive it until it's repaired |
parkOutside |
fire risk: park it outside and away from buildings until it's repaired |
remedyStatus |
whether a fix is available yet |
Urgent recalls are sorted to the top, and the response includes count and urgentCount, so a summary line needs no extra work.
Step 1: get a key
- Open the API on RapidAPI and subscribe to the free BASIC plan (200 requests a month).
- Copy your
X-RapidAPI-Keyand put it in an environment variable:
pip install requests
export RAPIDAPI_KEY="your-key-here"
Each VIN costs one request (two if it's mistyped, because the script then asks the API what's wrong with it).
Step 2: list your VINs
Make a text file with one VIN per line. These are the ones from the output above:
1FTYR10U66PA41234
KNDPB3ACXG7812345
1HGCM82633A004352
1HGCM82633AOO4352
If your VINs are in a spreadsheet, copy the column into vins.txt. Blank lines and lowercase letters are fine.
Step 3: the script
Save this as recall_check.py:
"""Check a list of VINs for safety recalls (US + Canada) and flag the urgent ones.
Usage: export RAPIDAPI_KEY=... then python recall_check.py vins.txt
vins.txt has one VIN per line. Writes recalls.csv next to it.
"""
import csv
import os
import sys
import requests
HOST = "vehicle-data-api-vin-decoder-recalls-safety-ratings.p.rapidapi.com"
BASE = f"https://{HOST}"
HEADERS = {"X-RapidAPI-Key": os.environ.get("RAPIDAPI_KEY", ""), "X-RapidAPI-Host": HOST}
def call(path, **params):
r = requests.get(BASE + path, params=params, headers=HEADERS, timeout=60)
body = r.json()
if r.status_code != 200:
raise ValueError(body.get("error") or body.get("message") or f"HTTP {r.status_code}")
return body["data"]
def recalls_for(vin):
"""Recalls for one VIN; if the VIN is mistyped, say what is wrong and suggest a fix."""
try:
return call("/recalls", vin=vin, country="us,ca")
except ValueError as e:
check = call(f"/vin/{vin}/validate")
if check["valid"]:
raise
hint = f" Did you mean {check['suggestions'][0]}?" if check["suggestions"] else ""
raise ValueError(" ".join(check["problems"]) + hint) from e
def flags(r):
out = []
if r["doNotDrive"]:
out.append("DO NOT DRIVE")
if r["parkOutside"]:
out.append("PARK OUTSIDE")
return ", ".join(out)
def main(path):
vins = [line.strip().upper() for line in open(path) if line.strip()]
rows = []
for vin in vins:
try:
data = recalls_for(vin)
except ValueError as e:
print(f"{vin} ERROR: {e}")
continue
v = data["vehicle"]
print(f"{vin} {v['year']} {v['make']} {v['model']}: {data['count']} recalls, {data['urgentCount']} urgent")
for r in data["recalls"]:
if r["urgent"]:
print(f" !! {flags(r)} {r['campaignNumber']} ({r['country'].upper()}) {r['component']}")
rows.append({"vin": vin, "year": v["year"], "make": v["make"], "model": v["model"],
"country": r["country"], "campaign": r["campaignNumber"], "date": r["reportDate"],
"component": r["component"], "urgent": r["urgent"], "flags": flags(r),
"remedy_status": r["remedyStatus"], "summary": (r["summary"] or "").strip()})
out = os.path.join(os.path.dirname(os.path.abspath(path)), "recalls.csv")
with open(out, "w", newline="", encoding="utf-8") as f:
writer = csv.DictWriter(f, fieldnames=["vin", "year", "make", "model", "country", "campaign", "date",
"component", "urgent", "flags", "remedy_status", "summary"])
writer.writeheader()
writer.writerows(rows)
print(f"\n{len(rows)} recalls for {len(vins)} VINs written to {out}")
if __name__ == "__main__":
main(sys.argv[1] if len(sys.argv) > 1 else "vins.txt")
Run it:
python recall_check.py vins.txt
How it works
One call per VIN. /recalls?vin=...&country=us,ca does the decoding and the matching. The response looks like this (trimmed):
{
"data": {
"vehicle": {"make": "FORD", "model": "Ranger", "year": 2006, "vin": "1FTYR10U66PA41234"},
"count": 19,
"urgentCount": 4,
"recalls": [
{
"country": "us",
"campaignNumber": "18V022000",
"component": "AIR BAGS:FRONTAL:PASSENGER SIDE:INFLATOR MODULE",
"reportDate": "2018-01-11",
"urgent": true,
"doNotDrive": true,
"parkOutside": false,
"remedyStatus": "available",
"summary": "Ford Motor Company (Ford) is recalling certain 2006 Ford Ranger trucks. ..."
}
]
}
}
Typos are explained, not just rejected. VINs are easy to mistype, and OCR from photos gets them wrong too: O for 0, I for 1, a wrong check digit. When /recalls can't decode a VIN, the script calls /vin/{vin}/validate, which says what's wrong in plain language and suggests a correction. That's how the last line of the output knows the VIN should be 1HGCM82633A004352.
The CSV keeps everything. Every recall becomes a row with its campaign number, date, component, flags, remedy status and summary. Sort by urgent in your spreadsheet and the vehicles that need attention first are at the top.
Ideas to take it further
- Run it on a schedule. Put it in cron or a GitHub Action and email yourself when
urgentCountgoes up for any vehicle. - Only urgent recalls: add
urgent_only="true"to the/recallscall to get just the do-not-drive and park-outside ones. - Whole fleets faster: on paid plans,
POST /vin/batchdecodes 50 VINs in one request,/report/{vin}adds complaint statistics, NCAP crash ratings and fuel economy, and/recalls/feed?since=YYYY-MM-DDreturns only the recalls published or changed since your last check.
Good to know
- Recalls are matched to the model and model year, not to one car's repair history. Whether a specific car has already been fixed is known only to the manufacturer, so confirm with a dealer or the manufacturer's recall lookup before acting.
- Canadian recalls include the French text too (
summaryFr), handy if you need both languages. - The API is also on Zyla and API.market if you already use one of those. Only the base URL and the key header change.
If you build something with it, or want a field that isn't there, leave a comment. I read them all.
Comments
Join the discussionNo comments yet. Be the first to comment.