KPI API Update - JWT-based merchant_id
Change Summary
Date: 2025-11-09
Type: Security Enhancement
Impact: Breaking Change (Request Format)
What Changed
The KPI endpoints now automatically extract merchant_id from the JWT token instead of requiring it in the request body. This is more secure and follows REST API best practices.
Before (Old - Insecure)
POST /api/v1/kpi/stats
{
"merchant_id": "IN-NATUR-CHEANN-7D2B-O9BP1", // β User could specify any merchant
"period_window": "mtd"
}
After (New - Secure)
POST /api/v1/kpi/stats
{
"period_window": "mtd" // β
merchant_id from JWT token
}
Why This Change?
Security Issues with Old Approach
- Authorization Bypass: Users could potentially request data for other merchants
- Validation Overhead: Required server-side validation to match JWT merchant_id
- Redundancy: merchant_id already in JWT token
- Error Prone: Mismatch between JWT and request body caused 403 errors
Benefits of New Approach
- β More Secure: Impossible to request other merchant's data
- β Simpler: No need to include merchant_id in request
- β Cleaner: Follows JWT authentication best practices
- β Consistent: Matches pattern used in other services
Updated Endpoints
1. POST /api/v1/kpi/stats
Old Request:
curl -X POST http://localhost:9100/api/v1/kpi/stats \
-H "Authorization: Bearer YOUR_JWT_TOKEN" \
-H "Content-Type: application/json" \
-d '{
"merchant_id": "IN-NATUR-CHEANN-7D2B-O9BP1",
"period_window": "mtd"
}'
New Request:
curl -X POST http://localhost:9100/api/v1/kpi/stats \
-H "Authorization: Bearer YOUR_JWT_TOKEN" \
-H "Content-Type: application/json" \
-d '{
"period_window": "mtd"
}'
2. POST /api/v1/kpi/stats/individual
Old Request:
curl -X POST http://localhost:9100/api/v1/kpi/stats/individual \
-H "Authorization: Bearer YOUR_JWT_TOKEN" \
-H "Content-Type: application/json" \
-d '{
"merchant_id": "IN-NATUR-CHEANN-7D2B-O9BP1",
"period_window": "mtd"
}'
New Request:
curl -X POST http://localhost:9100/api/v1/kpi/stats/individual \
-H "Authorization: Bearer YOUR_JWT_TOKEN" \
-H "Content-Type: application/json" \
-d '{
"period_window": "mtd"
}'
Request Schema
KPIStatsRequest (Updated)
class KPIStatsRequest(BaseModel):
"""Request schema for merchant KPI stats."""
period_window: PeriodWindow = Field(
PeriodWindow.MTD,
description="Period window (mtd, qtd, ytd)"
)
Fields:
period_window(required): One ofmtd,qtd,ytd
Removed:
- Now extracted from JWT tokenmerchant_id
Implementation Details
Router Changes
@router.post("/stats", response_model=KPIStatsResponse)
async def get_merchant_kpi_stats(
kpi_stats_request: KPIStatsRequest = Body(...),
current_user: dict = Depends(require_view_analytics_permission),
correlation_id: str = Depends(get_request_id)
):
# Get merchant_id from JWT token
merchant_id = current_user["merchant_id"] # β
Secure
# Get KPI stats
kpi_stats = await KPIService.get_merchant_kpi_stats(
merchant_id=merchant_id,
period_window=kpi_stats_request.period_window.value
)
Key Changes:
- Removed
merchant_idfrom request body - Extract
merchant_idfromcurrent_user(JWT payload) - Removed validation logic (no longer needed)
Migration Guide
For API Consumers
Step 1: Update Request Body
Remove merchant_id from all KPI API requests.
Before:
const response = await fetch('/api/v1/kpi/stats', {
method: 'POST',
headers: {
'Authorization': `Bearer ${token}`,
'Content-Type': 'application/json'
},
body: JSON.stringify({
merchant_id: 'IN-NATUR-CHEANN-7D2B-O9BP1', // β Remove this
period_window: 'mtd'
})
});
After:
const response = await fetch('/api/v1/kpi/stats', {
method: 'POST',
headers: {
'Authorization': `Bearer ${token}`,
'Content-Type': 'application/json'
},
body: JSON.stringify({
period_window: 'mtd' // β
Only this needed
})
});
Step 2: Ensure JWT Token is Valid
Make sure your JWT token contains the merchant_id claim:
{
"sub": "bloom",
"merchant_id": "IN-NATUR-CHEANN-7D2B-O9BP1",
"associate_id": "AST011",
"role_id": "admin",
"branch_id": "hq",
"exp": 1762701984
}
Step 3: Test
# Test with updated request
curl -X POST http://localhost:9100/api/v1/kpi/stats \
-H "Authorization: Bearer YOUR_JWT_TOKEN" \
-H "Content-Type: application/json" \
-d '{"period_window": "mtd"}'
Error Handling
Common Errors
1. Missing JWT Token
{
"detail": "Not authenticated"
}
Solution: Include valid JWT token in Authorization header
2. Invalid JWT Token
{
"detail": "Could not validate credentials"
}
Solution: Ensure JWT token is valid and not expired
3. Missing Permission
{
"detail": "Forbidden"
}
Solution: Ensure user has VIEW_ANALYTICS permission
4. No Data Found
{
"success": false,
"message": "No KPI stats found for merchant X with period mtd"
}
Solution: Ensure merchant_kpi_stats collection has data for this merchant
Testing
Updated Test Files
- test_kpi_stats_request.json
{
"period_window": "mtd"
}
- test_kpi_stats_endpoints.sh
All test requests updated to remove
merchant_id
Run Tests
# Shell tests
./test_kpi_stats_endpoints.sh http://localhost:9100 YOUR_JWT_TOKEN
# Python tests (if updated)
python test_kpi_stats.py
Backward Compatibility
β οΈ Breaking Change: This is a breaking change. Old requests with merchant_id in the body will still work but the field will be ignored.
Recommendation: Update all API consumers immediately to use the new format.
Security Benefits
Before (Vulnerable)
User sends: merchant_id = "MERCHANT_A"
JWT contains: merchant_id = "MERCHANT_B"
Server validates: β Mismatch β 403 Forbidden
After (Secure)
User sends: (no merchant_id)
JWT contains: merchant_id = "MERCHANT_B"
Server uses: β
JWT merchant_id β Secure
Result: Impossible to request data for other merchants
Documentation Updates
Files Updated
- β
app/schemas/kpi_schema.py- Removed merchant_id field - β
app/routers/kpi_router.py- Extract merchant_id from JWT - β
test_kpi_stats_request.json- Updated example - β
test_kpi_stats_endpoints.sh- Updated all tests - β
KPI_JWT_UPDATE.md- This document
Example: Complete Request
curl -X 'POST' \
'http://127.0.0.1:9100/api/v1/kpi/stats' \
-H 'accept: application/json' \
-H 'Authorization: Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiJibG9vbSIsIm1lcmNoYW50X2lkIjoiSU4tTkFUVVItQ0hFQU5OLTdEMkItTzlCUDEiLCJhc3NvY2lhdGVfaWQiOiJBU1QwMTEiLCJyb2xlX2lkIjoiYWRtaW4iLCJicmFuY2hfaWQiOiJocSIsImV4cCI6MTc2MjcwMTk4NH0.-A47U82dA4LOkoWEKIqCL7Fyv1CeA2bXbL_KZffewO0' \
-H 'Content-Type: application/json' \
-d '{
"period_window": "mtd"
}'
Expected Response:
{
"success": true,
"message": "Merchant KPI stats retrieved successfully",
"data": {
"merchant_id": "IN-NATUR-CHEANN-7D2B-O9BP1",
"period_window": "mtd",
"generated_at": "2025-11-09T00:00:00Z",
"kpis": {
"total_revenue": {
"title": "Total Revenue",
"value": 1234567.89,
"unit": "INR",
"delta": 0.08,
"source": "postgres"
}
// ... more KPIs
},
"charts": {
// ... chart data
}
}
}
Summary
β
More Secure: merchant_id from JWT token only
β
Simpler API: One less field in request
β
Better UX: No more 403 errors from mismatch
β
Best Practice: Follows JWT authentication pattern
Action Required: Update all API consumers to remove merchant_id from request body.
Updated: 2025-11-09
Version: 2.1
Status: β
Production Ready