fuzzy
Developers / REST API

REST API

Plain HTTP over the same live menus Fuzzy scans. Read-only, CORS-open, and no API key today, so you can call it straight from a browser.

Quickstart

const res = await fetch(
  "https://api.fuzzy.so/api/radar/brands/heavy-hitters/where-to-buy?state=CA"
);
const { stores, total } = await res.json();

Building a React site? The React kit wraps these calls in a ready-made store locator. Answering questions in an AI client? Use the MCP server.

Endpoints

GET /api/radar/brands/{slug}/where-to-buyStores carrying a brand, or one product
GET /api/radar/brands/{slug}/products/topA brand's products ranked by door count
GET /api/radar/brands/{slug}/schema.jsonldschema.org Product + offers for one product
GET /api/radar/brandsSearch or list brands
GET /api/radar/brands/{slug}Brand detail: doors, states, platforms, issues
GET /api/radar/retailers/{slug}A retailer's menu: brands carried, prices, issues
GET /api/radar/operators/{slug}A multi-location operator: banners, house brands

Full spec: api.fuzzy.so/docs.

API keys

Nothing needs a key today. That is ending: a key will be required for direct access to /api/radar/* (no date set), while app.fuzzy.so stays free to browse. Create an account or sign in to make one now, then send it as X-API-Key.

Good to know

  • Coverage is live dispensary menus on Dutchie and Jane, nationwide. Not Leafly, not a store's own site. A door count is a floor, not a census.
  • “Current” means the last completed scrape of that retailer, not a point-of-sale feed. Scrapes run on their own cadence, not a fixed day.
  • lastSeenAt is when a listing was last scraped present. inStock: null means the platform does not expose inventory; we never guess a number.
  • Issue rates are 0–1 fractions, not percentages. Figures drift with every scrape, so re-pull before quoting one.
For agents: full setup details+ expand

Base and auth

  • Base URL https://api.fuzzy.so. All public routes are GET under /api/radar, CORS-open to any origin.
  • No auth required today. Optional X-API-Key: fz_live_.... Scopes: read:radar (everything here), read:movement (door-level movement on brand pages). Keys can carry an expiry; per-key rate limits are not enforced yet.
  • Attribution: X-Fuzzy-Site: yourdomain.com header or ?site= (header wins if both are sent).
  • OpenAPI: https://api.fuzzy.so/openapi.json. It also lists non-public routes; use only the ones under /api/radar.

Parameters

GET /api/radar/brands/{slug}/where-to-buy
  product   product_cluster_id; narrows to stores carrying that exact product
  state     two-letter region code
  city      raw retailer city, case-insensitive exact match (not normalized)
  site      attribution, same as X-Fuzzy-Site
  page      default 1
  limit     default 25, max 100
  -> { stores[], total }

GET /api/radar/brands/{slug}/products/top
  state     two-letter code; omit for the national rollup
  category  e.g. vape, flower, edible
  limit     default 20, max 100
  -> { products[]: { name, doors, priceRange, thc, category, deepLink, ... }, total }

GET /api/radar/brands/{slug}/schema.jsonld?product={clusterId}
  -> schema.org Product + AggregateOffer, up to 100 Offer entries;
     offerCount is always the true store count

GET /api/radar/brands
  search, platform, state, category, sort, order, page, limit

GET /api/radar/brands/{slug}
GET /api/radar/retailers/{slug}
GET /api/radar/operators          (list; page, limit)
GET /api/radar/operators/{slug}

Example response (top products, trimmed)

{
  "products": [
    {
      "name": "Acapulco Gold | Ultra Extract High Purity Oil - 1G Vape",
      "doors": 94,
      "priceRange": { "min": "31.80", "median": "60", "max": "86.00" },
      "thc": "88.9",
      "category": "vape",
      "deepLink": "https://dutchie.com/dispensary/strain-stars/..."
    }
  ],
  "total": 3902
}

Errors

Every error is RFC 7807 Problem Details JSON:

{
  "type": "https://fuzzy.dev/errors/not-found",
  "title": "Brand not found",
  "status": 404,
  "instance": "http://api.fuzzy.so/api/radar/brands/not-a-real-brand"
}

Caching and limits

  • No hard rate limit today.
  • Cache-Control: 60 s on where-to-buy, 5 min on top products and schema.jsonld. Search and detail routes set none; cache them yourself if you call them in a loop.
  • issueRate and similar rates are 0–1 fractions; multiply by 100 for a percentage.
  • Figures change with every scrape. Re-pull before quoting one, and prefer the MCP's fuzzy_lookup when a verified link is needed.