Skip to main content

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.

Updated 7 min readMarkdown

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

  1. Open the API on RapidAPI and subscribe to the free BASIC plan (200 requests a month).
  2. Copy your X-RapidAPI-Key and 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 urgentCount goes up for any vehicle.
  • Only urgent recalls: add urgent_only="true" to the /recalls call to get just the do-not-drive and park-outside ones.
  • Whole fleets faster: on paid plans, POST /vin/batch decodes 50 VINs in one request, /report/{vin} adds complaint statistics, NCAP crash ratings and fuel economy, and /recalls/feed?since=YYYY-MM-DD returns 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.

No comments yet. Be the first to comment.