User API Reference
Base path: /api/user · Swagger UI: /api/user/docs
Base URLs
| Environment | Base URL |
|---|---|
| Production | https://prod-aquagen.azurewebsites.net |
| Local | http://localhost:5000 |
Example:
https://prod-aquagen.azurewebsites.net/api/user/user/login
https://prod-aquagen.azurewebsites.net/api/user/accounts/info
https://prod-aquagen.azurewebsites.net/api/user/deviceData/?date1=09/11/2024&type=DATE
https://prod-aquagen.azurewebsites.net/api/user/report?useShift=true&reportType=granular&reportFormat=xlsx&service=water&startDate=21/07/2026&endDate=21/07/2026&unitIds=FG26538F&jwt=<access_token>
All endpoints except login, OTP, refresh, and Swagger routes require a valid JWT passed as:
Authorization: Bearer <access_token>
or ?jwt=<access_token> as a query parameter.
Auth
GET /api/user/user/login
Authenticate and receive JWT tokens. All fields are passed as request headers.
GET /api/user/user/login
LoginType: DEFAULT
username: johndoe
password: secret123
| Header | Required | Description |
|---|---|---|
LoginType | Yes | DEFAULT, microsoft, google, otp, or external |
username | DEFAULT / otp | Username. For internal users: username/industryId to impersonate a specific industry |
password | DEFAULT / otp | Password (DEFAULT) or OTP code (otp) |
Authorization | microsoft / google | Bearer <identity_provider_token> |
tid | microsoft only | Azure AD tenant ID — 1403 if missing for microsoft login |
targetIndustryId | google only | Required for Google SSO to identify which industry to authenticate into |
otpTimestamp | otp only | Timestamp returned from /otplogin — used to verify the OTP window |
deviceId | No | Device identifier — stored in token_logs for session tracking |
Baseurl | No | Client origin URL — stored in token_logs |
X-Platform | No | Android or iOS — extends refresh token expiry for mobile sessions |
format | No | Response format version: v1 (default) |
Login type details:
LoginType | Verification method |
|---|---|
DEFAULT | UserService.verify_default_login() — bcrypt password check against Cosmos DB |
microsoft | UserService.verify_microsoft_login() — Azure AD token verified against tenant JWKS endpoint |
google | UserService.verify_google_login() — Google ID token verification |
otp | UserService.verify_otp_login() — TOTP code + otpTimestamp validated |
external | Same as DEFAULT but returns ExternalLoginResponseModel with status 201 instead of 200 |
How it works:
LoginTypedispatches to the correct service verification method- If user or industry not found →
1401with provider-specific message (SSO returns descriptive "email not authorized" message) - If
industry['disabled']oruser['disabled']→1405 - JWT claims built:
userId,email,username,loginType,role,permissions[] - For internal users (
industryId == Config.INTERNAL_INDUSTRY_ID),loginTypeis set toConfig.ADMIN_LOGIN_TYPE - SSO users without explicit
roledefault toSSOUserRole.ground_staff leakageEnabledcomputed:Trueif any unit inalerts_confighasalertEnabled.stable_flow == Trueindustry['leakageEnabled']is set before formatting — this is part of the login responseTokenService.save_token()persists the token totoken_logsCosmos containerWebSocketService(industry_id).get_socket_url()resolves the WebSocket URL for real-time alerts- Response formatted via
UserDataFormatter.format_user_data()including all users, categories, units
Response shape (LoginResponseModel) — 200:
{
"access_token": "eyJ...",
"refresh_token": "eyJ...",
"userId": "USR001",
"email": "user@example.com",
"username": "johndoe",
"industryId": "IND001",
"industryName": "Acme Water Works",
"leakageEnabled": false,
"websocketUrl": "wss://...",
"categories": { ... },
"units": [ ... ],
"reports": { ... },
"permissions": []
}
For LoginType: external, returns ExternalLoginResponseModel with HTTP status 201.
| Status | Cause |
|---|---|
200 | Login successful |
201 | Login successful (external type only) |
1401 | User not found or SSO email not registered in system |
1403 | LoginType missing, or Authorization/tid missing for microsoft login |
1405 | User or industry has been disabled |
Rate limit: 10/minute, 100/hour per IP (from RATE_LIMITS["login"]).
POST /api/user/user/otplogin
Generate and send a TOTP code to a registered phone number via SMS. Must be called before GET /login with LoginType: otp.
POST /api/user/user/otplogin
phNumber: 9876543210
| Header | Required | Description |
|---|---|---|
phNumber | Yes | Registered mobile number — digits only, no country code (e.g. 9876543210) |
How it works:
UserService.get_otp_user(phNumber)looks up the user by phone number- If not found or
user['active'] == False→1401 "Unregistered Number" - If
user['disabled'] == True→1405 otp_timestamp = int(time.time())— Unix timestamp used to scope the OTP windowUserService.generate_otp(phNumber, otp_timestamp)generates a TOTP code- Test mode: If
Config.TEST_OTP_ENABLED == TrueandphNumberis inConfig.TEST_OTP_PHONE_NUMBERS, skips SMS and returns immediately withotpTimestamp SMSSender().send_otp_sms("91" + phNumber, otp)— prepends91country code for India- Returns
otpTimestampin response — must be passed as theotpTimestampheader on the subsequent login call
Response (200):
{ "message": "OTP sent", "status": "Success", "otpTimestamp": 1731130200 }
| Status | Cause |
|---|---|
200 | OTP sent via SMS (or skipped in test mode) |
1401 | Phone number not registered or user inactive |
1405 | User has been disabled |
429 | Rate limited |
Rate limits: 3/minute, 10/hour, 20/day per phone number (from RATE_LIMITS["otp_generate"]).
GET /api/user/user/refresh
Issue a new access token using a valid refresh token. The refresh token is reused unless it is within 7 days of expiry — if the remaining lifetime is less than 7 days, a new refresh token is also issued and returned.
GET /api/user/user/refresh
Authorization: Bearer <refresh_token>
| Header | Required | Description |
|---|---|---|
Authorization | Yes | Bearer <refresh_token> — passing an access token returns 422 |
deviceId | No | Device ID for session update in token_logs |
Baseurl | No | Client origin URL for session context |
X-Platform | No | Android or iOS — extends refresh token expiry if a new one is issued |
format | No | Response format version: v1 (default) |
How it works:
@jwt_required(refresh=True)— Flask rejects access tokens here with422- Checks
industry_data.get('disabled')anduser_details.get('disabled')—1405if either is true - New
access_tokencreated with same claims - Rolling refresh: if
exp - now < 7 days, a newrefresh_tokenis also created TokenService.save_token()saves the new tokens totoken_logs- Returns full
LoginResponseModel(same shape as login response)
| Status | Cause |
|---|---|
200 | New access token issued (and optionally new refresh token) |
422 | An access token was passed instead of a refresh token |
1405 | User or industry has been disabled |
DELETE /api/user/user/logout
Invalidate the current session. Marks the token as inactive in token_logs, preventing further use even if the JWT hasn't expired.
DELETE /api/user/user/logout
Authorization: Bearer <access_token>
| Header | Required | Description |
|---|---|---|
Authorization | Yes | Bearer <access_token> |
deviceId | No | Device ID — used to scope the logout to a specific session |
How it works:
- JWT parsed — if
exp < now, returns401 "Token already expired"immediately TokenService({"industry_id", "user_id", "device_id", "token"}).logout()sets the token's status toINACTIVEintoken_logs- Any subsequent request with this token hits the
auth_interceptor→TokenService.validate_token()→440 Session expired
| Status | Cause |
|---|---|
200 | { "message": "...", "status": "success" } |
400 | Exception during logout |
401 | Token already expired |
GET /api/user/user/validateCredentials
Validate a username and password without issuing a token. Used for credential pre-checks (e.g. before showing a change-password form).
GET /api/user/user/validateCredentials
username: johndoe
password: secret123
| Header | Required | Description |
|---|---|---|
username | Yes | Username (supports username/industryId for internal users) |
password | Yes | Password |
How it works: UserService.verify_default_login(request.headers) performs the same bcrypt check as DEFAULT login but does not issue any tokens.
| Status | Cause |
|---|---|
200 | { "message": "Credentials are valid", "status": "success" } |
1401 | Invalid username or password |
1405 | User or industry disabled |
Accounts
Base path: /api/user/accounts
GET /api/user/accounts/settings
Returns the full industry account settings document. Requires ACCOUNT_SETTINGS permission — returns 401 if the user lacks it.
GET /api/user/accounts/settings
Authorization: Bearer <token>
How it works: AccountsService.get_full_account_settings_data() assembles five sections from current_user and UserService:
Response shape (AccountsSettingsModel):
{
"account": {
"industryName": "Acme Water Works",
"logo": "https://storage.googleapis.com/aquagen-f6d24.appspot.com/...",
"shift": { "startAt": "08:00:00", "endAt": "08:00:00", "shiftName": "Day Shift" },
"siUnit": { "SOURCE_CATEGORY": "KL", "STOCK_CATEGORY": "KL" }
},
"user": [
{ "id": "usr-001", "phoneNo": "9876543210", "active": true, "name": "John" }
],
"alerts": { ... },
"reports": { ... },
"sso": [
{ "email": "user@company.com", "role": "manager", "active": true }
]
}
Logo URL sanitization: If industryMeta["logo"] does not start with the allowed Firebase Storage prefixes, it is returned as "" (empty string) — not an error, just silently cleared.
| Status | Cause |
|---|---|
200 | Full settings object |
401 | User lacks ACCOUNT_SETTINGS permission |
POST /api/user/accounts/settings
Save full account settings. All top-level sections must be present. Validates user limits, logo URL, and SSO field requirements before writing.
POST /api/user/accounts/settings
Authorization: Bearer <token>
Content-Type: application/json
{
"account": { "industryName": "...", "logo": "...", "shift": {...}, "siUnit": {...} },
"user": [ { "phoneNo": "9876543210", "active": true, ... } ],
"alerts": { ... },
"reports": { ... },
"sso": [ { "email": "...", "role": "manager", "active": true } ]
}
Required sections (checked in order):
| Key | If missing/null | Status |
|---|---|---|
account | "Accounts missing" | 1403 |
user | "Users missing" | 1403 |
alerts | "Alerts missing" | 1403 |
reports | "Reports missing" | 1403 |
Logo URL validation: account.logo must start with one of:
https://storage.googleapis.com/aquagen-f6d24.appspot.com/https://firebasestorage.googleapis.com/v0/b/aquagen-f6d24.appspot.com/
Any other URL → 400 "Invalid logo URL". If logo is null / empty, no validation runs.
User limits (from industryMeta.limits):
| User type | Limit key | Default |
|---|---|---|
| OTP/regular users | maxUsers | 5 (DEFAULT_USER_LIMIT) |
| SSO users | maxSsoUsers | 10 (DEFAULT_SSO_USER_LIMIT) |
Limit check counts both existing untouched active users and newly submitted active users. Exceeding either limit → 400 "User limit exceeded. Maximum N users allowed." (or "SSO user limit exceeded."). Raised as UserLimitExceededError before any writes.
OTP user validation: Each entry in user must have phoneNo — raises ValueError if missing.
SSO user validation: Each entry in sso must have both email and role — raises ValueError if missing.
Session revocation on deactivation: After building the list of newly-inactive users and SSO users (those where active == False), their current tokens are invalidated via TokenService({"industry_id", "user_id": [list]}).logout() — all sessions for those users are terminated before the settings are saved.
Write order:
- User limit validation (all limits checked before any write)
- Deactivated user sessions revoked
AccountsService.post_full_account_settings_data():- Updates
industryName,logo,shift,siUnitinindustryMeta - Upserts each OTP user via
UserService.post_otp_user_by_industry() - Upserts each SSO user via
UserService.create_sso_user()(inherits creator's permissions) - Replaces
meta.alertsandmeta.reportsin memory - Patches industry doc in Cosmos DB (
/industryName+/meta)
- Updates
- Returns the full updated settings via
get_full_account_settings_data()
| Status | Cause |
|---|---|
200 | Settings saved; returns updated full settings object |
400 | Invalid logo URL, user limit exceeded, or SSO/OTP validation error |
401 | User lacks ACCOUNT_SETTINGS permission |
1403 | Missing required section key |
GET /api/user/accounts/info
Returns a per-category unit info dashboard — each unit enriched with its alert thresholds from alertsConfig.
GET /api/user/accounts/info
Authorization: Bearer <token>
How it works:
- Builds
unitsMappingfromcurrent_user['industryData']['units']indexed byunitId - Merges alert config from
current_user['alertsConfig']['units']:unitThreshold←configs.unitThresholdmonthlyThreshold←configs.monthlyThresholdmaxCapacity←configs.maxCapacity(if present)lowThreshold/highThreshold←configs.lowThreshold/highThreshold(stock) orparams[param].lt/.ht(quality)alertEnabled← bool derived fromdaily_threshold || monthly_threshold || level_exceed(source/stock), or per-param map (quality)
CategoryInfoBuilderbuilds a per-category info object for each active category- Result sorted in fixed order:
SOURCE_CATEGORY,STOCK_CATEGORY,GROUND_WATER_LEVEL,QUALITY_CATEGORY
Response shape (AccountsInfoModel): A keyed object per category containing unit info arrays, each unit with threshold values, alert enable state, and display metadata.
| Status | Cause |
|---|---|
200 | Category info map |
PATCH /api/user/accounts/info
Apply JSON Patch operations to unit thresholds and alert settings. Requires ACCOUNT_SETTINGS permission.
PATCH /api/user/accounts/info
Authorization: Bearer <token>
Content-Type: application/json
{
"operations": [
{ "path": "/units/0/unitThreshold", "value": 500 },
{ "path": "/units/0/alertEnabled", "value": true },
{ "path": "/units/2/lowThreshold/pH", "value": 6.5 }
]
}
Path translation logic (_translate_to_alerts_config_operations):
All paths use a unit index (position in current_user['industryData']['units']) not unitId. The service translates index → unitId from the units array, then routes to the correct Cosmos DB document:
| Path pattern | Target |
|---|---|
/units/{index}/unitThreshold | alertsConfig /units/{unitId}/configs/unitThreshold |
/units/{index}/monthlyThreshold | alertsConfig /units/{unitId}/configs/monthlyThreshold |
/units/{index}/maxCapacity | alertsConfig /units/{unitId}/configs/maxCapacity |
/units/{index}/lowThreshold/value | alertsConfig /units/{unitId}/configs/lowThreshold (stock) |
/units/{index}/highThreshold/value | alertsConfig /units/{unitId}/configs/highThreshold (stock) |
/units/{index}/lowThreshold/{param} | alertsConfig /units/{unitId}/params/{param}/lt (quality) |
/units/{index}/highThreshold/{param} | alertsConfig /units/{unitId}/params/{param}/ht (quality) |
/units/{index}/alertEnabled (bool) | alertsConfig /units/{unitId}/alertEnabled/daily_threshold + monthly_threshold (or level_exceed for stock/level) |
/units/{index}/alertEnabled/{param} | alertsConfig /units/{unitId}/params/{param}/alertEnabled (quality) |
| Any other path | Industry doc direct patch |
Batching: Operations are batched in groups of 10 for Cosmos DB patch calls. Each successfully applied industry-doc operation is also written to industry_checkpoint for audit trail with userId, createdOn, and path.
Missing unit bootstrap: If a unit referenced in an alertsConfig operation doesn't yet exist in alertsConfig, it is auto-created with empty alertEnabled, configs, params, isDeployed: True, and its standardCategoryId.
| Status | Cause |
|---|---|
200 | Patch applied |
401 | Permission denied |
1403 | Invalid patch operation format |
GET /api/user/accounts/info/lastupdated
Returns the last-updated checkpoint for a specific field path — used by the UI to show "last updated by user X at time Y" for any edited field.
GET /api/user/accounts/info/lastupdated?path=/units/0/unitThreshold
Authorization: Bearer <token>
| Parameter | Required | Description |
|---|---|---|
path | No | Field path to look up checkpoint for |
DatabaseSupporter.get_checkpoint_by_path(industryId, path) queries the industry_checkpoint container. Returns null if no checkpoint exists for the path.
Response (PatchInfoModel):
{ "path": "/units/0/unitThreshold", "lastupdated": "2024-11-09T08:30:00Z", "userId": "USR001" }
| Status | Cause |
|---|---|
200 | Checkpoint found, or empty success if none exists |
POST /api/user/accounts/logoupdate
Upload an industry logo image to Firebase Storage. Returns the public URL. Requires ACCOUNT_SETTINGS permission.
POST /api/user/accounts/logoupdate
Authorization: Bearer <token>
Content-Type: multipart/form-data
image: <file>
| Field | Required | Description |
|---|---|---|
image | Yes | Image file — multipart/form-data |
How it works: FireBaseSuppoter.upload_image_to_storage(image, industryId) uploads the file to the Firebase Storage bucket under the industry's folder. The returned URL must then be saved via POST /accounts/settings with account.logo set to the URL.
File validation: FileValidationError is raised (→ 400) if the file type or size is invalid.
| Status | Cause |
|---|---|
200 | { "logourl": "https://storage.googleapis.com/aquagen-f6d24.appspot.com/..." } |
400 | File validation error (wrong type or size) |
401 | Permission denied |
1403 | image field missing |
GET /api/user/accounts/joinedDate
Returns the industry's joining date from industryMeta["joiningDate"].
GET /api/user/accounts/joinedDate
Authorization: Bearer <token>
Response: { "joinedDate": "2022-03-15T00:00:00Z" }
GET /api/user/accounts/landingpage
Returns a combined payload for the accounts landing page. Conditionally includes full settings if the user has ACCOUNT_SETTINGS permission.
GET /api/user/accounts/landingpage
Authorization: Bearer <token>
How it works:
- Always includes:
AccountInfoPage←get_info_data()(per-unit threshold dashboard)meta←get_categories()(sorted:SOURCE_CATEGORY,STOCK_CATEGORY,GROUND_WATER_LEVEL,QUALITY_CATEGORY)siUnit←industryMeta["siUnit"]["SOURCE_CATEGORY"](defaults to"KL")joiningDate←industryMeta["joiningDate"]
- Only if user has
ACCOUNT_SETTINGSpermission:AccountSettingPage←get_full_account_settings_data()(same asGET /settings)
Response shape (accountsLandingPageModel):
{
"AccountInfoPage": { "SOURCE_CATEGORY": {...}, "STOCK_CATEGORY": {...}, ... },
"meta": { "SOURCE_CATEGORY": {...}, ... },
"siUnit": "KL",
"joiningDate": "2022-03-15T00:00:00Z",
"AccountSettingPage": { ... }
}
| Status | Cause |
|---|---|
200 | Landing page payload |
GET /api/user/accounts/allUnitThresholdUpdate
Bulk-set unitThreshold for all units in the Consumption Points sub-category of SOURCE_CATEGORY to a single value. Requires ACCOUNT_SETTINGS permission.
GET /api/user/accounts/allUnitThresholdUpdate?newThreshold=500
Authorization: Bearer <token>
| Parameter | Required | Description |
|---|---|---|
newThreshold | No | New threshold value as integer |
How it works:
- Iterates
current_user['industryData']['categories']['source']['subCategories'] - Filters sub-categories where
sub_category['group'] == 'Consumption Points' - For each unit ID in those sub-categories, finds the matching entry in
industryData['units']and setsunit_data['unitThreshold'] = newThreshold AccountsService.update_industry_doc(industryData)replaces the full industry document in Cosmos DB
This updates industryData['units'] directly (not alertsConfig) — a full document replace, not a patch.
| Status | Cause |
|---|---|
200 | { "message": "Unit threshold updated successfully", "status": "success" } |
401 | Permission denied |
Device Data
Base path: /api/user/deviceData
Retrieves processed device readings grouped by category. Runs four parallel queries via ThreadPoolExecutor on every call: latest reading per unit, last updated time, date-range data, and quality 7-day history (only for category=quality + type=HOUR).
GET /api/user/deviceData/?date1=09/11/2024&category=source&type=DATE&divisionFactor=1000
Authorization: Bearer <token>
| Parameter | Required | Default | Description |
|---|---|---|---|
date1 | Yes | — | DD/MM/YYYY |
date2 | No | — | DD/MM/YYYY — second date for side-by-side comparison |
category | Yes | — | source, stock, energy, or quality (CategoryType) |
subCategory | No | — | Sub-category ID within the category |
divisionFactor | No | 1000 | Divide all raw values (defaults to 1000 even if sent as 0) |
type | Yes | — | HOUR, DATE, MONTH, YEAR — missing returns 1403 |
includeToday | No | true | false skips the latest-data parallel query |
| Status | Cause |
|---|---|
200 | Device data payload |
1401 | category not in current_user['categories'] |
1403 | date1, type, or category missing |
For the full execution flow, shift-aware date conversion, and formatter pipeline see Device Data Flow →
Device Data V2
Base path: /api/user/deviceDataV2
Standard-category-aware device data. Automatically selects a formatter via DeviceDataV2FormatterBuilder based on standardCategoryId. Runs two parallel queries: last updated time and device data.
GET /api/user/deviceDataV2?date1=09/11/2024&category=ID_SOURCE&type=DAY
Authorization: Bearer <token>
| Parameter | Required | Default | Description |
|---|---|---|---|
date1 | Yes | — | DD/MM/YYYY |
date2 | No | — | DD/MM/YYYY |
category | Yes | ID_SOURCE | Standard category ID, or ALL_CATEGORIES_V2|<standardCategoryId> to aggregate across all matching categories |
unitId | No | — | Filter formatted response to one unit only |
divisionFactor | No | 1000 | Auto-overridden to 1 for GROUND_WATER_LEVEL |
type | No | DAY | DAY, MONTH, YEAR, CUSTOM |
formattedOutput | No | true | false returns raw Cosmos DB documents |
v2 | No | false | Enables v2 energy formatting path |
| Status | Cause |
|---|---|
200 | Formatted device data |
1401 | Category not found |
1403 | date1 or category missing |
GET /api/user/deviceDataV2/compare
Side-by-side comparison of two date periods for two groups of units or sub-categories. Returns { data1, data2 } formatted by the same formatter.
| Parameter | Required | Default | Description |
|---|---|---|---|
dateA1 | Yes | — | DD/MM/YYYY — group A start |
dateA2 | No | — | Group A end |
dateB1 | Yes | — | DD/MM/YYYY — group B start |
dateB2 | No | — | Group B end |
ids1 | Yes | — | Unit or sub-category IDs for group A (repeatable param) |
ids2 | Yes | — | Unit or sub-category IDs for group B (repeatable param) |
category | Yes | — | Standard category ID |
divisionFactor | No | 1000 | |
type | No | DAY | DAY, MONTH, YEAR, CUSTOM |
by | No | UNIT | UNIT or SUB_CATEGORY — how ids1/ids2 are resolved |
For the full formatter pipeline, ALL_CATEGORIES_V2 aggregation behaviour, and standardCategoryId → formatter mapping see Device Data V2 Flow →
Granular Data
Base path: /api/user/granular
5-minute interval raw readings stored in devices_data Cosmos DB container. Up to 288 readings per day (one per 5-minute slot). Uses the same shift-aware UTC conversion as Device Data v1.
GET /api/user/granular/unit
Returns 5-minute readings for a single unit. Optionally accepts date2 for side-by-side comparison — returns { data1, data2 } when both dates are provided.
GET /api/user/granular/unit?date1=09/11/2024&unitId=U001&divisionFactor=1000
Authorization: Bearer <token>
| Parameter | Required | Default | Description |
|---|---|---|---|
date1 | Yes | — | DD/MM/YYYY |
date2 | No | — | DD/MM/YYYY — second date for comparison |
unitId | Yes | — | Unit ID to fetch readings for |
divisionFactor | No | 1000 | Divide raw values |
How it works:
- Dates converted to UTC using
DeviceDataRouteHandler.get_date1_and_date2()(same shift logic as Device Data v1) GranularDataService().get_granular_data_for_units([unitId], date1_utc)queriesdevices_datafor 5-min readings in the 24-hour window starting at the shifted UTC time- If
date2provided, runs a second identical query GranularDataFormatter().format_data_unit_data()aligns readings to 5-min slots, fills missing slots with0, divides bydivisionFactor- Returns
GranularDataResponseModelwithdata1and optionallydata2
| Status | Cause |
|---|---|
200 | Granular readings returned |
1403 | date1 or unitId missing |
GET /api/user/granular/category
5-minute readings aggregated across all units in a sub-category. The unit list is resolved from the industry's category config.
GET /api/user/granular/category?date1=09/11/2024&categoryId=source&subCategoryId=INLET&divisionFactor=1000
Authorization: Bearer <token>
| Parameter | Required | Default | Description |
|---|---|---|---|
date1 | Yes | — | DD/MM/YYYY |
date2 | No | — | DD/MM/YYYY |
categoryId | Yes | — | Category ID (e.g. source) |
subCategoryId | Yes | — | Sub-category ID within the category |
isStandardCategory | No | false | If true, resolves units from standardCategoriesView[categoryId] instead of categories[categoryId] |
divisionFactor | No | 1000 | Divide raw values |
Unit resolution: When isStandardCategory=false, units are read from current_user['industryData']['categories'][categoryId]['subCategories'] where id == subCategoryId. When true, reads from current_user['industryData']['standardCategoriesView'][categoryId]['subCategories']. If the sub-category is not found, units is an empty list and the response returns empty arrays.
| Status | Cause |
|---|---|
200 | Aggregated granular readings |
1403 | date1, categoryId, or subCategoryId missing |
Reports
Base path: /api/user/report
For the full execution flow, service type → report class mapping, endDate auto-calculation rules, PDF backends (xhtml2pdf vs Playwright), and how to add a new report type see Report Generation Flow →
GET /api/user/report
Generate a report file on-demand. Always returns a file download (Content-Disposition: attachment). All generation is in-memory — no files are written to disk or stored anywhere.
Daily water PDF:
GET https://prod-aquagen.azurewebsites.net/api/user/report?reportType=daily&reportFormat=pdf&service=water&startDate=09/11/2024
Authorization: Bearer <token>
Monthly water XLSX:
GET https://prod-aquagen.azurewebsites.net/api/user/report?reportType=monthly&reportFormat=xlsx&service=water&startDate=01/11/2024
Authorization: Bearer <token>
Hourly PDF with time window (endDate and startTime/endTime required):
GET https://prod-aquagen.azurewebsites.net/api/user/report?reportType=hourly&reportFormat=pdf&service=water&startDate=09/11/2024&endDate=09/11/2024&startTime=6&endTime=18
Authorization: Bearer <token>
Granular report for a single unit:
GET https://prod-aquagen.azurewebsites.net/api/user/report?reportType=granular&reportFormat=xlsx&service=water&startDate=09/11/2024&endDate=09/11/2024&unitId=U001&format=v2
Authorization: Bearer <token>
Granular report using jwt query param (browser/direct download):
Use ?jwt=<access_token> as a query parameter instead of the Authorization header when triggering downloads directly from a browser URL or sharing a download link.
https://prod-aquagen.azurewebsites.net/api/user/report?useShift=true&reportType=granular&reportFormat=xlsx&service=water&startDate=21/07/2026&endDate=21/07/2026&unitIds=FG26538F&format=v2&jwt=<access_token>
Smart city report (sub-category filter):
GET https://prod-aquagen.azurewebsites.net/api/user/report?reportType=monthly&reportFormat=pdf&service=smart_city&startDate=01/11/2024&subCategories=INLET,OUTLET
Authorization: Bearer <token>
| Parameter | Required | Default | Description |
|---|---|---|---|
startDate | No | current_user['nowUTCWithShiftDateTime'] | DD/MM/YYYY — shift-adjusted current date from the JWT context; used when omitted |
endDate | Conditional | — | DD/MM/YYYY — required for hourly, granular, daily_range, monthly_range; computed from startDate for all other types (see table below) |
reportType | No | daily | Report period — see Report Generation Flow → for all values and supported services |
reportFormat | No | html | html, pdf, xlsx |
service | No | water | Report service type — see Report Generation Flow → for all values |
unitId | No | — | Filter report to a single unit (for hourly and granular) |
unitIds | No | — | Comma-separated unit IDs. If value is the string "null", treated as no filter |
useShift | No | true | Apply industry shift start time to date boundaries |
divisionFactor | No | 1000 | Divide all raw sensor values |
format | No | v1 | Jinja2 template version: v1 or v2 |
subCategories | No | — | Comma-separated sub-category IDs — used by smart_city service |
targetIndustryId | No | — | Header — internal users only, generates report for a different industry |
Response: Binary file stream with Content-Type matching the format (application/pdf, application/vnd.openxmlformats-officedocument.spreadsheetml.sheet, or text/html). Response includes a report_generation_time field in industry local timezone (HH:MM AM/PM).
| Status | Cause |
|---|---|
200 | File download |
400 | Invalid enum value for reportType, reportFormat, or service |
1401 | No data found for the given date/unit combination |
GET /api/user/report/dailySummary
Returns daily summary data as a JSON response (not a file download). Uses the same startDate, reportType, service, reportFormat, unitId, useShift, divisionFactor parameters as the main report endpoint. Calls DailySummaryService instead of ReportService.
Alerts
Base path: /api/user/alerts
Alerts are stored as notification documents in Cosmos DB. All alert GET endpoints query the notification container. Alert fetching is always done via NotificationService with date range parameters; filtering by type and unit happens post-query in the formatter.
For the full POST execution flow, alert type → content class → channel dispatch pipeline, and all channel types (Email, SMS, WhatsApp, FCM, Web) see Alerts Flow →
GET /api/user/alerts
Fetch alert history for the industry, grouped by alert category.
GET /api/user/alerts?date=09/11/2024&type=monthly
Authorization: Bearer <token>
| Parameter | Required | Default | Description |
|---|---|---|---|
date | Yes | — | DD/MM/YYYY — end date of the fetch window |
type | No | monthly | monthly (day 1 of the same month up to date) or daily (date only — single day) |
alert_type | No | — | Post-query filter: general, offline, insights, general_offline |
unitId | No | — | Post-query filter: return only alerts for this unit |
energyEnabled | No | false | Include energy-related alert types in the fetch |
adminAlerts | No | false | Return only alert types relevant to the admin view |
How it works:
dateis parsed asDD/MM/YYYY— returns1403on parse failure- If
type=monthly,startDateis set to day 1 of the same month; iftype=daily,startDate = date NotificationService.get_range_notifications()queries Cosmos DB for all notifications in[startDate, date]AlertsFormatter.format_alerts()groups results by alert category, appliesalert_typeandadminAlertsfilters
Response shape (AlertsResponseModel):
{
"date": "09/11/2024",
"alertCategories": [
{
"categoryId": "unit_threshold",
"categoryName": "Unit Threshold Alerts",
"alerts": [
{
"unitId": "U001",
"unitName": "Main Inlet",
"alertType": "unit_threshold_alert",
"createdOn": "2024-11-09T08:30:00Z",
"description": { "title": "...", "body": "..." },
"meta": { "alertType": "unit_threshold_alert", "value": 1250.5, "threshold": 1000 }
}
]
}
]
}
| Status | Cause |
|---|---|
200 | Alert categories returned |
1403 | date missing or not in DD/MM/YYYY format |
POST /api/user/alerts
Trigger alert processing for a specific event. The alert is validated, dispatched through AlertsProcessor, and delivered to all configured channels.
POST /api/user/alerts
Authorization: Bearer <token>
Content-Type: application/json
{
"unitId": "U001",
"type": "unit_threshold",
"createdOn": "2024-11-09T08:30:00Z",
"meta": {
"alertType": "unit_threshold_alert",
"value": 1250.5,
"threshold": 1000
}
}
| Header | Required | Description |
|---|---|---|
targetIndustryId | No | For internal users — trigger alert for a different industry |
Body fields:
| Field | Required | Description |
|---|---|---|
unitId | Yes | Must exist in current_user['unitsMapping'] — returns 404 if not found |
type | Yes | Alert type string |
createdOn | Yes | ISO 8601 UTC — YYYY-MM-DDTHH:MM:SSZ — returns 400 on parse failure |
meta.alertType | Yes | Alert category type identifier |
How it works:
createdOnvalidated against%Y-%m-%dT%H:%M:%SZ—400on mismatchunitIdchecked againstcurrent_user['unitsMapping']—404if absent- Timezone read from
current_user['meta']['timezone'](defaults to"Asia/Kolkata") AlertsRequestDataconstructed from body fieldsAlertsProcessor(alert_request).process_alerts()dispatches to channels (Email, SMS, WhatsApp, FCM, Web) based on industryalerts_config
| Status | Cause |
|---|---|
200 | Alert processed and delivered |
400 | createdOn not in YYYY-MM-DDTHH:MM:SSZ format |
404 | unitId not found in current_user['unitsMapping'] |
GET /api/user/alerts/unitGraph
Alert frequency time-series graph per unit. Returns one data point per time bucket (day/month/year) with alert counts, shaped for bar/line chart rendering.
GET /api/user/alerts/unitGraph?date1=01/11/2024&type=MONTH
Authorization: Bearer <token>
| Parameter | Required | Default | Description |
|---|---|---|---|
date1 | Yes | — | DD/MM/YYYY |
date2 | No | — | DD/MM/YYYY — required only when type=CUSTOM |
type | No | DAY | DAY (single day), MONTH (day 1 to date1), YEAR (Jan 1 to date1), CUSTOM (date1 to date2) |
unitId | No | — | Comma-separated list of unit IDs to filter |
alertType | No | — | Comma-separated alert type strings (e.g. stable_flow_alert) |
Date range resolution by type:
DAY→start = date1,end = date1MONTH→start = 1st of date1's month,end = date1YEAR→start = Jan 1 of date1's year,end = date1CUSTOM→start = date1,end = date2(returns1403ifdate2missing)
Date-to-UTC conversion: start_date is combined with industry_meta['shift']['startAt'] and converted via DateTimeUtil.convert_date_from_local_to_utc(). end_date is end_date + 1 day with the same shift, giving an exclusive upper bound.
How it works:
NotificationService.get_alerts_by_date_range()queries thenotificationcontainer with[start_utc, end_utc]and optional unit/alert-type filtersUnitGraphAlertsFormatter(pattern_type, timezone, start_date, end_date).format_alerts_for_graph()groups by time bucket and unit
| Status | Cause |
|---|---|
200 | Graph data returned |
1403 | date1 missing, invalid format, or date2 missing for CUSTOM type |
GET /api/user/alerts/dateRange
Alerts over an explicit custom date range. Same formatter pipeline as GET /alerts, but startDate and endDate are both explicit rather than computed from type.
GET /api/user/alerts/dateRange?startDate=01/11/2024&endDate=30/11/2024&alert_type=general
Authorization: Bearer <token>
| Parameter | Required | Default | Description |
|---|---|---|---|
startDate | Yes | — | DD/MM/YYYY |
endDate | Yes | — | DD/MM/YYYY — must be ≥ startDate |
alert_type | No | — | Post-query filter: general, offline, insights, general_offline |
unitId | No | — | Filter to a specific unit |
energyEnabled | No | false | Include energy alert types |
adminAlerts | No | false | Admin-relevant types only |
Edge cases:
- Both dates parsed as
%d/%m/%Y—1403on parse failure for either startDate > endDate→1403 "startDate cannot be after endDate"
| Status | Cause |
|---|---|
200 | Alerts grouped by category |
1403 | Missing/invalid date or startDate > endDate |
GET /api/user/alerts/byType
Fetch alerts filtered to specific AlertCategoryType values. Returns one key per requested category, each with a meta envelope and an alerts array.
GET /api/user/alerts/byType?startDate=01/11/2024&endDate=30/11/2024&categories=device_status,level_exceed
Authorization: Bearer <token>
| Parameter | Required | Description |
|---|---|---|
startDate | Yes | DD/MM/YYYY |
endDate | No | DD/MM/YYYY — defaults to startDate if omitted |
categories | Yes | Comma-separated AlertCategoryType values |
unit_ids | No | Comma-separated unit IDs |
Valid categories values (from AlertCategoryType enum):
device_status, unit_threshold, monthly_threshold, flow_rate, stable_flow, level_exceed, quality_threshold, energy_threshold, borewell_threshold
Any value not in the enum → 1403 with a message listing all valid values.
Response shape:
{
"device_status": {
"meta": { "totalAlerts": 3 },
"alerts": [ { "unitId": "U001", "alertType": "offline", ... } ]
},
"level_exceed": {
"meta": { "totalAlerts": 1 },
"alerts": [ ... ]
}
}
| Status | Cause |
|---|---|
200 | Per-category alert map |
1403 | Invalid date, startDate > endDate, or invalid category value |
GET /api/user/alerts/allIndustries
Cross-industry alert aggregation. Queries alerts for all active industries (excluding test IDs in Constants.TEST_INDUSTRY_IDS) and groups results by industry.
GET /api/user/alerts/allIndustries?startDate=09/11/2024&type=monthly
Authorization: Bearer <token>
| Parameter | Required | Default | Description |
|---|---|---|---|
startDate | Yes | — | DD/MM/YYYY |
endDate | No | — | DD/MM/YYYY — required when type=dateRange |
type | No | monthly | daily (single day), monthly (month to date), dateRange (startDate to endDate) |
adminAlerts | No | false | Admin-relevant types only |
Edge cases:
type=dateRangerequiresendDate— returns1403if missingstartDate > endDatefordateRange→1403
Response shape:
{
"totalAlerts": 47,
"totalIndustries": 12,
"startDate": "01/11/2024",
"endDate": "09/11/2024",
"fetchType": "monthly",
"excludedIndustries": ["INTERNAL", "DEMO2322", ...],
"adminAlertsOnly": false,
"alertsByIndustry": {
"IND001": { "industryName": "...", "alerts": [...] }
}
}
| Status | Cause |
|---|---|
200 | Cross-industry alerts |
1403 | Invalid date, missing endDate for dateRange, or startDate > endDate |
Notifications
Base path: /api/user/notification
Notifications are the real-time feed of alert events stored in Cosmos DB. Every alert triggered by AlertsProcessor creates a notification document. The notification and alert endpoints share the same underlying documents — notifications are the raw events, alerts are the grouped/formatted view.
For the full notification lifecycle, delivery channels, and admin notification endpoints see Notifications Flow →
GET /api/user/notification/
Fetch the notification feed for the industry. Returns all notification events from the past N days.
GET /api/user/notification/?days=7
Authorization: Bearer <token>
| Parameter | Required | Default | Description |
|---|---|---|---|
days | No | 0 | Number of past days to include. 0 = today only |
How it works:
NotificationService({"days": days}).get_all_notifications()queries Cosmos DB for notifications within the windowNotificationDataFormatter.format_all_notification()shapes each document- If no notifications found, returns
201with emptynotifications: []; otherwise200with the list
Response shape (NotificationSuccessResponseModel):
{
"status": "success",
"notifications": [
{
"id": "notif-001",
"unitId": "U001",
"title": "High Consumption Alert",
"body": "Unit Main Inlet exceeded threshold",
"date": "09/11/2024",
"time": "08:30 AM",
"subTitle": "Main Inlet",
"subCategoryId": "INLET",
"date_key": "09_11_2024",
"type": "unit_threshold",
"isRead": false,
"standardCategoryId": "ID_SOURCE",
"categoryId": "source",
"assignees": [],
"meta": { "alertType": "unit_threshold_alert", "value": 1250.5, "threshold": 1000 },
"resolved": false
}
],
"date": "2024-11-09T03:00:00+0000"
}
| Status | Meaning |
|---|---|
200 | Notifications returned |
201 | No notifications found for the period (empty list) |
POST /api/user/notification/register/fcm
Register a Firebase Cloud Messaging (FCM) token for push notification delivery on a specific device.
POST /api/user/notification/register/fcm
Authorization: Bearer <token>
X-Device-Id: device-abc123
Content-Type: application/json
{ "fcmToken": "fcm_token_string_here" }
| Header | Required | Description |
|---|---|---|
X-Device-Id | Yes | Unique device identifier |
| Body param | Required | Description |
|---|---|---|
fcmToken | Yes | Firebase Cloud Messaging device token |
How it works: NotificationService.register_fcm_user(args) upserts the FCM token into the user's document in Cosmos DB, keyed by X-Device-Id. On future alert triggers, this token receives a push via the FCM channel in AlertsProcessor.
| Status | Cause |
|---|---|
200 | { "message": "User registered successfully", "status": "success" } |
400 | Unexpected exception |
1403 | fcmToken or X-Device-Id missing |
GET /api/user/notificationRange
Fetch notifications over an explicit date range — all notification documents between startDate and endDate inclusive.
GET /api/user/notificationRange?startDate=01/11/2024&endDate=09/11/2024
Authorization: Bearer <token>
| Parameter | Required | Description |
|---|---|---|
startDate | Yes | DD/MM/YYYY — 1403 if missing |
endDate | Yes | DD/MM/YYYY — 1403 if missing |
Response: Same NotificationSuccessResponseModel shape as GET /notification/. Returns 201 with empty list if no notifications exist for the range.
| Status | Meaning |
|---|---|
200 | Notifications returned |
201 | No notifications in range |
1403 | startDate or endDate missing |
PATCH /api/user/notification/updateRead
Mark one or more notifications as read. Updates isRead: true on each specified notification document.
PATCH /api/user/notification/updateRead
Authorization: Bearer <token>
Content-Type: application/json
{
"notifications": [
{ "id": "notif-001", "date": "09/11/2024", "type": "unit_threshold" }
]
}
Body is a NotificationReadUpdateRequest — an object with a notifications array. Each entry requires id, date, and type fields (NotificationReadUpdateModel).
NotificationService.update_notification_read_status(body['notifications']) patches each document.
| Status | Meaning |
|---|---|
200 | { "message": "notifications updated successfully", "status": "success" } |
POST /api/user/notification/assignee
Assign a notification to a team member. Also broadcasts a WebSocket notification to the admin app dashboard.
POST /api/user/notification/assignee
Authorization: Bearer <token>
Content-Type: application/json
{ "notificationId": "notif-001", "assigneeId": "user-002" }
Body (NotificationAssigneeModel):
| Field | Required | Description |
|---|---|---|
notificationId | Yes | Notification to assign |
assigneeId | Yes | User ID to assign to |
After saving, AdminNotificationService().send_web_socket_notification() broadcasts a WebSocket event to the admin dashboard with title "Notification Assigned" and body "<current_user.userId> assigned an alert to <assigneeId>".
| Status | Meaning |
|---|---|
200 | { "message": "Assignee added successfully", "status": "success" } |
DELETE /api/user/notification/assignee
Remove an assignee from a notification.
Body: same NotificationAssigneeModel with notificationId and assigneeId.
| Status | Meaning |
|---|---|
200 | { "message": "Assignee deleted successfully", "status": "success" } |
PATCH /api/user/notification/assignee
Mark a notification as resolved. Sets resolved: true on the notification document.
Body: same NotificationAssigneeModel.
| Status | Meaning |
|---|---|
200 | { "message": "notifications marked as resolved successfully", "status": "success" } |
Landing Page
Base path: /api/user/landingPage
GET /api/user/landingPage/userData
Returns the full dashboard payload: consumption totals, online/offline device status, alert summary, and historical comparison data.
GET /api/user/landingPage/userData?date=09/11/2024&type=MONTH
Authorization: Bearer <token>
| Parameter | Required | Default | Description |
|---|---|---|---|
date | Yes | — | DD/MM/YYYY |
type | No | MONTH | MONTH, DAY, YEAR, CUSTOM |
date2 | No | — | End date for CUSTOM type |
pastThreeMonthsEnabled | No | true | Include last 3 months' comparison data |
summaryData | No | true | Include summary section |
alertsEnabled | No | true | Include alert counts |
formatter | No | default | default or ai — ai returns a raw unmodelled dict for LLM consumption |
How it works:
industryIdis taken fromcurrent_user— never from a request paramLandingPageRequestDatais built from the parsed args and passed toLandingPageServiceLandingPageService.get_data()runs multiple sub-queries in parallel:- Today's consumption totals per category
- Device online/offline status for all units
- Alert counts for the period
- Past 3 months' comparison data (if
pastThreeMonthsEnabled=true) - Summary section (if
summaryData=true)
- When
formatter=ai, the raw dict is returned directly without marshalling — bypasses the response model for LLM-readable output
| Status | Cause |
|---|---|
1403 | date missing |
Summary Page
Base path: /api/user/summaryPage
GET /api/user/summaryPage/
Monthly summary view across all units and all categories simultaneously.
GET /api/user/summaryPage/?date1=09/11/2024
Authorization: Bearer <token>
| Parameter | Required | Default | Description |
|---|---|---|---|
date1 | Yes | — | DD/MM/YYYY |
divisionFactor | No | 1000 | Divide raw values |
type | No | — | HOUR, DATE, MONTH, YEAR |
How it works:
current_user['categories']is extended withstandardCategoriesViewentries so all category types are queried in a single passDeviceDataRouteHandler.get_handler()runs withallCategory=True— queries device data across every enabled category in parallelNotificationServicefetches the day's alerts for the overlay- Three additional line graph queries run conditionally — only if the industry has these standard categories configured:
ID_RAW_WATER_STOCK→ stock level trend lineID_SOURCE→ source inflow trend lineID_CONSUMPTION→ consumption trend line
- For line graphs with 48 data points (30-minute intervals), a leading zero-point is prepended for clean chart rendering
SummaryPageFormatter().format_data()assembles the final response with all category data, alerts, and line graphs
Water Balance
Base path: /api/user/waterBalance
For the full graph model, node/edge storage structure, and flow value computation see Water Balance Flow →
GET /api/user/waterBalance/graphEditor
Returns the water balance graph configuration: nodes (units, junction points), edges (flow connections), and layout metadata.
POST /api/user/waterBalance/graphEditor
Save an updated water balance graph configuration. Body: graph object with nodes and edges.
GET /api/user/waterBalance/data
Water balance calculation data mapped to the configured graph — inflow, outflow, and balance/leakage figures per node for the given date range.
| Parameter | Required | Default | Description |
|---|---|---|---|
date1 | Yes | — | DD/MM/YYYY |
date2 | No | — | DD/MM/YYYY |
divisionFactor | No | — | Divide raw values |
type | No | — | HOUR or DATE |
SCADA
Base path: /api/user/scada
GET /api/user/scada/graphEditor
Returns the SCADA diagram configuration — process nodes, connection edges, layout, and page metadata. Optionally filtered by prefix and docType query params to support multiple SCADA diagrams per industry.
POST /api/user/scada/graphEditor
Save or update the SCADA diagram configuration. Merges JSON body with query args before passing to ScadaService().post_graph().
GET /api/user/scada/data
Returns live process data values mapped to the configured SCADA diagram nodes.
GET /api/user/scada/data?date=09/11/2024&type=DAY
Authorization: Bearer <token>
| Parameter | Required | Default | Description |
|---|---|---|---|
date | Yes | — | DD/MM/YYYY |
type | No | DAY | DAY, MONTH, YEAR (PatternTypeV2) |
docType | No | — | Selects which SCADA diagram to load data for |
How it works:
ScadaService(args).get_scada_data()loads the SCADA graph config and fetches live device readings for each physical nodeScadaDataFormatter(unitsMapping, industryData).format_viewer_response(pages_data)maps the readings to the diagram nodes- Returns
ScadaViewerResponse— a list of pages, each with nodes carrying their current values
Additional Features
Base path: /api/user/additionalFeature
GET /api/user/additionalFeature/
Returns one or more feature configuration documents for the industry from the additional_feature Cosmos DB container.
GET /api/user/additionalFeature/?featureData=waterBalance&featureData=GWI
Authorization: Bearer <token>
| Parameter | Required | Description |
|---|---|---|
featureData | Yes (repeated) | One or more feature type strings — repeat the param for multiple |
How it works: AdditionalFeatureService().get_feature_data(args) queries Cosmos DB for documents matching each requested featureData type for the industry. Returns 1403 if featureData is missing entirely.
featureData value | Description |
|---|---|
waterBalance | Water balance graph nodes, edges, and summary |
waterBalanceLeakage | Leakage detection configuration |
scadaWaterBalance | SCADA water balance |
adminScadaWaterBalance | Admin SCADA config |
alerts | Additional alert configuration |
executiveSummary | Executive summary dashboard config |
roEfficiencyLog | RO efficiency log settings |
GWI | Groundwater Intelligence configuration |
HMI_CONFIG | HMI configuration |
webHMI | Web HMI configuration |
map_view | Map view settings |
water_ratio | Water ratio standards |
BATHY_DATA | Bathymetry data (Lake Pulse) |
LAKE_BOUNDARY | Lake boundary polygon (Lake Pulse) |
SATELLITE_MODEL | Satellite model configuration (Lake Pulse) |
| Status | Cause |
|---|---|
200 | Feature config returned |
1403 | featureData param missing |
Standard Category View
Base path: /api/user/standardCategoryView
GET /api/user/standardCategoryView/
Returns the industry's standardCategoriesView — the display configuration for each active device category including standardCategoryId, display name, sub-categories, and enabled state.
Common Data
Base path: /api/user/common
GET /api/user/common/
Shared reference data used across multiple screens: standard categories master list, unit mappings, and industry metadata.
Batch Processing
Base path: /api/user/batchProcess
For the full Event Hub ingestion pipeline and admin-triggered batch processing see Batch Processing Flow →
GET /api/user/batchProcess/
Trigger a batch processing job for the industry. Re-aggregates processed_data from raw devices_data for the given date.
| Parameter | Required | Description |
|---|---|---|
date1 | Yes | DD/MM/YYYY |
date2 | No | DD/MM/YYYY |
GET /api/user/batchProcess/range
Trigger batch processing over a date range.
Rainfall Data
Base path: /api/user/rainfall
GET /api/user/rainfall/trend
Returns time-series rainfall readings for rainfall-enabled units.
GET /api/user/rainfall/trend?date1=09/11/2024&type=DAY
Authorization: Bearer <token>
Uses DeviceDataV2RouteHandler internally and formats with RainfallDataFormatter. Same date/shift-UTC conversion as device data v2.
GET /api/user/rainfall/efficiencySaving
Computes rainfall efficiency and water saving metrics. Supports optional date2 for period comparison. Appends an aggregated unit list to sub-categories before processing.
| Parameter | Required | Description |
|---|---|---|
date1 | Yes | DD/MM/YYYY |
date2 | No | DD/MM/YYYY — comparison period |
divisionFactor | No | Default 1000 |
POST /api/user/rainfall/fwi/fetchHistoricalRainfallData
Fetches historical rainfall data from an external source via RainfallService and inserts it into the database.
GET /api/user/rainfall/fwi/fetchAnalyticsRainfallData
Retrieves stored FWI rainfall analytics data for a specific unit.
| Parameter | Required | Description |
|---|---|---|
unitId | Yes | Unit to fetch FWI rainfall analytics for |
Water Neutrality
Base path: /api/user/neutrality
GET /api/user/neutrality/
Compares credited water (inflows/harvesting) against debited water (consumption) over a date range.
GET /api/user/neutrality/?date1=01/11/2024&date2=30/11/2024
Authorization: Bearer <token>
| Parameter | Required | Description |
|---|---|---|
date1 | Yes | DD/MM/YYYY |
date2 | Yes | DD/MM/YYYY |
divisionFactor | No | Default 1000 |
Prerequisites: Industry must have standardCategoriesView configured with both ID_WATER_CREDITED and ID_WATER_DEBITED category mappings. Returns 1401 "Category doesn't exist" if missing.
How it works:
- Both category keys validated from
current_user['industryData']['standardCategoriesView'] - Date range converted to UTC using shift
startAttime anddaysOffsetfromcurrent_user WaterNeutralityService().get_selected_dates_data()fetches readings for credit and debit unitsWaterNeutralityDataFormatter().format_data()computes the index and formats the response
| Status | Cause |
|---|---|
200 | Water neutrality data returned |
1401 | Required category config missing |
1403 | date1 or date2 missing |
Executive Summary
Base path: /api/user/executiveSummary
GET /api/user/executiveSummary/data
Returns the executive summary dashboard payload — KPIs, trend graphs, and unit comparisons across all industries configured in the executive summary feature.
GET /api/user/executiveSummary/data
Authorization: Bearer <token>
How it works:
- Loads the
executiveSummaryconfig from theadditional_featurecontainer ExecutiveSummaryDataFormatterfetches and formats all industry rows and header metadata
PUT /api/user/executiveSummary/pin
Toggles the isPinned flag on an industry entry within the executive summary feature data.
GET /api/user/executiveSummary/virtualLogin
Supports embedded iframe access without full JWT authentication — used for embedded executive summary views.
RO Efficiency
Base path: /api/user/roEfficiency
GET /api/user/roEfficiency/
Returns RO plant efficiency metrics — feed/permeate/reject flow rates, recovery ratio, salt rejection, and specific energy consumption.
GET /api/user/roEfficiency/?date1=09/11/2024&type=DAY
Authorization: Bearer <token>
| Parameter | Required | Default | Description |
|---|---|---|---|
date1 | Yes | — | DD/MM/YYYY |
date2 | No | — | DD/MM/YYYY |
type | No | DAY | DAY, MONTH, YEAR (PatternTypeV2) |
How it works:
- Checks
current_user['categoriesV2']forRO_EFFICIENCY_PLANT— returns1401if not configured RoEfficiencyService().get_efficiency_data(args)fetches flow, quality, and energy readings for RO unitsRoEfficiencyFormatter(type).format_data()computes recovery ratio and efficiency metrics- Post-marshal: the
energykey is stripped from any plant that has no energy unit configured
| Status | Cause |
|---|---|
200 | RO efficiency data |
1401 | RO_EFFICIENCY_PLANT category not configured for this industry |
1403 | date1 missing |
GET /api/user/roEfficiency/logActivities
Returns the RO efficiency activity log — manually recorded maintenance events, chemical dosing records, and cleaning events.
POST /api/user/roEfficiency/logActivities
Create a new RO activity log entry.
GWI
Base path: /api/user/gwi
GET /api/user/gwi/data
Groundwater Intelligence data — borewell levels, extraction rates, and aquifer health metrics from GWI-enabled units.
GET /api/user/gwi/data?month=11/2024
Authorization: Bearer <token>
| Parameter | Required | Description |
|---|---|---|
month | Yes | MM/YYYY format |
How it works:
monthis parsed asMM/YYYYGWIService.get_gwi_data(parsed_date)returns three datasets:static_data,yearly_data, andaquifer_dataGWIFormatterformats all three intoGWIResponseModel
| Status | Cause |
|---|---|
200 | GWI data returned |
1403 | month missing or invalid format |
FWI
Base path: /api/user/fwi
POST /api/user/fwi/userDataInput
Submit user-provided data for Fresh Water Intelligence processing. Accepts a multi-step stepper form — each submission advances the step.
POST /api/user/fwi/userDataInput
Authorization: Bearer <token>
Content-Type: application/json
{ "step": 1, "data": { ... } }
How it works: FwiFormatter processes the step data, then FWIUserFormDataService.save_fwi_form_data() persists it. The response includes the next step in the stepper flow.
GET /api/user/fwi/userDataInput
Retrieve previously submitted FWI form data for a specific type.
| Parameter | Required | Description |
|---|---|---|
typeId | Yes | FWI form type identifier |
PLC Register
Base path: /api/user/plc
Endpoints for sending Modbus commands to PLC / IoT Edge devices. All operations are audit-logged to Cosmos DB via _log_plc_operation.
POST /api/user/plc/write-registers
Send Modbus write frames to an IoT Edge device via PLCRegisterService. Used to update register values on a connected PLC.
POST /api/user/plc/write-registers
Authorization: Bearer <token>
Content-Type: application/json
The request body contains Modbus frame data. asset and command fields are derived from frame IDs if not explicitly provided in the body.
POST /api/user/plc/read-registers
Send Modbus read frames to an IoT Edge device and return the current register values.
POST /api/user/plc/relay/valve
Send an open/close C2D (cloud-to-device) command to a relay IoT device via RelayControlService.
POST /api/user/plc/relay/valve
Authorization: Bearer <token>
Content-Type: application/json
{ "deviceId": "RELAY001", "command": "open" }
| Status | Cause |
|---|---|
200 | Command sent successfully |
500 | IoT Hub delivery failure |
Rate Limits
All limits are per client IP. Strategy: fixed window, in-memory.
| Scope | Limit |
|---|---|
| All endpoints (default) | 200/day, 50/hour |
| Login | 10/min, 50/hour |
| OTP generate | 3/min, 10/hour, 20/day |
| OTP verify | 5/min, 15/hour |
| User create | 5/hour, 20/day |
Exceeded limits return 429 Too Many Requests.
Status Code Reference
| Code | Meaning |
|---|---|
200 | Success |
201 | Success but empty result set |
400 | Bad request, schema validation failure, or limit exceeded |
401 | Unauthorised — missing or expired token, or permission denied |
404 | Resource not found |
405 | SQL injection pattern detected in parameters |
420 | JWT expired, or user/industry in blocklist |
429 | Rate limit exceeded |
440 | Session expired — token revoked or inactive in token_logs |
1401 | Data not found or invalid credentials |
1403 | Missing required parameter or header |
1404 | Invalid tenant ID (SSO) |
1405 | User or industry has been disabled |
1430 | JSON schema validation error |