These endpoints require a valid JWT Bearer token. Pricing endpoints are accessible via the API gateway at /v1/pricing/* and forecasting endpoints at /v1/forecast/*.
Dynamic Pricing & Demand Forecasting API
AI-powered price optimization, automated pricing rules, and ML-driven demand forecasting for restaurant locations.
Overview
| Property | Details |
|---|---|
| Pricing Base Path | /api/v1/pricing |
| Forecast Base Path | /api/v1/forecast |
| Authentication | Bearer JWT token |
| Required Roles | manager, restaurant_manager, analytics_viewer, tenant_admin, platform_admin, system_admin, super_admin |
| Backend Service | Python Analytics (proxied via Go API Gateway) |
| Epic | #779 -- AI Demand Forecasting & Dynamic Pricing |
| Feature Gate | dynamic_pricing_enabled (pricing endpoints only) |
The Dynamic Pricing API provides two complementary capabilities:
| Capability | Description |
|---|---|
| Price Optimization | DQN (Deep Q-Network) agent recommends optimal prices based on historical demand-price-revenue data |
| Pricing Rules | Automated rules that trigger price adjustments based on daypart, demand, weather, events, and more |
| Price History | Full audit trail of all price changes with rollback support |
| Demand Forecasting | ML models (Vertex AI AutoML, Prophet, statistical fallback) predict future sales and order volume |
| Forecast Accuracy | Tracks model performance with MAPE, RMSE, and accuracy-within-target metrics |
Price Guardrails
All price changes are constrained to protect against extreme adjustments:
- Floor: 70% of base price
- Ceiling: 130% of base price
- Daily limit: Maximum 3 price changes per location per day
- Rule adjustment range: -50% to +50%
Pricing Endpoints
Get Price Optimization
Get an AI-powered price recommendation for a single menu item.
GET /api/v1/pricing/optimize/{location_id}/{item_id}
Authorization: Bearer {access_token}
Path Parameters:
| Parameter | Type | Description |
|---|---|---|
location_id | string | Location UUID |
item_id | string | Menu item UUID |
Response:
{
"item_id": "550e8400-e29b-41d4-a716-446655440201",
"recommended_price": 14.49,
"current_price": 12.99,
"base_price": 12.99,
"adjustment_percent": 0.1155,
"confidence": 0.87,
"expected_revenue_change": 0.094,
"expected_demand_change": -0.023,
"reasoning": [
"Current demand is 18% above average for this daypart",
"Competitor pricing supports a higher price point",
"Weather forecast indicates increased foot traffic"
]
}
| Field | Type | Description |
|---|---|---|
recommended_price | float | AI-recommended optimal price |
current_price | float | Current active price |
base_price | float | Original menu price (anchor for guardrails) |
adjustment_percent | float | Recommended change as a decimal (e.g., 0.1155 = +11.55%) |
confidence | float | Model confidence score (0.0 -- 1.0) |
expected_revenue_change | float | Predicted revenue impact as a decimal |
expected_demand_change | float | Predicted demand impact as a decimal (negative = fewer orders) |
reasoning | string[] | Human-readable explanations for the recommendation |
Batch Optimize All Items
Get price recommendations for every priceable item at a location. Does not auto-apply changes.
POST /api/v1/pricing/optimize-all/{location_id}
Authorization: Bearer {access_token}
Path Parameters:
| Parameter | Type | Description |
|---|---|---|
location_id | string | Location UUID |
Response:
{
"status": "success",
"location_id": "550e8400-e29b-41d4-a716-446655449110",
"recommendations": [
{
"item_id": "550e8400-e29b-41d4-a716-446655440201",
"item_name": "Classic Burger",
"current_price": 12.99,
"base_price": 12.99,
"recommended_price": 14.49,
"adjustment_percent": 0.1155,
"confidence": 0.87,
"reasoning": ["High demand detected for lunch daypart"]
},
{
"item_id": "550e8400-e29b-41d4-a716-446655440202",
"item_name": "Caesar Salad",
"current_price": 10.99,
"base_price": 10.99,
"recommended_price": 9.99,
"adjustment_percent": -0.091,
"confidence": 0.72,
"reasoning": ["Low demand period, price reduction may boost volume"]
}
],
"recommendation_count": 2,
"errors": [],
"error_count": 0
}
Apply Price Change
Apply a price change to a menu item. Records the change in the audit trail and enforces guardrails.
POST /api/v1/pricing/apply
Authorization: Bearer {access_token}
Content-Type: application/json
Request:
{
"location_id": "550e8400-e29b-41d4-a716-446655449110",
"item_id": "550e8400-e29b-41d4-a716-446655440201",
"new_price": 14.49,
"reason": "demand_surge"
}
| Field | Type | Required | Description |
|---|---|---|---|
location_id | string | Yes | Location UUID |
item_id | string | Yes | Menu item UUID |
new_price | float | Yes | New price (must be > 0, within 70%--130% of base) |
reason | string | No | Reason code: manual, demand_surge, demand_low, inventory_high, etc. Defaults to manual |
Response:
{
"status": "applied",
"change_id": "a3f1b2c4-d5e6-7890-abcd-ef1234567890",
"price_id": "b4c2d3e5-f6a7-8901-bcde-f12345678901",
"item_id": "550e8400-e29b-41d4-a716-446655440201",
"previous_price": 12.99,
"new_price": 14.49,
"adjustment_percent": 11.5
}
Rollback Price Change
Restore the previous price for an item by referencing a price change history record.
POST /api/v1/pricing/rollback/{change_id}
Authorization: Bearer {access_token}
Content-Type: application/json
Path Parameters:
| Parameter | Type | Description |
|---|---|---|
change_id | string | Price change history ID (from the audit trail) |
Request (optional body):
{
"reason": "Customer complaints about surge price"
}
Response:
{
"status": "rolled_back",
"change_id": "a3f1b2c4-d5e6-7890-abcd-ef1234567890",
"item_id": "550e8400-e29b-41d4-a716-446655440201",
"restored_price": 12.99
}
List Priceable Items
List menu items with current prices and dynamic pricing status for a location. Used by the Flutter dynamic pricing dashboard.
GET /api/v1/pricing/items/{location_id}?limit=100
Authorization: Bearer {access_token}
Path Parameters:
| Parameter | Type | Description |
|---|---|---|
location_id | string | Location UUID |
Query Parameters:
| Parameter | Type | Default | Description |
|---|---|---|---|
limit | int | 100 | Max items to return (1--500) |
Response:
{
"status": "success",
"data": [
{
"item_id": "550e8400-e29b-41d4-a716-446655440201",
"item_name": "Classic Burger",
"base_price": 12.99,
"current_price": 14.49,
"has_dynamic_price": true,
"last_updated": "2026-02-19T14:30:00Z"
},
{
"item_id": "550e8400-e29b-41d4-a716-446655440202",
"item_name": "Caesar Salad",
"base_price": 10.99,
"current_price": 10.99,
"has_dynamic_price": false,
"last_updated": null
}
],
"count": 2
}
Get Pricing Analytics
Aggregate pricing analytics summary for a location over a time period.
GET /api/v1/pricing/analytics/{location_id}?days=30
Authorization: Bearer {access_token}
Path Parameters:
| Parameter | Type | Description |
|---|---|---|
location_id | string | Location UUID |
Query Parameters:
| Parameter | Type | Default | Description |
|---|---|---|---|
days | int | 30 | Analysis period in days (1--90) |
Response:
{
"status": "success",
"data": {
"total_changes": 47,
"price_increases": 31,
"price_decreases": 16,
"average_adjustment_percent": 8.3,
"items_affected": 12,
"estimated_revenue_impact": 2340.50,
"most_adjusted_item": {
"item_id": "550e8400-e29b-41d4-a716-446655440201",
"item_name": "Classic Burger",
"change_count": 8
}
}
}
Get Price History
Retrieve the full audit trail of price changes for a location.
GET /api/v1/pricing/history/{location_id}?item_id={item_id}&limit=50
Authorization: Bearer {access_token}
Path Parameters:
| Parameter | Type | Description |
|---|---|---|
location_id | string | Location UUID |
Query Parameters:
| Parameter | Type | Default | Description |
|---|---|---|---|
item_id | string | null | Filter to a specific item |
limit | int | 50 | Max results (1--200) |
Response:
{
"status": "success",
"data": [
{
"id": "a3f1b2c4-d5e6-7890-abcd-ef1234567890",
"item_id": "550e8400-e29b-41d4-a716-446655440201",
"item_name": "Classic Burger",
"previous_price": 12.99,
"new_price": 14.49,
"adjustment_percent": 11.5,
"reason": "demand_surge",
"applied_by": "manual",
"created_at": "2026-02-19T14:30:00Z"
},
{
"id": "c5d6e7f8-a9b0-1234-cdef-567890abcdef",
"item_id": "550e8400-e29b-41d4-a716-446655440201",
"item_name": "Classic Burger",
"previous_price": 14.49,
"new_price": 12.99,
"adjustment_percent": -10.4,
"reason": "rollback",
"applied_by": "manual",
"created_at": "2026-02-19T16:00:00Z"
}
],
"count": 2
}
Pricing Rules Endpoints
List Pricing Rules
GET /api/v1/pricing/rules?location_id={location_id}&active_only=true
Authorization: Bearer {access_token}
Query Parameters:
| Parameter | Type | Default | Description |
|---|---|---|---|
location_id | string | null | Filter by location (null = all locations) |
active_only | bool | true | Return only active rules |
Response:
{
"status": "success",
"data": [
{
"id": "rule-001-uuid",
"name": "Lunch Rush Surge",
"rule_type": "daypart",
"adjustment_percent": 0.10,
"conditions": {
"start_hour": 11,
"end_hour": 14,
"days": ["monday", "tuesday", "wednesday", "thursday", "friday"]
},
"location_id": "550e8400-e29b-41d4-a716-446655449110",
"item_id": null,
"priority": 10,
"is_active": true,
"created_at": "2026-02-01T10:00:00Z"
},
{
"id": "rule-002-uuid",
"name": "High Demand Surge",
"rule_type": "demand_threshold",
"adjustment_percent": 0.15,
"conditions": {
"demand_percentile_above": 85
},
"location_id": null,
"item_id": null,
"priority": 20,
"is_active": true,
"created_at": "2026-02-05T09:00:00Z"
}
],
"count": 2
}
Create Pricing Rule
POST /api/v1/pricing/rules
Authorization: Bearer {access_token}
Content-Type: application/json
Request:
{
"name": "Happy Hour Discount",
"rule_type": "happy_hour",
"adjustment_percent": -0.15,
"conditions": {
"start_hour": 16,
"end_hour": 18,
"days": ["monday", "tuesday", "wednesday", "thursday", "friday"]
},
"location_id": "550e8400-e29b-41d4-a716-446655449110",
"item_id": null,
"priority": 5
}
| Field | Type | Required | Description |
|---|---|---|---|
name | string | Yes | Human-readable rule name |
rule_type | string | Yes | One of: daypart, day_of_week, demand_threshold, inventory_threshold, weather, event, happy_hour, surge |
adjustment_percent | float | Yes | Price adjustment as a decimal (-0.50 to 0.50) |
conditions | object | No | Rule-specific condition parameters |
location_id | string | No | Scope to a location (null = all locations) |
item_id | string | No | Scope to an item (null = all items) |
priority | int | No | Evaluation priority; higher values evaluated first. Default: 0 |
Response:
{
"status": "created",
"data": {
"id": "rule-003-uuid",
"name": "Happy Hour Discount",
"rule_type": "happy_hour",
"adjustment_percent": -0.15,
"conditions": {
"start_hour": 16,
"end_hour": 18,
"days": ["monday", "tuesday", "wednesday", "thursday", "friday"]
},
"location_id": "550e8400-e29b-41d4-a716-446655449110",
"item_id": null,
"priority": 5,
"is_active": true,
"created_at": "2026-02-19T12:00:00Z"
}
}
Update Pricing Rule
PUT /api/v1/pricing/rules/{rule_id}
Authorization: Bearer {access_token}
Content-Type: application/json
Path Parameters:
| Parameter | Type | Description |
|---|---|---|
rule_id | string | Rule UUID |
Request (all fields optional):
{
"name": "Happy Hour Deep Discount",
"adjustment_percent": -0.20,
"is_active": true,
"priority": 8
}
| Field | Type | Description |
|---|---|---|
name | string | Updated rule name |
rule_type | string | Updated rule type |
adjustment_percent | float | Updated adjustment (-0.50 to 0.50) |
conditions | object | Updated conditions |
is_active | bool | Enable or disable the rule |
priority | int | Updated priority |
Response:
{
"status": "updated",
"rule_id": "rule-003-uuid"
}
Toggle Pricing Rule
Toggle a rule between active and inactive without specifying the target state.
PUT /api/v1/pricing/rules/{rule_id}/toggle
Authorization: Bearer {access_token}
Path Parameters:
| Parameter | Type | Description |
|---|---|---|
rule_id | string | Rule UUID |
Response:
{
"status": "toggled",
"rule_id": "rule-003-uuid",
"is_active": false
}
Delete Pricing Rule
Soft-deletes a pricing rule by setting is_active to false.
DELETE /api/v1/pricing/rules/{rule_id}
Authorization: Bearer {access_token}
Path Parameters:
| Parameter | Type | Description |
|---|---|---|
rule_id | string | Rule UUID |
Response:
{
"status": "deleted",
"rule_id": "rule-003-uuid"
}
Demand Forecasting Endpoints
Get Demand Forecast
Generate a multi-day demand forecast for a location using ML models. The system tries Vertex AI AutoML first, falls back to Prophet, then to statistical methods.
GET /api/v1/forecast/demand/{location_id}?days=7&latitude=27.9506&longitude=-82.4572
Authorization: Bearer {access_token}
Path Parameters:
| Parameter | Type | Description |
|---|---|---|
location_id | string | Location UUID |
Query Parameters:
| Parameter | Type | Default | Description |
|---|---|---|---|
days | int | 7 | Forecast horizon in days (1--30) |
latitude | float | 40.7128 | Location latitude (for weather impact) |
longitude | float | -74.0060 | Location longitude (for weather impact) |
Response:
{
"status": "success",
"location_id": "550e8400-e29b-41d4-a716-446655449110",
"horizon_days": 7,
"data": [
{
"date": "2026-02-20",
"predicted_sales": 4825.50,
"predicted_orders": 312,
"confidence_low": 4102.68,
"confidence_high": 5548.33,
"confidence_level": 0.95,
"factors": {
"model": "prophet",
"weather_impact": 0.03,
"day_of_week_effect": 0.12,
"trend": "upward",
"seasonality": "winter_weekday"
}
},
{
"date": "2026-02-21",
"predicted_sales": 6210.00,
"predicted_orders": 401,
"confidence_low": 5278.50,
"confidence_high": 7141.50,
"confidence_level": 0.95,
"factors": {
"model": "prophet",
"weather_impact": -0.02,
"day_of_week_effect": 0.28,
"trend": "upward",
"seasonality": "winter_weekend"
}
}
]
}
| Field | Type | Description |
|---|---|---|
predicted_sales | float | Predicted total sales revenue for the day |
predicted_orders | int | Predicted number of orders |
confidence_low | float | Lower bound of 95% confidence interval |
confidence_high | float | Upper bound of 95% confidence interval |
confidence_level | float | Confidence interval level (default 0.95) |
factors | object | Contributing factors: model used, weather impact, day-of-week effects, trend, seasonality |
Get Demand Forecast by Date
Retrieve a stored forecast for a specific date. Forecasts are persisted when generated via the multi-day endpoint.
GET /api/v1/forecast/demand/{location_id}/{date}
Authorization: Bearer {access_token}
Path Parameters:
| Parameter | Type | Description |
|---|---|---|
location_id | string | Location UUID |
date | string | Forecast date in YYYY-MM-DD format |
Response:
{
"status": "success",
"data": {
"id": "fc-a1b2c3d4-e5f6-7890-abcd-ef1234567890",
"location_id": "550e8400-e29b-41d4-a716-446655449110",
"forecast_date": "2026-02-20",
"model_type": "prophet",
"predicted_sales": 4825.50,
"predicted_orders": 312,
"confidence_low": 4102.68,
"confidence_high": 5548.33,
"confidence_level": 0.95,
"factors": {
"model": "prophet",
"weather_impact": 0.03,
"day_of_week_effect": 0.12
},
"created_at": "2026-02-19T08:00:00Z"
}
}
If no forecast exists for the requested date, the endpoint returns 404. Generate forecasts first using GET /api/v1/forecast/demand/{location_id}?days=N.
Get Forecast Accuracy
Retrieve forecast accuracy metrics over a period. Compares past forecasts against actual sales data from ClickHouse.
GET /api/v1/forecast/accuracy/{location_id}?days=30
Authorization: Bearer {access_token}
Path Parameters:
| Parameter | Type | Description |
|---|---|---|
location_id | string | Location UUID |
Query Parameters:
| Parameter | Type | Default | Description |
|---|---|---|---|
days | int | 30 | Analysis period in days (7--90) |
Response:
{
"status": "success",
"data": {
"location_id": "550e8400-e29b-41d4-a716-446655449110",
"period_days": 30,
"total_forecasts_evaluated": 28,
"mape": 12.4,
"rmse": 587.30,
"within_20_percent": 0.82,
"by_model": {
"prophet": {
"count": 20,
"mape": 10.8,
"within_20_percent": 0.90
},
"statistical": {
"count": 8,
"mape": 16.2,
"within_20_percent": 0.625
}
}
}
}
| Field | Type | Description |
|---|---|---|
mape | float | Mean Absolute Percentage Error (lower is better) |
rmse | float | Root Mean Square Error in dollars |
within_20_percent | float | Fraction of forecasts within 20% of actual (target: 0.80+) |
by_model | object | Accuracy breakdown per model type |
Performance Targets
| Operation | Target | Timeout |
|---|---|---|
| Price optimization (single item) | Response in under 5 seconds | 5s |
| Price apply | Response in under 5 seconds | 5s |
| Batch optimize (all items) | Proportional to item count | None (sequential) |
| Demand forecast generation | Response in under 30 seconds | 30s |
| Forecast retrieval (stored) | Response in under 1 second | Default |
| Rules CRUD | Response in under 1 second | Default |
| Price history query | Response in under 2 seconds | Default |
Error Handling
HTTP Status Codes
| Code | Meaning | Common Cause |
|---|---|---|
400 | Bad Request | Invalid rule type, price outside guardrails, no update fields provided |
404 | Not Found | Item, rule, or forecast not found |
429 | Too Many Requests | Daily price change limit (3) exceeded for location |
503 | Service Unavailable | Spanner not configured for pricing service |
504 | Gateway Timeout | Price optimization or forecast generation timed out |
Error Response Format
{
"detail": "Maximum daily price changes (3) reached for this location"
}
Guardrail Violations
When a price change violates the 70%--130% base price bounds:
{
"detail": "Price must be between 9.09 and 16.89"
}
Invalid Rule Type
When creating a rule with an unsupported rule_type:
{
"detail": "Invalid rule_type. Must be one of: day_of_week, daypart, demand_threshold, event, happy_hour, inventory_threshold, surge, weather"
}
Rule Types Reference
| Rule Type | Description | Example Conditions |
|---|---|---|
daypart | Time-of-day pricing | {"start_hour": 11, "end_hour": 14} |
day_of_week | Day-specific pricing | {"days": ["friday", "saturday"]} |
demand_threshold | Trigger on demand level | {"demand_percentile_above": 85} |
inventory_threshold | Adjust based on stock | {"inventory_below": 10} |
weather | Weather-driven pricing | {"condition": "rain", "temp_above": 90} |
event | Local event pricing | {"event_type": "concert", "proximity_miles": 2} |
happy_hour | Scheduled discounts | {"start_hour": 16, "end_hour": 18} |
surge | High-traffic surge pricing | {"order_rate_above": 50} |
Related Documentation
- Menus API -- Menu item management
- Inventory API -- Stock tracking and reordering
- Orders API -- Order management
- Marketing API -- Campaigns and promotions