# CloudWorks Integration REST API — AI Reference

Machine-oriented reference for the CloudWorks Integration embedded REST API.
Derived from the API User Manual (August 2026) and the API_Controller.vb source.
For humans, prefer the Word manual; this document trades prose for precision and density.

- **Base URL:** `http://<server>:18500/api/<endpoint>` or `https://<server>:18501/api/<endpoint>` (ports configurable; 0 disables a listener; defaults shown)
- **Auth:** every request needs header `X-API-Key: <key>`. The key selects the customer database; all data is scoped to it. Missing/invalid key → `401` with empty body.
- **Content:** JSON everywhere unless stated. GET = read, POST = create/modify/delete.

## Quickstart

```bash
curl -H "X-API-Key: $KEY" "http://myserver:18500/api/pingServer"          # 200 = up + key valid
curl -H "X-API-Key: $KEY" "http://myserver:18500/api/getDeviceInformation?basicData=true"
curl -H "X-API-Key: $KEY" "http://myserver:18500/api/getDeviceData?deviceCode=2107070002&startDate=2026-08-01&endDate=2026-08-02&summarise=true"
```

## Task → endpoint map

| Task | Use |
|---|---|
| Health / key check | `GET pingServer` |
| Discover HTTPS port, DB connection details | `GET getServerConfiguration` (response holds live DB credentials — treat as secret) |
| List devices / device status | `GET getDeviceInformation?basicData=true` |
| Full device telemetry snapshot (current values only) | `GET getDeviceInformation` (basicData=false) |
| Historical readings / datalog for a device | `GET getDeviceData` |
| Hourly-averaged history | `GET getDeviceData&summarise=true` (interpolates gaps — see Summarise) |
| Live comms state, signal, latency, firmware | `GET getCommsRegister` |
| Command an ONLINE device now | `POST sendRemoteCommand` (blocks for reply; fails on sleeping devices) |
| Command a SLEEPING battery device | `POST saveScriptCommand` (delivered at next wakeup) |
| Manage zones / sites / users / devices | `get/save/delete` + `addDevice`, `moveSite`, `moveDevice`, `replaceDevice` |
| Verify a user's login credentials | `GET authenticateUser` (returns true/false, never an error for a wrong password) |
| Tank calibration table | `GET getTankTable` / `POST saveTankTable` (full replace, not merge) |
| Calibrate hydrostatic tank sensor from a dip reading | `POST calibrateTank` (queued like a script command; applied at next device contact) |
| Delete history | `POST deleteDatalog` (irreversible) |

## Validation rules (server-enforced, return 400 before any DB access)

- **Code parameters** (`deviceCode`, `siteCode`, `zoneCode`, `userCode`, `destinationZoneCode`, `destinationSiteCode`, `oldDeviceCode`, `newDeviceCode`): must match `^[A-Za-z0-9._ -]{1,32}$`. Violation → `400 "<param> contains invalid characters"`. Blank/omitted is allowed where the endpoint treats blank as "all"; endpoints requiring a code return their own blank-code 400.
- **Dates** (`startDate`, `endDate`): exact accepted formats, invariant culture:
  `yyyy-MM-dd`, `yyyy-MM-dd HH:mm`, `yyyy-MM-dd HH:mm:ss`, `yyyy-MM-ddTHH:mm`, `yyyy-MM-ddTHH:mm:ss`, `dd/MM/yyyy`, `dd/MM/yyyy HH:mm`, `dd/MM/yyyy HH:mm:ss`.
  Anything else → 400 naming the parameter. Additional rules: `startDate <= endDate`; dates within 2016 → now+2 days; span ≤ **400 days** (`400 "Date range too large - maximum 400 days per request"`).
- **Booleans** (`reload`, `alarm`, `alert`, `newUser`, `basicData`, `summarise`): `true`/`false`. Any other value or omission is silently treated as `false` (never an error).
- **Numbers in responses:** every fractional value is rounded to **3 decimal places** on output, EXCEPT latitude/longitude fields (6 dp). Values < 0.0005 therefore serialize as `0`.

## Time-zone model (critical)

- Loggers run UTC; all database storage is UTC.
- Query dates you SUPPLY are interpreted in the **site's local time zone** (from site record), not the caller's.
- All timestamps RETURNED are converted to site local time — `getDeviceData`, `getDeviceInformation` (lastSeen, lastDatalogDate, lastFtpUpload), datalog `logDate`.
- **Single exception:** `getCommsRegister` returns `lastSeenUTC`, which is UTC by design (the name says so).

## Error model

| Status | Meaning | Body |
|---|---|---|
| 200 | Success | JSON, plain text (sendRemoteCommand), or empty (action endpoints) |
| 400 | Caller error: bad/missing parameter, unknown record, business error | Plain-text reason (branch on it) |
| 401 | Bad/missing X-API-Key | Empty |
| 500 | Unexpected server error | Problem JSON: "Requested command threw an unexpected exception" |

Known 400 strings include: `"Device Code is blank"`, `"Device <code> not found in the database"`, `"<param> contains invalid characters"`, `"startDate is missing or invalid - ..."`, `"startDate must be before endDate"`, `"Dates are outside the supported range"`, `"Date range too large - maximum 400 days per request"`, `"Unsupported application type"`, `"No Response"`, `"Device is currently offline"`, `"Device not found in Comms Register"`, `"No Device Code"`, `"Dip Level Error"`, `"User Code is blank"`, `"Password is blank"`.

## Endpoint reference

Legend: **[RO]** read-only · **[W]** write/idempotent-ish · **[UPSERT]** insert-or-update on key · **[DESTRUCTIVE]** irreversible, confirm with a human first · **[SENSITIVE]** response contains secrets.

### General
- `GET pingServer` **[RO]** — no params. 200 empty = alive + key valid.
- `GET getServerConfiguration` **[RO][SENSITIVE]** — no params. Returns securePort, serverCommsPort, and live DB server/name/username/password. Never log or display the credentials.
- `POST reloadConfiguration?deviceCode&reload` **[W]** — flag device to reload config at next contact; `reload=false` cancels.
- `POST setAlarmAlert?deviceCode&alarm&alarmMessage&alert&alertMessage` **[W]** — set/clear alarm+alert states; false clears the message.
- `POST sendRemoteCommand?deviceCode&command` **[W]** — sends raw command to an ONLINE device, blocks for the reply, returns it as plain text. Fails 400 for sleeping/offline devices.
- `POST sendEmail` **[W]** — JSON body `{recipientAddress, subject, body, isHTML}`; `isHTML` is a **Boolean** (`true`/`false`, unquoted). 500 if the send fails.
- `GET getCommsRegister?deviceCode` **[RO]** — live in-memory register: status Online/Offline/Sleep, power, signal %, firmware, `lastSeenUTC` (UTC!), ip/port, latency, commsAnalyser[] (totalPackets, droppedPackets, averageLatency, averageSignal).

### Script commands (for battery devices)
- `GET getScriptCommandInformation?deviceCode` **[RO]** — queued commands + responses + complete flag, grouped per device.
- `POST saveScriptCommand?deviceCode&command` **[W]** — queue command for next wakeup.
- `POST clearScriptCommands?deviceCode` **[W]** — delete all queued commands for the device.

### Tank tables
- `GET getTankTable?deviceCode` **[RO]** — `{level, volume, capturedEntry}` rows ordered by level.
- `POST saveTankTable?deviceCode` + JSON array body **[W]** — FULL REPLACE of the table; always send every row. Levels stored to 3 dp, volumes 4 dp.
- `POST calibrateTank?deviceCode&dipLevel` **[W]** — queues a `[CALIBRATE TANK LEVEL]` script command so a hydrostatic-sensor tank device recalculates its pressure-to-level factor (calibrates out product density). `dipLevel` = dipped level in metres from the tank bottom, must be > 0 (`400 "Dip Level Error"` otherwise; `400 "No Device Code"` when blank). Applied at the device's next contact/wakeup, not immediately — advise dipping close to queueing time.

### Zones
- `GET getZoneInformation?zoneCode` **[RO]** — zones with alarm/alert roll-ups + contact fields.
- `POST saveZone` **[UPSERT]** — JSON body, key `zoneCode`; all fields required (description, contactName, address1-4, country, postalCode, telephone, cellular, emailAddress).
- `POST deleteZone?zoneCode` **[DESTRUCTIVE]** — cascades: deletes all sites in the zone and all devices at those sites.

### Sites
- `GET getSiteInformation?siteCode` **[RO]** — zone fields + `zoneCode` + `timeZone` (identifier used for all time conversion).
- `POST saveSite` **[UPSERT]** — body like saveZone + `timeZone`.
- `POST deleteSite?siteCode` **[DESTRUCTIVE]** — cascades to all devices at the site.
- `POST moveSite?siteCode&destinationZoneCode` **[W]**.

### Users
- `GET getUserInformation?userCode` **[RO][SENSITIVE]** — includes stored (encrypted) password. userType codes: N/S/O/T/A=admin roles, Z=Zone, U=Site, D=Device scoped (with `accessCode`).
- `GET authenticateUser?userCode&password` **[RO]** — encrypts the offered plain-text password server-side and compares it to the stored one. Returns `{message, userAuthenticated, name, userType, accessCode, emailAddress}` with 200 in all credential cases: `userAuthenticated` is `true` on match, `false` on mismatch OR unknown user (no user-existence leak). The profile fields (name, userType code, accessCode, emailAddress) are populated only on success and blank otherwise — intended for website session bootstrap. 400 only for blank userCode/password. Password rides in the query string — HTTPS only.
- `POST saveUser` **[UPSERT]** — body key `userCode`; new users get a random password — follow with `resetUserPassword?newUser=true` to email credentials.
- `POST deleteUser?userCode` **[DESTRUCTIVE]**.
- `POST resetUserPassword?userCode&newUser` **[W]** — generates new password, emails it (+.cwf shortcut). Requires user to have an email address.
- `POST changeUserPassword?userCode&newPassword` **[W]** — plain-text password; the server encrypts it with the system password encryption before storing (same scheme `authenticateUser` verifies against). `400 "Password is blank"` when newPassword is missing/empty, `400 "User does not exist"` for unknown users. Password rides in the query string — HTTPS only.

### Devices
- `GET getDeviceInformation?deviceCode&basicData` **[RO]** — basicData=true → identity/status set; false → plus every telemetry column (unused ones hold defaults).
- `POST addDevice?siteCode&deviceCode` **[W]** — creates a new device record at the site (400 `"Device already in Site - <site code>"` if the device exists; details populate when the device first connects). NOTE: there is no saveDevice endpoint — devices are created here and modified via their own device configuration flow.
- `POST deleteDevice?deviceCode` **[DESTRUCTIVE]**.
- `POST moveDevice?deviceCode&destinationSiteCode` **[W]**.
- `POST replaceDevice?oldDeviceCode&newDeviceCode` **[W]** — re-keys the record; keeps site, config, datalog history.
- `POST deleteDatalog?deviceCode&startDate&endDate` **[DESTRUCTIVE]** — deletes readings in range (site local time).
- `GET getDeviceData?deviceCode&startDate&endDate&summarise` **[RO]** — main data call; response shape depends on the device's applicationCode (see table below).

## getDeviceData response

Common envelope in every response: `message, commsStatus, lastSeen, timeZone, siteDescription, deviceCode, deviceType, applicationCode, dataIndex, description, serialNumber, gsmSignal, batteryStatus, powerStatus, powerMode, alarm, alert, alarmMessage, alertMessage, latitude, longitude, wakeupPeriod, datalogPeriod, lastDatalogDate, lastFtpUpload, userConfigurationData` — then app-specific current values, then `datalog[]`.

Illustrative full response (application 012, values illustrative):

```json
{
  "message": "Data for Device Code 2107070002 between 2026-08-01 00:00:00 and 2026-08-02 00:00:00",
  "commsStatus": "Sleep",
  "lastSeen": "2026-08-01T23:45:12",
  "timeZone": "(UTC+02:00) Harare, Pretoria",
  "siteDescription": "ST01 : Main Reservoir",
  "deviceCode": "2107070002",
  "deviceType": "CDS552 - Cirrus Loop",
  "applicationCode": "12",
  "dataIndex": 143,
  "description": "Reservoir pressure logger",
  "serialNumber": "SN00123",
  "gsmSignal": 78,
  "batteryStatus": 91,
  "powerStatus": "Battery",
  "powerMode": "Battery",
  "alarm": false,
  "alert": false,
  "alarmMessage": "",
  "alertMessage": "",
  "latitude": -33.924869,
  "longitude": 18.424055,
  "wakeupPeriod": 60,
  "datalogPeriod": 15,
  "lastDatalogDate": "2026-08-01T23:45:00",
  "lastFtpUpload": "2026-08-01T23:46:02",
  "userConfigurationData": "",
  "loopReading": 4.512,
  "loopUnits": "bar",
  "loopAlarm": "None",
  "datalog": [
    {
      "logDate": "2026-08-01T00:00:00",
      "loopReading": 4.498,
      "loopAlarm": "None",
      "batteryStatus": 91,
      "powerStatus": "Battery",
      "interpolatedEntry": false
    }
  ]
}
```

### Summarise semantics (`summarise=true`)

- Period: first whole hour ≥ first reading → last whole hour ≤ last reading; one entry per hour = LAST real reading in that hour.
- Empty hours are linearly interpolated between surrounding real readings; `interpolatedEntry=true`.
- Trailing empty hours carry the last known values forward, also flagged interpolated.
- **Interpolated entries are synthetic** — never present them as measured data. String/boolean fields (loopAlarm, deviceAlarm, interfaceError, powerStatus) only carry into interpolated entries when both surrounding real readings agree; otherwise they are blank/false.
- Consumption fields are computed as current-minus-previous over the FINAL array (after summarise), so hourly consumption from summarised data is per-hour.

### Application field sets

Every datalog entry also includes `logDate, batteryStatus, powerStatus, interpolatedEntry` (except 024). App code comes from `getDeviceInformation.applicationCode`. **App 036 is not implemented** → `400 "Unsupported application type"`.

| App | Device | Current values (beyond envelope) | Datalog entry (beyond logDate/battery/power/interpolated) |
|---|---|---|---|
| 000 | Dual pulse + loop + digital I/O | totaliser1(+Units), totaliser2(+Units), loopReading(+Units), input, output | totaliser1, totaliser2, consumption1, consumption2, loopReading, input, output |
| 001 | Dual pulse, pressure on loop | totaliser1/2(+Units), loopReading(pressure)(+Units), input, output | totaliser1, totaliser2, consumption, flow, pressure, input, output |
| 002 | Fwd/rev pulse, pressure on loop | forwardTotaliser(+Units), reverseTotaliser(+Units), loopReading(pressure)(+Units), input, output | forwardTotaliser, reverseTotaliser, forwardConsumption, reverseConsumption, flow, pressure, input, output |
| 003 | Serial meter, flow + pressure | interfaceError, fwd/rev totalisers(+Units), flow(+Units), pressure(+Units), loopReading(+Units), input, output | interfaceError, fwd/rev totalisers + consumptions, flow, pressure, loopReading, input, output |
| 004 | ECO meter + dual pulse | interfaceError, forwardTotaliser(+Units), medium, meterSerial, totaliser1/2(+Units), loopReading(+Units), input, output | ecoStatus, serialNumber, interfaceError, totaliser(ECO), consumption, flow, totaliser1, totaliser2, loopReading, input, output |
| 005 | Serial meter (= 003 data set) | as 003 | as 003 |
| 006 | MBus meter full + loop + I/O | interfaceError, meterBattery(days), cumulative/fwd/rev totalisers(+Units), medium, meterSerial, mbusStatus, flow(+Units), ambient/mediumTemperature(+Units), meterAlarmData, meterData, meterFirmwareVersion, loopReading(+Units), input, output | mbusStatus, serialNumber, interfaceError, cumulative/fwd/rev totalisers + consumptions, flow, medium/ambientTemperature, meterBatteryLife, meterAlarmData, meterData, loopReading, input, output |
| 007 | Simple dual pulse | totaliser1/2(+Units) | totaliser1, totaliser2, consumption1, consumption2 |
| 008 | Dual pulse, combined+flow | totaliser1/2(+Units) | totaliser1, totaliser2, consumption, flow |
| 009 | Fwd/rev pulse | fwd/rev totalisers(+Units) | fwd/rev totalisers + consumptions, flow |
| 010 | ECO meter + dual pulse (no loop/IO) | as 004 minus loop/input/output | as 004 minus loop/input/output |
| 011 | MBus meter full (no loop/IO) | as 006 minus loop/input/output | as 006 minus loop/input/output |
| 012 | Single monitored loop | loopReading(+Units), loopAlarm | loopReading, loopAlarm |
| 013 | Serial meter + loop alarm | interfaceError, fwd/rev totalisers(+Units), flow(+Units), pressure(+Units), loopReading(+Units), loopAlarm | interfaceError, fwd/rev totalisers + consumptions, flow, pressure, loopReading, loopAlarm |
| 014 | Serial meter + loop alarm (= 013) | as 013 | as 013 |
| 015 | Dual pulse + monitored loop | totaliser1/2(+Units), loopReading(+Units), loopAlarm | totaliser1, totaliser2, consumption1, consumption2, loopReading, loopAlarm |
| 016 | Dual pulse, pressure on monitored loop | totaliser1/2(+Units), loopReading(pressure)(+Units), loopAlarm | totaliser1, totaliser2, consumption, flow, pressure, loopAlarm |
| 017 | Fwd/rev pulse, pressure on monitored loop | fwd/rev totalisers(+Units), loopReading(pressure)(+Units), loopAlarm | fwd/rev totalisers + consumptions, flow, pressure, loopAlarm |
| 018 | Serial meter, flow, loop, I/O (no pressure) | interfaceError, fwd/rev totalisers(+Units), flow(+Units), loopReading(+Units), input, output | interfaceError, fwd/rev totalisers + consumptions, flow, loopReading, input, output |
| 019 | Serial meter, flow, monitored loop | interfaceError, fwd/rev totalisers(+Units), flow(+Units), loopReading(+Units), loopAlarm | interfaceError, fwd/rev totalisers + consumptions, flow, loopReading, loopAlarm |
| 020 | Tank + digital I/O | productLevel(+Units m), productVolume(+Units m3), productLevelPercentage, input, output | productLevel, productVolume, input, output |
| 021 | Tank on loop + alarms | productLevel/Volume(+Units), productLevelPercentage, loopAlarm | productLevel, productVolume, loopAlarm |
| 022 | Tank serial + I/O | interfaceError + as 020 | interfaceError, productLevel, productVolume, input, output |
| 023 | Tank serial + loop alarms | interfaceError + as 021 | interfaceError, productLevel, productVolume, loopAlarm |
| 024 | GPS tracker | loopReading(+Units), input, output | gpsLatitude, gpsLongitude, loopReading, gpsValidFix, gpsError, input, output — NO battery/power/interpolated |
| 025 | Krohne counters + flow + loop + I/O | interfaceError, counter1/2(m3), flow(+Units), loopReading(+Units), input, output | counter1, counter2, consumption1, consumption2, flow, loopReading, input, output, interfaceError |
| 026 | Krohne counters + monitored loop | interfaceError, counter1/2(m3), flow(+Units), loopReading(+Units), loopAlarm | counter1, counter2, consumption1, consumption2, flow, loopReading, loopAlarm, interfaceError |
| 027 | Dual pulse + pressure sensor + I/O | totaliser1/2(+Units), pressure(+Units), input, output | totaliser1, totaliser2, consumption, flow, pressure, input, output |
| 028 | Interface dual totalisers + monitored loop | interfaceError, totaliser1/2(m3), flow(+Units), loopReading(+Units), loopAlarm | totaliser1, totaliser2, consumption1, consumption2, flow, loopReading, loopAlarm, interfaceError |
| 029 | Single totaliser + two loops | totaliser(+Units), loop1Reading(+Units), loop1Alarm, loop2Reading(+Units), loop2Alarm | totaliser, consumption, loop1Reading, loop1Alarm, loop2Reading, loop2Alarm |
| 030 | MBus meter full (= 011 data set) | as 011 | as 011 |
| 031 | PRV monitor | totaliser(+Units), pressureUpstream(+Units), loop1Alarm, pressureDownstream(+Units), loop2Alarm | totaliser, consumption, pressureUpstream, loop1Alarm, pressureDownstream, loop2Alarm |
| 032 | Interface water meter + loop | interfaceError, totaliser(+Units), meterSerial, meterSize, loopReading(+Units), loopAlarm | totaliser, consumption, flow, meterSerial, loopReading, loopAlarm, interfaceError. NOTE: table column namur_status is NOT exposed via the API |
| 033 | Net/fwd/rev + alarms + loop | interfaceError, totaliser(net)(+Units), fwd/rev totalisers(+Units), flow(+Units), pressure(+Units), deviceAlarmData, loopReading(+Units), loopAlarm | interfaceError, totaliser(net), reverseTotaliser, consumption(net), reverseConsumption, flow, pressure, loopReading, loopAlarm |
| 034 | Aquamaster 4 | interfaceError, fwd/rev totalisers(+Units), flow(+Units), pressure(+Units), meterExternalVoltage, meterBatteryVoltage, meterAlarmData(bitmask), loopReading(+Units), loopAlarm | interfaceError, fwd/rev totalisers + consumptions, flow, pressure, alarmByte(bitmask), loopReading, loopAlarm |
| 035 | Interface water meter + medium + loop | interfaceError, totaliser(+Units), meterSerial, meterMedium, meterSize, loopReading(+Units), loopAlarm | totaliser, consumption, flow, meterSerial, loopReading, loopAlarm, interfaceError |
| 037 | Soil 3-in-1 | interfaceError, soilTemperature/Moisture/ElectricalConductivity(+Units) | interfaceError, soilTemperature, soilMoisture, soilElectricalConductivity |
| 038 | MAG8000 | interfaceError, totaliser1/2(+Units), flow(+Units), meterBattery(%), meterAlarmData(bitmask), loopReading(+Units), loopAlarm | interfaceError, totaliser1, totaliser2, consumption1, consumption2, flow, alarmByte, loopReading, loopAlarm |
| 039 | Aquamaster 3 | interfaceError, fwd/rev totalisers(+Units), flow(+Units), pressure(+Units), meterPowerSource, meterExternal/InternalPowerStatus, meterAlarmData(bitmask), loopReading(+Units), loopAlarm | interfaceError, fwd/rev totalisers + consumptions, flow, pressure, alarmByte, loopReading, loopAlarm |
| 040 | Pulse line status + loop | pulse1Status, pulse2Status (0=Low 1=High), loopReading(+Units), loopAlarm | pulse1Status, pulse2Status, loopReading, loopAlarm |
| 041 | Serial pressure | interfaceError, pressure(+Units) | pressure, interfaceError |
| 042 | Cathodic protection | cathodicVoltage, cathodicStatus (N=None C=Corroding O=Overprotected) | cathodicVoltage, cathodicStatus |
| 043 | QT/W803 | interfaceError, fwd/rev totalisers(+Units), flow(+Units), pressure(+Units), meterBattery(%), meterAlarmData(bitmask) | interfaceError, fwd/rev totalisers + consumptions, flow, pressure, alarmByte |
| 044 | Tank serial: level/volume/temp | interfaceError, productLevel/Volume/Temperature(+Units), productLevelPercentage | level, volume, temperature, interfaceError |
| 045 | Interface flow meter, decoded alarms, pressure, loop | interfaceError, totaliser(net)(+Units), fwd/rev totalisers(+Units), flow(+Units), pressure(+Units), deviceAlarmData(raw), deviceAlarm (decoded: ""=none / "Empty Pipe" / "Meter Battery Low" / "Unknown Alarm"), loopReading(+Units), loopAlarm | interfaceError, fwd/rev totalisers + consumptions, flow, pressure, loopReading, loopAlarm, deviceAlarm |
| 046 | Interface flow meter, no pressure/alarms | interfaceError, totaliser(net)(+Units), fwd/rev totalisers(+Units), flow(+Units), loopReading(+Units), loopAlarm | interfaceError, fwd/rev totalisers + consumptions, flow, loopReading, loopAlarm |

Enumerations: `commsStatus` = Online/Offline/Sleep/Unknown · `powerStatus` = Mains/Battery/Unknown · `powerMode` = Mains/Battery/Dual · `loopAlarm` device-level = "None"/"High Alarm"/"Low Alarm"/"Unknown", datalog-level = "None"/"High"/"Low"/"" · `input`/`output` = 0/1.

## getDeviceInformation fields

**basicData=true:** updateConfiguration, dataIndex, zoneCode, siteCode, deviceCode, commsStatus, lastSeen (site local), timeZone, deviceType, deviceDescription, applicationCode, applicationDescription, description, serialNumber, gsmSignal, batteryStatus, powerStatus, powerMode, alarm, alert, alarmMessage, alertMessage, latitude, longitude, wakeupPeriod, datalogPeriod, lastDatalogDate (site local), lastFtpUpload (site local), userConfigurationData.

**basicData=false:** all of the above plus every telemetry column (unused columns hold defaults) — totalisers, loops, pressures, flow, meter data, soil data, tank data, GPS, cathodic. All field names use standard camelCase (`forwardTotaliser`, `pressureDownstreamUnits`, `ambientTemperatureUnits` — historic typo variants of these three were fixed in August 2026 before release).

## Gotchas for agents

1. `getServerConfiguration` returns **live database credentials**. Never echo, log, or store them in output shown to third parties.
2. `getUserInformation` returns stored encrypted passwords — treat as sensitive.
3. Sleeping battery devices (commsStatus=Sleep) cannot take `sendRemoteCommand` — queue via `saveScriptCommand` instead.
4. `deleteZone` / `deleteSite` **cascade** to everything beneath them; `deleteDatalog`, `deleteDevice`, `deleteUser` are irreversible. Confirm with a human before calling any DESTRUCTIVE endpoint.
5. Save endpoints are upserts and require ALL body fields — omitting a field blanks it.
6. `saveTankTable` replaces the whole table; a partial payload silently deletes the missing rows.
7. Summarised data contains interpolated (synthetic) entries; check `interpolatedEntry` before treating a reading as measured.
8. All returned times are site-local except `getCommsRegister.lastSeenUTC`.
9. Values < 0.0005 appear as 0 due to 3-dp output rounding.
10. `batteryStatus` always reports the stored battery reading, regardless of power source. On mains-powered readings the value may be charging-influenced or stale — check `powerStatus` before drawing conclusions from it.
11. App 024 (GPS) datalog entries have no battery/power/interpolated fields; the endpoint does not support summarise for this app.
12. App 036 does not exist (dropped from the product line).
13. `sendEmail.isHTML` is a JSON Boolean — `"isHTML": true`. The legacy string form `"isHTML": "true"` is tolerated for backward compatibility, but any other type or value returns `400 "isHTML must be true or false"`. Field-level 400s name the offending field; malformed JSON returns `400 "Malformed JSON request body"`; a send failure at the SMTP relay returns 500.
