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
| Endpoint | Answers | Pricing 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 datanutrition.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
- JavaScript
- PHP
- Python
curl https://images.retaildigitals.com/api/v1/metadata/000413000222 \
-H "Authorization: Bearer $ACCESS_TOKEN"
const res = await fetch('https://images.retaildigitals.com/api/v1/metadata/000413000222', {
headers: { Authorization: `Bearer ${accessToken}` },
});
const { worth_requesting } = await res.json();
// Fetch only what actually exists
const include = worth_requesting.filter(s => s !== 'images').join(',');
$ch = curl_init('https://images.retaildigitals.com/api/v1/metadata/000413000222');
curl_setopt_array($ch, [
CURLOPT_RETURNTRANSFER => true,
CURLOPT_HTTPHEADER => ["Authorization: Bearer {$accessToken}"],
]);
$avail = json_decode(curl_exec($ch), true);
$include = implode(',', array_diff($avail['worth_requesting'], ['images']));
import requests
avail = requests.get(
"https://images.retaildigitals.com/api/v1/metadata/000413000222",
headers={"Authorization": f"Bearer {access_token}"},
).json()
include = ",".join(s for s in avail["worth_requesting"] if s != "images")
{
"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 }
}
| Field | Meaning |
|---|---|
metadata | One boolean per scope — does this scope have any data? |
metadata_fields | Per-scope breakdown to individual field level |
image_variants | Which image variants exist (front, back, front_clean, back_clean) |
worth_requesting | The 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}'
{
"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"
| Parameter | Meaning |
|---|---|
has= | Return only barcodes that have all the listed keys (ANDed) |
missing= | Return only barcodes that have none of the listed keys |
include_metadata=1 | Embed the per-scope map on each row |
include_fields=1 | Embed the per-field map on each row |
search= | Barcode prefix match |
updated_since= | Only products changed since a timestamp |
page, per_page | Pagination (per_page max 500) |
{
"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"
{
"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_forbiddenwhen fetched viaGET /products/{barcode}. image_metaavailability counts active image records only, matching what the fetch returns.- Freshness. Availability is computed live at request time; there is no cache to invalidate.