Build a pantry tracker on barcodes and expiry dates
One photo gives you a decoded barcode and an ISO expiry date per product. Open Food Facts fills in the rest, free and with no key. The whole pipeline.
5 min readPoint a phone at three tins on a shelf and you want back three rows in a database: what the product is, how many there are, and when it goes off. The barcode is already on the packaging and the expiry date is already printed next to it. Nobody should be typing either one.
Third post in a series on consumer apps that are mostly one API call. Earlier: a meal photo calorie tracker and a receipt scanner and bill splitter.
The pipeline
- One job. Photo of the shelf, schema back with a decoded barcode and an ISO expiry date per product.
- Open Food Facts. Look each barcode up. Free, no key, one GET.
- Your database. Insert. That is the app.
The reason this is short is that the two hard parts are not yours. Reading a barcode correctly is a decoding problem, and turning "BB 03/27" into a date your database will accept is a normalization problem. Both are handled before the result reaches you.
The job
format: "barcode" does not mean the model reads the stripes. Every image is passed through a real decoder before extraction, and the decoded payload is what fills the field. EAN, UPC, Code 128, Code 39 and ITF all resolve, along with QR and Micro QR through format: "qrcode".
That distinction is the whole point of a pantry app. An EAN is a database key, so one wrong digit is not a near miss, it is a different product or no product at all. A model looking at a picture of a barcode will hand you thirteen digits of the right shape, and nothing in your data will tell you they were invented. A decoder resolves the symbol or it does not, and when it does not, the field comes back null.
format: "date" handles the other mess. Expiry dates are printed in every layout a packaging designer ever invented:
A month and year with no day comes back as a month and year. The day is not invented to make the string longer, and a date whose parts are genuinely ambiguous is returned exactly as printed instead of being forced into a confident wrong ISO string. Store the raw value beside the normalized one and you can always show the user which is which.
The lookup
Open Food Facts serves product data by barcode with no key and no signup:
You now have a name, a brand, a pack size, a thumbnail and the nutrition panel, for free, keyed on a number nobody typed. USDA FoodData Central is the alternative worth knowing about: it needs a free key from api.data.gov, and its tables are curated rather than crowdsourced, which makes it stronger on raw ingredients and weaker on branded packaging.
Miss rates are real. Own brand and regional products are the usual gap, and a barcode that resolves cleanly can still have no row behind it. That is why name_printed is in the schema. When the lookup comes back empty you still have something to show the user, and the row is worth keeping.
What you still have to build
The scanning experience. Live camera preview, a shutter, a confirmation list. Each photo is a job, so batching a shelf into one image is cheaper and faster than one photo per tin.
Deduplication. The same EAN scanned twice is a quantity change, not a new row, unless the expiry dates differ, in which case it is a new lot. Decide this early. It reaches into every screen.
Expiry notifications. A daily query and a push. The reason anyone installs this kind of app is the notification, not the scanner.
The empty paths. No barcode in the photo, no expiry printed, no product in the database, job comes back failed. Four different screens, and manual entry is the honest fallback for all of them.
Cost and shape
Jobs run asynchronously. POST /v1/jobs/create returns an id and status: "pending" right away, then you take the result on a webhook_url or by polling GET /v1/jobs/:jobId/get. A phone that scans and immediately locks the screen is the normal case, so the webhook into your backend is the pattern that survives contact with users.
Pricing is per token, and the one number we publish is measured rather than modelled: $5.63 per 1,000 pages on vera-1.0-high, across the full 1,651 page OmniDocBench run. That benchmark transcribes entire pages, which is the most output heavy request possible. A shelf photo answered with five fields per product is a fraction of it. vera-1.0-low is worth a comparison run here too, since the decoded barcode is not the model's work in either engine.
Start here
The quickstart gets a first photo through in a few minutes. QR codes and barcodes is the longer argument for decoding over reading, and schemas and formats has the full format table.
Last in this series: a business card scanner that writes to your CRM.