Pagination
API responses are paginated to keep response times fast. The Products search endpoints return 20 results per page by default, up to 200 with per_page. Other list endpoints use a fixed or separately configured page size; see Page Size.
Basic Usage
Add the page parameter to your requests:
# First page (default)
curl "https://api.cannmenus.com/v2/products?states=California&category=Flower&page=1" \
-H "X-Token: YOUR_API_TOKEN"
# Second page
curl "https://api.cannmenus.com/v2/products?states=California&category=Flower&page=2" \
-H "X-Token: YOUR_API_TOKEN"
Response Structure
Every paginated response includes a pagination object:
{
"data": [...],
"pagination": {
"total_records": 1250,
"current_page": 1,
"total_pages": 63,
"next_page": 2,
"prev_page": null
}
}
| Field | Type | Description |
|---|---|---|
total_records | number | Total records matching your query: listing entries for product and retailer searches; distinct meta SKUs on /v1/products/meta |
current_page | number | Page number of current results |
total_pages | number | Total pages available |
next_page | number | null | Next page number, or null if on last page |
prev_page | number | null | Previous page number, or null if on first page |
Fetching All Pages
Python
import requests
API_URL = "https://api.cannmenus.com/v1"
headers = {"X-Token": "YOUR_API_TOKEN"}
def get_all_products(states, category):
all_products = []
page = 1
while True:
response = requests.get(
f"{API_URL}/products",
headers=headers,
params={"states": states, "category": category, "page": page, "per_page": 200}
)
response.raise_for_status()
data = response.json()
all_products.extend(data["data"])
print(f"Page {page}/{data['pagination']['total_pages']}: {len(data['data'])} SKUs")
if data["pagination"]["next_page"] is None:
break
page += 1
return all_products
# Fetch all flower products in New Jersey (200 per page keeps the request count low)
products = get_all_products("New Jersey", "Flower")
print(f"Total: {len(products)} SKUs")
JavaScript
const API_URL = "https://api.cannmenus.com/v1";
const API_TOKEN = "YOUR_API_TOKEN";
async function getAllProducts(states, category) {
const allProducts = [];
let page = 1;
while (true) {
const response = await fetch(
`${API_URL}/products?states=${states}&category=${category}&page=${page}`,
{ headers: { "X-Token": API_TOKEN } }
);
const data = await response.json();
allProducts.push(...data.data);
console.log(`Page ${page}/${data.pagination.total_pages}: ${data.data.length} SKUs`);
if (data.pagination.next_page === null) break;
page++;
}
return allProducts;
}
// Fetch all flower products in California
const products = await getAllProducts("California", "Flower");
console.log(`Total: ${products.length} SKUs`);
Best Practices
Check for Next Page
Always use next_page to determine if more results exist:
# Correct: Check next_page
if data["pagination"]["next_page"] is not None:
# Fetch next page
# Incorrect: Manually calculate
if page < data["pagination"]["total_pages"]: # May miss edge cases
# Fetch next page
Add Delays for Large Fetches
Rate limits are set per organization. When fetching many pages, add small delays, and if you receive HTTP 429, wait the number of seconds in the Retry-After header before retrying:
import time
while True:
response = requests.get(...)
# Process response
if data["pagination"]["next_page"] is None:
break
time.sleep(0.1) # 100ms delay between pages
page += 1
Use Filters to Reduce Pages
Narrow your query to reduce the number of pages:
# Broad query: 63 pages
?states=California&category=Flower
# Focused query: 5 pages
?states=California&category=Flower&lat=34.05&lng=-118.24&distance=5
Handle Empty Results
Some pages may return fewer items than the page size, and queries may return zero results:
data = response.json()
if not data["data"]:
print("No products found matching your criteria")
else:
print(f"Found {data['pagination']['total_records']} products")
Page Size
Page size depends on the endpoint:
| Endpoint | Default | Maximum | Parameter |
|---|---|---|---|
/v1/products, /v2/products | 20 | 200 | per_page |
/v1/products/meta | 20 | 200 | per_page (counts distinct meta SKUs; each page returns every listing for those SKUs) |
/v2/products/meta | 20 (fixed) | — | none |
/v1/retailers | 20 (fixed) | — | none |
/v1/brands | 20 (fixed) | — | none |
/v1/brands/{brand_id}/retailers | 50 | 500 | page_size |
A page can hold a different number of rows than the size you asked for. Don't infer the last page from a short page; keep requesting pages until pagination.next_page is null.
On the Products search endpoints, raise per_page to fetch more results per request:
# 200 results per page (10x fewer API calls)
?states=Colorado&per_page=200&page=1
Using per_page=200 is strongly recommended for bulk catalog pulls. A full state catalog of ~3,600 products would require 180 requests at per_page=20 vs just 18 requests at per_page=200.
MCP Tool Pagination
The MCP server (/mcp/ endpoint, used by AI assistants and integrations) uses a different pagination contract from the REST API. Listing tools return a pagination block in one of three modes:
| Mode | Tools | How to paginate |
|---|---|---|
page | search_brands, search_retailers, search_products, get_brand_retailers, search_strains, dispensary_summary | Pass page=N (REST-style, 20–200 per page depending on the tool) |
cursor | get_product_events, get_retailer_menu, retail_menu_deals, product_deals, deal_history (timeline and summary modes) | Pass the returned next_cursor back as the cursor argument, with the same filters. Page sizes: 100 events (get_product_events), 200 menu rows (get_retailer_menu), and 50 rows by default for the deal tools (up to 200 via limit) |
split | market_trends, brand_stock_status, price_comparison, brand_gap_analysis, strain_stock_status, bevalc_category_landscape | No paging — capped rows + aggregates.* for full-set totals |
deal_history in day_of_week mode is a bounded aggregation with no pagination. For deals, see Deals & Promotions.
Cursor mode example
// First call — no cursor
{
"data": [...200 menu rows...],
"pagination": {
"mode": "cursor",
"returned_count": 200,
"has_more": true,
"next_cursor": "eyJwIjoxLCJ2IjpbIkZsb3dlciIsIk51Z3oi..."
}
}
// Subsequent call — pass next_cursor as `cursor`
{
"method": "tools/call",
"params": {
"name": "get_retailer_menu",
"arguments": {
"retailer_id": 11639,
"state": "California",
"cursor": "eyJwIjoxLCJ2IjpbIkZsb3dlciIsIk51Z3oi..."
}
}
}
Cursor cap
Cursor pagination is capped at 50 follow-up pages per hour per API token per tool. Only calls that pass cursor count toward the cap; first-page calls don't. The counter resets at the top of each UTC hour. Hitting the cap returns an error inside the tool result, not an HTTP error:
{
"error": "pagination_rate_limit",
"message": "get_retailer_menu has fetched 51 pages this hour (limit: 50). ...",
"tool": "get_retailer_menu",
"pages_fetched_this_hour": 51,
"max_pages_per_hour": 50
}
Narrow your filters and stop paginating — don't retry the same call in a loop.
This cap is separate from your organization's rate limit. REST and MCP calls share one per-organization budget; going over it returns HTTP 429 with a Retry-After header, so wait that many seconds before retrying. See Errors.
See MCP Server Setup for full pagination semantics including the split mode used by aggregation tools.
