# domIO — XNet API v1

**Deployment status (28 September 2026):** The domIO catalog schema is present in
the Hetz `00_x` database. Listener `1.3.1+domio-delete` is running on VPS06962;
its DLL SHA-256 matches the local build. `domio.home.list`, `domio.home.save`,
`domio.home.structure.get` and `domio.home.delete` were called successfully on
the production endpoint using a temporary authenticated XNet user. The four
delete operations are in the deployed code; only `home.delete` has been
exercised through the production API. The physical device provider is not
configured, so `on`/`off` cannot be confirmed.

Production endpoint: `POST https://vps.xnet.mk:50443/api/commands`

Exact client identifier: `client.appId: "domIO"`

Allowed operations: `user.register`, `user.login`, `user.me`, and the `domio.*`
operations below. Other operations are rejected with HTTP 400 and
`error.code: "client-operation-not-configured"`.

Every request is UTF-8 JSON and uses a fresh `messageId`, a non-empty
`clientSessionId`, a non-empty conversation ID, an increasing positive `turn`, UTC
timestamps and a short future `expiresAtUtc`. domIO does not receive access or refresh
tokens. Protected calls send and verify username/password again.

## user.register

`authentication.username/password` must exactly match `data.username/password`.

```json
{
  "schemaVersion": 1,
  "messageId": "11111111-1111-4111-8111-111111111111",
  "authentication": {
    "username": "example_user",
    "password": "<user-password>"
  },
  "client": {
    "appId": "domIO",
    "appVersion": "1.0.0",
    "clientSessionId": "22222222-2222-4222-8222-222222222222"
  },
  "conversation": {
    "id": "33333333-3333-4333-8333-333333333333",
    "turn": 1
  },
  "operation": "user.register",
  "sentAtUtc": "2026-09-26T10:00:00Z",
  "expiresAtUtc": "2026-09-26T10:05:00Z",
  "data": {
    "name": "Example",
    "surname": "User",
    "username": "example_user",
    "password": "<user-password>",
    "pid": null,
    "address": null,
    "city": null,
    "country": null,
    "phone": null,
    "email": "example_user@example.invalid",
    "languageId": 1,
    "birthday": null,
    "primeRegistrant": null,
    "profilePicture": null
  }
}
```

Required fields: `username`, `password`, `email`. Optional fields: `name`, `surname`,
`pid`, `address`, `city`, `country`, `phone`, `languageId`, `birthday`,
`primeRegistrant`, `profilePicture`.

Successful response, HTTP 201:

```json
{
  "schemaVersion": 1,
  "replyToMessageId": "11111111-1111-4111-8111-111111111111",
  "operation": "user.register",
  "status": "completed",
  "data": {
    "userId": 12345,
    "username": "example_user",
    "email": "example_user@example.invalid",
    "confirmed": true,
    "emailDelivery": "not-required"
  },
  "error": null
}
```

## user.login

Use the v1 envelope above, set `operation` to `user.login`, send the registered
credentials in `authentication`, and send an empty object in `data`.

```json
"operation": "user.login",
"data": {}
```

Successful response, HTTP 200:

```json
{
  "operation": "user.login",
  "status": "completed",
  "data": {
    "userId": 12345,
    "username": "example_user",
    "email": "example_user@example.invalid",
    "profilePicture": null
  },
  "error": null
}
```

## user.me

Use the same verified credentials, set `operation` to `user.me`, and send `data: {}`.
The successful HTTP 200 response contains the safe current-user profile, including
`userId`, `username`, `email`, optional name/contact fields, `languageId`, `birthday`,
`confirmed`, `profilePicture`, and `isAdmin`. The password is never returned.

```json
{
  "operation": "user.me",
  "status": "completed",
  "data": {
    "userId": 12345,
    "username": "example_user",
    "email": "example_user@example.invalid",
    "name": "Example",
    "surname": "User",
    "languageId": 1,
    "confirmed": true,
    "profilePicture": null,
    "isAdmin": false
  },
  "error": null
}
```

The examples contain placeholders and reserved example data. Do not log passwords or
profile pictures. Do not automatically retry `user.register` after an uncertain network
outcome; first verify whether the username/email was created.

During the 28 September production checks, one later test registration returned
HTTP 503 `cache-identity-conflict` after the remote registration step. Its local
cache update was rolled back. This needs identity reconciliation before another
registration attempt with that username; a client must not retry it automatically.

## Homes, members, floors, rooms and devices

The authenticated XNet user is taken from `authentication.username/password`; a
client-supplied user ID never grants access. Home owners may edit the home and set
members. `admin` members may edit floors, rooms and device mappings. `member` users
may read the home and request device commands. Users outside the home receive 404.

All calls use the v1 envelope shown above with a new `messageId`, the same exact
`client.appId: "domIO"`, and verified credentials in every request.

| Operation | `data` | Result |
| --- | --- | --- |
| `domio.home.list` | `{ "limit": 50, "afterId": 0 }` (`limit` 1–100) | `items` containing accessible homes and the caller's role; `nextCursor` for the next page. |
| `domio.home.structure.get` | `{ "homeId": 12 }` | One response with home, floors, nested rooms and devices, including last confirmed state. |
| `domio.home.save` | `{ "id": null, "name": "Home", "address": "...", "city": "...", "latitude": null, "longitude": null }` | `homeId`, `created`; omit/null `id` to create, supply ID to update as owner. |
| `domio.home.delete` | `{ "homeId": 12 }` | Owner only; returns `homeId`, `deleted: true`. |
| `domio.home.members` | `{ "homeId": 12 }` | Owner and members with XNet user IDs, usernames and roles; owner/admin only. |
| `domio.home.member.set` | `{ "homeId": 12, "userId": 34, "role": "member" }` | `homeId`, `userId`, `role`; owner only. Roles: `admin`, `member`, `remove`. |
| `domio.floor.save` | `{ "id": null, "homeId": 12, "level": 0, "name": "Ground", "widthM": 10, "heightM": 8 }` | `floorId`, `created`. |
| `domio.floor.delete` | `{ "floorId": 3 }` | Owner/admin; returns `floorId`, `deleted: true`. |
| `domio.room.save` | `{ "id": null, "floorId": 3, "name": "Living", "widthM": 5, "heightM": 4, "positionX": 0, "positionY": 0 }` | `roomId`, `created`. |
| `domio.room.delete` | `{ "roomId": 9 }` | Owner/admin; returns `roomId`, `deleted: true`. |
| `domio.device.save` | `{ "id": null, "roomId": 9, "name": "Main light", "type": "light", "provider": "xnet", "externalDeviceId": "real-id", "positionX": 2, "positionY": 1, "isEnabled": true }` | `deviceId`, `created`. `provider` and external ID together identify the real device. |
| `domio.device.delete` | `{ "deviceId": 81 }` | Owner/admin; returns `deviceId`, `deleted: true`. |
| `domio.device.state.get` | `{ "deviceId": 81 }` | Last confirmed `state` (`on`, `off` or null), `isOnline` (true, false or null), `changedAtUtc` and `updatedAtUtc`. |
| `domio.device.command` | `{ "deviceId": 81, "command": "on", "requestId": "6f56604c-3308-4e10-a02d-3a6ef7ef74f0" }` | Only `light`/`switch` accept `on`/`off`. A successful reply must carry the provider-confirmed new state. |

The layout read groups the selected home's floors, rooms, devices and cached
states in one SQL read. The client can poll `domio.device.state.get`; there is no
domIO WebSocket/events feed. The stored state is unknown until a physical system
reports it, and it must never be changed merely because a command was requested.

Deleting a home also deletes its members, floors, rooms, devices and cached
states. Deleting a floor or room deletes its descendants; deleting a device
deletes its cached state. If any affected device has command history, the
whole delete returns HTTP 409 `device-command-history` and changes nothing.
A missing item or one outside the caller's permissions returns HTTP 404
`domio-item-not-found`. A client must not automatically retry a delete after
an uncertain network outcome; read the home structure first.

The user selects a physical device by entering the exact provider name and
`externalDeviceId` supplied by that provider's inventory. Their pair is unique
in the domIO catalog. The ID is not the domIO database device ID. An owner or
admin may save the mapping. There is currently no XNet discovery source or
provider integration available to verify or enumerate physical devices, so
there is no `domio.device.available` operation. Until the provider is connected,
the saved mapping is unverified and its state/online status remain unknown.

Production probe response data from 28 September 2026. The temporary home was
deleted after the read, so these IDs are historical examples:

```json
{ "items": [], "nextCursor": null }
```

`domio.home.save` (HTTP 200):

```json
{ "homeId": 1, "created": true }
```

`domio.home.structure.get` (HTTP 200; relevant response fields):

```json
{ "id": 1, "role": "owner", "floors": [], "stateRefresh": "polling" }
```

`domio.home.delete` (HTTP 200):

```json
{ "homeId": 1, "deleted": true }
```

Example layout data for a populated home (illustrative, not a production probe):

```json
{
  "id": 12,
  "name": "Home",
  "address": "Example address",
  "city": "Skopje",
  "latitude": null,
  "longitude": null,
  "role": "owner",
  "floors": [
    {
      "id": 3,
      "level": 0,
      "name": "Ground",
      "widthM": 10,
      "heightM": 8,
      "rooms": [
        {
          "id": 9,
          "name": "Living",
          "widthM": 5,
          "heightM": 4,
          "positionX": 0,
          "positionY": 0,
          "devices": [
            {
              "id": 81,
              "name": "Main light",
              "type": "light",
              "isEnabled": true,
              "positionX": 2,
              "positionY": 1,
              "state": null,
              "isOnline": null,
              "changedAtUtc": null
            }
          ]
        }
      ]
    }
  ],
  "stateRefresh": "polling"
}
```

**Physical device integration:** The XNet automation endpoint/protocol and its
acknowledgement format still need to be configured. Until then `domio.device.command`
returns HTTP 503 `device-provider-not-configured` without claiming success or
changing `device_states`. An explicitly offline device returns 409
`device-offline`; unknown device/access returns 404; malformed command returns 422.
No write command is automatically retried after an uncertain network outcome.
