# The WebSocket in Postman

`/v1/stream` carries requests as messages, subscriptions and pushed events (AsyncAPI: https://developers.igh.app/asyncapi.html).

Postman sends WebSocket requests, but its Collection v2.1 file format (the collection `.json`) can't hold them: that is a limit of the file format, not of Postman. Postman v12's Collection Format v3 can, so the WebSocket requests come as a v3 collection beside it; the same requests can be saved by hand in any version.

## The URLs

| Environment | `wsUrl` |
| --- | --- |
| IGH local (home network) | `wss://igh-{{serial}}.local:8490/v1/stream` |
| IGH staging cloud | `wss://api.staging.igh.app/v1/stream` |
| IGH cloud | `wss://api.igh.app/v1/stream` |

Authenticate the upgrade with `Authorization: Bearer {{token}}`, or `?token={{token}}` when a client can't set headers: the gateway and the cloud both accept the query token on the upgrade (the cloud strips it before relaying; on HTTPS requests only the header counts). On the home network the CA pinned in README step 1 applies; keep SSL verification on.

## Option A: the v3 collection (Postman v12)

[collections/IGH WebSocket/](collections/IGH%20WebSocket/) is a Collection Format v3 collection: one YAML file per request (`Stream (Authorization header)`, `Stream (token in the query)`), each with every message below saved. Bring it in with **Import** → drag the `IGH WebSocket` folder, or open a folder holding `postman/collections/IGH WebSocket` in Local View (Native Git). Select an IGH environment, Connect, pick a saved message, Send.

## Option B: by hand (any Postman version)

1. **New → WebSocket**. URL: `{{wsUrl}}`.
2. **Headers**: `Authorization` = `Bearer {{token}}` (or the URL `{{wsUrl}}?token={{token}}`).
3. **Save** it into a collection of its own, "IGH WebSocket" (a WebSocket request in the HTTP collection makes it a multi-protocol collection, with fewer features).
4. For each message below: paste it into the message box, set the type to JSON, **Save message** with its name. Variables (`{{lightId}}`) are replaced when sent.
5. **Connect**, then send a saved message.

## The messages to save

A request message `{ id, method, path, query?, body? }` (`path` with `/v1`, as on HTTPS) is answered `{ id, status, body }`; `{ id, subscribe: [...] }` chooses the topics (`state`, `sensor`, `scene`, `exposure`; all four until a client subscribes).

### Subscribe: state

Only `state` events from now on (answered `{ id, status: 200, body: { subscribed } }`).

```json
{
  "id": "subscribe-state",
  "subscribe": [
    "state"
  ]
}
```

### Subscribe: sensors

Only `sensor` events from now on (answered `{ id, status: 200, body: { subscribed } }`).

```json
{
  "id": "subscribe-sensor",
  "subscribe": [
    "sensor"
  ]
}
```

### Subscribe: scenes

Only `scene` events from now on (answered `{ id, status: 200, body: { subscribed } }`).

```json
{
  "id": "subscribe-scene",
  "subscribe": [
    "scene"
  ]
}
```

### Subscribe: exposure

Only `exposure` events from now on (answered `{ id, status: 200, body: { subscribed } }`).

```json
{
  "id": "subscribe-exposure",
  "subscribe": [
    "exposure"
  ]
}
```

### Subscribe: everything

All four topics (what a new socket gets until it subscribes).

```json
{
  "id": "subscribe-all",
  "subscribe": [
    "state",
    "sensor",
    "scene",
    "exposure"
  ]
}
```

### Request: state now

`GET /v1/state` over the socket: read it once after (re)connecting to catch up.

```json
{
  "id": "state",
  "method": "GET",
  "path": "/v1/state"
}
```

### Command: light on [switches a device]

A request over the socket, answered `{ id, status, body }` as over HTTPS. **Switches a real device.**

```json
{
  "id": "light-on",
  "method": "POST",
  "path": "/v1/devices/{{lightId}}/control",
  "body": {
    "op": "set_power",
    "args": {
      "on": true
    }
  }
}
```

### Command: light off [switches a device]

A request over the socket, answered `{ id, status, body }` as over HTTPS. **Switches a real device.**

```json
{
  "id": "light-off",
  "method": "POST",
  "path": "/v1/devices/{{lightId}}/control",
  "body": {
    "op": "set_power",
    "args": {
      "on": false
    }
  }
}
```

### Keep-alive (no ping message: a cheap request)

The stream has no ping message, and a text that is not JSON (`ping`) closes it (4400 `invalid_request`). WebSocket ping frames are answered by the server; to see traffic, send this cheap request.

```json
{
  "id": "keep-alive",
  "method": "GET",
  "path": "/v1/about"
}
```

## What arrives

### state

A unit's or gang's new state.

```json
{
  "event": "state",
  "house": "Cohen Home",
  "device": "8F3K2M9Q4T7W:5b0e6c1a-2f4d-4c3b-9a8e-7d6f5e4c3b2a:u-kitchen-light",
  "class": "light_dimmer",
  "state": {
    "on": true,
    "bri": 40
  },
  "at": 1760780000000
}
```

### sensor

A sensor reading or detection.

```json
{
  "event": "sensor",
  "house": "Cohen Home",
  "device": "8F3K2M9Q4T7W:5b0e6c1a-2f4d-4c3b-9a8e-7d6f5e4c3b2a:u-hall-sensor",
  "class": "sensor",
  "state": {
    "motion": true,
    "lux": 120
  },
  "at": 1760780000000
}
```

### scene

A scene runs (`running` true, then false).

```json
{
  "event": "scene",
  "house": "Cohen Home",
  "device": "8F3K2M9Q4T7W:5b0e6c1a-2f4d-4c3b-9a8e-7d6f5e4c3b2a:scene:s-evening",
  "class": "scene",
  "state": {
    "running": true
  },
  "at": 1760780000000
}
```

### exposure

What the integration shows changed: read `GET /v1/house` again.

```json
{
  "event": "exposure",
  "house": "Cohen Home",
  "at": 1760780000000
}
```

### answer

The answer to a request message.

```json
{
  "id": "light-on",
  "status": 200,
  "body": {
    "house": "Cohen Home",
    "device": "8F3K2M9Q4T7W:5b0e6c1a-2f4d-4c3b-9a8e-7d6f5e4c3b2a:u-kitchen-light",
    "state": {
      "on": true,
      "bri": 40
    }
  }
}
```

### error (rate limited, the socket stays open)

Through the cloud, a message past the limit.

```json
{
  "error": {
    "code": "rate_limited",
    "message": "Too many requests for this home. Wait and try again.",
    "retryAfter": 1
  }
}
```

### error (before a close)

A credential that stopped passing: this frame, then close 4403 `agent_access_disabled`.

```json
{
  "error": {
    "code": "agent_access_disabled",
    "message": "Agent access is off for this API integration.",
    "appAction": "integrations.record"
  }
}
```

## Errors and close codes

- A refused upgrade is the HTTP status with the error body (401 without or with a bad credential, 403, 429 past 20 sockets per home through the cloud, 503 `gateway_offline`). Postman shows it as the handshake's response.
- A message that is refused (bad JSON shape, 413 over 16 KiB, 429 `rate_limited` with `retryAfter` through the cloud) is answered with an error frame or `{ id, status, body }`; the socket stays open.
- A text that is not JSON (`ping`) closes the socket with 4400 `invalid_request`.
- A credential that stops passing (revoked, Agent access or Cloud access turned off, the integration disabled) gets an `{ error }` frame, then the close code **4000 + the HTTP status** with the error code as the reason: 4401 `unauthorized` or `session_expired`, 4403 `agent_access_disabled`, `cloud_access_disabled` or `record_disabled`, 4404, 4429, and 4503 `gateway_offline` when the gateway leaves the cloud.
- On AWS (api.igh.app, api.staging.igh.app) the close carries no code: the error frame is the only signal.
- Reconnect with a backoff, then send "Request: state now" to catch up.
