15CARTS ok Sign in

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

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}h or {N}d. Defaults to 24h.
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

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.