Skip to main content
Authenticated API

This endpoint requires a valid JWT Bearer token. Accessible via the API gateway at /v1/commerce/*.

P&L Reporting API

Profit & Loss reporting across your enterprise -- consolidated, brand-level, regional, comparison, and custom reports backed by ClickHouse Cloud analytics (Epic #1475).

Overview

AttributeValue
Base Path/api/v1/pnl
AuthenticationBearer Token (JWT)
Tenant IsolationAutomatic via tenant_id from JWT claims
Required Rolesmanager, restaurant_manager, tenant_admin, platform_admin, super_admin
Data SourceClickHouse Cloud (Epic #1475: BigQuery to ClickHouse Migration)

The P&L Reporting API provides:

FeatureDescription
Consolidated P&LEnterprise-wide profit and loss across all brands and locations
Brand P&LFinancial statement for a single brand
Regional P&LFinancial statement for a geographic region
Brand ComparisonSide-by-side comparison of 2+ brands
Custom ReportsSelectable sections, grouping, and filters
Trend AnalysisMetric trends over time with day/week/month granularity
Metrics CatalogDiscover available P&L metrics for report building
Sections CatalogDiscover available report sections for custom reports

Data Models

PnLSummary

Core financial metrics returned in P&L responses:

FieldTypeDescription
net_revenuefloatTotal revenue after discounts and refunds
gross_profitfloatRevenue minus cost of goods sold
gross_marginfloatGross profit as a percentage of revenue
labor_costfloatTotal labor expenses
labor_percentagefloatLabor cost as a percentage of revenue
food_costfloatTotal food cost
food_cost_percentagefloatFood cost as a percentage of revenue
operating_incomefloatIncome from core operations
operating_marginfloatOperating income as a percentage of revenue
net_incomefloatFinal bottom-line income
net_marginfloatNet income as a percentage of revenue

Standard Response Envelope

All endpoints return a consistent envelope:

{
"success": true,
"data": { ... }
}

Consolidated P&L

Generate an enterprise-wide P&L statement aggregated across all brands and locations. Optionally include budget variance and prior period comparison.

Request

POST /api/v1/pnl/consolidated
Authorization: Bearer {access_token}
Content-Type: application/json

Request Body

{
"start_date": "2026-01-01",
"end_date": "2026-01-31",
"brand_ids": ["brand-001", "brand-002"],
"include_budget": true,
"include_prior_period": true
}
FieldTypeRequiredDescription
start_datestring (YYYY-MM-DD)YesReport period start date
end_datestring (YYYY-MM-DD)YesReport period end date
brand_idsstring[]NoFilter to specific brands (omit for all)
include_budgetbooleanNoInclude budget comparison column (default: false)
include_prior_periodbooleanNoInclude prior period comparison column (default: false)

Response

{
"success": true,
"data": {
"period": {
"start_date": "2026-01-01",
"end_date": "2026-01-31"
},
"summary": {
"net_revenue": 485000.00,
"gross_profit": 316250.00,
"gross_margin": 65.2,
"labor_cost": 145500.00,
"labor_percentage": 30.0,
"food_cost": 150350.00,
"food_cost_percentage": 31.0,
"operating_income": 72750.00,
"operating_margin": 15.0,
"net_income": 58200.00,
"net_margin": 12.0
},
"budget_comparison": {
"net_revenue": { "actual": 485000.00, "budget": 500000.00, "variance": -15000.00, "variance_pct": -3.0 },
"food_cost_percentage": { "actual": 31.0, "budget": 30.0, "variance": 1.0, "variance_pct": 3.3 }
},
"prior_period": {
"net_revenue": { "current": 485000.00, "prior": 470000.00, "change": 15000.00, "change_pct": 3.2 },
"gross_margin": { "current": 65.2, "prior": 64.0, "change": 1.2, "change_pct": 1.9 }
},
"brands_included": 2
}
}

Brand P&L

Generate a P&L statement for a single brand.

Request

POST /api/v1/pnl/brand/{brand_id}
Authorization: Bearer {access_token}
Content-Type: application/json

Path Parameters

ParameterTypeDescription
brand_idstringBrand identifier

Request Body

{
"start_date": "2026-01-01",
"end_date": "2026-01-31"
}
FieldTypeRequiredDescription
start_datestring (YYYY-MM-DD)YesReport period start date
end_datestring (YYYY-MM-DD)YesReport period end date

Response

{
"success": true,
"data": {
"brand_id": "brand-001",
"brand_name": "Downtown Grill",
"period": {
"start_date": "2026-01-01",
"end_date": "2026-01-31"
},
"summary": {
"net_revenue": 245000.00,
"gross_profit": 161700.00,
"gross_margin": 66.0,
"labor_cost": 73500.00,
"labor_percentage": 30.0,
"food_cost": 73500.00,
"food_cost_percentage": 30.0,
"operating_income": 39200.00,
"operating_margin": 16.0,
"net_income": 31360.00,
"net_margin": 12.8
},
"revenue": {
"gross_revenue": 260000.00,
"discounts": -10000.00,
"refunds": -5000.00,
"net_revenue": 245000.00
},
"cost_of_goods_sold": {
"food_cost": 58800.00,
"beverage_cost": 14700.00,
"total_cogs": 73500.00
},
"operating_expenses": {
"labor_cost": 73500.00,
"occupancy_cost": 24500.00,
"utilities": 9800.00,
"marketing": 7350.00,
"other": 12250.00,
"total_operating_expenses": 127400.00
},
"locations_count": 3
}
}

Regional P&L

Generate a P&L statement for a specific geographic region.

Request

POST /api/v1/pnl/region/{region_id}
Authorization: Bearer {access_token}
Content-Type: application/json

Path Parameters

ParameterTypeDescription
region_idstringRegion identifier

Request Body

{
"start_date": "2026-01-01",
"end_date": "2026-01-31"
}
FieldTypeRequiredDescription
start_datestring (YYYY-MM-DD)YesReport period start date
end_datestring (YYYY-MM-DD)YesReport period end date

Response

{
"success": true,
"data": {
"region_id": "region-southeast",
"region_name": "Southeast",
"period": {
"start_date": "2026-01-01",
"end_date": "2026-01-31"
},
"summary": {
"net_revenue": 620000.00,
"gross_profit": 396800.00,
"gross_margin": 64.0,
"labor_cost": 192200.00,
"labor_percentage": 31.0,
"food_cost": 198400.00,
"food_cost_percentage": 32.0,
"operating_income": 86800.00,
"operating_margin": 14.0,
"net_income": 68200.00,
"net_margin": 11.0
},
"brands_in_region": 4,
"locations_in_region": 12
}
}

Compare Brands

Compare P&L metrics side-by-side across two or more brands. Requires at least 2 brand_ids.

Request

POST /api/v1/pnl/compare
Authorization: Bearer {access_token}
Content-Type: application/json

Request Body

{
"start_date": "2026-01-01",
"end_date": "2026-01-31",
"brand_ids": ["brand-001", "brand-002", "brand-003"],
"metrics": ["net_revenue", "gross_margin", "labor_percentage", "net_margin"]
}
FieldTypeRequiredDescription
start_datestring (YYYY-MM-DD)YesReport period start date
end_datestring (YYYY-MM-DD)YesReport period end date
brand_idsstring[]YesBrands to compare (minimum 2)
metricsstring[]NoSpecific metrics to compare (omit for all available metrics)

Response

{
"success": true,
"data": {
"period": {
"start_date": "2026-01-01",
"end_date": "2026-01-31"
},
"brands": [
{
"brand_id": "brand-001",
"brand_name": "Downtown Grill",
"metrics": {
"net_revenue": 245000.00,
"gross_margin": 66.0,
"labor_percentage": 30.0,
"net_margin": 12.8
}
},
{
"brand_id": "brand-002",
"brand_name": "Harbor Bistro",
"metrics": {
"net_revenue": 180000.00,
"gross_margin": 62.5,
"labor_percentage": 32.0,
"net_margin": 10.5
}
},
{
"brand_id": "brand-003",
"brand_name": "Uptown Kitchen",
"metrics": {
"net_revenue": 60000.00,
"gross_margin": 68.0,
"labor_percentage": 28.0,
"net_margin": 14.2
}
}
],
"rankings": {
"net_revenue": ["brand-001", "brand-002", "brand-003"],
"gross_margin": ["brand-003", "brand-001", "brand-002"],
"labor_percentage": ["brand-003", "brand-001", "brand-002"],
"net_margin": ["brand-003", "brand-001", "brand-002"]
}
}
}

Custom Report

Build a custom P&L report by selecting specific sections, grouping dimensions, and filters.

Request

POST /api/v1/pnl/custom
Authorization: Bearer {access_token}
Content-Type: application/json

Request Body

{
"start_date": "2026-01-01",
"end_date": "2026-01-31",
"sections": ["revenue", "cogs", "operating_expenses"],
"group_by": "brand",
"filters": {
"region_id": "region-southeast",
"min_revenue": 50000
}
}
FieldTypeRequiredDescription
start_datestring (YYYY-MM-DD)YesReport period start date
end_datestring (YYYY-MM-DD)YesReport period end date
sectionsstring[]YesSections to include (see List Sections)
group_bystringNoGrouping dimension: brand, region, or location
filtersobjectNoAdditional key-value filters

Response

{
"success": true,
"data": {
"period": {
"start_date": "2026-01-01",
"end_date": "2026-01-31"
},
"group_by": "brand",
"sections_included": ["revenue", "cogs", "operating_expenses"],
"groups": [
{
"group_key": "brand-001",
"group_name": "Downtown Grill",
"revenue": {
"gross_revenue": 260000.00,
"discounts": -10000.00,
"refunds": -5000.00,
"net_revenue": 245000.00
},
"cogs": {
"food_cost": 58800.00,
"beverage_cost": 14700.00,
"total_cogs": 73500.00
},
"operating_expenses": {
"labor_cost": 73500.00,
"occupancy_cost": 24500.00,
"utilities": 9800.00,
"marketing": 7350.00,
"total_operating_expenses": 115150.00
}
},
{
"group_key": "brand-002",
"group_name": "Harbor Bistro",
"revenue": {
"gross_revenue": 195000.00,
"discounts": -8000.00,
"refunds": -7000.00,
"net_revenue": 180000.00
},
"cogs": {
"food_cost": 50400.00,
"beverage_cost": 16200.00,
"total_cogs": 66600.00
},
"operating_expenses": {
"labor_cost": 57600.00,
"occupancy_cost": 18000.00,
"utilities": 7200.00,
"marketing": 5400.00,
"total_operating_expenses": 88200.00
}
}
],
"totals": {
"revenue": { "net_revenue": 425000.00 },
"cogs": { "total_cogs": 140100.00 },
"operating_expenses": { "total_operating_expenses": 203350.00 }
}
}
}

P&L Trend

Track a single P&L metric over time with configurable granularity. Returns time-series data suitable for charts and sparklines.

Request

POST /api/v1/pnl/trend
Authorization: Bearer {access_token}
Content-Type: application/json

Request Body

{
"start_date": "2025-07-01",
"end_date": "2026-01-31",
"metric": "net_revenue",
"granularity": "month",
"brand_id": "brand-001"
}
FieldTypeRequiredDescription
start_datestring (YYYY-MM-DD)YesTrend period start date
end_datestring (YYYY-MM-DD)YesTrend period end date
metricstringYesMetric to track (see List Metrics)
granularitystringNoTime granularity: day, week, or month (default: month)
brand_idstringNoFilter to a specific brand

Response

{
"success": true,
"data": {
"metric": "net_revenue",
"granularity": "month",
"brand_id": "brand-001",
"period": {
"start_date": "2025-07-01",
"end_date": "2026-01-31"
},
"data_points": [
{ "date": "2025-07-01", "value": 210000.00 },
{ "date": "2025-08-01", "value": 225000.00 },
{ "date": "2025-09-01", "value": 218000.00 },
{ "date": "2025-10-01", "value": 232000.00 },
{ "date": "2025-11-01", "value": 248000.00 },
{ "date": "2025-12-01", "value": 265000.00 },
{ "date": "2026-01-01", "value": 245000.00 }
],
"summary": {
"min": 210000.00,
"max": 265000.00,
"avg": 234714.29,
"trend_direction": "up",
"change_pct": 16.7
}
}
}

List Available Metrics

Returns all available P&L metrics organized by category. Use these metric names in trend and comparison requests.

Request

GET /api/v1/pnl/metrics
Authorization: Bearer {access_token}

Response

{
"success": true,
"data": {
"revenue": [
{ "name": "gross_revenue", "label": "Gross Revenue", "type": "currency" },
{ "name": "discounts", "label": "Discounts", "type": "currency" },
{ "name": "refunds", "label": "Refunds", "type": "currency" },
{ "name": "net_revenue", "label": "Net Revenue", "type": "currency" }
],
"cost_of_goods_sold": [
{ "name": "food_cost", "label": "Food Cost", "type": "currency" },
{ "name": "beverage_cost", "label": "Beverage Cost", "type": "currency" },
{ "name": "total_cogs", "label": "Total COGS", "type": "currency" },
{ "name": "food_cost_percentage", "label": "Food Cost %", "type": "percentage" }
],
"profitability": [
{ "name": "gross_profit", "label": "Gross Profit", "type": "currency" },
{ "name": "gross_margin", "label": "Gross Margin", "type": "percentage" },
{ "name": "operating_income", "label": "Operating Income", "type": "currency" },
{ "name": "operating_margin", "label": "Operating Margin", "type": "percentage" },
{ "name": "net_income", "label": "Net Income", "type": "currency" },
{ "name": "net_margin", "label": "Net Margin", "type": "percentage" }
],
"operating_expenses": [
{ "name": "labor_cost", "label": "Labor Cost", "type": "currency" },
{ "name": "labor_percentage", "label": "Labor %", "type": "percentage" },
{ "name": "occupancy_cost", "label": "Occupancy", "type": "currency" },
{ "name": "utilities", "label": "Utilities", "type": "currency" },
{ "name": "marketing", "label": "Marketing", "type": "currency" }
]
}
}

List Available Sections

Returns available report sections for use with the custom report builder.

Request

GET /api/v1/pnl/sections
Authorization: Bearer {access_token}

Response

{
"success": true,
"data": [
{ "type": "revenue", "name": "Revenue", "description": "Sales and income" },
{ "type": "cogs", "name": "Cost of Goods Sold", "description": "Direct costs of products" },
{ "type": "operating_expenses", "name": "Operating Expenses", "description": "Indirect operational costs" },
{ "type": "other_income", "name": "Other Income", "description": "Non-operational income" },
{ "type": "other_expenses", "name": "Other Expenses", "description": "Non-operational expenses" }
]
}

Error Responses

All error responses follow a consistent format:

{
"detail": "Error description"
}

Common Errors

StatusConditionDetail
400Missing tenantTenant ID required
400Fewer than 2 brands in comparisonAt least 2 brands required for comparison
400Invalid granularity valueInvalid granularity. Must be one of: ['day', 'week', 'month']
401Missing or invalid JWTNot authenticated
403Insufficient role permissionsInsufficient permissions
500ClickHouse query failureFailed to generate P&L: {error}
500Brand P&L failureFailed to generate brand P&L: {error}
500Regional P&L failureFailed to generate regional P&L: {error}
500Comparison failureFailed to compare brands: {error}
500Custom report failureFailed to generate custom report: {error}
500Trend failureFailed to get trend data: {error}