# Acquanet App API: guide for AI agents

This guide comes with `app_api.openapi.yaml`, the OpenAPI 3.0 spec generated by Scribe. You
receive both files together, in the same folder. Read the spec alongside this guide.
**The spec is the source of truth for paths, parameters and response shapes.** This file
covers what the spec doesn't: what the data means, how the endpoints fit together, and
the known quirks you have to handle.

## What the API is about

Acquanet monitors water installations, such as showers at swimming pools. The domain
model:

- **Client**: an organisation or location, e.g. "Meridian Swimming Pool". Clients form a
  tree, so a client can have child clients.
- **Control box** (called a **master** in responses): a device installed at a client. It
  runs *cycles*, flushing the water outlets with hot water, and logs the temperatures.
- **Slave**: a water outlet connected to a control box. Its `type` is one of `generic`,
  `shower`, `group shower`, `faucet` or `urinal`.
- **Cycle**: one flushing run of a control box. A *hot cycle* (`is_mix_cycle: false`)
  must get every outlet up to the required temperature to count as successful. A *mixed cycle* (`is_mix_cycle: true`) uses cold or warm water.

## Connecting

- Base URL: use one of these hosts:
  - `https://acqua-net.online`
  - `https://acs2.acqua-net.online`
  - `https://rotterdam.acqua-net.online`

  Use the host you were told to use. The `servers[0].url` in `app_api.openapi.yaml`
  (`http://acquanet.test`) is a local development address; don't use it. All paths start with
  `/app_api/`.
- Authentication: send the API key in the `X-API-Key` header on every request. Keys are
  per user. A superuser creates them under *Admin → API keys*. Every request runs as the
  user the key belongs to, and only returns data that user may see.
- Always send `Accept: application/json`. Without it, some errors (such as an unknown
  client ID) come back as an HTML page instead of JSON.
- All endpoints are read-only `GET` requests.
- Rate limit: none is configured for `/app_api` at the moment, but don't send requests
  in tight loops.

```bash
curl -H "X-API-Key: $ACQUANET_API_KEY" -H "Accept: application/json" \
     "https://{$BASE_URL}/app_api/me"
```

## Endpoints

| Endpoint | Use it to |
|---|---|
| `GET /app_api/me` | Check that the key works and find out which user and `client_id` it belongs to. |
| `GET /app_api/clients` | List every client the user can see that has at least one control box, with its control boxes and outlets. |
| `GET /app_api/clients/{client}/details` | Get one client with its control boxes and outlets. |
| `GET /app_api/controlboxes/{controlbox}/cycles?sincedate=Y-m-d` | Get the cycles a control box logged since a date, with the result per outlet. |

### Typical flow

1. Call `GET /app_api/me`. A `401` means the key is wrong or revoked; stop and report it.
   Otherwise note `client_id`.
2. Call `GET /app_api/clients` for an overview of every location and its control boxes.
3. Call `GET /app_api/clients/{client_id}/details` for one client. Use the `client_id`
   from step 1 for the user's own client.
4. Call `GET /app_api/controlboxes/{id}/cycles?sincedate=...` to check whether cycles
   succeeded.

## Reading the data

- **Times**: fields called `*_date`, `last_contact` and `last_syncs.*` are ISO 8601 UTC,
  e.g. `2026-10-03T06:38:45.000000Z`. The exception is `shower_data[].start_date` and
  `end_date`: these are passed on unchanged from the control box's log (e.g.
  `2026-10-03 07:30:00 UTC`).
- **Temperatures** are in °C. **Water usage** is in liters.
- **Whether a cycle succeeded**: `successful` is `true` only if every outlet reached the
  required temperature. Use `cycle_conclusions` to find which outlets failed.
- **`shower_data`** contains statistics calculated from the raw log, one entry per
  outlet. Each `seconds_over_N` field counts the measurements above N °C. The log has
  about one measurement per second.
- **Names** in `cycle_conclusions[].name` and `cycle_water_usage[*].name` are the outlet
  aliases, such as "Shower 1". The names in `shower_data[].name` are the control box's
  internal names, such as "Shower0", so you can't match the two lists by name. Use their
  order instead.
- **`last_syncs`** says when data of each type last arrived from the control box. A
  value of `null` means that type has never arrived. If `last_contact` is old, the
  control box is probably offline.

## Known quirks and limitations

These are how the API behaves right now. Handle them; don't treat them as errors on
your side.

1. **The responses don't contain any IDs.** Neither `/clients` nor `/details` returns a
   client ID or a control box ID. You can only get the user's own `client_id`, from
   `/me`. For other clients and for control boxes, the user has to give you the ID.
2. **`/clients` ignores pagination.** `page` and `limit` are validated and returned in
   the response, but every client is always returned. `total` is the real number of
   clients. Don't loop over pages.
3. **`online` is always `true`**, on both clients and control boxes. Don't use it. Look
   at `last_contact` instead.
4. **`ip` is always an empty string.**
5. **`last_syncs.eventlogs` is the same as `last_syncs.hot_cycle`** (it shows the last
   hot cycle log, not the last event log).
6. **`sincedate` also depends on the time of day.** The API returns cycles logged after
   *`sincedate` at the current time of day*, not after midnight. To be sure you get
   everything from a given day, ask for the day before and filter on `start_date`
   yourself.
7. **An unknown control box ID returns `500`**, not `404`. A control box the user can't
   access returns `404`. An unknown client ID returns `404`.
8. **`cycle_water_usage` is an object, not an array.** Its keys are internal names that
   start with a space (e.g. `" Shower0-1"`). Use the values and ignore the keys.
9. **`shower_data[].highest_temp` is a number** in practice, even though the example in
   the spec shows a string.
10. **`/me` returns the full user record.** Only use the fields documented in the spec.
    The other fields are internal and can change.

## Errors

| Status | Meaning | What to do |
|---|---|---|
| `401` `{"message":"Unauthorized"}` | The key is missing, wrong or revoked. | Stop and ask for a valid key. Don't retry. |
| `404` `{"message":"Not Found"}` | The resource doesn't exist, or the user can't access it. | Check the ID. The API doesn't say which of the two it is. |
| `422` `{"message":..., "errors":{field:[...]}}` | A parameter is invalid, e.g. `sincedate` isn't in `Y-m-d` format. | Fix the parameter using `errors`. |
| `500` | Server error, or an unknown control box ID (see quirk 7). | Check the ID first. If the ID is right, try again once, then report the error. |
