Skip to main content

User API Reference

Base path: /api/user · Swagger UI: /api/user/docs

Base URLs

EnvironmentBase URL
Productionhttps://prod-aquagen.azurewebsites.net
Localhttp://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
HeaderRequiredDescription
LoginTypeYesDEFAULT, microsoft, google, otp, or external
usernameDEFAULT / otpUsername. For internal users: username/industryId to impersonate a specific industry
passwordDEFAULT / otpPassword (DEFAULT) or OTP code (otp)
Authorizationmicrosoft / googleBearer <identity_provider_token>
tidmicrosoft onlyAzure AD tenant ID — 1403 if missing for microsoft login
targetIndustryIdgoogle onlyRequired for Google SSO to identify which industry to authenticate into
otpTimestampotp onlyTimestamp returned from /otplogin — used to verify the OTP window
deviceIdNoDevice identifier — stored in token_logs for session tracking
BaseurlNoClient origin URL — stored in token_logs
X-PlatformNoAndroid or iOS — extends refresh token expiry for mobile sessions
formatNoResponse format version: v1 (default)

Login type details:

LoginTypeVerification method
DEFAULTUserService.verify_default_login() — bcrypt password check against Cosmos DB
microsoftUserService.verify_microsoft_login() — Azure AD token verified against tenant JWKS endpoint
googleUserService.verify_google_login() — Google ID token verification
otpUserService.verify_otp_login() — TOTP code + otpTimestamp validated
externalSame as DEFAULT but returns ExternalLoginResponseModel with status 201 instead of 200

How it works:

Login Flow
  1. LoginType dispatches to the correct service verification method
  2. If user or industry not found → 1401 with provider-specific message (SSO returns descriptive "email not authorized" message)
  3. If industry['disabled'] or user['disabled']1405
  4. JWT claims built: userId, email, username, loginType, role, permissions[]
  5. For internal users (industryId == Config.INTERNAL_INDUSTRY_ID), loginType is set to Config.ADMIN_LOGIN_TYPE
  6. SSO users without explicit role default to SSOUserRole.ground_staff
  7. leakageEnabled computed: True if any unit in alerts_config has alertEnabled.stable_flow == True
  8. industry['leakageEnabled'] is set before formatting — this is part of the login response
  9. TokenService.save_token() persists the token to token_logs Cosmos container
  10. WebSocketService(industry_id).get_socket_url() resolves the WebSocket URL for real-time alerts
  11. 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.

StatusCause
200Login successful
201Login successful (external type only)
1401User not found or SSO email not registered in system
1403LoginType missing, or Authorization/tid missing for microsoft login
1405User or industry has been disabled
Rate Limit

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
HeaderRequiredDescription
phNumberYesRegistered mobile number — digits only, no country code (e.g. 9876543210)

How it works:

OTP Login Flow
  1. UserService.get_otp_user(phNumber) looks up the user by phone number
  2. If not found or user['active'] == False1401 "Unregistered Number"
  3. If user['disabled'] == True1405
  4. otp_timestamp = int(time.time()) — Unix timestamp used to scope the OTP window
  5. UserService.generate_otp(phNumber, otp_timestamp) generates a TOTP code
  6. Test mode: If Config.TEST_OTP_ENABLED == True and phNumber is in Config.TEST_OTP_PHONE_NUMBERS, skips SMS and returns immediately with otpTimestamp
  7. SMSSender().send_otp_sms("91" + phNumber, otp) — prepends 91 country code for India
  8. Returns otpTimestamp in response — must be passed as the otpTimestamp header on the subsequent login call

Response (200):

{ "message": "OTP sent", "status": "Success", "otpTimestamp": 1731130200 }
StatusCause
200OTP sent via SMS (or skipped in test mode)
1401Phone number not registered or user inactive
1405User has been disabled
429Rate limited
Rate Limits

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>
HeaderRequiredDescription
AuthorizationYesBearer <refresh_token> — passing an access token returns 422
deviceIdNoDevice ID for session update in token_logs
BaseurlNoClient origin URL for session context
X-PlatformNoAndroid or iOS — extends refresh token expiry if a new one is issued
formatNoResponse format version: v1 (default)

How it works:

Refresh Flow
  1. @jwt_required(refresh=True) — Flask rejects access tokens here with 422
  2. Checks industry_data.get('disabled') and user_details.get('disabled')1405 if either is true
  3. New access_token created with same claims
  4. Rolling refresh: if exp - now < 7 days, a new refresh_token is also created
  5. TokenService.save_token() saves the new tokens to token_logs
  6. Returns full LoginResponseModel (same shape as login response)
StatusCause
200New access token issued (and optionally new refresh token)
422An access token was passed instead of a refresh token
1405User 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>
HeaderRequiredDescription
AuthorizationYesBearer <access_token>
deviceIdNoDevice ID — used to scope the logout to a specific session

How it works:

Logout Flow
  1. JWT parsed — if exp < now, returns 401 "Token already expired" immediately
  2. TokenService({"industry_id", "user_id", "device_id", "token"}).logout() sets the token's status to INACTIVE in token_logs
  3. Any subsequent request with this token hits the auth_interceptorTokenService.validate_token()440 Session expired
StatusCause
200{ "message": "...", "status": "success" }
400Exception during logout
401Token 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
HeaderRequiredDescription
usernameYesUsername (supports username/industryId for internal users)
passwordYesPassword

How it works: UserService.verify_default_login(request.headers) performs the same bcrypt check as DEFAULT login but does not issue any tokens.

StatusCause
200{ "message": "Credentials are valid", "status": "success" }
1401Invalid username or password
1405User 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>
Settings Assembly

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

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.

StatusCause
200Full settings object
401User 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):

KeyIf missing/nullStatus
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 typeLimit keyDefault
OTP/regular usersmaxUsers5 (DEFAULT_USER_LIMIT)
SSO usersmaxSsoUsers10 (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

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

Write order:

  1. User limit validation (all limits checked before any write)
  2. Deactivated user sessions revoked
  3. AccountsService.post_full_account_settings_data():
    • Updates industryName, logo, shift, siUnit in industryMeta
    • 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.alerts and meta.reports in memory
    • Patches industry doc in Cosmos DB (/industryName + /meta)
  4. Returns the full updated settings via get_full_account_settings_data()
StatusCause
200Settings saved; returns updated full settings object
400Invalid logo URL, user limit exceeded, or SSO/OTP validation error
401User lacks ACCOUNT_SETTINGS permission
1403Missing 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:

Accounts Info Flow
  1. Builds unitsMapping from current_user['industryData']['units'] indexed by unitId
  2. Merges alert config from current_user['alertsConfig']['units']:
    • unitThresholdconfigs.unitThreshold
    • monthlyThresholdconfigs.monthlyThreshold
    • maxCapacityconfigs.maxCapacity (if present)
    • lowThreshold / highThresholdconfigs.lowThreshold / highThreshold (stock) or params[param].lt / .ht (quality)
    • alertEnabled ← bool derived from daily_threshold || monthly_threshold || level_exceed (source/stock), or per-param map (quality)
  3. CategoryInfoBuilder builds a per-category info object for each active category
  4. 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.

StatusCause
200Category 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 patternTarget
/units/{index}/unitThresholdalertsConfig /units/{unitId}/configs/unitThreshold
/units/{index}/monthlyThresholdalertsConfig /units/{unitId}/configs/monthlyThreshold
/units/{index}/maxCapacityalertsConfig /units/{unitId}/configs/maxCapacity
/units/{index}/lowThreshold/valuealertsConfig /units/{unitId}/configs/lowThreshold (stock)
/units/{index}/highThreshold/valuealertsConfig /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 pathIndustry 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.

StatusCause
200Patch applied
401Permission denied
1403Invalid 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>
ParameterRequiredDescription
pathNoField 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" }
StatusCause
200Checkpoint 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>
FieldRequiredDescription
imageYesImage 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.

StatusCause
200{ "logourl": "https://storage.googleapis.com/aquagen-f6d24.appspot.com/..." }
400File validation error (wrong type or size)
401Permission denied
1403image 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:

Landing Page Assembly
  1. Always includes:
    • AccountInfoPageget_info_data() (per-unit threshold dashboard)
    • metaget_categories() (sorted: SOURCE_CATEGORY, STOCK_CATEGORY, GROUND_WATER_LEVEL, QUALITY_CATEGORY)
    • siUnitindustryMeta["siUnit"]["SOURCE_CATEGORY"] (defaults to "KL")
    • joiningDateindustryMeta["joiningDate"]
  2. Only if user has ACCOUNT_SETTINGS permission:
    • AccountSettingPageget_full_account_settings_data() (same as GET /settings)

Response shape (accountsLandingPageModel):

{
"AccountInfoPage": { "SOURCE_CATEGORY": {...}, "STOCK_CATEGORY": {...}, ... },
"meta": { "SOURCE_CATEGORY": {...}, ... },
"siUnit": "KL",
"joiningDate": "2022-03-15T00:00:00Z",
"AccountSettingPage": { ... }
}
StatusCause
200Landing 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>
ParameterRequiredDescription
newThresholdNoNew threshold value as integer

How it works:

Bulk Threshold Update Flow
  1. Iterates current_user['industryData']['categories']['source']['subCategories']
  2. Filters sub-categories where sub_category['group'] == 'Consumption Points'
  3. For each unit ID in those sub-categories, finds the matching entry in industryData['units'] and sets unit_data['unitThreshold'] = newThreshold
  4. AccountsService.update_industry_doc(industryData) replaces the full industry document in Cosmos DB
note

This updates industryData['units'] directly (not alertsConfig) — a full document replace, not a patch.

StatusCause
200{ "message": "Unit threshold updated successfully", "status": "success" }
401Permission 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>
ParameterRequiredDefaultDescription
date1YesDD/MM/YYYY
date2NoDD/MM/YYYY — second date for side-by-side comparison
categoryYessource, stock, energy, or quality (CategoryType)
subCategoryNoSub-category ID within the category
divisionFactorNo1000Divide all raw values (defaults to 1000 even if sent as 0)
typeYesHOUR, DATE, MONTH, YEAR — missing returns 1403
includeTodayNotruefalse skips the latest-data parallel query
StatusCause
200Device data payload
1401category not in current_user['categories']
1403date1, type, or category missing
info

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>
ParameterRequiredDefaultDescription
date1YesDD/MM/YYYY
date2NoDD/MM/YYYY
categoryYesID_SOURCEStandard category ID, or ALL_CATEGORIES_V2|<standardCategoryId> to aggregate across all matching categories
unitIdNoFilter formatted response to one unit only
divisionFactorNo1000Auto-overridden to 1 for GROUND_WATER_LEVEL
typeNoDAYDAY, MONTH, YEAR, CUSTOM
formattedOutputNotruefalse returns raw Cosmos DB documents
v2NofalseEnables v2 energy formatting path
StatusCause
200Formatted device data
1401Category not found
1403date1 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.

ParameterRequiredDefaultDescription
dateA1YesDD/MM/YYYY — group A start
dateA2NoGroup A end
dateB1YesDD/MM/YYYY — group B start
dateB2NoGroup B end
ids1YesUnit or sub-category IDs for group A (repeatable param)
ids2YesUnit or sub-category IDs for group B (repeatable param)
categoryYesStandard category ID
divisionFactorNo1000
typeNoDAYDAY, MONTH, YEAR, CUSTOM
byNoUNITUNIT or SUB_CATEGORY — how ids1/ids2 are resolved
info

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>
ParameterRequiredDefaultDescription
date1YesDD/MM/YYYY
date2NoDD/MM/YYYY — second date for comparison
unitIdYesUnit ID to fetch readings for
divisionFactorNo1000Divide raw values

How it works:

Granular Unit Flow
  1. Dates converted to UTC using DeviceDataRouteHandler.get_date1_and_date2() (same shift logic as Device Data v1)
  2. GranularDataService().get_granular_data_for_units([unitId], date1_utc) queries devices_data for 5-min readings in the 24-hour window starting at the shifted UTC time
  3. If date2 provided, runs a second identical query
  4. GranularDataFormatter().format_data_unit_data() aligns readings to 5-min slots, fills missing slots with 0, divides by divisionFactor
  5. Returns GranularDataResponseModel with data1 and optionally data2
StatusCause
200Granular readings returned
1403date1 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>
ParameterRequiredDefaultDescription
date1YesDD/MM/YYYY
date2NoDD/MM/YYYY
categoryIdYesCategory ID (e.g. source)
subCategoryIdYesSub-category ID within the category
isStandardCategoryNofalseIf true, resolves units from standardCategoriesView[categoryId] instead of categories[categoryId]
divisionFactorNo1000Divide 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.

StatusCause
200Aggregated granular readings
1403date1, categoryId, or subCategoryId missing

Reports

Base path: /api/user/report

info

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):

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>
ParameterRequiredDefaultDescription
startDateNocurrent_user['nowUTCWithShiftDateTime']DD/MM/YYYY — shift-adjusted current date from the JWT context; used when omitted
endDateConditionalDD/MM/YYYYrequired for hourly, granular, daily_range, monthly_range; computed from startDate for all other types (see table below)
reportTypeNodailyReport period — see Report Generation Flow → for all values and supported services
reportFormatNohtmlhtml, pdf, xlsx
serviceNowaterReport service type — see Report Generation Flow → for all values
unitIdNoFilter report to a single unit (for hourly and granular)
unitIdsNoComma-separated unit IDs. If value is the string "null", treated as no filter
useShiftNotrueApply industry shift start time to date boundaries
divisionFactorNo1000Divide all raw sensor values
formatNov1Jinja2 template version: v1 or v2
subCategoriesNoComma-separated sub-category IDs — used by smart_city service
targetIndustryIdNoHeader — 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).

StatusCause
200File download
400Invalid enum value for reportType, reportFormat, or service
1401No 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.

info

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>
ParameterRequiredDefaultDescription
dateYesDD/MM/YYYY — end date of the fetch window
typeNomonthlymonthly (day 1 of the same month up to date) or daily (date only — single day)
alert_typeNoPost-query filter: general, offline, insights, general_offline
unitIdNoPost-query filter: return only alerts for this unit
energyEnabledNofalseInclude energy-related alert types in the fetch
adminAlertsNofalseReturn only alert types relevant to the admin view

How it works:

Alerts Fetch Flow
  1. date is parsed as DD/MM/YYYY — returns 1403 on parse failure
  2. If type=monthly, startDate is set to day 1 of the same month; if type=daily, startDate = date
  3. NotificationService.get_range_notifications() queries Cosmos DB for all notifications in [startDate, date]
  4. AlertsFormatter.format_alerts() groups results by alert category, applies alert_type and adminAlerts filters

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 }
}
]
}
]
}
StatusCause
200Alert categories returned
1403date 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
}
}
HeaderRequiredDescription
targetIndustryIdNoFor internal users — trigger alert for a different industry

Body fields:

FieldRequiredDescription
unitIdYesMust exist in current_user['unitsMapping'] — returns 404 if not found
typeYesAlert type string
createdOnYesISO 8601 UTC — YYYY-MM-DDTHH:MM:SSZ — returns 400 on parse failure
meta.alertTypeYesAlert category type identifier

How it works:

Alert Processing Flow
  1. createdOn validated against %Y-%m-%dT%H:%M:%SZ400 on mismatch
  2. unitId checked against current_user['unitsMapping']404 if absent
  3. Timezone read from current_user['meta']['timezone'] (defaults to "Asia/Kolkata")
  4. AlertsRequestData constructed from body fields
  5. AlertsProcessor(alert_request).process_alerts() dispatches to channels (Email, SMS, WhatsApp, FCM, Web) based on industry alerts_config
StatusCause
200Alert processed and delivered
400createdOn not in YYYY-MM-DDTHH:MM:SSZ format
404unitId 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>
ParameterRequiredDefaultDescription
date1YesDD/MM/YYYY
date2NoDD/MM/YYYY — required only when type=CUSTOM
typeNoDAYDAY (single day), MONTH (day 1 to date1), YEAR (Jan 1 to date1), CUSTOM (date1 to date2)
unitIdNoComma-separated list of unit IDs to filter
alertTypeNoComma-separated alert type strings (e.g. stable_flow_alert)

Date range resolution by type:

  • DAYstart = date1, end = date1
  • MONTHstart = 1st of date1's month, end = date1
  • YEARstart = Jan 1 of date1's year, end = date1
  • CUSTOMstart = date1, end = date2 (returns 1403 if date2 missing)

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:

Unit Graph Alert Flow
  1. NotificationService.get_alerts_by_date_range() queries the notification container with [start_utc, end_utc] and optional unit/alert-type filters
  2. UnitGraphAlertsFormatter(pattern_type, timezone, start_date, end_date).format_alerts_for_graph() groups by time bucket and unit
StatusCause
200Graph data returned
1403date1 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>
ParameterRequiredDefaultDescription
startDateYesDD/MM/YYYY
endDateYesDD/MM/YYYY — must be ≥ startDate
alert_typeNoPost-query filter: general, offline, insights, general_offline
unitIdNoFilter to a specific unit
energyEnabledNofalseInclude energy alert types
adminAlertsNofalseAdmin-relevant types only

Edge cases:

  • Both dates parsed as %d/%m/%Y1403 on parse failure for either
  • startDate > endDate1403 "startDate cannot be after endDate"
StatusCause
200Alerts grouped by category
1403Missing/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>
ParameterRequiredDescription
startDateYesDD/MM/YYYY
endDateNoDD/MM/YYYY — defaults to startDate if omitted
categoriesYesComma-separated AlertCategoryType values
unit_idsNoComma-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": [ ... ]
}
}
StatusCause
200Per-category alert map
1403Invalid 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>
ParameterRequiredDefaultDescription
startDateYesDD/MM/YYYY
endDateNoDD/MM/YYYY — required when type=dateRange
typeNomonthlydaily (single day), monthly (month to date), dateRange (startDate to endDate)
adminAlertsNofalseAdmin-relevant types only

Edge cases:

  • type=dateRange requires endDate — returns 1403 if missing
  • startDate > endDate for dateRange1403

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": [...] }
}
}
StatusCause
200Cross-industry alerts
1403Invalid 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.

info

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>
ParameterRequiredDefaultDescription
daysNo0Number of past days to include. 0 = today only

How it works:

Notification Fetch Flow
  1. NotificationService({"days": days}).get_all_notifications() queries Cosmos DB for notifications within the window
  2. NotificationDataFormatter.format_all_notification() shapes each document
  3. If no notifications found, returns 201 with empty notifications: []; otherwise 200 with 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"
}
StatusMeaning
200Notifications returned
201No 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" }
HeaderRequiredDescription
X-Device-IdYesUnique device identifier
Body paramRequiredDescription
fcmTokenYesFirebase Cloud Messaging device token
FCM Registration

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.

StatusCause
200{ "message": "User registered successfully", "status": "success" }
400Unexpected exception
1403fcmToken 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>
ParameterRequiredDescription
startDateYesDD/MM/YYYY1403 if missing
endDateYesDD/MM/YYYY1403 if missing

Response: Same NotificationSuccessResponseModel shape as GET /notification/. Returns 201 with empty list if no notifications exist for the range.

StatusMeaning
200Notifications returned
201No notifications in range
1403startDate 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.

StatusMeaning
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):

FieldRequiredDescription
notificationIdYesNotification to assign
assigneeIdYesUser ID to assign to
note

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>".

StatusMeaning
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.

StatusMeaning
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.

StatusMeaning
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>
ParameterRequiredDefaultDescription
dateYesDD/MM/YYYY
typeNoMONTHMONTH, DAY, YEAR, CUSTOM
date2NoEnd date for CUSTOM type
pastThreeMonthsEnabledNotrueInclude last 3 months' comparison data
summaryDataNotrueInclude summary section
alertsEnabledNotrueInclude alert counts
formatterNodefaultdefault or aiai returns a raw unmodelled dict for LLM consumption

How it works:

Landing Page Data Flow
  1. industryId is taken from current_user — never from a request param
  2. LandingPageRequestData is built from the parsed args and passed to LandingPageService
  3. LandingPageService.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)
  4. When formatter=ai, the raw dict is returned directly without marshalling — bypasses the response model for LLM-readable output
StatusCause
1403date 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>
ParameterRequiredDefaultDescription
date1YesDD/MM/YYYY
divisionFactorNo1000Divide raw values
typeNoHOUR, DATE, MONTH, YEAR

How it works:

Summary Page Flow
  1. current_user['categories'] is extended with standardCategoriesView entries so all category types are queried in a single pass
  2. DeviceDataRouteHandler.get_handler() runs with allCategory=True — queries device data across every enabled category in parallel
  3. NotificationService fetches the day's alerts for the overlay
  4. Three additional line graph queries run conditionally — only if the industry has these standard categories configured:
    • ID_RAW_WATER_STOCK → stock level trend line
    • ID_SOURCE → source inflow trend line
    • ID_CONSUMPTION → consumption trend line
  5. For line graphs with 48 data points (30-minute intervals), a leading zero-point is prepended for clean chart rendering
  6. SummaryPageFormatter().format_data() assembles the final response with all category data, alerts, and line graphs

Water Balance

Base path: /api/user/waterBalance

info

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.

ParameterRequiredDefaultDescription
date1YesDD/MM/YYYY
date2NoDD/MM/YYYY
divisionFactorNoDivide raw values
typeNoHOUR 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>
ParameterRequiredDefaultDescription
dateYesDD/MM/YYYY
typeNoDAYDAY, MONTH, YEAR (PatternTypeV2)
docTypeNoSelects which SCADA diagram to load data for

How it works:

SCADA Data Flow
  1. ScadaService(args).get_scada_data() loads the SCADA graph config and fetches live device readings for each physical node
  2. ScadaDataFormatter(unitsMapping, industryData).format_viewer_response(pages_data) maps the readings to the diagram nodes
  3. 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>
ParameterRequiredDescription
featureDataYes (repeated)One or more feature type strings — repeat the param for multiple
Additional Feature Fetch

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 valueDescription
waterBalanceWater balance graph nodes, edges, and summary
waterBalanceLeakageLeakage detection configuration
scadaWaterBalanceSCADA water balance
adminScadaWaterBalanceAdmin SCADA config
alertsAdditional alert configuration
executiveSummaryExecutive summary dashboard config
roEfficiencyLogRO efficiency log settings
GWIGroundwater Intelligence configuration
HMI_CONFIGHMI configuration
webHMIWeb HMI configuration
map_viewMap view settings
water_ratioWater ratio standards
BATHY_DATABathymetry data (Lake Pulse)
LAKE_BOUNDARYLake boundary polygon (Lake Pulse)
SATELLITE_MODELSatellite model configuration (Lake Pulse)
StatusCause
200Feature config returned
1403featureData 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

info

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.

ParameterRequiredDescription
date1YesDD/MM/YYYY
date2NoDD/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.

ParameterRequiredDescription
date1YesDD/MM/YYYY
date2NoDD/MM/YYYY — comparison period
divisionFactorNoDefault 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.

ParameterRequiredDescription
unitIdYesUnit 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>
ParameterRequiredDescription
date1YesDD/MM/YYYY
date2YesDD/MM/YYYY
divisionFactorNoDefault 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:

Water Neutrality Flow
  1. Both category keys validated from current_user['industryData']['standardCategoriesView']
  2. Date range converted to UTC using shift startAt time and daysOffset from current_user
  3. WaterNeutralityService().get_selected_dates_data() fetches readings for credit and debit units
  4. WaterNeutralityDataFormatter().format_data() computes the index and formats the response
StatusCause
200Water neutrality data returned
1401Required category config missing
1403date1 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:

  1. Loads the executiveSummary config from the additional_feature container
  2. ExecutiveSummaryDataFormatter fetches 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>
ParameterRequiredDefaultDescription
date1YesDD/MM/YYYY
date2NoDD/MM/YYYY
typeNoDAYDAY, MONTH, YEAR (PatternTypeV2)

How it works:

RO Efficiency Flow
  1. Checks current_user['categoriesV2'] for RO_EFFICIENCY_PLANT — returns 1401 if not configured
  2. RoEfficiencyService().get_efficiency_data(args) fetches flow, quality, and energy readings for RO units
  3. RoEfficiencyFormatter(type).format_data() computes recovery ratio and efficiency metrics
  4. Post-marshal: the energy key is stripped from any plant that has no energy unit configured
StatusCause
200RO efficiency data
1401RO_EFFICIENCY_PLANT category not configured for this industry
1403date1 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>
ParameterRequiredDescription
monthYesMM/YYYY format

How it works:

GWI Data Flow
  1. month is parsed as MM/YYYY
  2. GWIService.get_gwi_data(parsed_date) returns three datasets: static_data, yearly_data, and aquifer_data
  3. GWIFormatter formats all three into GWIResponseModel
StatusCause
200GWI data returned
1403month 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.

ParameterRequiredDescription
typeIdYesFWI 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" }
StatusCause
200Command sent successfully
500IoT Hub delivery failure

Rate Limits

All limits are per client IP. Strategy: fixed window, in-memory.

ScopeLimit
All endpoints (default)200/day, 50/hour
Login10/min, 50/hour
OTP generate3/min, 10/hour, 20/day
OTP verify5/min, 15/hour
User create5/hour, 20/day

Exceeded limits return 429 Too Many Requests.


Status Code Reference

CodeMeaning
200Success
201Success but empty result set
400Bad request, schema validation failure, or limit exceeded
401Unauthorised — missing or expired token, or permission denied
404Resource not found
405SQL injection pattern detected in parameters
420JWT expired, or user/industry in blocklist
429Rate limit exceeded
440Session expired — token revoked or inactive in token_logs
1401Data not found or invalid credentials
1403Missing required parameter or header
1404Invalid tenant ID (SSO)
1405User or industry has been disabled
1430JSON schema validation error