Prices updated recently · 249 products tracked across 50 brands

API Documentation

ProteinPrice publishes free JSON data feeds. Checked daily via our scraper. 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 scraper. 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.

License. Free for commercial and non-commercial use. Attribution to ProteinPrice.com appreciated. No warranty: prices change frequently, always confirm at the retailer before purchase.

Authentication & rate limits

The 9 endpoints

#EndpointWhat it returnsUpdates
1/data/products.jsonProduct catalog with prices, specs, imagesEvery 2h
2/data/brands.jsonAll 28+ brands with descriptions and metadataOn change
3/data/retailers.json12 retailers we trackOn change
4/data/scope.jsonDefinitive scope of retailers, brands, products trackedOn change
5/data/scraper_status.jsonLive health: per-retailer success rates, last scrape timeEvery run (~hourly)
6/data/last_scrape.jsonPer-product, per-retailer last successful scrape timestampsEvery run
7/data/price_lows.jsonLowest price in last 30 days per productEvery run
8/data/price_history-YYYY-MM.jsonlAppend-only price change log (1 record per line)Every change
9/data/retailer_urls.jsonDirect PDP URLs per product per retailerOn change
ENDPOINT 1

GET /data/products.json

Checked daily · ~110 KB
https://proteinprice.com/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

FieldTypeDescription
idstringStable URL slug, unique per SKU.
brandIdstringForeign key to brands.json.
namestringProduct name as marketed.
categoryenumwhey-blend, whey-isolate, whey-concentrate, casein, plant, mass-gainer, collagen, clear-whey.
flavorstringFlavor variant.
sizeG / sizeLbnumberTub size in grams and pounds.
servingsintServings per container.
proteinGnumberGrams of protein per serving.
servingGnumberServing size in grams.
valueScoreint 0–100Normalized grams-of-protein-per-dollar score. Higher is better.
imgURLProduct hero image.
pricesobjectKeyed by retailer ID. Each entry: { price: number, inStock: boolean }.

Common use cases

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))
ENDPOINT 2

GET /data/brands.json

Updates on change · ~11 KB
https://proteinprice.com/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

FieldTypeDescription
idstringStable slug.
namestringFull brand name.
shortNamestring2–3 letter abbreviation used in UI.
descstringOne-line marketing description.
foundedintYear founded.
countryISO-2Country of brand HQ.
tubColor / lidColor / cardBgstyleVisual styling values for rendering brand cards.

Common use cases

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')
ENDPOINT 3

GET /data/retailers.json

Updates on change · ~2 KB
https://proteinprice.com/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

FieldTypeDescription
idstringStable slug: same key used inside each product's prices object.
namestringDisplay name.
shortNamestring1–3 character label.
color / bghexBrand color and background tint.
urlURLRetailer homepage URL.

Common use cases

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']}")
ENDPOINT 4

GET /data/scope.json

Updates on change · ~6 KB
https://proteinprice.com/data/scope.json

The definitive "lock-in" document for what ProteinPrice tracks. Lists active retailers, rejected retailers (and why), tracked brands, Phase 2 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

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])
ENDPOINT 5

GET /data/scraper_status.json

Updates every run · ~5 KB
https://proteinprice.com/data/scraper_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

FieldTypeDescription
lastRunISO-8601Timestamp of the most recent scraper invocation.
lastSuccessRatefloat 0–1Successes / attempts on the most recent run.
trendenumimproving, stable, or degrading.
recent10Runs[]arrayPer-run telemetry (timestamps, attempts, rejections by reason).
retailerHealthobjectPer-retailer status, 24h success rate, last successful scrape.
summarystringHuman-readable one-line status.

Common use cases

cURL

curl https://proteinprice.com/data/scraper_status.json

JavaScript

const s = await fetch('https://proteinprice.com/data/scraper_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/scraper_status.json').json()
blocked = [r for r, h in s['retailerHealth'].items() if h['status'] == 'blocked']
print('Blocked retailers:', blocked)
ENDPOINT 6

GET /data/last_scrape.json

Updates every run · ~3 KB
https://proteinprice.com/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

PathTypeDescription
{productId}objectKeyed by product ID (matches products.json).
{productId}.{retailerId}ISO-8601Timestamp of last successful scrape for that product at that retailer.

Common use cases

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)")
ENDPOINT 7

GET /data/price_lows.json

Updates every run · ~1 KB
https://proteinprice.com/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

FieldTypeDescription
30d_lowUSDLowest price observed in the last 30 days.
30d_low_dateISO-8601When that low was observed.
30d_low_retailerstringRetailer ID where the low was seen.
current_lowUSDToday's best price across all retailers.
discount_from_30dfloatFractional discount: (30d_low − current_low) / 30d_low. 0 means current price equals the 30d low.

Common use cases

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")
ENDPOINT 8

GET /data/price_history-YYYY-MM.jsonl

Append-only · monthly file
https://proteinprice.com/data/price_history-2026-05.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

FieldTypeDescription
tsISO-8601When the change was detected.
productIdstringProduct slug.
retailerstringRetailer ID.
oldPriceUSD or nullPrevious price (null on first observation).
newPriceUSDNew observed price.
sourceenumscrape, manual, api.
urlSourceenumpdp (direct product URL) or search (search-result fallback).

Common use cases

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')
ENDPOINT 9

GET /data/retailer_urls.json

Updates on change · ~19 KB
https://proteinprice.com/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

PathTypeDescription
coverage.{retailerId}intNumber of products with a verified PDP at that retailer.
urls.{productId}.{retailerId}URLDirect PDP URL.
Amazon is intentionally omitted (use Amazon PA-API for affiliate-compliant links).

Common use cases

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

Be polite. Reasonable behaviour means a polling interval of 15+ minutes, a descriptive User-Agent, and graceful retries. If you become a heavy consumer, drop us an email and we'll work with you.

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.