- Fix column name from s.dutchie_plus_url to s.dutchie_url - Add availability tracking and product freshness APIs - Add crawl script for sequential dispensary processing 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <noreply@anthropic.com>
28 KiB
Cannabrands Dashboard API - Front-End Ready Specification
Overview
This document defines the front-end-ready API responses for the Cannabrands brand dashboard. All responses are optimized for direct consumption by dashboard cards, widgets, and chart components.
Base URL: https://api.cannabrands.app/v1
Authentication: Bearer token (JWT) with brand_key claim
1. Brand Key Resolution Flow
Resolution Steps
API Request: GET /v1/brands/cb_12345/dashboard/summary
↓
brand_key = "cb_12345"
↓
┌───────────────┴───────────────┐
│ 1. JWT Validation │
│ Extract brand_key from │
│ token claims │
│ Verify: token.brand_key │
│ === "cb_12345" │
└───────────────┬───────────────┘
↓
┌───────────────┴───────────────┐
│ 2. Brand Resolution │
│ Step A: Try cannabrands_id│
│ SELECT id FROM │
│ canonical_brands WHERE │
│ cannabrands_id = │
│ 'cb_12345' │
│ │
│ Step B (fallback): Try │
│ cannabrands_slug │
│ SELECT id FROM │
│ canonical_brands WHERE │
│ cannabrands_slug = │
│ 'cb_12345' │
└───────────────┬───────────────┘
↓
canonical_brand_id = 42
↓
┌───────────────┴───────────────┐
│ 3. All Queries Scoped │
│ WHERE canonical_brand_id │
│ = 42 │
└───────────────────────────────┘
TypeScript Implementation
async function resolveBrandKey(db: Pool, brandKey: string): Promise<number | null> {
// Step A: Try cannabrands_id (primary identifier)
const byId = await db.query(
`SELECT id FROM canonical_brands WHERE cannabrands_id = $1`,
[brandKey]
);
if (byId.rows.length > 0) {
return byId.rows[0].id;
}
// Step B: Fallback to cannabrands_slug
const bySlug = await db.query(
`SELECT id FROM canonical_brands WHERE cannabrands_slug = $1`,
[brandKey]
);
if (bySlug.rows.length > 0) {
return bySlug.rows[0].id;
}
return null;
}
2. Dashboard Summary Endpoint
GET /v1/brands/:brand_key/dashboard/summary
Returns all KPIs needed for the main dashboard cards in a single request.
Query Parameters:
| Param | Type | Required | Default | Description |
|---|---|---|---|---|
window |
string | No | 30d |
Comparison window: 7d, 30d, 90d |
Response (Success - 200):
{
"brand_key": "cb_12345",
"brand_name": "Raw Garden",
"as_of": "2025-01-15T08:00:00.000Z",
"window": "30d",
"store_footprint": {
"current": 127,
"previous": 112,
"delta": 15,
"delta_percent": 13.39,
"direction": "up"
},
"active_skus": {
"current": 543,
"previous": 476,
"delta": 67,
"delta_percent": 14.08,
"direction": "up"
},
"in_stock_rate": {
"current": 90.1,
"previous": 88.7,
"delta": 1.4,
"direction": "up"
},
"promo_exposure": {
"skus_on_promo": 34,
"stores_running_promos": 23,
"percent_of_skus": 6.3,
"direction": "stable"
},
"price_summary": {
"avg_cents": 4250,
"min_cents": 2500,
"max_cents": 8500,
"avg_formatted": "$42.50",
"min_formatted": "$25.00",
"max_formatted": "$85.00"
},
"activity_last_7d": {
"stores_added": 3,
"stores_removed": 0,
"new_skus": 12,
"removed_skus": 2,
"price_changes": 8
}
}
Dashboard Card Mapping:
| Card | Fields |
|---|---|
| "Store Distribution" | store_footprint.current, store_footprint.delta, store_footprint.direction |
| "Active SKUs" | active_skus.current, active_skus.delta, active_skus.direction |
| "In-Stock Rate" | in_stock_rate.current + "%" suffix, in_stock_rate.direction |
| "Promotional Activity" | promo_exposure.skus_on_promo, promo_exposure.stores_running_promos |
| "Avg Price" | price_summary.avg_formatted |
| "Recent Activity" | activity_last_7d.* |
Response (Empty State - 200):
{
"brand_key": "cb_99999",
"brand_name": "New Brand",
"as_of": "2025-01-15T08:00:00.000Z",
"window": "30d",
"store_footprint": {
"current": 0,
"previous": 0,
"delta": 0,
"delta_percent": 0,
"direction": "stable"
},
"active_skus": {
"current": 0,
"previous": 0,
"delta": 0,
"delta_percent": 0,
"direction": "stable"
},
"in_stock_rate": {
"current": null,
"previous": null,
"delta": null,
"direction": "stable"
},
"promo_exposure": {
"skus_on_promo": 0,
"stores_running_promos": 0,
"percent_of_skus": 0,
"direction": "stable"
},
"price_summary": {
"avg_cents": null,
"min_cents": null,
"max_cents": null,
"avg_formatted": null,
"min_formatted": null,
"max_formatted": null
},
"activity_last_7d": {
"stores_added": 0,
"stores_removed": 0,
"new_skus": 0,
"removed_skus": 0,
"price_changes": 0
}
}
Direction Values:
| Value | Meaning | UI Treatment |
|---|---|---|
"up" |
Increased from previous period | Green arrow, positive indicator |
"down" |
Decreased from previous period | Red arrow, negative indicator |
"stable" |
No significant change | Gray dash, neutral indicator |
3. Store Timeseries Endpoint
GET /v1/brands/:brand_key/dashboard/store-timeseries
Returns daily store/SKU data points optimized for line/area charts.
Query Parameters:
| Param | Type | Required | Default | Description |
|---|---|---|---|---|
from |
string | No | 30 days ago | ISO date YYYY-MM-DD |
to |
string | No | today | ISO date YYYY-MM-DD |
interval |
string | No | day |
Aggregation: day, week, month |
Response (Success - 200):
{
"brand_key": "cb_12345",
"period": {
"from": "2024-12-16",
"to": "2025-01-15",
"interval": "day",
"points_count": 31
},
"points": [
{
"date": "2024-12-16",
"stores": 112,
"stores_added": 2,
"stores_removed": 0,
"stores_net": 2,
"skus": 476,
"skus_in_stock": 423,
"skus_new": 5,
"skus_removed": 1,
"skus_net": 4
},
{
"date": "2024-12-17",
"stores": 114,
"stores_added": 2,
"stores_removed": 0,
"stores_net": 2,
"skus": 482,
"skus_in_stock": 430,
"skus_new": 8,
"skus_removed": 2,
"skus_net": 6
}
],
"summary": {
"stores_start": 112,
"stores_end": 127,
"stores_peak": 128,
"stores_low": 112,
"skus_start": 476,
"skus_end": 543,
"skus_peak": 545,
"skus_low": 476
}
}
Chart Component Usage:
// Example: Recharts line chart
const storeChartData = response.points.map(p => ({
date: p.date,
stores: p.stores,
skus: p.skus
}));
<LineChart data={storeChartData}>
<Line dataKey="stores" stroke="#3b82f6" name="Stores" />
<Line dataKey="skus" stroke="#10b981" name="SKUs" />
</LineChart>
Response (Empty State - 200):
{
"brand_key": "cb_99999",
"period": {
"from": "2024-12-16",
"to": "2025-01-15",
"interval": "day",
"points_count": 0
},
"points": [],
"summary": {
"stores_start": null,
"stores_end": null,
"stores_peak": null,
"stores_low": null,
"skus_start": null,
"skus_end": null,
"skus_peak": null,
"skus_low": null
}
}
4. Promo Timeseries Endpoint
GET /v1/brands/:brand_key/dashboard/promo-timeseries
Returns daily promotional activity data points for stacked bar/area charts.
Query Parameters:
| Param | Type | Required | Default | Description |
|---|---|---|---|---|
from |
string | No | 30 days ago | ISO date YYYY-MM-DD |
to |
string | No | today | ISO date YYYY-MM-DD |
interval |
string | No | day |
Aggregation: day, week, month |
Response (Success - 200):
{
"brand_key": "cb_12345",
"period": {
"from": "2024-12-16",
"to": "2025-01-15",
"interval": "day",
"points_count": 31
},
"points": [
{
"date": "2024-12-16",
"total_promos": 45,
"percent_off": 28,
"dollar_off": 5,
"bogo": 8,
"bundle": 2,
"set_price": 1,
"other": 1,
"avg_discount_percent": 18.5,
"stores_with_promos": 23
},
{
"date": "2024-12-17",
"total_promos": 52,
"percent_off": 32,
"dollar_off": 6,
"bogo": 10,
"bundle": 2,
"set_price": 1,
"other": 1,
"avg_discount_percent": 19.2,
"stores_with_promos": 27
}
],
"summary": {
"total_promo_days": 45,
"peak_promos": 67,
"peak_date": "2024-12-24",
"avg_daily_promos": 48.2,
"most_common_type": "percent_off",
"avg_discount_percent": 18.8
}
}
Stacked Bar Chart Usage:
// Promo breakdown stacked bar
const promoTypes = ['percent_off', 'dollar_off', 'bogo', 'bundle', 'set_price', 'other'];
<BarChart data={response.points}>
{promoTypes.map((type, i) => (
<Bar key={type} dataKey={type} stackId="promos" fill={colors[i]} />
))}
</BarChart>
Response (Empty State - 200):
{
"brand_key": "cb_99999",
"period": {
"from": "2024-12-16",
"to": "2025-01-15",
"interval": "day",
"points_count": 0
},
"points": [],
"summary": {
"total_promo_days": 0,
"peak_promos": null,
"peak_date": null,
"avg_daily_promos": 0,
"most_common_type": null,
"avg_discount_percent": null
}
}
5. Store Changes Endpoint
GET /v1/brands/:brand_key/dashboard/store-changes
Returns recent store additions and removals for "New Stores" / "Lost Stores" widgets.
Query Parameters:
| Param | Type | Required | Default | Description |
|---|---|---|---|---|
days |
number | No | 30 |
Lookback period (max 90) |
limit |
number | No | 10 |
Max results per list |
Response (Success - 200):
{
"brand_key": "cb_12345",
"period": {
"days": 30,
"from": "2024-12-16",
"to": "2025-01-15"
},
"stores_added": {
"count": 15,
"list": [
{
"store_id": 142,
"store_name": "Green Thumb LA",
"store_slug": "green-thumb-la",
"dispensary_name": "Green Thumb Dispensary",
"city": "Los Angeles",
"state": "CA",
"date_added": "2025-01-14",
"days_ago": 1,
"initial_sku_count": 12
},
{
"store_id": 156,
"store_name": "Harvest House SF",
"store_slug": "harvest-house-sf",
"dispensary_name": "Harvest House",
"city": "San Francisco",
"state": "CA",
"date_added": "2025-01-12",
"days_ago": 3,
"initial_sku_count": 8
}
]
},
"stores_removed": {
"count": 2,
"list": [
{
"store_id": 78,
"store_name": "MedMen Venice",
"store_slug": "medmen-venice",
"dispensary_name": "MedMen",
"city": "Venice",
"state": "CA",
"date_removed": "2025-01-10",
"days_ago": 5,
"tenure_days": 156,
"last_sku_count": 6
}
]
},
"stores_reappeared": {
"count": 1,
"list": [
{
"store_id": 92,
"store_name": "The Cannabist - Sacramento",
"store_slug": "cannabist-sacramento",
"dispensary_name": "The Cannabist",
"city": "Sacramento",
"state": "CA",
"date_reappeared": "2025-01-08",
"days_ago": 7,
"days_absent": 21,
"sku_count": 4
}
]
}
}
Widget Mapping:
| Widget | Data Source |
|---|---|
| "New Stores" badge count | stores_added.count |
| "New Stores" list | stores_added.list |
| "Lost Stores" badge count | stores_removed.count |
| "Lost Stores" list | stores_removed.list |
| "Returned" badge count | stores_reappeared.count |
Response (Empty State - 200):
{
"brand_key": "cb_99999",
"period": {
"days": 30,
"from": "2024-12-16",
"to": "2025-01-15"
},
"stores_added": {
"count": 0,
"list": []
},
"stores_removed": {
"count": 0,
"list": []
},
"stores_reappeared": {
"count": 0,
"list": []
}
}
6. Stores Drill-Down Endpoint
GET /v1/brands/:brand_key/dashboard/stores
Returns all stores carrying this brand with current metrics for a detailed table view.
Query Parameters:
| Param | Type | Required | Default | Description |
|---|---|---|---|---|
status |
string | No | active |
Filter: active, removed, all |
state |
string | No | - | Filter by state code (e.g., CA) |
sort |
string | No | sku_count |
Sort: sku_count, name, date_added, tenure |
order |
string | No | desc |
Order: asc, desc |
limit |
number | No | 50 |
Max results (max 200) |
offset |
number | No | 0 |
Pagination offset |
search |
string | No | - | Search store/dispensary name |
Response (Success - 200):
{
"brand_key": "cb_12345",
"filters": {
"status": "active",
"state": null,
"search": null
},
"pagination": {
"total": 127,
"limit": 50,
"offset": 0,
"has_more": true
},
"stores": [
{
"store_id": 42,
"store_name": "Green Thumb LA",
"store_slug": "green-thumb-la",
"dispensary_name": "Green Thumb Dispensary",
"city": "Los Angeles",
"state": "CA",
"dutchie_url": "https://dutchie.com/dispensary/green-thumb-la",
"presence": {
"status": "active",
"first_seen": "2024-06-15",
"last_seen": "2025-01-15",
"tenure_days": 214
},
"sku_counts": {
"active": 24,
"in_stock": 21,
"on_promo": 3,
"total_ever": 28
},
"price_stats": {
"avg_cents": 4500,
"min_cents": 2500,
"max_cents": 8500
},
"activity_7d": {
"new_skus": 2,
"removed_skus": 0,
"price_changes": 1
}
}
],
"aggregates": {
"total_stores": 127,
"states_count": 8,
"avg_skus_per_store": 4.3,
"top_states": [
{ "state": "CA", "count": 45 },
{ "state": "CO", "count": 28 },
{ "state": "WA", "count": 18 }
]
}
}
Table Column Mapping:
| Column | Field |
|---|---|
| Store Name | store_name (link to dutchie_url) |
| Location | city, state |
| SKUs | sku_counts.active |
| In Stock | sku_counts.in_stock |
| On Promo | sku_counts.on_promo |
| Avg Price | price_stats.avg_cents / 100 |
| Since | presence.first_seen |
| Tenure | presence.tenure_days + " days" |
Response (Empty State - 200):
{
"brand_key": "cb_99999",
"filters": {
"status": "active",
"state": null,
"search": null
},
"pagination": {
"total": 0,
"limit": 50,
"offset": 0,
"has_more": false
},
"stores": [],
"aggregates": {
"total_stores": 0,
"states_count": 0,
"avg_skus_per_store": 0,
"top_states": []
}
}
7. Products Drill-Down Endpoint
GET /v1/brands/:brand_key/dashboard/products
Returns all products for this brand with lifecycle and pricing data.
Query Parameters:
| Param | Type | Required | Default | Description |
|---|---|---|---|---|
store_slug |
string | No | - | Filter by specific store |
status |
string | No | active |
Filter: active, stale, removed, all |
in_stock |
string | No | - | Filter: true, false |
on_promo |
string | No | - | Filter: true, false |
sort |
string | No | store_count |
Sort: store_count, name, price, date_added |
order |
string | No | desc |
Order: asc, desc |
limit |
number | No | 50 |
Max results (max 200) |
offset |
number | No | 0 |
Pagination offset |
search |
string | No | - | Search product name |
Response (Success - 200):
{
"brand_key": "cb_12345",
"filters": {
"store_slug": null,
"status": "active",
"in_stock": null,
"on_promo": null,
"search": null
},
"pagination": {
"total": 543,
"limit": 50,
"offset": 0,
"has_more": true
},
"products": [
{
"product_id": 12345,
"fingerprint": "a1b2c3d4e5f6...",
"name": "Live Resin - Slurm OG",
"full_name": "Raw Garden Live Resin - Slurm OG",
"variant": "1g",
"weight": "1g",
"distribution": {
"store_count_current": 45,
"store_count_previous_30d": 38,
"store_count_delta": 7,
"store_count_direction": "up",
"states": ["CA", "CO", "WA", "NV"]
},
"pricing": {
"avg_price_cents": 4500,
"min_price_cents": 3999,
"max_price_cents": 5499,
"price_spread_percent": 37.5,
"avg_formatted": "$45.00",
"min_formatted": "$39.99",
"max_formatted": "$54.99"
},
"stock": {
"in_stock_count": 42,
"out_of_stock_count": 3,
"in_stock_percent": 93.3
},
"promos": {
"stores_on_promo": 8,
"percent_on_promo": 17.8,
"percent_time_on_special_30d": 23.5,
"common_promo_type": "percent_off"
},
"lifecycle": {
"first_seen": "2024-08-10",
"days_tracked": 158,
"status": "active"
}
}
],
"aggregates": {
"total_products": 543,
"total_in_stock": 489,
"total_on_promo": 34,
"avg_stores_per_product": 4.2,
"categories": [
{ "name": "Concentrates", "count": 312 },
{ "name": "Vapes", "count": 156 },
{ "name": "Flower", "count": 75 }
]
}
}
Table Column Mapping:
| Column | Field |
|---|---|
| Product | name |
| Size | variant or weight |
| Stores | distribution.store_count_current |
| Avg Price | pricing.avg_formatted |
| Price Range | pricing.min_formatted - pricing.max_formatted |
| In Stock % | stock.in_stock_percent |
| On Promo | promos.stores_on_promo stores |
| First Seen | lifecycle.first_seen |
Response (Empty State - 200):
{
"brand_key": "cb_99999",
"filters": {
"store_slug": null,
"status": "active",
"in_stock": null,
"on_promo": null,
"search": null
},
"pagination": {
"total": 0,
"limit": 50,
"offset": 0,
"has_more": false
},
"products": [],
"aggregates": {
"total_products": 0,
"total_in_stock": 0,
"total_on_promo": 0,
"avg_stores_per_product": 0,
"categories": []
}
}
8. Empty State Behavior Summary
All endpoints return HTTP 200 with structured empty data rather than errors when a brand has no data:
| Endpoint | Empty Indicator | UI Treatment |
|---|---|---|
/dashboard/summary |
store_footprint.current === 0 |
Show "No data yet" card overlay |
/dashboard/store-timeseries |
points.length === 0 |
Show empty chart with "No data" message |
/dashboard/promo-timeseries |
points.length === 0 |
Show empty chart with "No data" message |
/dashboard/store-changes |
All .count === 0 |
Show "No changes in this period" |
/dashboard/stores |
pagination.total === 0 |
Show empty state illustration |
/dashboard/products |
pagination.total === 0 |
Show empty state illustration |
Empty State Detection (TypeScript):
function hasBrandData(summary: DashboardSummary): boolean {
return summary.store_footprint.current > 0 || summary.active_skus.current > 0;
}
function hasTimeseriesData(data: TimeseriesResponse): boolean {
return data.points.length > 0;
}
function hasStoreChanges(data: StoreChangesResponse): boolean {
return data.stores_added.count > 0 ||
data.stores_removed.count > 0 ||
data.stores_reappeared.count > 0;
}
9. Error Responses
All endpoints use consistent error response format:
401 Unauthorized
{
"error": "Unauthorized",
"code": "AUTH_REQUIRED",
"message": "Valid authentication token required"
}
403 Forbidden
{
"error": "Access denied",
"code": "BRAND_ACCESS_DENIED",
"message": "You do not have access to brand: cb_12345"
}
404 Brand Not Found
{
"error": "Brand not found",
"code": "BRAND_NOT_FOUND",
"message": "No brand found with key: cb_unknown"
}
404 Feature Disabled
{
"error": "Feature not available",
"code": "FEATURE_DISABLED",
"message": "Brand intelligence features are temporarily unavailable"
}
422 Validation Error
{
"error": "Validation error",
"code": "INVALID_PARAMS",
"message": "Invalid parameters",
"details": {
"window": "Must be one of: 7d, 30d, 90d",
"limit": "Must be between 1 and 200"
}
}
500 Server Error
{
"error": "Internal server error",
"code": "INTERNAL_ERROR",
"message": "An unexpected error occurred",
"request_id": "req_abc123"
}
10. TypeScript Response Types
// Common types
type Direction = 'up' | 'down' | 'stable';
interface DeltaMetric {
current: number;
previous: number;
delta: number;
delta_percent: number;
direction: Direction;
}
interface NullableDeltaMetric {
current: number | null;
previous: number | null;
delta: number | null;
direction: Direction;
}
// Summary endpoint
interface DashboardSummary {
brand_key: string;
brand_name: string;
as_of: string;
window: '7d' | '30d' | '90d';
store_footprint: DeltaMetric;
active_skus: DeltaMetric;
in_stock_rate: NullableDeltaMetric;
promo_exposure: {
skus_on_promo: number;
stores_running_promos: number;
percent_of_skus: number;
direction: Direction;
};
price_summary: {
avg_cents: number | null;
min_cents: number | null;
max_cents: number | null;
avg_formatted: string | null;
min_formatted: string | null;
max_formatted: string | null;
};
activity_last_7d: {
stores_added: number;
stores_removed: number;
new_skus: number;
removed_skus: number;
price_changes: number;
};
}
// Timeseries endpoints
interface TimeseriesPeriod {
from: string;
to: string;
interval: 'day' | 'week' | 'month';
points_count: number;
}
interface StoreTimeseriesPoint {
date: string;
stores: number;
stores_added: number;
stores_removed: number;
stores_net: number;
skus: number;
skus_in_stock: number;
skus_new: number;
skus_removed: number;
skus_net: number;
}
interface StoreTimeseriesResponse {
brand_key: string;
period: TimeseriesPeriod;
points: StoreTimeseriesPoint[];
summary: {
stores_start: number | null;
stores_end: number | null;
stores_peak: number | null;
stores_low: number | null;
skus_start: number | null;
skus_end: number | null;
skus_peak: number | null;
skus_low: number | null;
};
}
interface PromoTimeseriesPoint {
date: string;
total_promos: number;
percent_off: number;
dollar_off: number;
bogo: number;
bundle: number;
set_price: number;
other: number;
avg_discount_percent: number;
stores_with_promos: number;
}
interface PromoTimeseriesResponse {
brand_key: string;
period: TimeseriesPeriod;
points: PromoTimeseriesPoint[];
summary: {
total_promo_days: number;
peak_promos: number | null;
peak_date: string | null;
avg_daily_promos: number;
most_common_type: string | null;
avg_discount_percent: number | null;
};
}
// Store changes
interface StoreChangeEntry {
store_id: number;
store_name: string;
store_slug: string;
dispensary_name: string;
city: string;
state: string;
}
interface StoreAddedEntry extends StoreChangeEntry {
date_added: string;
days_ago: number;
initial_sku_count: number;
}
interface StoreRemovedEntry extends StoreChangeEntry {
date_removed: string;
days_ago: number;
tenure_days: number;
last_sku_count: number;
}
interface StoreReappearedEntry extends StoreChangeEntry {
date_reappeared: string;
days_ago: number;
days_absent: number;
sku_count: number;
}
interface StoreChangesResponse {
brand_key: string;
period: {
days: number;
from: string;
to: string;
};
stores_added: {
count: number;
list: StoreAddedEntry[];
};
stores_removed: {
count: number;
list: StoreRemovedEntry[];
};
stores_reappeared: {
count: number;
list: StoreReappearedEntry[];
};
}
// Stores drill-down
interface StoreListEntry {
store_id: number;
store_name: string;
store_slug: string;
dispensary_name: string;
city: string;
state: string;
dutchie_url: string;
presence: {
status: 'active' | 'removed';
first_seen: string;
last_seen: string;
tenure_days: number;
};
sku_counts: {
active: number;
in_stock: number;
on_promo: number;
total_ever: number;
};
price_stats: {
avg_cents: number;
min_cents: number;
max_cents: number;
};
activity_7d: {
new_skus: number;
removed_skus: number;
price_changes: number;
};
}
interface StoresResponse {
brand_key: string;
filters: {
status: string;
state: string | null;
search: string | null;
};
pagination: {
total: number;
limit: number;
offset: number;
has_more: boolean;
};
stores: StoreListEntry[];
aggregates: {
total_stores: number;
states_count: number;
avg_skus_per_store: number;
top_states: { state: string; count: number }[];
};
}
// Products drill-down
interface ProductListEntry {
product_id: number;
fingerprint: string;
name: string;
full_name: string;
variant: string;
weight: string;
distribution: {
store_count_current: number;
store_count_previous_30d: number;
store_count_delta: number;
store_count_direction: Direction;
states: string[];
};
pricing: {
avg_price_cents: number;
min_price_cents: number;
max_price_cents: number;
price_spread_percent: number;
avg_formatted: string;
min_formatted: string;
max_formatted: string;
};
stock: {
in_stock_count: number;
out_of_stock_count: number;
in_stock_percent: number;
};
promos: {
stores_on_promo: number;
percent_on_promo: number;
percent_time_on_special_30d: number;
common_promo_type: string | null;
};
lifecycle: {
first_seen: string;
days_tracked: number;
status: 'active' | 'stale' | 'removed';
};
}
interface ProductsResponse {
brand_key: string;
filters: {
store_slug: string | null;
status: string;
in_stock: string | null;
on_promo: string | null;
search: string | null;
};
pagination: {
total: number;
limit: number;
offset: number;
has_more: boolean;
};
products: ProductListEntry[];
aggregates: {
total_products: number;
total_in_stock: number;
total_on_promo: number;
avg_stores_per_product: number;
categories: { name: string; count: number }[];
};
}
Document Version
| Version | Date | Author | Changes |
|---|---|---|---|
| 1.0 | 2025-01-15 | Claude | Initial front-end ready specification |