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>
344 lines
7.3 KiB
Markdown
344 lines
7.3 KiB
Markdown
# 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 ID
|
|
- `name` - Product name
|
|
- `brand` - Brand name
|
|
- `category` - Product category
|
|
- `subcategory` - Product subcategory
|
|
- `strain_type` - Indica/Sativa/Hybrid/CBD
|
|
- `price` - Current price
|
|
- `price_med` - Medical price
|
|
- `price_rec` - Recreational price
|
|
- `thc` - THC percentage
|
|
- `cbd` - CBD percentage
|
|
- `weight` - Product weight/size
|
|
- `status` - Stock status
|
|
- `in_stock` - Boolean in-stock flag
|
|
- `image_url` - Product image
|
|
- `description` - 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
|
|
|
|
```json
|
|
{
|
|
"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 products
|
|
- `avg_price` - Average price
|
|
- `min_price` - Lowest price
|
|
- `max_price` - Highest price
|
|
- `avg_thc` - Average THC percentage
|
|
- `in_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
|
|
|
|
```json
|
|
{
|
|
"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:
|
|
|
|
```javascript
|
|
// 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
|
|
|
|
```javascript
|
|
// 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
|
|
|
|
```javascript
|
|
// 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
|
|
|
|
```javascript
|
|
// 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
|
|
|
|
```javascript
|
|
// 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
|
|
|
|
```json
|
|
{
|
|
"success": false,
|
|
"error": "Error message here"
|
|
}
|
|
```
|
|
|
|
Common errors:
|
|
- `404` - Store or payload not found
|
|
- `400` - Missing required parameter
|
|
- `401` - Invalid or missing API key
|
|
- `429` - Rate limit exceeded
|