SA retailer API landscape: what's accessible, what's blocked, what's next
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.
Five SA retailers and two international barcode databases were probed. Results:
Woolworths runs Constructor.io for product search and discovery. The API key is embedded in their public frontend. Two endpoints available:
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%.| 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.
Three real SA EAN-13 barcodes (prefix 600x) were tested against all sources. Results:
| Barcode | Woolworths | Open Food Facts | Resolved | Time |
|---|---|---|---|---|
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 |
Two services were produced as part of the spike, ready to integrate into the main app:
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.
barcodes tablebarcodes table. Builds SA barcode DB organically.| Risk | Severity | Mitigation |
|---|---|---|
| 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. |
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.
| Test | Result | Note |
|---|---|---|
| Real SA barcodes (3) | 0/3 resolved | Expected. No source supports EAN-indexed SA lookup. |
| Woolworths text search (4 queries) | 4/4 found | Results with prices returned. Relevance mixed. |
| Catalog ingester — dairy (30 products) | Pass | Names, prices, brands all present. Barcode field 0%. |
| Stub retailers (checkers, shoprite, pnp, spar) | Pass | All yield empty gracefully with reason logs. |
| Empty barcode input | Pass | Throws: "Barcode must be a non-empty string" |
| Null input | Pass | Throws correctly |
| Invalid barcode (letters) | Pass | Returns null gracefully (255ms) |
| Invalid retailer name | Pass | Throws with supported retailer list |
| Timeout handling | Pass | AbortController 5s, AbortError caught in both services |
barcodes table in the recommended schema.