Query API: - GET /api/payloads/store/:id/query - Filter products with flexible params (brand, category, price_min/max, thc_min/max, search, sort, pagination) - GET /api/payloads/store/:id/aggregate - Group by brand/category with metrics (count, avg_price, min_price, max_price, avg_thc, in_stock_count) - Documentation at docs/QUERY_API.md Trusted Origins Admin: - GET/POST/PUT/DELETE /api/admin/trusted-origins - Manage auth bypass list - Trusted IPs, domains, and regex patterns stored in DB - 5-minute cache with invalidation on admin updates - Fallback to hardcoded defaults if DB unavailable - Migration 085 creates table with seed data 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
7.3 KiB
CannaiQ Query API
Query raw crawl payload data with flexible filters, sorting, and aggregation.
Base URL
https://cannaiq.co/api/payloads
Authentication
Include your API key in the header:
X-API-Key: your-api-key
Endpoints
1. Query Products
Filter and search products from a store's latest crawl data.
GET /api/payloads/store/{dispensaryId}/query
Query Parameters
| Parameter | Type | Description |
|---|---|---|
brand |
string | Filter by brand name (partial match) |
category |
string | Filter by category (flower, vape, edible, etc.) |
subcategory |
string | Filter by subcategory |
strain_type |
string | Filter by strain (indica, sativa, hybrid, cbd) |
in_stock |
boolean | Filter by stock status (true/false) |
price_min |
number | Minimum price |
price_max |
number | Maximum price |
thc_min |
number | Minimum THC percentage |
thc_max |
number | Maximum THC percentage |
search |
string | Search product name (partial match) |
fields |
string | Comma-separated fields to return |
limit |
number | Max results (default 100, max 1000) |
offset |
number | Skip results for pagination |
sort |
string | Sort by: name, price, thc, brand |
order |
string | Sort order: asc, desc |
Available Fields
When using fields parameter, you can request:
id- Product IDname- Product namebrand- Brand namecategory- Product categorysubcategory- Product subcategorystrain_type- Indica/Sativa/Hybrid/CBDprice- Current priceprice_med- Medical priceprice_rec- Recreational pricethc- THC percentagecbd- CBD percentageweight- Product weight/sizestatus- Stock statusin_stock- Boolean in-stock flagimage_url- Product imagedescription- Product description
Examples
Get all flower products under $40:
GET /api/payloads/store/112/query?category=flower&price_max=40
Search for "Blue Dream" with high THC:
GET /api/payloads/store/112/query?search=blue+dream&thc_min=20
Get only name and price for Alien Labs products:
GET /api/payloads/store/112/query?brand=Alien+Labs&fields=name,price,thc
Get top 10 highest THC products:
GET /api/payloads/store/112/query?sort=thc&order=desc&limit=10
Paginate through in-stock products:
GET /api/payloads/store/112/query?in_stock=true&limit=50&offset=0
GET /api/payloads/store/112/query?in_stock=true&limit=50&offset=50
Response
{
"success": true,
"dispensaryId": 112,
"payloadId": 45,
"fetchedAt": "2025-12-11T10:30:00Z",
"query": {
"filters": {
"brand": "Alien Labs",
"category": null,
"price_max": null
},
"sort": "price",
"order": "asc",
"limit": 100,
"offset": 0
},
"pagination": {
"total": 15,
"returned": 15,
"limit": 100,
"offset": 0,
"has_more": false
},
"products": [
{
"id": "507f1f77bcf86cd799439011",
"name": "Alien Labs - Baklava 3.5g",
"brand": "Alien Labs",
"category": "flower",
"strain_type": "hybrid",
"price": 55,
"thc": "28.5",
"in_stock": true
}
]
}
2. Aggregate Data
Group products and calculate metrics.
GET /api/payloads/store/{dispensaryId}/aggregate
Query Parameters
| Parameter | Type | Description |
|---|---|---|
group_by |
string | Required. Field to group by: brand, category, subcategory, strain_type |
metrics |
string | Comma-separated metrics (default: count) |
Available Metrics
count- Number of productsavg_price- Average pricemin_price- Lowest pricemax_price- Highest priceavg_thc- Average THC percentagein_stock_count- Number of in-stock products
Examples
Count products by brand:
GET /api/payloads/store/112/aggregate?group_by=brand
Get price stats by category:
GET /api/payloads/store/112/aggregate?group_by=category&metrics=count,avg_price,min_price,max_price
Get THC averages by strain type:
GET /api/payloads/store/112/aggregate?group_by=strain_type&metrics=count,avg_thc
Brand analysis with stock info:
GET /api/payloads/store/112/aggregate?group_by=brand&metrics=count,avg_price,in_stock_count
Response
{
"success": true,
"dispensaryId": 112,
"payloadId": 45,
"fetchedAt": "2025-12-11T10:30:00Z",
"groupBy": "brand",
"metrics": ["count", "avg_price"],
"totalProducts": 450,
"groupCount": 85,
"aggregations": [
{
"brand": "Alien Labs",
"count": 15,
"avg_price": 52.33
},
{
"brand": "Connected",
"count": 12,
"avg_price": 48.50
}
]
}
3. Compare Stores (Price Comparison)
Query the same data from multiple stores and compare in your app:
// Get flower prices from Store A
const storeA = await fetch('/api/payloads/store/112/query?category=flower&fields=name,brand,price');
// Get flower prices from Store B
const storeB = await fetch('/api/payloads/store/115/query?category=flower&fields=name,brand,price');
// Compare in your app
const dataA = await storeA.json();
const dataB = await storeB.json();
// Find matching products and compare prices
4. Price History
For historical price data, use the snapshots endpoint:
GET /api/v1/products/{productId}/history?days=30
Or compare payloads over time:
GET /api/payloads/store/{dispensaryId}/diff?from={payloadId1}&to={payloadId2}
The diff endpoint shows:
- Products added
- Products removed
- Price changes
- Stock changes
5. List Stores
Get available dispensaries to query:
GET /api/stores
Returns all stores with their IDs, names, and locations.
Use Cases
Price Comparison App
// 1. Get stores in Arizona
const stores = await fetch('/api/stores?state=AZ').then(r => r.json());
// 2. Query flower prices from each store
const prices = await Promise.all(
stores.map(store =>
fetch(`/api/payloads/store/${store.id}/query?category=flower&fields=name,brand,price`)
.then(r => r.json())
)
);
// 3. Build comparison matrix in your app
Brand Analytics Dashboard
// Get brand presence across stores
const brandData = await Promise.all(
storeIds.map(id =>
fetch(`/api/payloads/store/${id}/aggregate?group_by=brand&metrics=count,avg_price`)
.then(r => r.json())
)
);
// Aggregate brand presence across all stores
Deal Finder
// Find high-THC flower under $30
const deals = await fetch(
'/api/payloads/store/112/query?category=flower&price_max=30&thc_min=20&in_stock=true&sort=thc&order=desc'
).then(r => r.json());
Inventory Tracker
// Get products that went out of stock
const diff = await fetch('/api/payloads/store/112/diff').then(r => r.json());
const outOfStock = diff.details.stockChanges.filter(
p => p.newStatus !== 'Active'
);
Rate Limits
- Default: 100 requests/minute per API key
- Contact support for higher limits
Error Responses
{
"success": false,
"error": "Error message here"
}
Common errors:
404- Store or payload not found400- Missing required parameter401- Invalid or missing API key429- Rate limit exceeded