Hoppscotch
The Hoppscotch package
To try the API in the Hoppscotch desktop app, import the Hoppscotch package from the public repository's hoppscotch/ folder (or download it below): the collection with every operation, the authorization flows and the refusals, scripted with Hoppscotch's own hopp API; an environment each for the home network and the cloud; and a guide for the WebSocket in the Realtime tab, which imports nothing, with the ClientMessage messages to send. It holds the same requests as the Postman package and is generated with these pages, so it always matches the reference. On the home network, pin the gateway's CA for its address first (GET /v1/gateway/tls, TLS and CA pinning).
IGH home API in Hoppscotch
Version 1.1.0. Generated from the API's OpenAPI and AsyncAPI documents with the rest of https://developers.igh.app, from the same requests as the Postman package: nothing here is edited by hand.
| File | What |
|---|---|
| igh-api.hoppscotch.json | The collection (Hoppscotch collection format v12, requests v17): every operation, the authorization flows, errors, a read-only smoke run. |
| igh-local.hoppscotch_environment.json | Environment: IGH local (home network). |
| igh-staging-cloud.hoppscotch_environment.json | Environment: IGH staging cloud. |
| igh-cloud.hoppscotch_environment.json | Environment: IGH cloud. |
| WEBSOCKETS.md | The WebSocket in Hoppscotch's Realtime tab: the URLs, the messages to send, the events and close codes. |
Made for the Hoppscotch desktop app 26.9 (its data formats: collection v12, request v17, environment v2). The scripts use Hoppscotch's own hopp API, which runs in the experimental scripting sandbox: Settings → Experiments → Experimental scripting sandbox stays on (the default).
0. Install and start
Install the desktop app from https://hoppscotch.com/download (Linux: AppImage, .deb or .rpm) and start it from the applications menu, or with hoppscotch where the package puts that command on your PATH. Requests go through the desktop app's Native interceptor (Settings → Interceptor), the default: it reaches the home network and holds the CA of step 1. The web app at hoppscotch.io can't reach the gateway on the home network.
1. Trust the gateway's CA (local environment)
On the home network the gateway answers on port 8490 with HTTPS signed by its own CA. Pin that CA for the gateway's address only; never turn off Verify Host or Verify Peer.
- Get
ca.pem, either:- in the IGH app: Settings → Integrations → the API integration → Download CA (
igh-<serial>-ca.pem; it shows the SHA-256 fingerprint too); - or through the cloud: select a cloud environment, set
token(step 4), send TLS → Gateway CA (to pin): the response pane's Console printsca; save it asca.pem. Comparefingerprintwithopenssl x509 -in ca.pem -noout -fingerprint -sha256.
- in the IGH app: Settings → Integrations → the API integration → Download CA (
- Settings → Interceptor → Native → the gear button (Manage Domains Overrides) → add the gateway's address exactly as the requests use it, host and port:
igh-<serial>.local:8490(or192.168.1.20:8490when you use the LAN address), then select it. - For that domain: Verify Host and Verify Peer on; CA Certificates → Add Certificate File →
ca.pem, turned on. - Not under Global Defaults: a CA certificate there is used for every address (the native relay hands it to curl as the CA bundle, in place of the default one), so the cloud's certificates would no longer verify.
- Select the IGH local environment and set
serial(the gateway id, lower case). Whereigh-<serial>.localdoesn't resolve, put the gateway's LAN address inbaseUrl(https://192.168.1.20:8490/v1): the certificate names it too. - After the CA is regenerated in the app, replace the file.
2. Import
- Collections (left sidebar) → Import / Export → Import from Hoppscotch → Import from File →
igh-api.hoppscotch.json→ Import. - Environments → Import / Export → Import from Hoppscotch → select the three environment files.
- Pick an environment in the environment selector (top right).
- The WebSocket is not part of the import: see WEBSOCKETS.md.
3. The environments
Switching the environment is the only change between the home network and the cloud: every request uses <<baseUrl>> and <<token>>. The device flow, OAuth and MCP always go to a cloud (pairBase, recordUrl).
| File | Name | baseUrl | Notes |
|---|---|---|---|
| igh-local.hoppscotch_environment.json | IGH local (home network) | https://igh-<<serial>>.local:8490/v1 |
The gateway on the home network, port 8490, HTTPS with the gateway's own CA (pin it for this address: Settings → Interceptor → Native → Manage Domains Overrides, README step 1). serial is the gateway id in lower case. Without mDNS, put the gateway's LAN address in baseUrl instead (https://192.168.1.20:8490/v1): the certificate names the LAN addresses too. Pairing, MCP and OAuth go to the production cloud; for a gateway on staging change pairBase to https://api.staging.igh.app/v1 and mcpUrl to https://mcp.staging.igh.app. |
| igh-staging-cloud.hoppscotch_environment.json | IGH staging cloud | https://api.staging.igh.app/v1 |
The staging cloud's relay (api.staging.igh.app), MCP server (mcp.staging.igh.app) and OAuth server (cloud.staging.igh.app). The home's gateway must use the staging cloud, and the integration needs Cloud access on. |
| igh-cloud.hoppscotch_environment.json | IGH cloud | https://api.igh.app/v1 |
The production cloud's relay (api.igh.app), MCP server (mcp.igh.app) and OAuth server (cloud.igh.app). The integration needs Cloud access on. |
Every variable is in the environment, the ids and the scripts' state too: Hoppscotch's scripts write only environments, and a collection variable would outrank an environment's value.
| Variable | What |
|---|---|
transport |
local or cloud: requests marked (local only) or (cloud only) aren't sent in the other. |
baseUrl |
The API, ending in /v1. |
wsUrl |
The WebSocket /v1/stream. |
serial |
The gateway id, lower case (on the integration's page and in mDNS _igh-api._tcp). The local environment's addresses use it. |
recordUrl |
The integration's own MCP address https://mcp.igh.app/r/<id>, from its page in the app (Agent access on): the device flow, OAuth and MCP use it. |
pairBase |
The cloud API that runs the device flow (/v1/pair). |
mcpUrl |
The cloud's MCP server; an integration's own address (recordUrl) is <mcpUrl>/r/<id>. |
token (secret) |
The credential every request sends (Authorization: Bearer <<token>>). The Authorization folders set it. |
apiKey (secret) |
An API key to paste (folder (a)). |
oauthRefreshToken (secret) |
OAuth's refresh token: folder (d) request 5 sets it. |
revokedToken (secret) |
A revoked credential, for a negative case. |
otherRecordToken (secret) |
Another integration's credential, for a negative case. |
deviceName |
Its current name (for the rename request). |
unitRef |
deviceId's unit: the third part of the device id. |
dimmerId |
The first dimmer. |
shutterId |
The first shutter. |
acId |
The first air conditioner. |
irId |
The first IR remote. |
irKey |
A key the IR remote learned. |
lockId |
The first lock. |
gateId |
The first gate. |
roomName |
Its current name. |
createdSceneId |
The test scene. |
sceneRevision |
Its revision. |
automationRevision |
Its revision. |
scheduleRevision |
Its revision. |
intentValue |
Its value, as JSON. |
caFingerprint |
The gateway CA's SHA-256 fingerprint. |
tokenKind |
key, agent, session or oauth. |
pairCode |
The device flow's device code. |
pairUserCode |
Its matching code. |
pairInterval |
Seconds between polls. |
pairPolls |
Polls so far. |
mcpCode |
check_access's code. |
oauthResourceMetadata |
The protected-resource metadata URL. |
oauthIssuer |
The authorization server. |
oauthAuthUrl |
Set by the scripts. |
oauthTokenUrl |
Set by the scripts. |
oauthRegisterUrl |
Set by the scripts. |
oauthRevokeUrl |
Set by the scripts. |
oauthScope |
Set by the scripts. |
oauthClientId |
Hoppscotch's registered client id. |
oauthRedirectUri |
Where the browser lands after OAuth approval (a loopback address: nothing answers, the code is in the address bar). |
oauthAuthorizeUrl |
The authorization address request 4 makes, to open in a browser. |
oauthState |
Its state, checked when the code comes back. |
oauthVerifier (secret) |
Its PKCE code verifier. |
oauthCode (secret) |
The address the browser landed on (or only its code), pasted for request 5. |
deviceId, lightId, sceneId, automationId, scheduleId, roomId, intentId, sensorId |
An id, empty: Getting started → House and the list requests fill it (only while empty); set it to choose. |
Secret variables are masked and kept on this device only (Hoppscotch never syncs or exports their values). No environment holds a credential or an id: fill them in Hoppscotch.
4. Get a credential
Each folder under Authorization goes top to bottom and ends with token set.
| Use | Folder | Credential |
|---|---|---|
| Scripts, your own programs | (a) API key: an administrator creates it on the integration's page (Settings → Integrations → API integration → Create key); paste it into apiKey |
igh_key_… until deleted |
| Agent-like tools, terminals, anything without a browser | (b) Device flow: POST /v1/pair with recordUrl, the administrator compares the matching code and accepts in the IGH app, POST /v1/pair/token until approved |
igh_agent_… until revoked |
| The same, for one session | (c) Device flow, the administrator picks Accept for this session | igh_session_… (ends with the session) |
| Hosted platforms (Claude, ChatGPT) | (d) OAuth 2.1 with PKCE: discovery, dynamic registration, the authorization address in a browser, the code exchange | an access token of about an hour, and a refresh token |
| MCP clients | (e) MCP over JSON-RPC: request_access, then check_access until accepted |
igh_agent_… or igh_session_… |
(f) holds the refusals: no token, a bad or revoked one, Agent access or Cloud access off, another integration's credential on its address.
Polling (b, c, e). Hoppscotch can't send a request again from a script, so the poll is one request you send again until it answers with the credential; from the second poll on it first waits pairInterval seconds (and 5 s longer after each slow_down). Hands-free: the folder → Run Collection → select only the poll request, Iterations 120, Delay 0, Keep variable values on, Stop run if an error occurs on. Once the request is answered (approved, denied or expired) the next poll stops before it is sent, which ends the run.
OAuth (d). Hoppscotch's own OAuth 2.0 authorization (Authorization Code with PKCE) sends the browser back to the app's page: https://hoppscotch.io/oauth in the web app, which IGH accepts, but app://hoppscotch/oauth in the desktop app, which IGH refuses (redirects are https, or http on a loopback address). In the desktop app the folder does the same flow in requests: 4 makes the authorization address with a fresh PKCE verifier (S256) and prints it; open it in a browser and approve; the browser lands on http://127.0.0.1/igh-callback?code=… (nothing answers there); copy that whole address into the environment's oauthCode and send 5, which sets token and oauthRefreshToken. The folder's last request is the same flow with Hoppscotch's OAuth 2.0 authorization, for the web app.
5. Run
- Getting started → House fills the id variables (
deviceId,lightId,sceneId,roomId, …) with the house's first of each kind, only while they are empty: clear one to pick again, or set it to the device you want. - Smoke run (read-only) → Run Collection (with Stop run if an error occurs off): every read, nothing switched or created.
- The other folders top to bottom. Requests named [switches a device] operate real units. Scenes, Automations and Schedules create one test object each (a scene of one delay, a disabled automation, a paused schedule): the API never deletes, so remove them in the app.
- Errors and edge cases provokes each refusal; some need a setting of the integration changed first (the request says which).
Every request checks its status (its Variables tab holds what the collection's scripts check: igh.expect, igh.code, …). A refusal is checked for { error: { code, message } }, and the response pane's Console prints its appAction (the app screen that does what the API won't) and retryAfter. A request that can't work in the selected environment, or needs an empty variable, stops before it is sent and says why. Through the cloud, 50 requests a second and 600 a minute per home: past that, 429 with retryAfter.
Guides: https://developers.igh.app · Reference: https://developers.igh.app/reference.html · The same requests for Postman: https://developers.igh.app/postman.html
The WebSocket in Hoppscotch
/v1/stream carries requests as messages, subscriptions and pushed events (AsyncAPI: https://developers.igh.app/asyncapi.html).
Hoppscotch has no WebSocket requests to import: a collection holds HTTP and GraphQL requests only, and the Realtime → WebSocket tab keeps just one URL and its protocols, with no saved messages, no headers and no environment variables. So the WebSocket is set up by hand there, once, from this page.
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 |
The Realtime tab can't set an Authorization header (it uses the app window's own WebSocket), so the token goes in the query: ?token=<the token>. The gateway and the cloud both accept the query token on the upgrade (the cloud strips it before relaying). The tab doesn't fill <<…>> variables either: paste the token itself, copied from the environment (the eye button shows a secret's value).
Connect
- Realtime (left bar) → WebSocket.
- URL: the environment's
wsUrlwith the token, for examplewss://api.igh.app/v1/stream?token=igh_agent_…. Leave Protocols empty. - Connect. The log shows the connection, then every frame.
- In the message box, pick JSON, paste a message below, replace each
<<…>>with its value (the Realtime tab sends the text as it is), Send.
On the home network (wss://igh-<serial>.local:8490/v1/stream) the WebSocket is opened by the app window, not by the Native interceptor, so the CA certificate of README step 1 doesn't apply to it: it trusts the system's CAs. Either add the gateway's CA to the system's trust (Fedora: sudo trust anchor ca.pem; every program on the computer then trusts it), or use a cloud URL. Never accept an untrusted certificate instead.
The messages to send
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 } }).
{
"id": "subscribe-state",
"subscribe": [
"state"
]
}
Subscribe: sensors
Only sensor events from now on (answered { id, status: 200, body: { subscribed } }).
{
"id": "subscribe-sensor",
"subscribe": [
"sensor"
]
}
Subscribe: scenes
Only scene events from now on (answered { id, status: 200, body: { subscribed } }).
{
"id": "subscribe-scene",
"subscribe": [
"scene"
]
}
Subscribe: exposure
Only exposure events from now on (answered { id, status: 200, body: { subscribed } }).
{
"id": "subscribe-exposure",
"subscribe": [
"exposure"
]
}
Subscribe: everything
All four topics (what a new socket gets until it subscribes).
{
"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.
{
"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.
{
"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.
{
"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.
{
"id": "keep-alive",
"method": "GET",
"path": "/v1/about"
}
What arrives
state
A unit's or gang's new state.
{
"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.
{
"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).
{
"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.
{
"event": "exposure",
"house": "Cohen Home",
"at": 1760780000000
}
answer
The answer to a request message.
{
"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.
{
"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.
{
"error": {
"code": "agent_access_disabled",
"message": "Agent access is off for this API integration.",
"appAction": "integrations.record"
}
}
Errors and close codes
- A refused upgrade (401 without or with a bad credential, 403, 429 past 20 sockets per home through the cloud, 503
gateway_offline) shows in the log as a failed connection: the Realtime tab doesn't show the HTTP answer. SendGET /v1/integrationfrom the collection with the same token to see why. - A message that is refused (bad JSON shape, 413 over 16 KiB, 429
rate_limitedwithretryAfterthrough 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 4400invalid_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: 4401unauthorizedorsession_expired, 4403agent_access_disabled,cloud_access_disabledorrecord_disabled, 4404, 4429, and 4503gateway_offlinewhen 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.
IGH home API 1.1.0. Generated from the doc source; do not edit.