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 read

Point 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

  1. One job. Photo of the shelf, schema back with a decoded barcode and an ISO expiry date per product.
  2. Open Food Facts. Look each barcode up. Free, no key, one GET.
  3. 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

json
{
  "sources": [{ "type": "image", "data": "data:image/jpeg;base64,/9j/4AAQ..." }],
  "engine": "vera-1.0-high",
  "schema": {
    "type": "object",
    "properties": {
      "products": {
        "type": "array",
        "description": "One entry per distinct product visible in the photo.",
        "items": {
          "type": "object",
          "properties": {
            "ean":          { "type": "string", "format": "barcode" },
            "name_printed": { "type": "string", "description": "Product name as printed on the pack." },
            "count":        { "type": "integer", "minimum": 1, "description": "How many identical units are visible." },
            "expires_at":   { "type": "string", "format": "date", "description": "Best before or use by date, as printed." },
            "lot":          { "type": "string" }
          },
          "required": ["name_printed"]
        }
      }
    },
    "required": ["products"]
  }
}

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:

PrintedYou get
12/03/27 on a European pack2027-03-12
BEST BEFORE 03/20272027-03
EXP 20272027
Consumir antes de: 14 mar 20272027-03-14

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:

ts
async function lookupProduct(ean: string) {
  const url =
    `https://world.openfoodfacts.org/api/v2/product/${ean}.json` +
    "?fields=product_name,brands,quantity,image_front_small_url,nutriments"

  const response = await fetch(url, {
    //open food facts asks every client to identify itself
    headers: { "User-Agent": "PantryApp/1.0 (hello@yourapp.com)" },
  })
  if (!response.ok) return null

  const body = await response.json()
  return body.status === 1 ? body.product : null
}

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.

Try it on your own documents

Add funds from $5, create a key, and send your first document. Every job reports exactly what it cost.

Start extracting

Keep reading