Writing

Looking up a recall by barcode, and where the other four fifths went

If you want to know whether a product has been recalled, the obvious thing to reach for is the barcode. It is printed on the box, it is already in your catalogue, it is unambiguous, and unlike a product name it does not come in four languages. Every question anyone has ever asked me about this data has been some version of can I just pass you a GTIN?

You can. It works about one time in five.

That number is not a complaint about the data, and it took me a while to understand why. Here is what a barcode can actually do against seven government recall feeds, where the rest of the corpus went, and what to search by when the barcode comes back empty.

The measurement

Of the 176,198 recalls I currently hold, 36,392 carry at least one barcode — a GTIN, UPC or EAN, which are all the same number space wearing different hats. That is 20.7%.

Widen it to any structured identifier — model number, lot code, NDC, VIN — and coverage roughly doubles, to 99,052 records. Which still leaves 77,146 recalls, nearly 44% of the corpus, where there is nothing machine-matchable at all. Just prose: a brand, a description, a hazard, and a photograph.

I want to be careful about how I frame that, because the obvious framing is wrong. This is not seven agencies doing a sloppy job of publishing product data. It is seven agencies publishing something that was never product data in the first place.

A recall notice is a safety communication. Its job is to make a person who owns the thing recognise the thing. For that purpose a photograph and the words "blue plastic kettle, sold at Lidl between March and June" beat a fourteen-digit number, because the number is on the packaging that went in the bin nine months ago. The agency is optimising for the recognition task, and it is right to.

Barcodes appear when the recall happens to pass through a system that had one. That is the whole rule, and it explains the distribution completely.

Which feeds carry barcodes

Recalls with at least one GTIN, by source:

source              recalls   with barcode
rappelconso          18,031      10,521      58.3%
openfda-food         29,263       9,258      31.6%
eu-safety-gate       46,442      10,973      23.6%
openfda-drug         17,831       1,872      10.5%
openfda-device       39,587       3,435       8.7%
cpsc                  9,912         332       3.3%
nhtsa                15,132           1       0.0%

The spread is enormous, and it maps cleanly onto how each agency learns about a recall.

RappelConso is the outlier, and it is an instructive one. France's system is fed by the company doing the recalling, through a form, and the form asks for the barcode. When you ask, you get. Nearly six in ten French records carry one, which is why French data punches far above its size here.

Safety Gate is assembled from national authority submissions across the EU, and those authorities are themselves working from whatever the local investigation turned up. Sometimes that included the packaging, often it did not.

openFDA food does well because food recalls are distribution events. Somebody has to tell retailers what to pull off which shelf, and that instruction is expressed in the retail identifiers those retailers use.

CPSC's 3.3% is the one that surprises people, because CPSC is the feed that looks most like a consumer product database. But its notices are written for the public — they name the product, show it, tell you what to do — and the retail identifier simply is not part of that format.

And NHTSA is a rounding error above zero, which is correct rather than broken. Vehicles are not sold by barcode. The identifier that means something for a car is the VIN, and it identifies one specific vehicle rather than a product line, which is a genuinely different kind of lookup.

The one barcode in fifteen thousand vehicle campaigns

That single NHTSA record bothered me. One barcode across 15,132 campaigns is exactly the shape of a false positive — some fourteen-digit part number that wandered through the extractor and passed the check digit by luck, which happens about one time in ten.

So I looked it up:

gtin        00792363643610
source      nhtsa
confidence  extracted
title       KENWAY LED TRAILER LIGHT KIT — EXTERIOR LIGHTING

It is a trailer light kit. NHTSA regulates it because it is motor vehicle equipment, but you buy it off a shelf in a box, and the box has a barcode on it. The one barcode in the vehicle feed belongs to the one product in the vehicle feed that you carry to a till.

I have kept the query around, because it is the neatest illustration I have of the underlying rule. Barcodes are not a property of danger. They are a property of retail.

Most of the barcodes I have were not in a barcode field

Of the 49,009 barcode records extracted, 27,715 — just over 56% — were mined out of free text rather than read from a structured field.

They live in sentences like this one, which is a real openFDA code_info value, lightly trimmed:

Lot #1234567, Exp 03/2027; UPC 0 36000 29145 2; also lots 1234568,
1234569 (UPC 036000291452) distributed to CA, NV, AZ

One barcode, written two different ways, in a field with no consistent delimiter, alongside three lot codes. Again: this is a field designed for a human reading a recall notice, and it does that job perfectly well.

Getting identifiers out of that means a regex pass, and a regex pass over free text means false positives. 0 36000 29145 2 is a barcode. So is a twelve-digit order reference that happens to sit near the word "UPC". The thing that separates them is the check digit — a real GTIN's last digit is a mod-10 checksum over the others, so a random twelve-digit number passes about one time in ten.

Validating it is roughly fifteen lines of code and it throws out about a fifth of what a naive regex hands you. If you skip it, your lookup will confidently report recalls against numbers that were never barcodes, and it will do it in a way that looks completely normal.

The rule that matters more than any of this

If someone hands you a malformed barcode, never answer "no recalls found."

This is the one failure mode a recall lookup genuinely cannot have. "No recalls found" is read by every human being as "this product is fine". If the barcode was mistyped, or truncated by a spreadsheet that ate the leading zero, or was never a barcode at all, then you did not look anything up — and you just told someone their product is safe.

It is wrong in the direction nobody checks. A false positive gets investigated and corrected within the hour. A false clear gets filed.

So a malformed identifier has to be an error, loudly. Take the Crayola barcode from earlier and mistype the last digit:

GET /v1/identifiers/gtin/071662114764

400 Bad Request
{
  "type": "/problems/invalid-parameter",
  "title": "Invalid query parameter",
  "status": 400,
  "detail": "\"071662114764\" is not a valid gtin. GTINs must pass their
             check digit; VINs must be 17 characters.",
  "instance": "/v1/identifiers/gtin/071662114764",
  "parameter": "value"
}

Not an empty result set. Not a 200 with "count": 0. An error, naming the reason, so that whatever is on the other end has to deal with it rather than filing a clean bill of health.

That single digit is the whole point. 071662114763 is a recalled box of finger paints in two jurisdictions. 071662114764 is not a barcode at all, and the gap between "no recalls" and "that is not a number I can look up" is where somebody gets hurt.

The same normalisation runs on lookup as on ingestion, which matters for a subtler reason: if the two ever drift apart, a customer's barcode stops matching the identical barcode already stored, and the symptom is a silent empty result rather than a crash. Same failure, harder to see.

When a barcode does work, it crosses borders

The upside of barcode matching is that a barcode does not care which agency published the notice, and this is where it earns its keep.

Here is a real example. The barcode printed on the box is the twelve-digit UPC 071662114763, which zero-pads to the fourteen-digit form 00071662114763 that everything is stored and matched against:

eu-safety-gate   2023-11-03   4 Tubes de peinture aux doigts lavables
rappelconso      2023-12-28   crayola mini kids réf. style#: 81-1476

Two agencies, fifty-five days apart, describing one product — Crayola washable finger paints — in two languages, with no shared record id, no cross-reference, and titles that no string-matching algorithm on earth would connect. The barcode connects them in one query.

That said, I should be honest about the scale of it: only 331 barcodes in the entire corpus appear in more than one source. Cross-border overlap is real and it is valuable when it hits, but it is rare, because agencies mostly recall different products and only occasionally the same one. Anybody selling you "cross-source deduplication" as the headline feature is selling you something that fires 331 times out of 44,732.

What to search by for the other four fifths

If the barcode comes back empty, or you never had one, the corpus is far from exhausted. Distinct values currently held:

model    108,383
batch     15,059
ndc       12,155
vin          611

Model numbers are the workhorse, and it is not close. They cover far more of the corpus than barcodes do, because a model number is what an agency naturally writes down when describing a product to the public. They are messier — inconsistent casing, spacing and punctuation, so you need to normalise both sides — but if you are only matching on GTIN you are leaving most of the available coverage on the table.

NDC is the one to use for pharmaceuticals, where it is close to universal and properly structured. Batch and lot codes are how you narrow a food or drug recall from "this product" to "this production run", which is often the difference between pulling one pallet and pulling a product line. VIN is vehicles, and vehicles only.

In practice, a catalogue check that only asks about barcodes is answering a much narrower question than the person running it believes.

A clear answer has a shelf life

One last thing, because it undoes some of the above.

Agencies revise recalls after publication. A notice covering three lot codes in March can cover eleven by June. The URL does not change, there is no changelog, and nothing notifies anyone. Across this corpus, 15,154 records have been amended since they were first published.

So a barcode lookup that came back clear in March is a statement about March. If the recall widens in June to include your product, nothing tells you, and any record you generated on the day now quietly disagrees with what the recall says today. If you are keeping these lookups as evidence of diligence — which under the EU's GPSR is the actual obligation — that gap is the whole problem, and I have written about it separately in proving you checked.

If you are building this yourself

Genuinely worth doing — the data is free and the problem is more interesting than it looks. Where the time actually goes:

  • Validate check digits before you trust a barcode. Fifteen lines, discards about a fifth of

what a regex finds, and without it your matches include numbers that were never barcodes.

  • Reject malformed identifiers instead of answering them. The single most important line of

code in the whole thing, and the easiest one to leave out by accident.

  • Normalise to GTIN-14 on both sides. UPC-A, EAN-13 and GTIN-14 are the same number with

different padding, and a lookup that does not zero-pad will miss matches it already has stored.

  • Mine the free-text fields. Over half the barcodes here were not in a barcode field. If you

only read structured columns you will get less than half the coverage available.

  • Do not stop at barcodes. Model numbers cover considerably more of the corpus.
  • Diff on every ingest. Amendments are unannounced, and they are the reason a stored answer

goes stale without anything appearing to change.

The barcode is the right thing to ask with. It is just not, on its own, the thing that answers.

---

RecallProven turns seven official recall feeds — EU Safety Gate, RappelConso, CPSC, NHTSA and three FDA enforcement databases — into one API with consistent identifiers, amendment history, and a frozen, digest-verified receipt for every check. Every figure above was measured against the live corpus on the day of writing. See how it works, the documentation, or the technical notes in Seven government recall feeds.


RecallProven turns these seven feeds into one API with a consistent schema, stable identifiers and a per-check audit trail. See the pricing or read the documentation.