API Documentation
ProteinPrice publishes free JSON data feeds. Checked daily via our system. Use them however you want: AI training, price tracking apps, comparison tools, research.
Overview
ProteinPrice publishes free JSON data feeds covering the entire US retail protein market. Checked daily via our system. Use them however you want: AI training, price tracking apps, comparison tools, research.
All feeds are static files served from the same domain as the site. No SDK to install, no auth dance, no quota. Just GET a URL.
Authentication & rate limits
- No API key required. All endpoints are public, anonymous, and unauthenticated.
- No rate limits are currently enforced. Be reasonable: see etiquette below.
- All endpoints are served over HTTPS only.
- Standard HTTP caching headers apply. Most feeds set a reasonable
Cache-Control.
The 9 endpoints
| # | Endpoint | What it returns | Updates |
|---|---|---|---|
| 1 | /data/products.json | Product catalog with prices, specs, images | Every 2h |
| 2 | /data/brands.json | All 28+ brands with descriptions and metadata | On change |
| 3 | /data/retailers.json | 12 retailers we track | On change |
| 4 | /data/scope.json | Definitive scope of retailers, brands, products tracked | On change |
| 5 | /data/tracker_status.json | Live health: per-retailer success rates, last scrape time | Every run (~hourly) |
| 6 | /data/last_scrape.json | Per-product, per-retailer last successful scrape timestamps | Every run |
| 7 | /data/price_lows.json | Lowest price in last 30 days per product | Every run |
| 8 | /data/price_history-YYYY-MM.jsonl | Append-only price change log (1 record per line) | Every change |
| 9 | /data/retailer_urls.json | Direct PDP URLs per product per retailer | On change |
GET /data/products.json
The core feed. Every tracked SKU with current per-retailer prices, sizing, macros, an image URL, and our computed Value Score.
Example response
{
"lastUpdated": "2026-05-20T08:47:57Z",
"totalProducts": 199,
"products": [
{
"id": "on-gold-standard-whey-chocolate-5lb",
"brandId": "optimum-nutrition",
"name": "Gold Standard 100% Whey",
"category": "whey-blend",
"flavor": "Double Rich Chocolate",
"sizeG": 2270,
"sizeLb": 5,
"servings": 74,
"proteinG": 24,
"servingG": 30,
"valueScore": 94,
"img": "https://cdn.shopify.com/.../GSW_DRC_5lb_FOP.png",
"prices": {
"walmart": { "price": 54.99, "inStock": true },
"amazon": { "price": 55.49, "inStock": true }
}
}
]
}
Field schema
| Field | Type | Description |
|---|---|---|
id | string | Stable URL slug, unique per SKU. |
brandId | string | Foreign key to brands.json. |
name | string | Product name as marketed. |
category | enum | whey-blend, whey-isolate, whey-concentrate, casein, plant, mass-gainer, collagen, clear-whey. |
flavor | string | Flavor variant. |
sizeG / sizeLb | number | Tub size in grams and pounds. |
servings | int | Servings per container. |
proteinG | number | Grams of protein per serving. |
servingG | number | Serving size in grams. |
valueScore | int 0–100 | Normalized grams-of-protein-per-dollar score. Higher is better. |
img | URL | Product hero image. |
prices | object | Keyed by retailer ID. Each entry: { price: number, inStock: boolean }. |
Common use cases
- Build a price-comparison table or widget.
- Train an LLM on real protein-supplement pricing.
- Power a "best protein under $X" filter.
- Detect stockouts across retailers.
cURL
curl https://proteinprice.com/data/products.json
JavaScript (fetch)
const res = await fetch('https://proteinprice.com/data/products.json'); const data = await res.json(); // Sort by Value Score descending const ranked = data.products.sort((a, b) => b.valueScore - a.valueScore); console.log(ranked[0].name, ranked[0].valueScore);
Python
import requests data = requests.get('https://proteinprice.com/data/products.json').json() # Cheapest current price across all retailers, per product for p in data['products'][:5]: in_stock = [v['price'] for v in p['prices'].values() if v.get('inStock')] if in_stock: print(p['name'], '$', min(in_stock))
GET /data/brands.json
All 28+ brands with descriptions, founding year, country, and visual styling values (used to render brand cards on the site).
Example response
{
"lastUpdated": "2026-05-20T08:30:00Z",
"totalBrands": 34,
"brands": [
{
"id": "optimum-nutrition",
"name": "Optimum Nutrition",
"shortName": "ON",
"tubColor": ["#C0392B", "#7B241C"],
"lidColor": "#E74C3C",
"cardBg": "linear-gradient(145deg,#FDF5E8,#F5E6D0)",
"desc": "The world's best-selling protein brand...",
"founded": 1986,
"country": "US"
}
]
}
Field schema
| Field | Type | Description |
|---|---|---|
id | string | Stable slug. |
name | string | Full brand name. |
shortName | string | 2–3 letter abbreviation used in UI. |
desc | string | One-line marketing description. |
founded | int | Year founded. |
country | ISO-2 | Country of brand HQ. |
tubColor / lidColor / cardBg | style | Visual styling values for rendering brand cards. |
Common use cases
- Render a brand picker or directory.
- Resolve
brandIdon a product into a display name and logo styling. - Cross-reference founding year to filter "established brands only".
cURL
curl https://proteinprice.com/data/brands.json
JavaScript
const { brands } = await fetch('https://proteinprice.com/data/brands.json').then(r => r.json()); const byId = Object.fromEntries(brands.map(b => [b.id, b])); console.log(byId['optimum-nutrition'].name);
Python
import requests brands = requests.get('https://proteinprice.com/data/brands.json').json()['brands'] us_brands = [b for b in brands if b['country'] == 'US'] print(len(us_brands), 'US-based brands')
GET /data/retailers.json
All 12 US retailers we track. Each has an ID (used as a key in products.json → prices), a display name, a brand color, and a homepage URL.
Example response
{
"lastUpdated": "2026-05-20T08:00:00Z",
"totalRetailers": 12,
"retailers": [
{
"id": "walmart",
"name": "Walmart",
"shortName": "W",
"color": "#0071CE",
"bg": "#DBEAFE",
"url": "https://walmart.com"
}
]
}
Field schema
| Field | Type | Description |
|---|---|---|
id | string | Stable slug: same key used inside each product's prices object. |
name | string | Display name. |
shortName | string | 1–3 character label. |
color / bg | hex | Brand color and background tint. |
url | URL | Retailer homepage URL. |
Common use cases
- Render retailer logos / colored badges alongside prices.
- Resolve retailer ID → display name in a price-comparison UI.
cURL
curl https://proteinprice.com/data/retailers.json
JavaScript
const { retailers } = await fetch('https://proteinprice.com/data/retailers.json').then(r => r.json()); const retailerName = Object.fromEntries(retailers.map(r => [r.id, r.name]));
Python
import requests retailers = requests.get('https://proteinprice.com/data/retailers.json').json()['retailers'] for r in retailers: print(f"{r['id']:<18} {r['name']}")
GET /data/scope.json
The definitive "lock-in" document for what ProteinPrice tracks. Lists active retailers, rejected retailers (and why), tracked brands, expansion candidates, active categories, and scraping SLOs.
Example response (truncated)
{
"version": "1.0",
"lastUpdated": "2026-05-20T08:00:00Z",
"philosophy": "Lock-in document. Defines exactly which retailers, brands...",
"retailers": {
"active": [
{ "id": "amazon", "name": "Amazon", "tier": 1, "expectedAvailability": "blocked-without-api" },
{ "id": "walmart", "name": "Walmart", "tier": 1, "expectedAvailability": "blocked" }
],
"rejected": [
{ "id": "supplementwarehouse", "reason": "Smaller catalog..." }
],
"totalActive": 12
},
"brands": {
"tracked": ["optimum-nutrition", "dymatize", "myprotein", /* ... */],
"totalTracked": 28
},
"categoriesActive": ["whey-blend", "whey-isolate", "casein", /* ... */]
}
Common use cases
- Discover which retailers are intentionally not tracked and why.
- Find expansion candidate brands we plan to add.
- Read scraping SLO targets to understand how fresh the data is supposed to be.
cURL
curl https://proteinprice.com/data/scope.json
JavaScript
const scope = await fetch('https://proteinprice.com/data/scope.json').then(r => r.json()); console.log('Active retailers:', scope.retailers.active.map(r => r.id)); console.log('Tracked brands:', scope.brands.totalTracked);
Python
import requests scope = requests.get('https://proteinprice.com/data/scope.json').json() tier1 = [r for r in scope['retailers']['active'] if r['tier'] == 1] print('Tier-1 retailers:', [r['name'] for r in tier1])
GET /data/tracker_status.json
Live operational health of the scraping pipeline. Includes the last 10 runs, per-retailer 24h success rates, status flags, and a human-readable summary.
Example response
{
"lastRun": "2026-05-20T08:49:25Z",
"lastSuccessRate": 0.0,
"trend": "stable",
"recent10Runs": [
{
"runId": "2026-05-20T08:29:07Z",
"durationSeconds": 38,
"productsScraped": 3,
"totalAttempts": 16,
"successes": 1,
"successRate": 0.062,
"rejections": { "swing": 2, "blocked": 3, "failed_fetch": 6 }
}
],
"retailerHealth": {
"amazon": {
"status": "blocked",
"last24hSuccessRate": 0.408,
"last5RunsSuccessRate": 0.0,
"lastSuccess": "2026-05-20T08:37:43Z"
}
},
"summary": "Last run scraped 3 products. 0 retailers contributing real prices..."
}
Field schema
| Field | Type | Description |
|---|---|---|
lastRun | ISO-8601 | Timestamp of the most recent system update. |
lastSuccessRate | float 0–1 | Successes / attempts on the most recent run. |
trend | enum | improving, stable, or degrading. |
recent10Runs[] | array | Per-run telemetry (timestamps, attempts, rejections by reason). |
retailerHealth | object | Per-retailer status, 24h success rate, last successful scrape. |
summary | string | Human-readable one-line status. |
Common use cases
- Render an "is the data fresh?" badge in your app.
- Detect when a retailer has been blocked for >24h and fall back to alternatives.
- Build a status-page mirror without scraping us.
cURL
curl https://proteinprice.com/data/tracker_status.json
JavaScript
const s = await fetch('https://proteinprice.com/data/tracker_status.json').then(r => r.json()); const minutesSince = (Date.now() - new Date(s.lastRun)) / 60000; console.log(`Last scrape: ${minutesSince.toFixed(0)}m ago: ${s.summary}`);
Python
import requests s = requests.get('https://proteinprice.com/data/tracker_status.json').json() blocked = [r for r, h in s['retailerHealth'].items() if h['status'] == 'blocked'] print('Blocked retailers:', blocked)
GET /data/last_scrape.json
Per-product, per-retailer ISO timestamps recording the last successful price scrape. Use to tell which prices are fresh vs. stale.
Example response
{
"body-fortress-whey-protein-chocolate-5lb": {
"amazon": "2026-05-20T08:41:49Z",
"target": "2026-05-20T08:41:49Z",
"walmart": "2026-05-20T08:41:49Z"
},
"bsn-syntha6-chocolate-5lb": {
"amazon": "2026-05-20T08:37:43Z",
"bodybuilding": "2026-05-20T08:37:43Z",
"gnc": "2026-05-20T08:37:43Z"
}
}
Field schema
| Path | Type | Description |
|---|---|---|
{productId} | object | Keyed by product ID (matches products.json). |
{productId}.{retailerId} | ISO-8601 | Timestamp of last successful scrape for that product at that retailer. |
Common use cases
- Show a "last verified" badge next to each retailer price.
- Filter out prices older than N hours.
- Compute mean staleness per retailer over your catalog.
cURL
curl https://proteinprice.com/data/last_scrape.json
JavaScript
const last = await fetch('https://proteinprice.com/data/last_scrape.json').then(r => r.json()); const ts = last['on-gold-standard-whey-chocolate-5lb']?.amazon; console.log('Amazon price verified at:', ts);
Python
from datetime import datetime, timezone import requests last = requests.get('https://proteinprice.com/data/last_scrape.json').json() now = datetime.now(timezone.utc) for pid, retailers in last.items(): for r, ts in retailers.items(): age_h = (now - datetime.fromisoformat(ts.replace('Z', '+00:00'))).total_seconds() / 3600 if age_h > 24: print(f"{pid} / {r}: stale ({age_h:.1f}h)")
GET /data/price_lows.json
For each product, the lowest observed price in the last 30 days, plus the retailer and date: and the discount the current best price represents off that 30-day low.
Example response
{
"lastComputed": "2026-05-20T08:41:49Z",
"windowDays": 30,
"lows": {
"on-gold-standard-whey-chocolate-5lb": {
"30d_low": 56.99,
"30d_low_date": "2026-05-20T08:37:43Z",
"30d_low_retailer": "amazon",
"current_low": 56.99,
"discount_from_30d": 0.0
}
}
}
Field schema
| Field | Type | Description |
|---|---|---|
30d_low | USD | Lowest price observed in the last 30 days. |
30d_low_date | ISO-8601 | When that low was observed. |
30d_low_retailer | string | Retailer ID where the low was seen. |
current_low | USD | Today's best price across all retailers. |
discount_from_30d | float | Fractional discount: (30d_low − current_low) / 30d_low. 0 means current price equals the 30d low. |
Common use cases
- Show "lowest in 30 days" badges.
- Trigger a deal alert when
discount_from_30d ≥ 0.1(10%+ off). - Build a "near all-time low" list.
cURL
curl https://proteinprice.com/data/price_lows.json
JavaScript
const { lows } = await fetch('https://proteinprice.com/data/price_lows.json').then(r => r.json()); const deals = Object.entries(lows) .filter(([, v]) => v.discount_from_30d >= 0.1) .sort((a, b) => b[1].discount_from_30d - a[1].discount_from_30d); console.log('Best deals right now:', deals);
Python
import requests lows = requests.get('https://proteinprice.com/data/price_lows.json').json()['lows'] deals = [(pid, v) for pid, v in lows.items() if v['discount_from_30d'] >= 0.1] for pid, v in deals: print(f"{pid}: {v['discount_from_30d']*100:.0f}% off")
GET /data/price_history-YYYY-MM.jsonl
JSON-Lines append-only log of every detected price change. One record per line. Files rotate monthly (YYYY-MM). Replace 2026-05 with the year-month you want.
Example response (one line per change)
{"newPrice": 56.99, "oldPrice": 55.49, "productId": "on-gold-standard-whey-chocolate-5lb", "retailer": "amazon", "source": "scrape", "ts": "2026-05-20T08:37:43Z", "urlSource": "search"}
{"newPrice": 28.99, "oldPrice": 26.49, "productId": "on-gold-standard-whey-chocolate-2lb", "retailer": "amazon", "source": "scrape", "ts": "2026-05-20T08:37:43Z", "urlSource": "search"}
{"newPrice": 59.99, "oldPrice": 65.99, "productId": "dymatize-iso100-birthday-cake-5lb", "retailer": "amazon", "source": "scrape", "ts": "2026-05-20T08:37:43Z", "urlSource": "search"}
Field schema
| Field | Type | Description |
|---|---|---|
ts | ISO-8601 | When the change was detected. |
productId | string | Product slug. |
retailer | string | Retailer ID. |
oldPrice | USD or null | Previous price (null on first observation). |
newPrice | USD | New observed price. |
source | enum | scrape, manual, api. |
urlSource | enum | pdp (direct product URL) or search (search-result fallback). |
Common use cases
- Plot a price chart for any product / retailer.
- Compute volatility: how often does this retailer move price?
- Train a drop-prediction model on historical movement.
cURL
curl https://proteinprice.com/data/price_history-2026-05.jsonl
JavaScript
const txt = await fetch('https://proteinprice.com/data/price_history-2026-05.jsonl').then(r => r.text()); const rows = txt.trim().split('\n').map(JSON.parse); const forProduct = rows.filter(r => r.productId === 'on-gold-standard-whey-chocolate-5lb'); console.log(`${forProduct.length} price changes this month`);
Python
import json, requests url = 'https://proteinprice.com/data/price_history-2026-05.jsonl' rows = [json.loads(line) for line in requests.get(url).text.splitlines() if line] drops = [r for r in rows if r['oldPrice'] and r['newPrice'] < r['oldPrice']] print(len(drops), 'price drops this month')
GET /data/retailer_urls.json
Per-product, per-retailer verified product-detail-page URLs. Useful for building "buy at Walmart / iHerb / GNC" buttons that go directly to the right PDP.
Example response
{
"lastUpdated": "2026-05-20T09:30:00Z",
"totalProducts": 86,
"coverage": {
"walmart": 32, "iherb": 19, "gnc": 14,
"bodybuilding": 34, "target": 9
},
"urls": {
"on-gold-standard-whey-chocolate-5lb": {
"walmart": "https://www.walmart.com/ip/.../32686992",
"iherb": "https://www.iherb.com/pr/.../27509",
"gnc": "https://www.gnc.com/whey-protein/GoldStandard.html",
"bodybuilding": "https://www.bodybuilding.com/store/.../gold-standard-whey-protein.html",
"target": "https://www.target.com/p/.../A-89392663"
}
}
}
Field schema
| Path | Type | Description |
|---|---|---|
coverage.{retailerId} | int | Number of products with a verified PDP at that retailer. |
urls.{productId}.{retailerId} | URL | Direct PDP URL. |
| Amazon is intentionally omitted (use Amazon PA-API for affiliate-compliant links). | ||
Common use cases
- Build "Buy at X" affiliate buttons that link straight to the product.
- Skip Google for users who already know what they want.
- Cross-check that a brand sells the same SKU at multiple retailers.
cURL
curl https://proteinprice.com/data/retailer_urls.json
JavaScript
const { urls } = await fetch('https://proteinprice.com/data/retailer_urls.json').then(r => r.json()); const walmartUrl = urls['on-gold-standard-whey-chocolate-5lb']?.walmart;
Python
import requests data = requests.get('https://proteinprice.com/data/retailer_urls.json').json() print('Coverage per retailer:', data['coverage']) print('Walmart URL:', data['urls']['on-gold-standard-whey-chocolate-5lb']['walmart'])
Use cases & recipes
Five concrete recipes that combine the endpoints above.
1. Find the cheapest product right now
Load products.json, compute the in-stock minimum across each product's prices, then sort.
import requests products = requests.get('https://proteinprice.com/data/products.json').json()['products'] def min_price(p): in_stock = [v['price'] for v in p['prices'].values() if v.get('inStock')] return min(in_stock) if in_stock else float('inf') cheapest = sorted(products, key=min_price)[:10] for p in cheapest: print(f"${min_price(p):>6.2f} {p['name']} ({p['flavor']})")
2. Track price drops over time
Read the monthly price_history-YYYY-MM.jsonl file and filter on price decreases.
import json, requests url = 'https://proteinprice.com/data/price_history-2026-05.jsonl' rows = [json.loads(l) for l in requests.get(url).text.splitlines() if l] drops = [r for r in rows if r['oldPrice'] and r['newPrice'] < r['oldPrice']] drops.sort(key=lambda r: (r['newPrice'] - r['oldPrice']) / r['oldPrice']) for d in drops[:5]: pct = (d['newPrice'] - d['oldPrice']) / d['oldPrice'] * 100 print(f"{pct:+.1f}% {d['productId']} @ {d['retailer']}")
3. Build a Value Score leaderboard
The valueScore field already encodes "g of protein per dollar at best retailer," normalized 0–100. Just sort descending.
const { products } = await fetch('/data/products.json').then(r => r.json()); const top = products .filter(p => Object.values(p.prices).some(v => v.inStock)) .sort((a, b) => b.valueScore - a.valueScore) .slice(0, 20); console.table(top.map(p => ({ name: p.name, score: p.valueScore })));
4. Find the best retailer for a brand
Join products.json with itself on brandId, then count which retailer is cheapest most often.
import requests, collections products = requests.get('https://proteinprice.com/data/products.json').json()['products'] brand = 'optimum-nutrition' wins = collections.Counter() for p in products: if p['brandId'] != brand: continue instock = {r: v['price'] for r, v in p['prices'].items() if v.get('inStock')} if instock: wins[min(instock, key=instock.get)] += 1 print(f"Cheapest retailer for {brand}: {wins.most_common(3)}")
5. Build a custom price alert system
Poll price_lows.json daily. When discount_from_30d crosses your threshold for a product on your watchlist, send a notification.
import requests WATCHLIST = {'on-gold-standard-whey-chocolate-5lb', 'dymatize-iso100-chocolate-5lb'} THRESHOLD = 0.10 # 10% off 30-day low lows = requests.get('https://proteinprice.com/data/price_lows.json').json()['lows'] for pid in WATCHLIST & lows.keys(): v = lows[pid] if v['discount_from_30d'] >= THRESHOLD: print(f"ALERT: {pid} is {v['discount_from_30d']*100:.0f}% off: ${v['current_low']}") # send_email() / send_push() / etc.
Rate limits & etiquette
- Cache responses for at least 15 minutes. The data only updates on each update cycle: anything faster is wasted.
- Identify your tool with a
User-Agentif you're at scale.MyProteinTracker/1.2 (contact@example.com)is fine. - If a feed errors transiently, back off and retry: don't hammer.
- If you're building something cool, email us at hello@proteinprice.com: we'd love to know.
Versioning
Current version: v1 (implicit: all paths above are unversioned). We treat the v1 schema as a stable contract.
When we ship a breaking change, a /api/v2/ tree will appear and the old paths will continue to work in parallel. v1 stays alive for at least 12 months after v2 launches.
Non-breaking changes (new optional fields, new endpoints, more products) ship at any time. Subscribe to our blog or follow our repo for change announcements.
Built something with our data?
We'd love to hear about it: and possibly feature it.