Products Endpoint

Search for cannabis products across thousands of dispensaries. Filter by location, category, brand, potency, price, and more.


Request

GET https://api.cannmenus.com/v2/products

V1 is also available at /v1/products — see V1 Legacy Format below.


Core Parameters

  • Name
    states
    Type
    string
    Description

    US state(s) or Canadian province(s) to search, comma-separated. Use full names (e.g., "California", "New Jersey", "Ontario"), not abbreviations like "NJ". The parameter is states (plural); state is not a Products parameter. Optional in V2 (but recommended for performance). Required in V1 unless lat, lng, and distance are all provided.

    states=California
    states=California,Oregon
    states=New%20Jersey
    

Location Parameters

Narrow results by geographic area:

  • Name
    lat
    Type
    number
    Description

    Latitude coordinate for location-based search. Must be used with lng.

  • Name
    lng
    Type
    number
    Description

    Longitude coordinate for location-based search. Must be used with lat.

  • Name
    distance
    Type
    number
    Description

    Radius in miles from the lat/lng point. Optional.


Product Filters

  • Name
    q
    Type
    string
    Description

    Text search on product name and brand name. Use the dedicated parameters (category, tags, skus, location) to filter on other fields.

    q=blue dream
    
  • Name
    category
    Type
    string
    Description

    Product category. See Categories reference for valid values.

    category=Flower
    category=Edible
    category=Vape
    
  • Name
    subcategory
    Type
    string
    Description

    Product subcategory. See Subcategories reference for valid values.

    subcategory=Gummy
    subcategory=Cartridge
    
  • Name
    tags
    Type
    string
    Description

    Product tags for detailed filtering, comma-separated. Products matching any listed tag are returned. See Tags reference for valid values.

    tags=Live%20Resin
    tags=Indica,Sugar%20Free
    
  • Name
    brand_name
    Type
    string
    Description

    Filter by brand name. Partial matching supported.

    brand_name=Stiiizy
    brand_name=Cookies
    
  • Name
    brands
    Type
    string
    Description

    Filter by brand IDs, comma-separated (faster than brand_name). Get IDs from the Brands endpoint.

    brands=789
    brands=789,1042
    
  • Name
    retailers
    Type
    string
    Description

    Filter to specific retailer IDs, comma-separated. Use the Retailers endpoint to find IDs.

    retailers=10600
    retailers=10600,10601
    
  • Name
    product_name
    Type
    string
    Description

    Search product names. Partial matching supported.

    product_name=Blue%20Dream
    
  • Name
    skus
    Type
    string
    Description

    Filter by specific Cann SKU IDs, comma-separated.

    skus=SKU123
    skus=SKU123,SKU456
    
  • Name
    display_weight
    Type
    string
    Description

    Filter by exact product weight/size.

    display_weight=3.5g
    
  • Name
    quantity_per_package
    Type
    number
    Description

    Filter by package quantity (e.g., number of gummies in a pack).

    quantity_per_package=10
    
  • Name
    menu_provider
    Type
    string
    Description

    Filter by menu data source. Options: Dutchie, Weedmaps, Leafly, IHeartJane.

    menu_provider=Dutchie
    

Potency Filters

Filter by exact cannabinoid content:

  • Name
    percentage_thc
    Type
    number
    Description

    Filter by exact THC percentage. For flower, concentrates, vapes.

    percentage_thc=25.5
    
  • Name
    percentage_cbd
    Type
    number
    Description

    Filter by exact CBD percentage.

    percentage_cbd=1.0
    
  • Name
    mg_thc
    Type
    number
    Description

    Filter by exact THC milligrams. For edibles, tinctures, topicals.

    mg_thc=100
    
  • Name
    mg_cbd
    Type
    number
    Description

    Filter by exact CBD milligrams.

    mg_cbd=50
    

Price Filter

  • Name
    latest_price
    Type
    number
    Description

    Filter by exact product price in dollars.

    latest_price=45.00
    

  • Name
    recreational
    Type
    boolean
    Description

    Filter to recreational products only.

    recreational=true
    
  • Name
    medical
    Type
    boolean
    Description

    Filter to medical products only.

    medical=true
    

Inventory & Source Filters

  • Name
    include_below_threshold
    Type
    boolean
    Description

    Whether to include listings flagged is_below_threshold=true. Default: true. Set to false to exclude them entirely. Each returned product carries the boolean on its listing so you can surface an in-store-only warning in your UI.

    What the flag means: the listing has been removed from the partner's online ecommerce menu (e.g. Dutchie) but the product may still have in-store inventory. The url returned for these listings is typically a dead link.

    UX guidance: if you render these listings, do NOT link the url without a clear warning such as "In-store only — removed from online menu." Consider replacing the external CTA with a non-clickable label so users aren't sent to a broken ecommerce page.

    include_below_threshold=false
    
  • Name
    include_all_menu_providers
    Type
    boolean
    Description

    Include listings from all menu data sources. Default: false (returns only the best source per retailer for faster results).

    include_all_menu_providers=true
    

Terpene Profiles

Terpene data is opt-in. Add include_terpenes=true to a /v2/products or /v1/products request to get each listing's terpene profile.

  • Name
    include_terpenes
    Type
    boolean
    Description

    Return terpene profiles. Default: false. With true, each listing that has a measured profile gets a terpene_profile object and terpene_source: "exact". Listings without a profile return null in both fields.

    include_terpenes=true
    
  • Name
    include_terpene_fallback
    Type
    boolean
    Description

    Applies only with include_terpenes=true. Default: false. When a listing has no profile of its own, the API looks for one on another listing of the same product (the same Meta SKU) in the same state. A profile found this way has terpene_source: "uuid_match", and terpene_source_product_id holds the ID of the listing it came from. That listing may be at a different store, so its profile describes the same product but not necessarily the same batch.

    include_terpenes=true&include_terpene_fallback=true
    

Terpene Response Fields

FieldTypeDescription
terpene_profileobject or nullMaps each terpene name to its concentration as a fraction of product weight (0 to 1). Multiply by 100 for a percentage: 0.0052 is 0.52%. null when the listing has no profile, or when include_terpenes isn't set.
terpene_sourcestring or null"exact": measured on this listing. "uuid_match": taken from another listing of the same product in the same state (only with include_terpene_fallback=true). null when no profile was found for the listing, or when include_terpenes isn't set.
terpene_source_product_idnumber or nullThe ID of the listing the profile came from when terpene_source is "uuid_match". Otherwise null, including on every "exact" profile.

Reading a profile:

  • Keys are terpene names as published on the dispensary's menu, such as Myrcene, Limonene, Beta-Caryophyllene, Alpha-Pinene, and Linalool. Names aren't fully standardized across sources (both Bisabolol and AlphaBisabolol appear, for example), so normalize names before you aggregate across products.
  • A profile lists only the terpenes with a reported value for that listing. A terpene that's missing from the object has no value in the response; don't treat it as zero.
  • Profiles belong to listings, not products. The same SKU at two stores can have slightly different profiles, and one store's listing can have a profile while another's has none.

Coverage

Most listings don't have a terpene profile, so many rows, sometimes a whole page, come back with terpene_profile: null even when the request is correct. Profiles come from lab results that dispensaries publish on their online menus. Coverage depends on menu provider, state, and category, and it changes as dispensaries update their menus.

Menu provider. Profiles currently come only from listings whose menu_provider is Dutchie or IHeartJane. Listings from other providers, including Weedmaps, have no terpene data. In some states, such as California and Colorado, even Dutchie listings rarely carry profiles.

State. Based on a sample of in-stock listings taken in October 2026:

CoverageStates and provinces
HigherConnecticut, Maryland, Nevada, New Jersey, New York, Ohio, Pennsylvania
ModerateMassachusetts, Michigan, Missouri
LowArizona, Florida, Illinois, Mississippi, Oklahoma
Rare or noneAlaska, California, Colorado, Maine, New Mexico, Oregon, Washington, and the Canadian provinces sampled (Alberta, British Columbia, Manitoba, Newfoundland and Labrador, Ontario, Saskatchewan)

Even in the higher-coverage states, many listings have no profile. States that aren't listed had too few listings in the sample to rate.

Category. Flower and concentrates have profiles most often, followed by vapes and pre-rolls. Tinctures, topicals, and edibles rarely have one, and Other (accessories) almost never.

Getting More Terpene Data

  • Filter to the providers that publish terpenes. Add menu_provider=Dutchie or menu_provider=IHeartJane. menu_provider takes one value per request and is case-sensitive: menu_provider=dutchie returns no results.
  • Request the categories that carry profiles: Flower, Concentrate, Vape, and Pre-roll.
  • Don't judge coverage from one page. Results aren't ordered by data completeness, so one page can be all null even in a higher-coverage state. Check several pages, or filter by provider, before you conclude a market has no terpene data.
  • Add include_terpene_fallback=true. Listings without a profile of their own can take one from another listing of the same product in the same state.
  • Expect new listings to lag. Terpene profiles are refreshed on their own schedule, less often than menu listings, and there is no guaranteed refresh interval. A product that just appeared on a menu can show null until the next terpene refresh.

Terpene profiles aren't available from:

Terpene Examples

Flower in Maryland with terpene profiles:

curl "https://api.cannmenus.com/v2/products?states=Maryland&category=Flower&per_page=20&include_terpenes=true" \
  -H "X-Token: YOUR_API_TOKEN"

Only Dutchie listings, which carry most terpene data:

curl "https://api.cannmenus.com/v2/products?states=Maryland&category=Flower&menu_provider=Dutchie&per_page=20&include_terpenes=true" \
  -H "X-Token: YOUR_API_TOKEN"

Only IHeartJane listings, in Nevada:

curl "https://api.cannmenus.com/v2/products?states=Nevada&category=Flower&menu_provider=IHeartJane&per_page=20&include_terpenes=true" \
  -H "X-Token: YOUR_API_TOKEN"

Both flags, for pre-rolls in New Jersey:

curl "https://api.cannmenus.com/v2/products?states=New%20Jersey&category=Pre-roll&per_page=20&include_terpenes=true&include_terpene_fallback=true" \
  -H "X-Token: YOUR_API_TOKEN"

The same Dutchie request on V1:

curl "https://api.cannmenus.com/v1/products?states=Maryland&category=Flower&menu_provider=Dutchie&per_page=20&include_terpenes=true" \
  -H "X-Token: YOUR_API_TOKEN"

A listing from the Dutchie request (abridged):

{
  "retailer_id": "3511",
  "sku": "00fkdAaOGVYObviGUYD5ffRp",
  "products": [
    {
      "brand_name": "Curio Wellness",
      "product_name": "Hot Wing Baby Buds",
      "category": "Flower",
      "menu_provider": "Dutchie",
      "terpene_profile": {
        "Limonene": 0.0094,
        "Myrcene": 0.0052,
        "Beta-Caryophyllene": 0.0033,
        "Linalool": 0.0024,
        "Beta-Pinene": 0.0012
      },
      "terpene_source": "exact",
      "terpene_source_product_id": null
    }
  ]
}

Print the top three terpenes, as percentages, for each listing that has a profile:

import requests

resp = requests.get(
    "https://api.cannmenus.com/v2/products",
    headers={"X-Token": "YOUR_API_TOKEN"},
    params={
        "states": "Maryland",
        "category": "Flower",
        "menu_provider": "Dutchie",
        "per_page": 20,
        "include_terpenes": "true",
    },
)
resp.raise_for_status()

for entry in resp.json()["data"]:
    for listing in entry["products"]:
        profile = listing["terpene_profile"]
        if not profile:
            continue  # no measured profile for this listing
        top = sorted(profile.items(), key=lambda kv: kv[1], reverse=True)[:3]
        print(
            listing["product_name"],
            listing["terpene_source"],
            ", ".join(f"{name} {value * 100:.2f}%" for name, value in top),
        )

Pagination

  • Name
    page
    Type
    number
    Description

    Page number to retrieve. Default: 1.

  • Name
    per_page
    Type
    number
    Description

    Results per page. Default: 20. Max: 200. Use higher values to reduce total API calls for bulk pulls.

    per_page=200
    

Example Requests

Basic Search

Search for flower in California:

curl "https://api.cannmenus.com/v2/products?states=California&category=Flower&page=1" \
  -H "X-Token: YOUR_API_TOKEN"

Location-Based Search

Find edibles within 5 miles of Denver:

curl "https://api.cannmenus.com/v2/products?states=Colorado&category=Edible&lat=39.7392&lng=-104.9903&distance=5&page=1" \
  -H "X-Token: YOUR_API_TOKEN"

Search by Brand Name

Find Stiiizy products in California:

curl "https://api.cannmenus.com/v2/products?states=California&brand_name=Stiiizy&page=1" \
  -H "X-Token: YOUR_API_TOKEN"

Text Search

Search for "blue dream" in product and brand names:

curl "https://api.cannmenus.com/v2/products?states=Washington&q=blue%20dream&page=1" \
  -H "X-Token: YOUR_API_TOKEN"

Multiple Filters

Live resin vapes from a specific brand in Nevada:

curl "https://api.cannmenus.com/v2/products?states=Nevada&category=Vape&brand_name=Stiiizy&tags=Live%20Resin&page=1" \
  -H "X-Token: YOUR_API_TOKEN"

Response

The terpene fields in this example are filled because the request set include_terpenes=true; without it they're null.

{
  "data": [
    {
      "retailer_id": "10600",
      "sku": "07A0Sbb8OieeaR8rIW6MVHRw",
      "products": [
        {
          "cann_sku_id": "07A0Sbb8OieeaR8rIW6MVHRw",
          "brand_name": "Cookies",
          "brand_id": 1042,
          "url": "https://dutchie.com/store/example/product/gary-payton",
          "image_url": "https://images.dutchie.com/products/gary-payton.jpg",
          "raw_product_name": "Cookies - Gary Payton 3.5g",
          "product_name": "Gary Payton",
          "raw_weight_string": "3.5g",
          "display_weight": "3.5g",
          "raw_product_category": "Flower",
          "category": "Flower",
          "raw_subcategory": "Flower",
          "subcategory": null,
          "product_tags": ["Hybrid", "Indoor"],
          "percentage_thc": 28.5,
          "percentage_cbd": 0.1,
          "cbd_thc_ratio": "20:1",
          "mg_thc": null,
          "mg_cbd": null,
          "quantity_per_package": 1,
          "medical": false,
          "recreational": true,
          "latest_price": 55.00,
          "original_price": 65.00,
          "menu_provider": "Dutchie",
          "is_below_threshold": false,
          "is_stock_image": false,
          "terpene_profile": {
            "Myrcene": 0.0089,
            "Limonene": 0.0072,
            "Beta-Caryophyllene": 0.0045
          },
          "terpene_source": "exact",
          "terpene_source_product_id": null
        }
      ]
    }
  ],
  "pagination": {
    "total_records": 1250,
    "current_page": 1,
    "total_pages": 63,
    "next_page": 2,
    "prev_page": null
  }
}

Response Fields

FieldTypeDescription
retailer_idstringUnique dispensary identifier
skustringUnique product variant identifier
productsarrayListings for this SKU (may include multiple menu providers)
cann_sku_idstringSame as parent sku
brand_namestringBrand name
brand_idnumberUnique brand identifier
urlstringDirect link to product on dispensary menu
image_urlstringProduct image URL
raw_product_namestringOriginal product name from menu provider
product_namestringNormalized product name
display_weightstringProduct size/weight
categorystringNormalized category
subcategorystringNormalized subcategory (if applicable)
product_tagsarrayApplied product tags
percentage_thcnumberTHC percentage (for flower, concentrates)
percentage_cbdnumberCBD percentage
cbd_thc_ratiostringCBD:THC ratio (e.g. "1:1", "2:1", "20:1"). Null if unspecified.
mg_thcnumberTHC milligrams (for edibles, tinctures)
mg_cbdnumberCBD milligrams
latest_pricenumberCurrent listed (shelf) price, as shown on the menu at the last refresh. Deals applied at checkout (discount_in_price: "cart" in the MCP deal tools) are not included.
original_pricenumberCompare-at price from the menu; it may equal latest_price or be null. The listing is on sale when original_price is present and greater than latest_price. null means no compare-at price was listed, not that no deal exists. Deal terms are available through the MCP server; see Deals & Promotions.
menu_providerstringSource platform (Dutchie, Weedmaps, Leafly, IHeartJane)
is_below_thresholdbooleantrue when the listing has been removed from the partner's online ecommerce menu but the product may still be in-store stock. The url for such listings is typically a dead link — do not link directly to it without a clear warning (e.g. "In-store only — removed from online menu"). Use include_below_threshold=false to exclude these listings entirely.
is_stock_imagebooleantrue if the product image is a stock/placeholder photo. V1 only — not included in V2 responses.
terpene_profileobjectTerpene concentrations as fractions of product weight (e.g., {"Myrcene": 0.0089} is 0.89% myrcene). Always null unless the request sets include_terpenes=true, and null for listings without a profile. See Terpene Profiles for coverage.
terpene_sourcestringexact = measured on this listing, uuid_match = taken from another listing of the same product in the same state (only with include_terpene_fallback=true). null when no profile was found or include_terpenes isn't set.
terpene_source_product_idnumberWhen terpene_source is uuid_match, the ID of the listing the profile came from. Otherwise null.
recreationalbooleanAvailable for recreational purchase
medicalbooleanAvailable for medical purchase

Data Freshness & History

Product data is refreshed from dispensary menus periodically throughout the day; there is no guaranteed refresh interval. Each response is a snapshot of the most recent menu data. The REST API does not return past prices or past availability.

For history, use the MCP server (tool availability depends on your plan):

  • get_product_events: stock and price events, such as stockouts, restocks, and price changes
  • deal_history: past deals and promotions (see Deals & Promotions)

Best Practices

Use Location Filters for Performance

Queries with lat/lng are faster and return more relevant results:

# Faster - location-scoped
?states=California&lat=34.0522&lng=-118.2437&distance=10

# Slower - state-wide scan
?states=California

Use IDs Instead of Text Search

Use brands and retailers with numeric IDs instead of brand_name for significantly faster queries:

# Faster - ID-based
?states=California&brands=789

# Slower - text search
?states=California&brand_name=Stiiizy

Paginate Through All Results

Don't assume all results fit on one page. Always check pagination.next_page:

all_products = []
page = 1

while True:
    response = requests.get(
        f"{API_URL}/products",
        headers=headers,
        params={"states": "California", "category": "Flower", "page": page, "per_page": 200}
    )
    data = response.json()
    all_products.extend(data["data"])

    if data["pagination"]["next_page"] is None:
        break
    page += 1

Cache Retailer IDs

If you frequently query products from the same dispensaries, cache their retailer IDs and use the retailers parameter for faster lookups.

Use per_page=200 for Bulk Pulls

The default is 20 results per page. Set per_page=200 to reduce total API calls by 10x:

?states=Colorado&per_page=200&page=1

Always Filter Medical vs Recreational

For states with adult-use/recreational markets (CA, CO, IL, MI, MA, NV, AZ, NJ, etc.), always set recreational=true to avoid double-counting from shared med/rec inventory systems. For medical-only states (FL, PA, OK), use medical=true.

# Recreational state
?states=California&recreational=true

# Medical-only state
?states=Florida&medical=true

V1 Legacy Format

V1 uses repeated query parameters instead of comma-separated strings:

GET https://api.cannmenus.com/v1/products
V2 (Recommended)V1 (Legacy)
states=California,Coloradostates=California&states=Colorado
brands=123,456brands=123&brands=456
retailers=100,200retailers=100&retailers=200
tags=Indica,Livetags=Indica&tags=Live
category=Flower,Ediblecategory=Flower (single value only)

V1 returns the same response format and supports per_page and terpene profiles. It differs from V2 in three ways:

  • List parameters are repeated instead of comma-separated (table above).
  • states is required unless lat, lng, and distance are all provided.
  • V1 adds inclusive range filters that V2 does not currently support:
  • Name
    price_min
    Type
    number
    Description

    Minimum latest_price, inclusive. V1 only.

    price_min=20
    
  • Name
    price_max
    Type
    number
    Description

    Maximum latest_price, inclusive. V1 only.

    price_max=40
    
  • Name
    thc_min
    Type
    number
    Description

    Minimum total THC percentage, inclusive. V1 only.

    thc_min=20
    
  • Name
    thc_max
    Type
    number
    Description

    Maximum total THC percentage, inclusive. V1 only.

    thc_max=30
    

Meta SKU Endpoint

The meta endpoint tags each listing with its Meta SKU (a UUID that identifies the same product across different retailers) instead of grouping by retailer SKU. Results are sorted by meta_sku, so listings of the same product at different retailers sit next to each other. This is useful for cross-retailer product matching and deduplication.

GET https://api.cannmenus.com/v2/products/meta

Parameters

The meta endpoint accepts a subset of the main products parameters:

  • Location: lat, lng, distance
  • states (required)
  • IDs: retailers, brands
  • Text: brand_name, product_name
  • Product: display_weight, category (single value), subcategory, tags, quantity_per_package
  • Potency: percentage_thc, percentage_cbd, mg_thc, mg_cbd
  • Price: latest_price
  • Source and menu type: menu_provider, recreational, medical
  • page

It does not accept per_page (results are fixed at 20 per page), q, skus, or the include_* options. It adds:

ParameterTypeDescription
uuidsstring[]Filter by specific Meta SKU UUIDs (repeated param)

Response

The response uses the same pagination format. Each data entry is one listing: its retailer_id, its meta_sku, and a products array holding that listing. On /v2/products/meta, listings that share a meta_sku can span more than one page:

{
  "data": [
    {
      "retailer_id": "10600",
      "meta_sku": "a1b2c3d4-5678-90ab-cdef-1234567890ab",
      "products": [
        {
          "product_name": "Blue Dream",
          "brand_name": "Cookies",
          "category": "Flower",
          "latest_price": 45.00
        }
      ]
    }
  ],
  "pagination": {
    "total_records": 523,
    "current_page": 1,
    "total_pages": 27,
    "next_page": 2,
    "prev_page": null
  }
}

Example Request

curl "https://api.cannmenus.com/v2/products/meta?states=California&category=Flower&page=1" \
  -H "X-Token: YOUR_API_KEY"

The V1 meta endpoint (/v1/products/meta) accepts the same parameters, plus per_page (default 20, max 200), include_below_threshold, and include_all_menu_providers.