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-buy | Stores carrying a brand, or one product |
GET /api/radar/brands/{slug}/products/top | A brand's products ranked by door count |
GET /api/radar/brands/{slug}/schema.jsonld | schema.org Product + offers for one product |
GET /api/radar/brands | Search 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.
lastSeenAtis when a listing was last scraped present.inStock: nullmeans 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− collapse
Base and auth
- Base URL
https://api.fuzzy.so. All public routes areGETunder/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.comheader 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.issueRateand 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_lookupwhen a verified link is needed.