Skip to main content

Metadata availability

Catalogue coverage is partial — roughly 20–37% of barcodes carry any given metadata scope, so a request for include=nutrition on an arbitrary barcode is more likely to come back empty than full.

These endpoints tell you what a barcode has before you fetch it. They return booleans only — never the metadata values themselves.

:::note Billing note GET /products/{barcode} and POST /products/bulk-check already bill only for the scopes and image variants that actually return data — an empty scope never costs anything, whether or not you check availability first. These endpoints exist to make large syncs efficient (skip the round-trip entirely for barcodes with nothing to fetch), not to change what you're charged. See Pricing → Per-scope billing. :::

The four endpoints

EndpointAnswersPricing key
GET /metadata/{barcode}"What does this one barcode have?"metadata.get.per_barcode
POST /metadata/bulk-check"What do these 500 barcodes have?"metadata.bulk_check.per_barcode
GET /metadata/products"Which barcodes have nutrition?"metadata.products.list
GET /metadata-fields"What can I filter on?"metadata.fields.catalogue

All rates are admin-set and readable from GET /pricing. Never hard-code prices — fetch them at daemon startup and cache by action key.

Existing endpoints are unchanged

This is an additive surface. GET /products, GET /products/{barcode} and POST /products/bulk-check behave exactly as documented — same parameters, same response shapes. Nothing you already run needs to change.

Keys: scopes and fields

A key is either a scope or a scope.field pair:

  • nutrition — the barcode has any nutrition data
  • nutrition.calories — the barcode specifically has a calories value

Scope-level tells you whether an include= call is worth making. Field-level tells you whether the one field you actually care about is populated — a nutrition record with a serving size but no vitamin D is still "has nutrition" at scope level.

The six scopes: info, hechshers, nutrition, ingredients, catalog_meta, image_meta.

Call GET /metadata-fields for the authoritative list of every valid key — don't hard-code field names.

GET /metadata/{barcode}

Field-level availability for a single barcode.

curl https://images.retaildigitals.com/api/v1/metadata/000413000222 \
-H "Authorization: Bearer $ACCESS_TOKEN"
200 OK
{
"barcode": "000413000222",
"metadata": {
"info": true, "hechshers": true, "nutrition": true,
"ingredients": false, "catalog_meta": true, "image_meta": true
},
"metadata_fields": {
"nutrition": { "serving_size": true, "calories": true, "vitamin_d": false },
"info": { "brand": true, "product_name": true, "organic": false }
},
"image_variants": ["front", "back"],
"worth_requesting": ["info", "hechshers", "nutrition", "catalog_meta", "image_meta", "images"],
"meta": { "request_id": "req_...", "credits_debited": 0, "credits_remaining": 4321.8 }
}
FieldMeaning
metadataOne boolean per scope — does this scope have any data?
metadata_fieldsPer-scope breakdown to individual field level
image_variantsWhich image variants exist (front, back, front_clean, back_clean)
worth_requestingThe include= scopes that would return data. Request only these.

Returns 404 not_found if the barcode isn't in the catalogue.

POST /metadata/bulk-check

The batch form — up to 500 barcodes per call.

curl -X POST https://images.retaildigitals.com/api/v1/metadata/bulk-check \
-H "Authorization: Bearer $ACCESS_TOKEN" \
-H "Content-Type: application/json" \
-H "Idempotency-Key: $(uuidgen)" \
-d '{"barcodes": ["000413000222", "3035"], "fields": true}'
200 OK
{
"results": {
"000413000222": {
"exists": true,
"metadata": { "info": true, "nutrition": true, "ingredients": false },
"metadata_fields": { "nutrition": { "calories": true, "vitamin_d": false } }
},
"3035": {
"exists": true,
"metadata": { "info": false, "nutrition": false, "catalog_meta": true }
}
},
"meta": { "credits_debited": 0, "credits_remaining": 4321.8 }
}

Omit fields (or send false) for scope-level only — charged at metadata.bulk_check.per_barcode. Send fields: true for per-field detail — charged at metadata.bulk_check.fields.per_barcode.

Barcodes not in the catalogue come back as {"exists": false}. 4-digit PLU codes are accepted here.

Idempotent replay: send Idempotency-Key: <20–255 chars> to make retries safe. Replaying the same key and body within 24 hours returns the cached response without re-charging.

GET /metadata/products

The discovery counterpart — list barcodes filtered by what metadata they have or lack.

# Products with a usable nutrition panel
curl "https://images.retaildigitals.com/api/v1/metadata/products?has=nutrition.calories&per_page=500" \
-H "Authorization: Bearer $ACCESS_TOKEN"

# Products that have brand AND calories
curl "https://images.retaildigitals.com/api/v1/metadata/products?has=info.brand,nutrition.calories" \
-H "Authorization: Bearer $ACCESS_TOKEN"

# Your backfill backlog — everything with no info record
curl "https://images.retaildigitals.com/api/v1/metadata/products?missing=info" \
-H "Authorization: Bearer $ACCESS_TOKEN"
ParameterMeaning
has=Return only barcodes that have all the listed keys (ANDed)
missing=Return only barcodes that have none of the listed keys
include_metadata=1Embed the per-scope map on each row
include_fields=1Embed the per-field map on each row
search=Barcode prefix match
updated_since=Only products changed since a timestamp
page, per_pagePagination (per_page max 500)
200 OK
{
"data": [{ "barcode": "000413000222", "variants": ["front", "back"] }],
"filters": { "has": "nutrition.calories", "missing": null },
"pagination": { "page": 1, "per_page": 50, "total": 5942, "total_pages": 119 },
"meta": { "credits_debited": 0 }
}

An unknown key returns 422 invalid_metadata_key with a hint pointing at /metadata-fields.

GET /metadata-fields

The catalogue of valid keys. Pass counts=1 to also size the data before you sync.

curl "https://images.retaildigitals.com/api/v1/metadata-fields?counts=1" \
-H "Authorization: Bearer $ACCESS_TOKEN"
200 OK
{
"scopes": [
{
"scope": "nutrition",
"source_fields": ["servings_per_container", "serving_size", "calories", "..."],
"barcodes_with_scope": 6824,
"barcodes_with_field": { "calories": 5942, "vitamin_d": 4516 }
}
],
"total_products": 30313,
"usage": { "single": "GET /v1/metadata/{barcode}", "...": "..." }
}

Use barcodes_with_field to decide whether a field is worth building a feature on — a field present on 4% of the catalogue behaves very differently from one present on 30%.

Pattern: plan a sync efficiently

Skipping the prefilter still costs you nothing extra per barcode — empty scopes are always free — but it means a wasted network round-trip for every barcode that has nothing to return:

# ── Without a prefilter: one HTTP call per barcode, most return nothing
for barcode in "${barcodes[@]}"; do
curl ".../products/$barcode?include=nutrition" # free if empty, but still a network round-trip
done

# ── With a prefilter: one bulk-check call, then fetch only the barcodes with data
curl -X POST ".../metadata/bulk-check" \
-d "{\"barcodes\": $(printf '%s\n' "${barcodes[@]}" | jq -R . | jq -sc .)}" \
| jq -r '.results | to_entries[] | select(.value.metadata.nutrition) | .key' \
| while read -r barcode; do
curl ".../products/$barcode?include=nutrition" # only barcodes with data
done

For a full catalogue sync, skip the prefilter entirely and drive the loop from GET /metadata/products?has=nutrition — it returns only the barcodes worth fetching.

Notes and limits

  • Availability only. These endpoints never return metadata values, so they are safe to call on barcodes you have no intention of buying.
  • Field locks still apply to the actual fetch. Availability reflects what exists in the catalogue, not what your key is permitted to read — if an admin has locked a scope on your key, that scope still returns 403 scope_forbidden when fetched via GET /products/{barcode}.
  • image_meta availability counts active image records only, matching what the fetch returns.
  • Freshness. Availability is computed live at request time; there is no cache to invalidate.