All endpoints require a valid JWT Bearer token, tenant authorization, and the ANALYTICS_CORE_POLICY gating policy. Accessible via the API gateway at /v1/coursing/analytics/*.
Coursing Analytics API
Analyze course fire timing, station coordination, and kitchen performance.
Overview
The Coursing Analytics API provides detailed metrics for multi-course meal orchestration. It surfaces how well the kitchen and front-of-house coordinate course fires, tracks delay root causes, and generates actionable recommendations.
| Feature | Description |
|---|---|
| Course Timing | Fire delay, on-time percentage, prep time, gap timing per course |
| Station Performance | Ticket counts, average and P95 times, sync delay per station |
| Service Metrics | Fire response time, pace selection, auto vs manual fire breakdown |
| Timing Trends | Time-series data with day, week, or month granularity |
| Delay Breakdown | Categorized delays: kitchen, fire, server, coordination |
| Dashboard | Full summary with efficiency score and recommendations |
| Export | CSV or JSON export with selectable data sections |
Related Issues: Epic #790 (Course Management), #1062 (Python Coursing Analytics)
Authentication
All endpoints require three layers of authorization:
| Layer | Mechanism | Description |
|---|---|---|
| JWT | Authorization: Bearer {token} | Valid access token from the Auth service |
| Tenant Auth | Automatic from token | Request is scoped to the caller's tenant |
| Gating Policy | ANALYTICS_CORE_POLICY | Feature must be enabled for the tenant |
Common Query Parameters
Most endpoints share these query parameters for date range control:
| Parameter | Type | Default | Description |
|---|---|---|---|
start_date | date | Computed from days | Analysis start date (YYYY-MM-DD) |
end_date | date | Today (UTC) | Analysis end date (YYYY-MM-DD) |
days | integer | 7 | Days to analyze when dates are not provided (1-90) |
If start_date and end_date are omitted, the API defaults to the last days days ending today.
Get Course Timing Metrics
Returns per-course timing performance including fire delay, on-time rate, prep time, variance, and gap timing.
Request
GET /api/v1/coursing/analytics/timing/{location_id}?
start_date=2026-02-01&
end_date=2026-02-07
Authorization: Bearer {access_token}
Path Parameters
| Parameter | Type | Required | Description |
|---|---|---|---|
location_id | string | Yes | Location identifier (UUID) |
Query Parameters
| Parameter | Type | Default | Description |
|---|---|---|---|
start_date | date | Computed | Analysis start date |
end_date | date | Today | Analysis end date |
days | integer | 7 | Days to analyze if dates not provided (1-90) |
Response
{
"success": true,
"data": {
"location_id": "550e8400-e29b-41d4-a716-446655449110",
"tenant_id": "550e8400-e29b-41d4-a716-446655449100",
"start_date": "2026-02-01",
"end_date": "2026-02-07",
"courses": [
{
"course_name": "Appetizers",
"course_number": 1,
"avg_fire_delay_minutes": 1.2,
"fire_on_time_pct": 92.5,
"auto_fire_rate": 68.0,
"avg_prep_time_minutes": 8.4,
"prep_time_variance": 2.1,
"avg_hold_time_minutes": 1.8,
"avg_gap_from_prior_minutes": 0.0,
"gap_too_short_pct": 0.0,
"gap_too_long_pct": 0.0,
"avg_course_duration_minutes": 14.2,
"course_completion_rate": 98.5,
"total_orders": 312
},
{
"course_name": "Entrees",
"course_number": 2,
"avg_fire_delay_minutes": 2.4,
"fire_on_time_pct": 85.3,
"auto_fire_rate": 45.0,
"avg_prep_time_minutes": 14.6,
"prep_time_variance": 4.8,
"avg_hold_time_minutes": 2.5,
"avg_gap_from_prior_minutes": 12.3,
"gap_too_short_pct": 8.2,
"gap_too_long_pct": 5.1,
"avg_course_duration_minutes": 22.8,
"course_completion_rate": 97.1,
"total_orders": 298
},
{
"course_name": "Desserts",
"course_number": 3,
"avg_fire_delay_minutes": 1.8,
"fire_on_time_pct": 88.7,
"auto_fire_rate": 55.0,
"avg_prep_time_minutes": 6.2,
"prep_time_variance": 1.5,
"avg_hold_time_minutes": 1.1,
"avg_gap_from_prior_minutes": 18.5,
"gap_too_short_pct": 3.4,
"gap_too_long_pct": 12.0,
"avg_course_duration_minutes": 10.5,
"course_completion_rate": 94.2,
"total_orders": 185
}
]
}
}
Response Fields
| Field | Type | Description |
|---|---|---|
course_name | string | Name of the course template |
course_number | integer | Sequential course number |
avg_fire_delay_minutes | float | Average delay from scheduled fire to actual fire |
fire_on_time_pct | float | Percentage of fires that happened on time |
auto_fire_rate | float | Percentage of courses fired automatically |
avg_prep_time_minutes | float | Average kitchen preparation time |
prep_time_variance | float | Standard deviation of prep time |
avg_hold_time_minutes | float | Average time course waited at the pass |
avg_gap_from_prior_minutes | float | Average gap from the previous course |
gap_too_short_pct | float | Percentage of gaps that were too short |
gap_too_long_pct | float | Percentage of gaps that were too long |
avg_course_duration_minutes | float | Average total duration of the course |
course_completion_rate | float | Percentage of courses that completed normally |
total_orders | integer | Number of orders that included this course |
Get Station Performance
Returns performance metrics for each kitchen station, measuring how well stations coordinate during coursed service.
Request
GET /api/v1/coursing/analytics/stations/{location_id}?
start_date=2026-02-01&
end_date=2026-02-07
Authorization: Bearer {access_token}
Path Parameters
| Parameter | Type | Required | Description |
|---|---|---|---|
location_id | string | Yes | Location identifier (UUID) |
Query Parameters
| Parameter | Type | Default | Description |
|---|---|---|---|
start_date | date | Computed | Analysis start date |
end_date | date | Today | Analysis end date |
days | integer | 7 | Days to analyze if dates not provided (1-90) |
Response
{
"success": true,
"data": {
"location_id": "550e8400-e29b-41d4-a716-446655449110",
"tenant_id": "550e8400-e29b-41d4-a716-446655449100",
"start_date": "2026-02-01",
"end_date": "2026-02-07",
"stations": [
{
"station_name": "Grill",
"tickets_completed": 485,
"items_completed": 1120,
"avg_items_per_hour": 18.5,
"avg_ticket_time_minutes": 12.3,
"ticket_time_p95_minutes": 22.1,
"avg_sync_delay_minutes": 1.8,
"sync_success_rate": 91.2,
"remake_rate": 2.1,
"void_rate": 0.8
},
{
"station_name": "Saute",
"tickets_completed": 390,
"items_completed": 875,
"avg_items_per_hour": 14.2,
"avg_ticket_time_minutes": 10.8,
"ticket_time_p95_minutes": 18.5,
"avg_sync_delay_minutes": 2.4,
"sync_success_rate": 87.5,
"remake_rate": 1.5,
"void_rate": 0.5
},
{
"station_name": "Pastry",
"tickets_completed": 210,
"items_completed": 420,
"avg_items_per_hour": 10.8,
"avg_ticket_time_minutes": 7.2,
"ticket_time_p95_minutes": 12.4,
"avg_sync_delay_minutes": 0.9,
"sync_success_rate": 95.8,
"remake_rate": 0.8,
"void_rate": 0.3
}
]
}
}
Response Fields
| Field | Type | Description |
|---|---|---|
station_name | string | Kitchen station name |
tickets_completed | integer | Total tickets completed in the period |
items_completed | integer | Total items completed |
avg_items_per_hour | float | Average throughput per hour |
avg_ticket_time_minutes | float | Average time to complete a ticket |
ticket_time_p95_minutes | float | 95th percentile ticket completion time |
avg_sync_delay_minutes | float | Average delay when syncing with other stations |
sync_success_rate | float | Percentage of successful cross-station syncs |
remake_rate | float | Percentage of items that required a remake |
void_rate | float | Percentage of items that were voided |
Get Service Metrics
Returns server-side coursing metrics covering fire response time, pace selection patterns, and the breakdown of automatic versus manual course fires.
Request
GET /api/v1/coursing/analytics/service/{location_id}?
start_date=2026-02-01&
end_date=2026-02-07
Authorization: Bearer {access_token}
Path Parameters
| Parameter | Type | Required | Description |
|---|---|---|---|
location_id | string | Yes | Location identifier (UUID) |
Query Parameters
| Parameter | Type | Default | Description |
|---|---|---|---|
start_date | date | Computed | Analysis start date |
end_date | date | Today | Analysis end date |
days | integer | 7 | Days to analyze if dates not provided (1-90) |
Response
{
"success": true,
"data": {
"location_id": "550e8400-e29b-41d4-a716-446655449110",
"tenant_id": "550e8400-e29b-41d4-a716-446655449100",
"start_date": "2026-02-01",
"end_date": "2026-02-07",
"metrics": {
"avg_fire_response_seconds": 24.5,
"fire_response_rate": 94.2,
"avg_pace_selection": "normal",
"pace_change_rate": 18.3,
"avg_pickup_delay_seconds": 42.8,
"total_fires": 1245,
"auto_fires": 720,
"manual_fires": 525
}
}
}
Response Fields
| Field | Type | Description |
|---|---|---|
avg_fire_response_seconds | float | Average time for servers to respond to fire notifications |
fire_response_rate | float | Percentage of fires acknowledged by the server |
avg_pace_selection | string | Most common pace setting (leisurely, normal, quick, custom) |
pace_change_rate | float | Percentage of orders where guests changed the pace mid-meal |
avg_pickup_delay_seconds | float | Average delay between course ready and server pickup |
total_fires | integer | Total number of course fires in the period |
auto_fires | integer | Number of courses fired automatically by the system |
manual_fires | integer | Number of courses fired manually by a server |
Get Timing Trends
Returns time-series trend data for a selected timing metric, aggregated by day, week, or month.
Request
GET /api/v1/coursing/analytics/trends/{location_id}?
metric=fire_delay&
period=day&
lookback_days=30
Authorization: Bearer {access_token}
Path Parameters
| Parameter | Type | Required | Description |
|---|---|---|---|
location_id | string | Yes | Location identifier (UUID) |
Query Parameters
| Parameter | Type | Default | Description |
|---|---|---|---|
metric | string | fire_delay | Metric to trend (see values below) |
period | string | day | Aggregation granularity: day, week, month |
lookback_days | integer | 30 | Number of days to look back (7-365) |
Available Metrics
| Metric | Description |
|---|---|
fire_delay | Time from scheduled fire to actual fire |
prep_time | Kitchen preparation time per course |
gap_time | Gap between consecutive courses |
hold_time | Time course held at the window before pickup |
sync_delay | Station coordination delay |
Response
{
"success": true,
"data": {
"location_id": "550e8400-e29b-41d4-a716-446655449110",
"tenant_id": "550e8400-e29b-41d4-a716-446655449100",
"metric": "fire_delay",
"period": "day",
"lookback_days": 30,
"trends": [
{
"date": "2026-01-21",
"value": 2.8,
"sample_count": 45
},
{
"date": "2026-01-22",
"value": 2.5,
"sample_count": 52
},
{
"date": "2026-01-23",
"value": 3.1,
"sample_count": 38
},
{
"date": "2026-01-24",
"value": 1.9,
"sample_count": 61
},
{
"date": "2026-01-25",
"value": 2.2,
"sample_count": 58
}
]
}
}
Response Fields
| Field | Type | Description |
|---|---|---|
date | string | Period start date (format depends on period granularity) |
value | float | Average metric value for the period |
sample_count | integer | Number of data points aggregated into this period |
Get Delay Breakdown
Returns a categorized breakdown of delay causes across kitchen, fire, server, and coordination categories.
Request
GET /api/v1/coursing/analytics/delays/{location_id}?
start_date=2026-02-01&
end_date=2026-02-07
Authorization: Bearer {access_token}
Path Parameters
| Parameter | Type | Required | Description |
|---|---|---|---|
location_id | string | Yes | Location identifier (UUID) |
Query Parameters
| Parameter | Type | Default | Description |
|---|---|---|---|
start_date | date | Computed | Analysis start date |
end_date | date | Today | Analysis end date |
days | integer | 7 | Days to analyze if dates not provided (1-90) |
Response
{
"success": true,
"data": {
"location_id": "550e8400-e29b-41d4-a716-446655449110",
"tenant_id": "550e8400-e29b-41d4-a716-446655449100",
"start_date": "2026-02-01",
"end_date": "2026-02-07",
"breakdown": {
"kitchen_delays": 42,
"fire_delays": 28,
"server_delays": 15,
"coordination_delays": 10,
"total_delays": 95,
"avg_delay_minutes": 3.8,
"kitchen_pct": 44.2,
"fire_pct": 29.5,
"server_pct": 15.8,
"coordination_pct": 10.5
}
}
}
Delay Categories
| Category | Description |
|---|---|
kitchen_delays | Kitchen preparation took longer than expected |
fire_delays | Server was slow to fire the next course |
server_delays | Server was slow to pick up a ready course |
coordination_delays | Multiple stations failed to sync their output |
Response Fields
| Field | Type | Description |
|---|---|---|
kitchen_delays | integer | Count of kitchen-caused delays |
fire_delays | integer | Count of fire-timing delays |
server_delays | integer | Count of server pickup delays |
coordination_delays | integer | Count of station sync delays |
total_delays | integer | Total delay events |
avg_delay_minutes | float | Average delay duration in minutes |
kitchen_pct | float | Kitchen delays as a percentage of total |
fire_pct | float | Fire delays as a percentage of total |
server_pct | float | Server delays as a percentage of total |
coordination_pct | float | Coordination delays as a percentage of total |
Get Dashboard Summary
Returns a comprehensive coursing analytics dashboard including overall statistics, per-course metrics, an efficiency score, and actionable recommendations.
Request
GET /api/v1/coursing/analytics/dashboard/{location_id}?days=7
Authorization: Bearer {access_token}
Path Parameters
| Parameter | Type | Required | Description |
|---|---|---|---|
location_id | string | Yes | Location identifier (UUID) |
Query Parameters
| Parameter | Type | Default | Description |
|---|---|---|---|
days | integer | 7 | Number of days to analyze (1-90) |
Response
{
"success": true,
"data": {
"location_id": "550e8400-e29b-41d4-a716-446655449110",
"period_days": 7,
"total_coursed_orders": 795,
"avg_courses_per_order": 3,
"on_time_rate": 88.83,
"avg_meal_duration_minutes": 47.5,
"most_common_pace": "normal",
"top_delay_cause": "kitchen",
"efficiency_score": 87.4,
"course_metrics": [
{
"course_name": "Appetizers",
"course_number": 1,
"avg_fire_delay_minutes": 1.2,
"fire_on_time_pct": 92.5,
"auto_fire_rate": 68.0,
"avg_prep_time_minutes": 8.4,
"prep_time_variance": 2.1,
"avg_hold_time_minutes": 1.8,
"avg_gap_from_prior_minutes": 0.0,
"gap_too_short_pct": 0.0,
"gap_too_long_pct": 0.0,
"avg_course_duration_minutes": 14.2,
"course_completion_rate": 98.5,
"total_orders": 312
},
{
"course_name": "Entrees",
"course_number": 2,
"avg_fire_delay_minutes": 2.4,
"fire_on_time_pct": 85.3,
"auto_fire_rate": 45.0,
"avg_prep_time_minutes": 14.6,
"prep_time_variance": 4.8,
"avg_hold_time_minutes": 2.5,
"avg_gap_from_prior_minutes": 12.3,
"gap_too_short_pct": 8.2,
"gap_too_long_pct": 5.1,
"avg_course_duration_minutes": 22.8,
"course_completion_rate": 97.1,
"total_orders": 298
},
{
"course_name": "Desserts",
"course_number": 3,
"avg_fire_delay_minutes": 1.8,
"fire_on_time_pct": 88.7,
"auto_fire_rate": 55.0,
"avg_prep_time_minutes": 6.2,
"prep_time_variance": 1.5,
"avg_hold_time_minutes": 1.1,
"avg_gap_from_prior_minutes": 18.5,
"gap_too_short_pct": 3.4,
"gap_too_long_pct": 12.0,
"avg_course_duration_minutes": 10.5,
"course_completion_rate": 94.2,
"total_orders": 185
}
],
"recommendations": [
"Kitchen is the primary bottleneck. Review station staffing and prep workflows.",
"Course timing is below target. Consider adjusting prep times or gap settings."
],
"generated_at": "2026-02-20T14:30:00.000000+00:00"
}
}
Response Fields
| Field | Type | Description |
|---|---|---|
location_id | string | The queried location |
period_days | integer | Number of days analyzed |
total_coursed_orders | integer | Total orders that used coursing |
avg_courses_per_order | float | Average number of courses per order |
on_time_rate | float | Average on-time fire percentage across all courses |
avg_meal_duration_minutes | float | Average total meal duration |
most_common_pace | string | Most frequently selected pace (leisurely, normal, quick, custom) |
top_delay_cause | string | Category with the most delays (kitchen, fire, server, coordination, none) |
efficiency_score | float | Composite efficiency score from 0 to 100 |
course_metrics | array | Per-course timing metrics (same schema as the timing endpoint) |
recommendations | array | List of actionable recommendation strings |
generated_at | string | ISO 8601 timestamp when the dashboard was generated |
Export Coursing Analytics
Exports coursing analytics data in CSV or JSON format. Callers can select which data sections to include in the export.
Request
GET /api/v1/coursing/analytics/export/{location_id}?
start_date=2026-02-01&
end_date=2026-02-07&
format=json&
include_course_metrics=true&
include_station_metrics=true&
include_service_metrics=true&
include_delay_breakdown=true
Authorization: Bearer {access_token}
Path Parameters
| Parameter | Type | Required | Description |
|---|---|---|---|
location_id | string | Yes | Location identifier (UUID) |
Query Parameters
| Parameter | Type | Default | Description |
|---|---|---|---|
start_date | date | Computed | Export start date |
end_date | date | Today | Export end date |
days | integer | 7 | Days to export if dates not provided (1-90) |
format | string | csv | Export format: csv or json |
include_course_metrics | boolean | true | Include per-course timing data |
include_station_metrics | boolean | true | Include station performance data |
include_service_metrics | boolean | true | Include server-side service data |
include_delay_breakdown | boolean | true | Include delay categorization data |
Response (JSON format)
{
"success": true,
"data": {
"metadata": {
"tenant_id": "550e8400-e29b-41d4-a716-446655449100",
"location_id": "550e8400-e29b-41d4-a716-446655449110",
"start_date": "2026-02-01",
"end_date": "2026-02-07",
"exported_at": "2026-02-20T14:30:00.000000+00:00",
"format": "json"
},
"sections": {
"course_timing": [
{
"course_name": "Appetizers",
"course_number": 1,
"avg_fire_delay_minutes": 1.2,
"fire_on_time_pct": 92.5,
"auto_fire_rate": 68.0,
"avg_prep_time_minutes": 8.4,
"prep_time_variance": 2.1,
"avg_hold_time_minutes": 1.8,
"avg_gap_from_prior_minutes": 0.0,
"gap_too_short_pct": 0.0,
"gap_too_long_pct": 0.0,
"avg_course_duration_minutes": 14.2,
"course_completion_rate": 98.5,
"total_orders": 312
}
],
"station_performance": [
{
"station_name": "Grill",
"tickets_completed": 485,
"items_completed": 1120,
"avg_items_per_hour": 18.5,
"avg_ticket_time_minutes": 12.3,
"ticket_time_p95_minutes": 22.1,
"avg_sync_delay_minutes": 1.8,
"sync_success_rate": 91.2,
"remake_rate": 2.1,
"void_rate": 0.8
}
],
"service_metrics": {
"avg_fire_response_seconds": 24.5,
"fire_response_rate": 94.2,
"avg_pace_selection": "normal",
"pace_change_rate": 18.3,
"avg_pickup_delay_seconds": 42.8,
"total_fires": 1245,
"auto_fires": 720,
"manual_fires": 525
},
"delay_breakdown": {
"kitchen_delays": 42,
"fire_delays": 28,
"server_delays": 15,
"coordination_delays": 10,
"total_delays": 95,
"avg_delay_minutes": 3.8,
"kitchen_pct": 44.2,
"fire_pct": 29.5,
"server_pct": 15.8,
"coordination_pct": 10.5
}
}
}
}
Response (CSV format)
When format=csv, the response includes a csv_files object containing base64-encoded CSV content for each included section:
{
"success": true,
"data": {
"metadata": {
"tenant_id": "550e8400-e29b-41d4-a716-446655449100",
"location_id": "550e8400-e29b-41d4-a716-446655449110",
"start_date": "2026-02-01",
"end_date": "2026-02-07",
"exported_at": "2026-02-20T14:30:00.000000+00:00",
"format": "csv"
},
"sections": { ... },
"csv_files": {
"course_timing.csv": "course_name,course_number,avg_fire_delay_minutes,...\nAppetizers,1,1.2,...\n",
"station_performance.csv": "station_name,tickets_completed,...\nGrill,485,...\n",
"service_metrics.csv": "avg_fire_response_seconds,...\n24.5,...\n",
"delay_breakdown.csv": "kitchen_delays,fire_delays,...\n42,28,...\n"
}
}
}
Error Responses
Invalid Date Range (400)
Returned when start_date is after end_date.
{
"detail": "start_date must be before or equal to end_date"
}
Unauthorized (401)
Returned when the JWT token is missing or invalid.
{
"detail": "Not authenticated"
}
Forbidden (403)
Returned when the tenant does not have the ANALYTICS_CORE_POLICY gating policy enabled.
{
"detail": "Feature not enabled for this tenant"
}
Location Not Found (404)
Returned when the location does not exist or is not accessible to the caller's tenant.
{
"detail": "Location not found"
}
Internal Server Error (500)
Returned when the analytics backend (ClickHouse) encounters an error. The endpoints degrade gracefully by returning empty result sets rather than failing outright.
{
"detail": "Internal server error"
}
Related Documentation
- Dashboard API - General operational dashboard metrics
- Reports API - Generate and export business reports
- AI Insights - AI-powered analytics and recommendations