DEV Community

Cover image for Recall alerts for the stuff you own, by text message
APITube News API
APITube News API

Posted on

Recall alerts for the stuff you own, by text message

Over 31 days, a list of 26 brands from one home produced 38 text messages, 21 of them about a distinct product recall, for about $2 in Twilio fees. That is the whole result. The rest of this post is the 80-line script that did it and what the log looked like.

The recall tracker is a Python script that runs once a day, asks a news API for headlines with the word "recall" and any name from a text file, and sends each new headline to your phone through Twilio. No app, no account with a recall service, no six agency mailing lists. If you can run a Python file from a terminal, you can run this.

One honest note before the numbers. We wrote the script this month. Instead of waiting a month to publish, we replayed the same daily query over August 17 to September 16, 2026 with a --dry-run flag that prints texts instead of sending them. Every number below comes from that replay, and the CSV it wrote is next to this post.

Takeaways

  • 26 names on the list, 47 API hits in 31 days, 38 texts after dedup and a name check.
  • 21 distinct recalls, 10 repeat headlines about a recall already texted, 6 stories about a recall, 1 wrong match.
  • 17 of the 26 names never came up. Walmart alone was 17 of 38 texts.
  • 66 SMS segments cost $0.55 at Twilio plus carrier fees, plus $1.15 for the number.
  • A per-brand cooldown does not help: one day of cooldown removes 13 texts and 3 real recalls.

Why not just subscribe to recalls.gov

Because the official lists send everything. Recalls.gov forwards notices from six US agencies with no way to say "only Toyota and Whirlpool". The U.S. Consumer Product Safety Commission (CPSC) email covers CPSC products only, so a car recall from the National Highway Traffic Safety Administration (NHTSA) or a frozen berry recall from the Food and Drug Administration (FDA) never arrives. Consumer Reports has a car recall tracker that texts you, but it is vehicles by VIN and nothing else.

Source Covers Filter by brand Texts you API
cpsc.gov email / RSS CPSC products only No No Yes
recalls.gov email Six agencies No No No
Consumer Reports car tracker NHTSA vehicles By VIN Yes No
News API + this script Anything a newsroom reported Yes, your list Yes, via Twilio Yes

Unlike the recalls.gov email, which sends every recall from six agencies, the script sends only headlines that name something on your list, which means 38 texts a month for 26 names instead of hundreds.

Step 1. Write down what you own

One name per line in my_stuff.txt. We used a plausible home: appliances, two cars, kid gear, the stores we shop at, a few food brands.

Samsung
LG
Whirlpool
Bosch
KitchenAid
Instant Pot
Dyson
Philips
Toyota
Honda
Ford
Subaru
Fisher-Price
Graco
IKEA
Peloton
Walmart
Costco
Trader Joe's
Kroger
Target
Great Value
Kirkland
Nestle
Tyson
Blue Buffalo
Enter fullscreen mode Exit fullscreen mode

Store names matter more than you would guess. Food recalls are usually reported as "sold at Walmart, Target and Kroger", not by the packer's name.

Step 2. Find the filter that actually works

We used APITube, our News API Intelligence (disclosure at the bottom). Articles come back already tagged with entities, categories and events, which is why the obvious filters were the first thing we tried. They did not do what we hoped, so here is what each one returned, measured on the same day:

Filter What happened
event.type=recall Accepted silently, returns the unfiltered feed. First hit was a story about tariffs.
event.name=recall 400 ER0228: "entity event name 'recall' not found".
category.id=medtop:20000207 (IPTC "product recall") 18 of the latest 197 English articles had "recall" in the headline.
query=recall AND (Walmart OR Ford OR …) 47 hits in 31 days, 45 of them with a listed name in the headline.

So the working route is the boolean query parameter: a bare word searches headlines, and AND/OR combine them. One request covers the whole list.

curl -G "https://api.apitube.io/v1/news/everything" \
  -H "X-API-Key: YOUR_API_KEY" \
  --data-urlencode 'query=recall AND (Walmart OR Ford OR Toyota OR "Great Value")' \
  --data-urlencode "language.code=en" \
  --data-urlencode "published_at.start=2026-09-16" \
  --data-urlencode "sort.by=published_at" \
  --data-urlencode "sort.order=asc" \
  --data-urlencode "per_page=200"
Enter fullscreen mode Exit fullscreen mode

Each result is a large object. The script uses five fields of it:

{
  "id": 3083205852,
  "title": "Fuel Tank Issue Sparks Ford F-150 Recall",
  "href": "https://lite987.com/ixp/39/p/ford-recall-new-york-state-drivers/",
  "published_at": "2026-09-16T13:21:38.000Z",
  "source": { "domain": "lite987.com" }
}
Enter fullscreen mode Exit fullscreen mode

Names with a space, apostrophe or hyphen go in quotes ("Great Value", "Trader Joe's", "Fisher-Price"). The script does that for you.

Step 3. The script

Save this as recall_alerts.py next to my_stuff.txt. It needs requests and twilio (pip install requests twilio).

import csv, os, re, sys, pathlib, datetime as dt
import requests

API = "https://api.apitube.io/v1/news/everything"
HERE = pathlib.Path(__file__).parent
NAMES = [n.strip() for n in (HERE / "my_stuff.txt").read_text().splitlines() if n.strip()]
LOG = HERE / "alerts.csv"
DRY = "--dry-run" in sys.argv


def arg(flag, default=None):
    return sys.argv[sys.argv.index(flag) + 1] if flag in sys.argv else default


def term(name):
    return f'"{name}"' if re.search(r"[\s'\-]", name) else name


QUERY = "recall AND (" + " OR ".join(term(n) for n in NAMES) + ")"


def fetch(start, end=None):
    params = {
        "query": QUERY,
        "language.code": "en",
        "published_at.start": start,
        "sort.by": "published_at",
        "sort.order": "asc",
        "per_page": 200,
        "page": 1,
    }
    if end:
        params["published_at.end"] = end
    while True:
        r = requests.get(API, headers={"X-API-Key": os.environ["APITUBE_API_KEY"]}, params=params, timeout=60)
        r.raise_for_status()
        data = r.json()
        yield from data["results"]
        if not data.get("has_next_pages"):
            return
        params["page"] += 1


def title_key(title):
    return re.sub(r"[^a-z0-9]+", " ", title.lower()).strip()


def names_in(title):
    return [n for n in NAMES if re.search(r"(?<![A-Za-z])" + re.escape(n) + r"(?![A-Za-z])", title)]


def send_sms(text):
    if DRY:
        print(text, "\n")
        return "dry-run"
    from twilio.rest import Client
    client = Client(os.environ["TWILIO_ACCOUNT_SID"], os.environ["TWILIO_AUTH_TOKEN"])
    msg = client.messages.create(body=text, from_=os.environ["TWILIO_FROM"], to=os.environ["MY_PHONE"])
    return msg.sid


rows = list(csv.DictReader(LOG.open())) if LOG.exists() else []
seen = {row["title_key"] for row in rows}
since = arg("--since") or (rows[-1]["published_at"] if rows else (dt.datetime.utcnow() - dt.timedelta(days=1)).strftime("%Y-%m-%dT%H:%M:%SZ"))
until = arg("--until")

with LOG.open("a", newline="") as f:
    w = csv.writer(f)
    if not rows:
        w.writerow(["published_at", "article_id", "names", "title_key", "title", "url", "source", "sms_chars", "sid"])
    for a in fetch(since, until):
        key = title_key(a["title"])
        names = names_in(a["title"])
        if key in seen or not names:
            continue
        text = f"RECALL ({', '.join(names)}): {a['title']} {a['href']}"
        sid = send_sms(text)
        w.writerow([a["published_at"], a["id"], "|".join(names), key, a["title"], a["href"], a["source"]["domain"], len(text), sid])
        seen.add(key)
Enter fullscreen mode Exit fullscreen mode

What each part does, in plain words:

  1. QUERY builds the recall AND (…) string from your file.
  2. fetch() asks the API for everything since the last run, 200 at a time, and follows has_next_pages.
  3. title_key() turns a headline into lowercase letters and digits. Syndicated copies (the same F-150 headline ran on three radio station sites) collapse into one key, so you get one text, not three.
  4. names_in() checks the headline again for your names, case-sensitive and whole-word. The API matches loosely; this second check decides which names go into the text.
  5. send_sms() prints in --dry-run mode and calls Twilio otherwise. Four environment variables: TWILIO_ACCOUNT_SID, TWILIO_AUTH_TOKEN, TWILIO_FROM (your Twilio number) and MY_PHONE. A trial account can only text numbers you verified in the console, and it prefixes every message with a trial notice.
  6. alerts.csv is both the log and the memory. The last published_at in it is where the next run starts.

Step 4. Run it, then let cron run it

First run, dry, from a date you pick:

export APITUBE_API_KEY=...   # free tier at apitube.io
python3 recall_alerts.py --dry-run --since 2026-09-10
Enter fullscreen mode Exit fullscreen mode

Once the printed texts look right, add the Twilio variables and a cron line. Once a day at 08:00 is enough. Recalls are not breaking news; the article is usually hours behind the official notice anyway.

0 8 * * * cd ~/recall-alerts && python3 recall_alerts.py
Enter fullscreen mode Exit fullscreen mode

What 31 days looked like

The replay ran --since 2026-08-17 --until 2026-09-16T23:59:59Z. 47 API hits became 40 unique headlines, and 38 texts after the name check. We then read all 38 and labelled them by hand.

Label Texts What it means
Distinct recall 21 First headline about a recall you had not been told about
Repeat coverage 10 Another outlet, another headline, same recall
About a recall 6 A stock story, a sales story, a lawyer ad, a listicle, commentary, a Honda "check your recall status" notice
Wrong 1 Doug Ford, the Ontario premier, being asked to recall the legislature

Walmart produced 17 of 38 texts, Ford 11, Toyota 6; 17 of the 26 names on the list never produced a text in 31 days

Name Texts
Walmart 17
Ford 11
Toyota 6
Target 5
Kroger 3
Costco 2
Great Value 2
Honda 1
Subaru 1

(A text can carry more than one name, so the column adds up past 38.) The other 17 names, from Samsung to Blue Buffalo, produced nothing in 31 days. The Walmart frozen-berry E. coli recall alone produced 8 texts over two weeks as it expanded state by state. The busiest day was September 4 with 5 texts; 24 of the 31 days had at least one.

The name check dropped 2 of the 47 hits. One drop was right: a Tesla door-handle recall matched because the headline contained the verb "target". One was a loss: a Goal Zero power-station recall posted on a Costco fan site, where "Costco" appeared only in the body. Title-only matching has a price, and that was it for the month.

What it costs

A recall tracker for 26 names costs about $2 a month: 66 SMS segments in 31 days at Twilio's US long-code rate of $0.0083 per segment ($0.55), carrier pass-through fees of $0.0035–0.005 per segment ($0.23–0.33), and $1.15 a month for the number. $1.93 to $2.03 for the month. 28 texts needed two segments because the article URL pushes them past 160 characters; 10 fit in one.

One detail that matters for the bill: the text starts with RECALL (Walmart): and not Recall — Walmart. An em dash is not in the GSM-7 character set, and a single one switches the whole message to 70-character segments.

Should you add a cooldown?

The obvious fix for 8 berry texts is "don't text about the same name twice in N days". We replayed the log with that rule:

No cooldown sends 38 texts and catches all 21 recalls; a one-day cooldown drops to 25 texts and 18 recalls; seven days drops to 14 texts and 10 recalls

Cooldown per name Texts Distinct recalls caught
None 38 21 of 21
1 day 25 18 of 21
3 days 20 15 of 21
7 days 14 10 of 21

A one-day cooldown removes 13 texts, and 3 of those were distinct recalls: a Ford Bronco airbag recall, a Toyota GR Supra recall in China and a UK roundup naming Ford and Toyota. Ford had five separate recalls in the month, and they land days apart. A per-name cooldown is the wrong lever for recall alerts, because it throws away real recalls faster than it removes repeats. Our recommendation: no cooldown. If 38 texts is too many, shorten the list. Taking Walmart off it leaves 28 texts; taking Walmart and Ford off leaves 18.

FAQ

How do I get notified about product recalls?

The two ways to get notified about product recalls are the recalls.gov email, which sends every notice from six US agencies, and a script like the one above, which texts you only when a headline names something you own. The official email is complete but unfiltered; the script is filtered but only as good as the newsroom coverage.

Is there a product recall API?

There is no single product recall API for all products. The CPSC Recall API covers consumer products under its jurisdiction, not vehicles, food or drugs. For everything else, a news API with a headline search covers whatever gets reported: query=recall AND (Brand OR Brand) returned 47 headlines in 31 days for 26 names.

Is there an app for product recalls?

We did not find an app that watches product recalls for a personal list of brands across all agencies. Consumer Reports covers vehicles and several retailers cover their own sales. This script is the app: a text file, a cron line and about $2 a month.

Does Amazon notify you of recalls?

Amazon notifies you of recalls only for items bought on Amazon, by email and a notice in Your Orders. It does not know about the Instant Pot you bought elsewhere, which is what the list in my_stuff.txt is for.

Limits

  • Title-only matching misses recalls whose headline names the product and not the brand or store. The Goal Zero power-station recall was the one we lost this month: "Costco" was in the body, not the headline.
  • Title dedup merges identical headlines, not different headlines about one recall. Expect a few texts per big recall.
  • The news is hours behind the official notice. The text links to the article; check the lot number and dates there or on the agency site before you throw anything out.
  • 17 of 26 names were silent for a month. That is a normal month, not a broken script.

The script, my_stuff.txt and the 38-row data.csv from the replay are next to this post. Change the list and run it.

Resources

Disclosure: APITube is our product, a News API Intelligence, and the API used above. It has a free tier at apitube.io.

Top comments (0)