Skip to main content
Authenticated API

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

PropertyDetails
Pricing Base Path/api/v1/pricing
Forecast Base Path/api/v1/forecast
AuthenticationBearer JWT token
Required Rolesmanager, restaurant_manager, analytics_viewer, tenant_admin, platform_admin, system_admin, super_admin
Backend ServicePython Analytics (proxied via Go API Gateway)
Epic#779 -- AI Demand Forecasting & Dynamic Pricing
Feature Gatedynamic_pricing_enabled (pricing endpoints only)

The Dynamic Pricing API provides two complementary capabilities:

CapabilityDescription
Price OptimizationDQN (Deep Q-Network) agent recommends optimal prices based on historical demand-price-revenue data
Pricing RulesAutomated rules that trigger price adjustments based on daypart, demand, weather, events, and more
Price HistoryFull audit trail of all price changes with rollback support
Demand ForecastingML models (Vertex AI AutoML, Prophet, statistical fallback) predict future sales and order volume
Forecast AccuracyTracks 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:

ParameterTypeDescription
location_idstringLocation UUID
item_idstringMenu 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"
]
}
FieldTypeDescription
recommended_pricefloatAI-recommended optimal price
current_pricefloatCurrent active price
base_pricefloatOriginal menu price (anchor for guardrails)
adjustment_percentfloatRecommended change as a decimal (e.g., 0.1155 = +11.55%)
confidencefloatModel confidence score (0.0 -- 1.0)
expected_revenue_changefloatPredicted revenue impact as a decimal
expected_demand_changefloatPredicted demand impact as a decimal (negative = fewer orders)
reasoningstring[]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:

ParameterTypeDescription
location_idstringLocation 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"
}
FieldTypeRequiredDescription
location_idstringYesLocation UUID
item_idstringYesMenu item UUID
new_pricefloatYesNew price (must be > 0, within 70%--130% of base)
reasonstringNoReason 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:

ParameterTypeDescription
change_idstringPrice 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:

ParameterTypeDescription
location_idstringLocation UUID

Query Parameters:

ParameterTypeDefaultDescription
limitint100Max 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:

ParameterTypeDescription
location_idstringLocation UUID

Query Parameters:

ParameterTypeDefaultDescription
daysint30Analysis 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:

ParameterTypeDescription
location_idstringLocation UUID

Query Parameters:

ParameterTypeDefaultDescription
item_idstringnullFilter to a specific item
limitint50Max 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:

ParameterTypeDefaultDescription
location_idstringnullFilter by location (null = all locations)
active_onlybooltrueReturn 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
}
FieldTypeRequiredDescription
namestringYesHuman-readable rule name
rule_typestringYesOne of: daypart, day_of_week, demand_threshold, inventory_threshold, weather, event, happy_hour, surge
adjustment_percentfloatYesPrice adjustment as a decimal (-0.50 to 0.50)
conditionsobjectNoRule-specific condition parameters
location_idstringNoScope to a location (null = all locations)
item_idstringNoScope to an item (null = all items)
priorityintNoEvaluation 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:

ParameterTypeDescription
rule_idstringRule UUID

Request (all fields optional):

{
"name": "Happy Hour Deep Discount",
"adjustment_percent": -0.20,
"is_active": true,
"priority": 8
}
FieldTypeDescription
namestringUpdated rule name
rule_typestringUpdated rule type
adjustment_percentfloatUpdated adjustment (-0.50 to 0.50)
conditionsobjectUpdated conditions
is_activeboolEnable or disable the rule
priorityintUpdated 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:

ParameterTypeDescription
rule_idstringRule 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:

ParameterTypeDescription
rule_idstringRule 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:

ParameterTypeDescription
location_idstringLocation UUID

Query Parameters:

ParameterTypeDefaultDescription
daysint7Forecast horizon in days (1--30)
latitudefloat40.7128Location latitude (for weather impact)
longitudefloat-74.0060Location 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"
}
}
]
}
FieldTypeDescription
predicted_salesfloatPredicted total sales revenue for the day
predicted_ordersintPredicted number of orders
confidence_lowfloatLower bound of 95% confidence interval
confidence_highfloatUpper bound of 95% confidence interval
confidence_levelfloatConfidence interval level (default 0.95)
factorsobjectContributing 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:

ParameterTypeDescription
location_idstringLocation UUID
datestringForecast 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"
}
}
Generate before querying

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:

ParameterTypeDescription
location_idstringLocation UUID

Query Parameters:

ParameterTypeDefaultDescription
daysint30Analysis 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
}
}
}
}
FieldTypeDescription
mapefloatMean Absolute Percentage Error (lower is better)
rmsefloatRoot Mean Square Error in dollars
within_20_percentfloatFraction of forecasts within 20% of actual (target: 0.80+)
by_modelobjectAccuracy breakdown per model type

Performance Targets

OperationTargetTimeout
Price optimization (single item)Response in under 5 seconds5s
Price applyResponse in under 5 seconds5s
Batch optimize (all items)Proportional to item countNone (sequential)
Demand forecast generationResponse in under 30 seconds30s
Forecast retrieval (stored)Response in under 1 secondDefault
Rules CRUDResponse in under 1 secondDefault
Price history queryResponse in under 2 secondsDefault

Error Handling

HTTP Status Codes

CodeMeaningCommon Cause
400Bad RequestInvalid rule type, price outside guardrails, no update fields provided
404Not FoundItem, rule, or forecast not found
429Too Many RequestsDaily price change limit (3) exceeded for location
503Service UnavailableSpanner not configured for pricing service
504Gateway TimeoutPrice 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 TypeDescriptionExample Conditions
daypartTime-of-day pricing{"start_hour": 11, "end_hour": 14}
day_of_weekDay-specific pricing{"days": ["friday", "saturday"]}
demand_thresholdTrigger on demand level{"demand_percentile_above": 85}
inventory_thresholdAdjust based on stock{"inventory_below": 10}
weatherWeather-driven pricing{"condition": "rain", "temp_above": 90}
eventLocal event pricing{"event_type": "concert", "proximity_miles": 2}
happy_hourScheduled discounts{"start_hour": 16, "end_hour": 18}
surgeHigh-traffic surge pricing{"order_rate_above": 50}