Product Search Guide
This guide walks through common workflows for finding cannabis products using the CannMenus API, with complete code examples.
Prerequisites
Install the requests library for Python examples:
pip install requests
Set up your API credentials:
import requests
API_URL = "https://api.cannmenus.com/v2"
RETAILERS_URL = "https://api.cannmenus.com/v1/retailers" # retailers are V1-only
headers = {"X-Token": "YOUR_API_TOKEN"}
V2 is recommended for new product integrations. The product examples below use /v2/products. If you need V1 parameter format (repeated query params instead of comma-separated), replace v2 with v1 in the URL.
Retailers and brands are V1-only endpoints (/v1/retailers, /v1/brands); there is no V2 retailers endpoint.
The examples search adult-use states, so they pass recreational=true to avoid double-counting listings shared between medical and recreational menus. For medical-only states, use medical=true instead. See Products → Best Practices.
Basic Product Search
Search for products in a state:
response = requests.get(
f"{API_URL}/products",
headers=headers,
params={
"states": "California",
"category": "Flower",
"recreational": True,
"page": 1
}
)
data = response.json()
print(f"Found {data['pagination']['total_records']} products")
for sku_group in data["data"][:5]:
product = sku_group["products"][0]
print(f"- {product['brand_name']} {product['product_name']}: ${product['latest_price']}")
Find a Retailer and Get All Products
A common workflow: find a specific dispensary, then fetch all its products.
Step 1: Search for the Retailer
# Find a retailer by name in Illinois (use the full state name)
response = requests.get(
RETAILERS_URL,
headers=headers,
params={
"state": "Illinois",
"name": "rise",
"is_active": True,
"page": 1
}
)
retailers = response.json()["data"]
if not retailers:
print("No retailers found")
exit()
retailer = retailers[0]
print(f"Found: {retailer['dispensary_name']}")
print(f"Address: {retailer['physical_address']}, {retailer['city']}")
print(f"Retailer ID: {retailer['dispensary_id']}")
Step 2: Fetch All Products with Pagination
def get_all_products(retailer_id, state):
"""Fetch all products from a retailer, handling pagination."""
all_products = []
page = 1
while True:
response = requests.get(
f"{API_URL}/products",
headers=headers,
params={
"states": state,
"retailers": retailer_id,
"recreational": True,
"page": page
}
)
data = response.json()
all_products.extend(data["data"])
print(f"Page {page}/{data['pagination']['total_pages']}: fetched {len(data['data'])} SKUs")
if data["pagination"]["next_page"] is None:
break
page += 1
return all_products
# Get all products for the retailer
products = get_all_products(retailer["dispensary_id"], "Illinois")
print(f"\nTotal SKUs: {len(products)}")
Step 3: Analyze the Products
# Count products by category
from collections import Counter
categories = Counter()
for sku_group in products:
for product in sku_group["products"]:
categories[product["category"]] += 1
print("\nProducts by category:")
for category, count in categories.most_common():
print(f" {category}: {count}")
Complete Example: Retailer Product Export
Here's a complete script that finds a retailer and exports all products:
import requests
import json
from datetime import datetime
API_URL = "https://api.cannmenus.com/v2"
RETAILERS_URL = "https://api.cannmenus.com/v1/retailers" # retailers are V1-only
headers = {"X-Token": "YOUR_API_TOKEN"}
def find_retailer(state, name):
"""Search for a retailer by full state name and retailer name."""
response = requests.get(
RETAILERS_URL,
headers=headers,
params={"state": state, "name": name, "is_active": True, "page": 1}
)
response.raise_for_status()
retailers = response.json()["data"]
if not retailers:
raise ValueError(f"No retailer found matching '{name}' in {state}")
return retailers[0]
def get_all_products(retailer_id, state):
"""Fetch all products from a retailer with pagination."""
all_products = []
page = 1
while True:
response = requests.get(
f"{API_URL}/products",
headers=headers,
params={"states": state, "retailers": retailer_id, "recreational": True, "page": page}
)
response.raise_for_status()
data = response.json()
all_products.extend(data["data"])
if data["pagination"]["next_page"] is None:
break
page += 1
return all_products
def flatten_products(sku_groups):
"""Convert nested SKU groups to flat product list."""
products = []
for group in sku_groups:
for product in group["products"]:
products.append({
"retailer_id": group["retailer_id"],
"sku": group["sku"],
**product
})
return products
def main():
# Find the retailer
retailer = find_retailer("Illinois", "rise")
print(f"Found: {retailer['dispensary_name']}")
print(f"Location: {retailer['city']}, {retailer['state']}")
# Get all products
print("\nFetching products...")
sku_groups = get_all_products(retailer["dispensary_id"], "Illinois")
products = flatten_products(sku_groups)
print(f"Total products: {len(products)}")
# Export to JSON
output = {
"retailer": retailer,
"exported_at": datetime.now().isoformat(),
"product_count": len(products),
"products": products
}
filename = f"{retailer['dispensary_name'].replace(' ', '_')}_products.json"
with open(filename, "w") as f:
json.dump(output, f, indent=2)
print(f"\nExported to {filename}")
if __name__ == "__main__":
main()
Location-Based Search
Find products near a specific location:
# Search for edibles within 5 miles of downtown Denver
response = requests.get(
f"{API_URL}/products",
headers=headers,
params={
"states": "Colorado",
"category": "Edible",
"recreational": True,
"lat": 39.7392,
"lng": -104.9903,
"distance": 5,
"page": 1
}
)
data = response.json()
print(f"Found {data['pagination']['total_records']} edibles near Denver")
Price Comparison Across Retailers
Compare prices for a specific product. A listing is on sale when original_price is present and greater than latest_price; latest_price does not include deals applied at checkout (see Deals & Promotions).
def compare_prices(state, brand, product_name):
"""Price range for a product across retailers, from the first page of results (up to 200 listings).
product_name is a substring match, so results can include other variants (sizes, flavors);
check display_weight / product_name before treating listings as the same product,
and follow pagination.next_page for a full statewide view.
"""
response = requests.get(
f"{API_URL}/products",
headers=headers,
params={
"states": state,
"brand_name": brand,
"product_name": product_name,
"recreational": True,
"page": 1,
"per_page": 200
}
)
data = response.json()
prices = []
for sku_group in data["data"]:
for product in sku_group["products"]:
if product["latest_price"] is None:
continue
original = product.get("original_price")
prices.append({
"retailer_id": sku_group["retailer_id"],
"price": product["latest_price"],
"original_price": original,
"on_sale": original is not None and original > product["latest_price"],
"menu_provider": product["menu_provider"]
})
if prices:
prices.sort(key=lambda x: x["price"])
print(f"\n{brand} {product_name} prices in {state}:")
print(f" Lowest: ${prices[0]['price']}")
print(f" Highest: ${prices[-1]['price']}")
retailers = {p["retailer_id"] for p in prices}
on_sale = {p["retailer_id"] for p in prices if p["on_sale"]}
print(f" {len(prices)} listings at {len(retailers)} retailers (first page)")
print(f" On sale at {len(on_sale)} retailers")
return prices
compare_prices("California", "Stiiizy", "OG Kush")
Filter by Potency
Filter products by exact cannabinoid content:
# Flower with exactly 25.5% THC
response = requests.get(
f"{API_URL}/products",
headers=headers,
params={
"states": "Washington",
"category": "Flower",
"recreational": True,
"percentage_thc": 25.5,
"page": 1
}
)
# Edibles with exactly 100mg THC
response = requests.get(
f"{API_URL}/products",
headers=headers,
params={
"states": "Colorado",
"category": "Edible",
"recreational": True,
"mg_thc": 100,
"page": 1
}
)
Error Handling
Handle the status codes the REST API returns:
403: no API token was sent404: invalid or expired token (or an unknown endpoint)422: invalid or missing parameters429: rate limited; wait for the number of seconds in theRetry-Afterheader
See Errors for details.
def safe_api_call(url, params):
"""Make an API call with proper error handling."""
try:
response = requests.get(url, headers=headers, params=params, timeout=30)
except requests.exceptions.RequestException as e:
raise ConnectionError(f"Network error: {e}")
if response.status_code == 200:
return response.json()
try:
detail = response.json().get("detail")
except ValueError:
detail = response.text
if response.status_code in (403, 404):
raise PermissionError(f"Auth problem or unknown endpoint: {detail}")
elif response.status_code == 422:
raise ValueError(f"Invalid parameters: {detail}")
elif response.status_code == 429:
retry_after = response.headers.get("Retry-After")
raise RuntimeError(f"Rate limited; retry after {retry_after}s: {detail}")
else:
raise Exception(f"API error {response.status_code}: {detail}")
# Usage
try:
data = safe_api_call(f"{API_URL}/products", {"states": "California", "recreational": True, "page": 1})
print(f"Found {data['pagination']['total_records']} products")
except Exception as e:
print(f"Error: {e}")
Next Steps
- See Products endpoint for all available parameters
- Learn about SKU organization for understanding response structure
- Check Categories and Tags for filtering options
- Review Pagination for handling large result sets
