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
  }
}
FieldTypeDescription
total_recordsnumberTotal records matching your query: listing entries for product and retailer searches; distinct meta SKUs on /v1/products/meta
current_pagenumberPage number of current results
total_pagesnumberTotal pages available
next_pagenumber | nullNext page number, or null if on last page
prev_pagenumber | nullPrevious 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:

EndpointDefaultMaximumParameter
/v1/products, /v2/products20200per_page
/v1/products/meta20200per_page (counts distinct meta SKUs; each page returns every listing for those SKUs)
/v2/products/meta20 (fixed)—none
/v1/retailers20 (fixed)—none
/v1/brands20 (fixed)—none
/v1/brands/{brand_id}/retailers50500page_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

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:

ModeToolsHow to paginate
pagesearch_brands, search_retailers, search_products, get_brand_retailers, search_strains, dispensary_summaryPass page=N (REST-style, 20–200 per page depending on the tool)
cursorget_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)
splitmarket_trends, brand_stock_status, price_comparison, brand_gap_analysis, strain_stock_status, bevalc_category_landscapeNo 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.