API
Every report page on this site has a JSON twin at the same path under /api/.
Same query params, no auth, CORS open. Data updates every 30 min during peak
hours and hourly off-peak. If you're building something on top of this, please cache
aggressively; if you need higher freshness or a stable contract, say hi.
Heads up: when the soft paywall launches (see /pricing),
full /api/ responses will require an account token; an anonymous free slice will
remain. Plan migrations accordingly. This page updates at the same time the gating ships.
Conventions
- Base URL:
https://15carts.com - All responses are
application/json; charset=utf-8. - No auth headers. No API key. If that changes, this page changes first.
- Soft rate limit: 60 requests/minute per IP. Bursts above that return
429withRetry-After: 60. Cache aggressively if you're scraping. - Prices are integer cents under
*_centskeys. THC potency is a float percent. - Timestamps are UTC ISO-8601 with
Zsuffix. - Shop identifiers are kebab-case slugs (e.g.
pure-new-buffalo). See/api/shops/summaryfor the canonical list.
GET /api/shops/summary
One entry per shop. Name, slug, address, lat/lon, road cluster, best 15-pack, best ounce, current specials count, outbound link to the shop's own menu.
curl https://15carts.com/api/shops/summary
{
"shops": [
{
"slug": "blow",
"name": "Blow Cannabis",
"address": "19839 US-12, New Buffalo, MI, 49117",
"lat": 41.761329, "lon": -86.798847,
"cluster": "US-12 south", "miles_to_i94": 3,
"menu_url": "https://dutchie.com/dispensary/blow",
"inventory": 167,
"best_15pack": {"total_cents": 9000, "cents_per_mg": 0.652, "avg_thc": 91.96, "n": 15},
"best_oz": {"price_cents": 1500, "product_name": "Huff | 28g Prepack Shake | ...",
"brand": "Huff", "on_special": false},
"specials": {"count": 927, "top_discount_pct": 70.0, "top_discount": { /* ... */ }},
"website": null
}
// 31 more shops
]
}
GET /api/carts/15pack
Cheapest 15×1g cart basket per shop, ranked by $/mg THC. The canonical "max-out a Michigan recreational visit" report.
pack- Integer. Number of carts in the basket. Defaults to
15(the legal limit). require_thc- Boolean. Drop SKUs with no posted THC percentage. Defaults to
false. no_disposables- Boolean. Drop disposable carts; keep only screw-tip 510-thread. Defaults to
false. brand- String. Filter to one brand. Case-insensitive substring match. Defaults to all brands.
curl 'https://15carts.com/api/carts/15pack?require_thc=true&no_disposables=true'
{
"filters": {"require_thc": false, "no_disposables": false, "pack_size": 15, "brand": null},
"baskets": [
{
"shop": "pure-new-buffalo",
"shop_name": "Pure New Buffalo",
"shop_url": "https://dutchie.com/dispensary/pure-new-buffalo",
"cheap": {"n": 15, "total_cents": 3975, "cents_per_mg": 0.289, "avg_thc": 91.6},
"thc": {"n": 15, "total_cents": 3975, "cents_per_mg": 0.289, "avg_thc": 91.7},
"cheap_basket": [
{"name": "Bossy Cart - Blueberry Pancake", "brand": "Bossy 1g Cart",
"weight": "1g", "price_cents": 265, "list_price_cents": 379,
"thc_pct": 93.45, "thc_mg": 934.5, "cents_per_mg": 0.284,
"on_special": true, "disposable": false /* ... */}
// 14 more SKUs
]
}
// more shops, sorted by cents_per_mg ascending
],
"county_top": [/* top 15 cart SKUs across the whole county */]
}
GET /api/bulk-deals/{category}
Where the larger pack beats N× smaller, by category.
category- Path segment. One of
flower,concentrates,carts. min_savings- Float. Minimum dollar savings to include. Defaults to
5.0. shop- String. Shop slug to filter to. Defaults to all shops.
curl 'https://15carts.com/api/bulk-deals/flower?min_savings=10'
{
"filters": {"category": "flower", "min_savings_pct": 5.0, "shop": null},
"within_shop": [
{
"shop": "mango-cannabis-michigan-2",
"name": "Apple Fritter Shake - Kutz Premium Cannabis",
"buy": {"weight": "1oz", "grams": 28.0, "price_cents": 1500},
"vs": {"weight": "1g", "grams": 1.0,
"unit_price_cents": 225, "stack_units": 28, "stack_price_cents": 6300},
"savings_pct": 76.19
}
// ...
],
"cross_shop": [/* same shape, but "buy" and "vs" come from different shops */]
}
GET /api/restocks
Products that flipped from out-of-stock back to in-stock within the lookback window.
since- String. Lookback window. Format
{N}hor{N}d. Defaults to24h. limit- Integer. Max rows. Defaults to
50.
curl 'https://15carts.com/api/restocks?since=6h&limit=20'
{
"filters": {"since": "24h", "limit": 50},
"items": [
{
"shop": "vibe-new-buffalo", "shop_name": "Vibe New Buffalo",
"shop_url": "https://dutchie.com/dispensary/vibe-new-buffalo",
"product_id": "69c0fa3c9e20329439bcaa27",
"product_url": "https://dutchie.com/dispensary/vibe-new-buffalo/product/69c0fa3c9e20329439bcaa27",
"name": "Apple Tartz | Bulk Moon Rocks | REC",
"brand": "Mitten Extracts", "category": "Flower", "weight": "1/8oz",
"price_cents": 3938, "list_price_cents": 5250,
"on_special": true,
"captured_at": "2026-05-18T20:52:41+00:00"
}
// ...
]
}
GET /api/edibles/cheapest
Edibles sorted by $/mg THC, plus the bulk-pack-beats-smaller comparisons.
min_mg- Integer. Filter to products with at least this total THC. Defaults to
0. limit- Integer. Max rows. Defaults to
50.
curl 'https://15carts.com/api/edibles/cheapest?min_mg=200&limit=25'
{
"filters": {"min_mg": 0, "limit": 50},
"cheapest": [
{
"shop": "supergood-new-buffalo", "shop_name": "Supergood New Buffalo",
"name": "Nibblez | Fruit Punch | 200mg Gummies",
"brand": "Nibblez", "mg": 3028,
"price_cents": 240, "list_price_cents": 300,
"cents_per_mg": 0.079, "on_special": true
}
// ...
],
"bulks": [/* bulk-pack-beats-N-smaller-packs comparisons */]
}
GET /api/search
Substring match on product name and brand across every shop's current menu.
q- String. Min two characters to return results.
limit- Integer. Max rows. Defaults to
60.
curl 'https://15carts.com/api/search?q=papaya'
{
"query": "papaya",
"groups": [
{
"key": "f0b8b3b751",
"name": "2g MKX Disposable - Papaya Bomb 2g Disposable - Hybrid",
"brand": "MKX Oil Co.",
"category": "Vaporizers", "weight": "2g",
"thc_pct": 91.2,
"shop_count": 4, "in_stock_count": 3,
"best_price_cents": 2999, "best_shop": "vibe-new-buffalo",
"spread_cents": 1101, "on_special_anywhere": true
}
// ...
],
"total_groups": 12, "limit": 60, "truncated": false, "too_short": false
}
GET /api/p/{key}
Per-product detail: latest snapshot per shop, price history, $/g normalization, in-stock status. Keys come back from /api/search and the report endpoints.
curl https://15carts.com/api/p/some-product-key
{
"key": "f0b8b3b751",
"history_days": 7,
"group": {
"key": "f0b8b3b751",
"name": "2g MKX Disposable - Papaya Bomb 2g Disposable - Hybrid",
"brand": "MKX Oil Co.",
"shops": [
{
"shop_slug": "vibe-new-buffalo", "shop_name": "Vibe New Buffalo",
"price_cents": 2999, "special_price_cents": null,
"in_stock": true, "product_id": "...",
"history": [
{"captured_at": "2026-05-12T12:00:00+00:00",
"price_cents": 3299, "special_price_cents": null,
"effective_cents": 3299, "in_stock": true}
// up to history_days of snapshots
]
}
// more shops carrying this SKU
]
}
}
GET /health
Status of the data pipeline. Returns ok, stale, or failing plus the timestamp of the last successful update. Cheap to poll.
curl https://15carts.com/health
{
"status": "ok",
"consecutive_failures": 0,
"dispensaries_registered": 32,
"latest_snapshot_age_minutes": 5.9,
"last_successful_run": {
"id": 146,
"started_at": "2026-05-18T20:46:42+00:00",
"ended_at": "2026-05-18T20:52:58+00:00",
"trigger": "startup", "status": "ok",
"shops_ok": 32, "shops_failed": 0,
"products_total": 56656, "snapshots_total": 81190,
"error": null
},
"last_run": {/* shape same as last_successful_run */},
"snapshots_total": 9327432
}
Other public endpoints
GET /sitemap.xml— every indexable page on this site, withlastmodtimestamps. Useful if you're crawling.GET /robots.txt— standard crawler directives./adminand/age-gatedisallowed.
Stability
This site is one person's side project. Endpoints and response shapes are stable in the sense that I don't break them on purpose — but there's no formal versioning. If you build something load-bearing on top, pin a cache and drop a note via /contact so I can warn you before any breaking change.