← Reports Hub

Barcode Discovery Spike

SA retailer API landscape: what's accessible, what's blocked, what's next

2026-03-31 feat/barcode-discovery Spike Complete
1
API Working
3
Blocked
0%
Barcode Coverage
619
WW Dairy Products
928+
WW Milk Results
Key Finding

Woolworths exposes a live product search API via Constructor.io. No authentication required. Full product names, ZAR prices, brand, and ratings. 5 retailers probed — only one viable endpoint found. No retailer supports barcode-indexed lookup.

Retailer Status

Five SA retailers and two international barcode databases were probed. Results:

Woolworths Working
Barcode lookupNo
Text searchYes
ZAR pricesYes
Auth requiredNo
MethodConstructor.io
Checkers / Sixty60 Blocked
Barcode lookupNo
Text searchBlocked
ZAR pricesNo
BlockerCloudFront WAF
HTTP status403
Pick n Pay Blocked
Barcode lookupNo
Text searchBlocked
ZAR pricesNo
BlockerAngular SPA
ResponseHTML shell
SPAR No API
Barcode lookupNo
Text searchNo
ZAR pricesNo
Website typeBrochure only
Open Food Facts Limited
Barcode lookupPartial
SA coverage~8,800 products
ZAR pricesNo
Auth requiredNo
Trundler Paid option
RetailersWW, PnP, Makro
ZAR pricesYes
BarcodesYes
CostPaid, TBD

Woolworths: Constructor.io

Woolworths runs Constructor.io for product search and discovery. The API key is embedded in their public frontend. Two endpoints available:

Search endpoint
https://ac.cnstrc.com/search/{query} ?key=key_SsbVHddjxFcZQ9uI &section=Products &num_results_per_page=24 &page=1
Response fields available
value → product name data.brand → brand name data.p10 → price tier (ZAR) data.p30 → price tier (ZAR) data.p60 → price tier (ZAR) data.category → category (often absent) data.ean → EAN barcode (NOT present — always empty)
Critical gap: no barcode in response
The data.ean field is empty for 100% of products. Woolworths does not expose barcodes via this API. Text search works; barcode-indexed lookup does not. Field coverage for 30 dairy products: name 100%, brand 100%, price 100% — barcode 0%.

Text search quality

Query Result Name returned Price (ZAR) Time Relevance
milk Found Farmhouse Mature Cheddar 200g R104.99 312ms Poor
bread Found 100% Rye Bread 400g R51.99 100ms Good
rice Found Babes Fresh Lamb, Veggies, Lentils and Brown Rice Meal 150g R44.99 104ms Poor
sugar Found Nescafe Reduced Sugar Cappuccino Sticks 10 x 12.5g R79.99 129ms Poor

Relevance is mixed: 3 of 4 queries returned loosely related products. Constructor.io ranking does not prioritise exact matches. Works well for specific product name searches, less well for generic category terms.

Barcode Resolution Results

Three real SA EAN-13 barcodes (prefix 600x) were tested against all sources. Results:

0/3 barcodes resolved across all sources
Woolworths Constructor.io is text-only — EAN-13 queries return no results. Open Food Facts has near-zero SA barcode coverage. None of the three SA barcodes matched in either system. Average response time under 200ms; the resolver is fast, just has no data to return.
BarcodeWoolworthsOpen Food FactsResolvedTime
6001007000028 Not Found 289ms Not Found 754ms Miss 187ms
6009803440085 Not Found 90ms Not Found 170ms Miss 185ms
6001414200031 Not Found 84ms Not Found 173ms Miss 184ms

What Was Built

Two services were produced as part of the spike, ready to integrate into the main app:

1
barcode-resolver.js
Tiered lookup: Supabase cache → Woolworths text search → Open Food Facts → manual. Returns first result with price data. Slot reserved for FatSecret Premier.
Migrated
2
catalog-ingester.js
Async generator that streams Woolworths products page by page across 12 categories. 200ms rate-limit delay built in. Stubs for Checkers, PnP, SPAR yield empty with clear reason logging.
Migrated

Recommended Architecture

The spike recommends a barcode-to-product mapping layer sitting between the scanner and the retailer API. Since no retailer API is barcode-indexed, we build our own mapping and use names to search retailers for pricing.

Proposed data model
barcodes ean → EAN-13 barcode (PK) product_name → canonical name brand category source → "user" | "openfoodfacts" | "manual" prices barcode_id → FK to barcodes retailer → "woolworths" | "pnp" | "checkers" price_zar unit → "each" | "per kg" | "per litre" fetched_at → timestamp source → "constructor_io" | "trundler" | "manual" price_history barcode_id, retailer, price_zar, recorded_at

Scan flow with this architecture

1
Camera captures EAN-13
BarcodeDetector / Quagga2 (already in prod)
2
Look up barcode in local barcodes table
Supabase. Seeded with Open Food Facts SA data (~8,800 entries).
3
Use product name to search Woolworths
Constructor.io returns current ZAR price. Cache result with 24h TTL.
4
Cache miss: prompt user for product name
User-confirmed name saved back to barcodes table. Builds SA barcode DB organically.

Risks

RiskSeverityMitigation
Constructor.io ToS — key is public but Woolworths may not permit third-party use High Approach Woolworths for formal partnership. Hirt & Carter relationship may help (they hold Checkers/Shoprite product data). Ready to switch to Trundler if access revoked.
API stability — key or endpoint structure could change without notice Medium Abstract retailer API behind service layer. Swapping implementations is straightforward.
Rate limiting — no limits observed now, but could be enforced Medium 24h TTL cache on price data. Batch searches. 200ms delay between catalog ingestion pages.
Price accuracy — online prices may differ from in-store Low Display as "estimated online price." Users can override.
POPIA — barcode scans are anonymous product queries, no PII transmitted Low No personal data leaves the device. Local barcode cache stores product data only.

QA Validation

Four test suites were run against the two service files. Edge case handling is solid; the resolver architecture is correct. The gap is data, not code.

TestResultNote
Real SA barcodes (3)0/3 resolvedExpected. No source supports EAN-indexed SA lookup.
Woolworths text search (4 queries)4/4 foundResults with prices returned. Relevance mixed.
Catalog ingester — dairy (30 products)PassNames, prices, brands all present. Barcode field 0%.
Stub retailers (checkers, shoprite, pnp, spar)PassAll yield empty gracefully with reason logs.
Empty barcode inputPassThrows: "Barcode must be a non-empty string"
Null inputPassThrows correctly
Invalid barcode (letters)PassReturns null gracefully (255ms)
Invalid retailer namePassThrows with supported retailer list
Timeout handlingPassAbortController 5s, AbortError caught in both services

Recommendations

1. Use Woolworths Constructor.io as primary price source
Text search by product name returns ZAR prices reliably. Integrate into the scan flow: barcode → product name lookup → Woolworths price search. Cache prices for 24h.
2. Build a barcode-to-product mapping table
Seed with Open Food Facts SA data (~8,800 entries). Enrich organically via user scans — same model as MyFitnessPal's barcode database. This is the barcodes table in the recommended schema.
3. Approach Woolworths or Hirt & Carter formally
Hirt & Carter hold product image and barcode data for Checkers and other SA retailers, and are already a portfolio client of Taps'. A formal data partnership would solve both the ToS risk and the barcode coverage gap.
4. Evaluate Trundler for multi-retailer price comparison
Trundler covers Woolworths, Pick n Pay, and Makro. Paid service. Pricing TBD. Would enable "cheapest retailer for this item" — a high-value feature for Lerato. Consider for Sprint 6+.
5. Investigate FatSecret Premier barcode scope
FatSecret application submitted 2026-03-31. Premier Free tier unlocks barcode lookup. Confirmed SA brand coverage (Woolies, PnP, Checkers). If approved, this becomes Tier 2 in the resolver, above Open Food Facts.