External API Reference
Base path: /api/external · Swagger UI: /api/external/docs
Base URLs
| Environment | Base URL |
|---|---|
| Production | https://prod-aquagen.azurewebsites.net |
| Local | http://localhost:5000 |
Example:
https://prod-aquagen.azurewebsites.net/api/external/latest/unit?unitId=U001
https://prod-aquagen.azurewebsites.net/api/external/latest/all
https://prod-aquagen.azurewebsites.net/api/external/waterRatio/?startDate=01/11/2024&endDate=30/11/2024&type=daily
The External API provides a minimal, stable surface for third-party systems that need to read live device data or submit water ratio records without access to the full User or Admin API. Authentication is identical to the User API — the same JWT token issued by GET /api/user/user/login works here.
Authentication
External API endpoints use the same JWT issued by the User API login endpoint.
Authorization: Bearer <access_token>
Obtain a token via:
GET /api/user/user/login
Authorization: Bearer <azure_ad_token>
loginType: microsoft
(or any other loginType supported by the User API — see User API Reference → Authentication)
Token validation: Every request passes through auth_interceptor (@app.before_request), which calls TokenService.validate_token() against token_logs in Cosmos DB. A deactivated or expired token returns 440 before the route handler runs.
Swagger UI browsing only: GET /api/external/auth/swaggerLogin renders an HTML login form. Submit username and password — on success, an access_token_cookie is set (httponly, 1-hour max-age) and the browser is redirected to /api/external/docs. Not for programmatic use.
Device Data
GET /api/external/latest/unit
Returns the single most recent raw reading for a specific unit. This is a real-time endpoint — it always returns the device's latest transmitted value regardless of date.
GET /api/external/latest/unit?unitId=U001
Authorization: Bearer <access_token>
| Parameter | Required | Description |
|---|---|---|
unitId | Yes | Unit to retrieve the latest reading for — 1403 if missing |
unitIdis validated againstcurrent_user['unitsMapping']— only units belonging to the authenticated user's industry are accessibleExternalUnitDataService(args).get_top_one_device_data_for_unit()queries Cosmos DB:SELECT TOP 1 * FROM c WHERE c.unitId = @unitId ORDER BY c._ts DESCExternalUnitDataFormatter().format_device_data(current_user['unitsMapping'], items)applies display metadata:- Unit display name from
unitsMapping - SI unit (e.g.
KL,m³,m) - Raw value divided by
divisionFactor(default1000)
- Unit display name from
- If no data is found →
404
Response shape:
{
"data": {
"unitId": "U001",
"unitName": "Main Inlet",
"value": 1234.56,
"siUnit": "KL",
"timestamp": "2024-11-09T10:30:00Z"
},
"status": "Success"
}
| Status | Cause |
|---|---|
200 | Latest reading returned |
404 | No data found for the unit |
401 | Missing or expired token |
420 | JWT expired |
440 | Session revoked — token deactivated in token_logs |
1403 | unitId missing |
GET /api/external/latest/all
Returns the latest reading for every unit belonging to the authenticated user's industry. No parameters required — the industry is inferred from the JWT.
GET /api/external/latest/all
Authorization: Bearer <access_token>
How it works:
industryIdis taken fromcurrent_user['industryId']in the JWTExternalIndustryUnitDataService(args).get_top_one_device_data_for_industry()queries Cosmos DB for the most recent reading for each unit in the industry- All results are passed through
ExternalUnitDataFormatter().format_device_data(current_user['unitsMapping'], items)— same formatting as/unit(display name, SI unit, division factor) - If no data is found for any unit →
404
Response shape:
{
"data": [
{
"unitId": "U001",
"unitName": "Main Inlet",
"value": 1234.56,
"siUnit": "KL",
"timestamp": "2024-11-09T10:30:00Z"
},
{
"unitId": "U002",
"unitName": "Overhead Tank",
"value": 45.2,
"siUnit": "KL",
"timestamp": "2024-11-09T10:28:00Z"
}
],
"status": "Success"
}
Each unit's latest reading is fetched independently. Units that have never transmitted data are silently omitted from the array — only units with at least one reading appear in the response.
| Status | Cause |
|---|---|
200 | Latest reading for each unit in the industry |
404 | No data found for any unit |
401 | Missing or expired token |
420 | JWT expired |
440 | Session revoked |
Water Ratio Data
Base path: /api/external/waterRatio
Water ratio tracks the relationship between water consumed and water produced/supplied. Records are keyed by date + time — each combination must be unique. All three methods are JWT-protected and scoped to the authenticated user's industryId.
GET /api/external/waterRatio/
Retrieve water ratio records for a date range.
GET /api/external/waterRatio/?startDate=01/11/2024&endDate=30/11/2024&type=daily
Authorization: Bearer <access_token>
| Parameter | Required | Default | Description |
|---|---|---|---|
startDate | Yes | — | DD/MM/YYYY — start of the range — 1403 if missing |
endDate | No | — | DD/MM/YYYY — end of range. Defaults to startDate if omitted (single-day fetch) |
type | No | daily | daily — day-level records; monthly — month-level aggregates |
industryIdresolved fromcurrent_user['industryId']WaterRatioDataService(request_data).get_data()queries Cosmos DB for all water ratio documents within[startDate, endDate]for the industry- For
type=monthly, results are aggregated per month - Returned as an array sorted by date ascending
Response shape:
[
{
"date": "01/11/2024",
"time": "08:00:00",
"industryId": "IND001",
"ratio": 0.85,
"produced": 10000,
"consumed": 8500
}
]
| Status | Cause |
|---|---|
200 | Array of water ratio records |
1403 | startDate missing |
POST /api/external/waterRatio/
Submit a new water ratio data point. The date + time combination must be unique per industry — attempting to insert a duplicate returns 1403.
POST /api/external/waterRatio/?date=09/11/2024&time=08:00:00
Authorization: Bearer <access_token>
Content-Type: application/json
{
"ratio": 0.85,
"produced": 10000,
"consumed": 8500
}
| Parameter | Required | Description |
|---|---|---|
date | Yes | DD/MM/YYYY — record date — 1403 if missing |
time | Yes | HH:MM:SS — record time — 1403 if missing |
Body: JSON object with the ratio data fields to store. Shape is industry-specific.
industryIdresolved fromcurrent_user['industryId']WaterRatioDataService(request_data).save_data()checks if a document already exists for(industryId, date, time)- If a record already exists → returns
None→ route returns1403 "Already data exists for the given date and time." - Otherwise writes the new document to Cosmos DB
The duplicate check is strict on both date AND time. Two records with the same date but different times are allowed.
| Status | Cause |
|---|---|
200 | Record created, returns the created document |
1403 | date or time missing, OR record already exists for this date/time combination |
PUT /api/external/waterRatio/
Update an existing water ratio data point identified by date and time.
PUT /api/external/waterRatio/?date=09/11/2024&time=08:00:00
Authorization: Bearer <access_token>
Content-Type: application/json
{
"ratio": 0.90,
"produced": 10000,
"consumed": 9000
}
| Parameter | Required | Description |
|---|---|---|
date | Yes | DD/MM/YYYY — identifies the record to update — 1403 if missing |
time | Yes | HH:MM:SS — identifies the record to update — 1403 if missing |
Body: JSON object with the fields to update.
WaterRatioDataService(request_data).update_data()looks up the document by(industryId, date, time)- If no record found → returns
None→ route returns1403 "No data found on the given date" - Otherwise updates the document fields with the request body values
| Status | Cause |
|---|---|
200 | Record updated, returns the updated document |
1403 | date or time missing, OR no record found for the date/time |
Status Code Reference
| Code | Meaning |
|---|---|
200 | Success |
404 | No data found for the query |
401 | Missing or invalid JWT |
420 | JWT token expired — obtain a new token via /api/user/user/login |
440 | Session revoked — token was deactivated in token_logs (e.g. due to logout on another device) |
1403 | Missing required parameter, or conflict (record already exists for POST / not found for PUT) |