REST API
The REST API lets your own software read your locations, run commands and manage schedules. It's the same set of routes the processor's own pages use, opened to API keys with the permissions you choose. The processor has no Swagger page, so this page is the reference.
Before you start
- The processor needs the API Licence and a Manager Licence or a User App Licence.
- Make an API key. The routes a key can use depend on its permissions.
- Send requests to the processor's admin HTTPS port,
https://<processor address>:8443unless you changed it. Keys aren't taken on any other port. - Trust the processor's certificate on the computer that sends the requests. Download it from Settings → Deployment → Downloads, or from
http://<processor address>:8080/box-ca.crt. The examples use--cacert box-ca.crtfor it. For other ways to trust it, see Certificate.
Sign in with a key
Send the key in the X-Api-Key header on every request:
curl --cacert box-ca.crt \
-H "X-Api-Key: $CENTRO_API_KEY" \
https://192.168.10.22:8443/api/locations
Don't put the key in the address. The processor reads it only from the header, so a key in the address is refused with 401. Never send a key over plain HTTP: the processor refuses it with 400 and doesn't redirect, because the key would already have been sent in the clear.
Responses are JSON. Names are in camel case and values such as capabilities are written as words. Times are UTC.
The routes
A key needs the permission shown. Where two are shown, either is enough. The permissions are the ones you tick on the API keys page.
Locations
| Method and route | What it does | Permission |
|---|---|---|
GET /api/locations | Every location. | See locations and what is happening in them |
GET /api/locations/{locationId} | One location. | Same |
GET /api/dashboardLayout/{locationId}/grms | The location's current state. | Same |
GET /api/dashboardLayout/{locationId}/grms/notifications | The location's live notifications. | Same |
GET /api/dashboardLayout/{locationId}/grms/connectivity | Whether the location's connections are online, with their details. | Same |
GET /api/LocationsCommands | What every location can be commanded on, and which connection carries each capability. | See schedules, and what each location can be sent |
Commands
| Method and route | What it does | Permission |
|---|---|---|
POST /api/LocationsCommands/{locationId}/grms | Runs one command in one location. | Send commands to locations |
POST /api/LocationsCommands/grms/execute | Runs one command in several locations. | Send commands to locations |
POST /api/LocationsCommands/grms/commands/status | Where queued commands have got to. It only reads, although it's a POST. | See locations and what is happening in them or See whether commands were delivered |
Schedules
| Method and route | What it does | Permission |
|---|---|---|
GET /api/CommandScheduler | Every schedule. Add ?tags=a&tags=b for the ones with one of those tags. | See schedules, and what each location can be sent |
GET /api/CommandScheduler/{id} | One schedule. | Same |
GET /api/CommandScheduler/{id}/runs | What each command did at the schedule's latest runs, location by location. | Same |
GET /api/CommandScheduler/{id}/fire-history | When it last ran, and the runs it missed. | Same |
POST /api/CommandScheduler | Makes a schedule. | Create schedules |
PUT /api/CommandScheduler/{id} | Replaces a schedule. | Change schedules |
PATCH /api/CommandScheduler/{id} | Changes some fields of a schedule. | Change schedules |
DELETE /api/CommandScheduler/{id} | Deletes a schedule. | Delete schedules |
Schedules also need a Manager Licence. With a User App Licence alone every schedule route is refused with 423. See Licences and limits.
Connections
| Method and route | What it does | Permission |
|---|---|---|
GET /api/box/system/connectors | Every connection as the System page lists it, worst first, with the processors they run on. | See the status of connections |
A key that has See locations and what is happening in them but not See the status of connections still reads locations. Each location's connectivity.connectors is then null: the key sees whether a location is online and how many connections serve it, but not their details.
What isn't there
Everything else the processor's pages do — settings, the licence, upgrades, backup and restore, certificates, Administrators and API keys — answers a key with 401, as it would answer a stranger. A key can't make another key. See Licences and limits.
Examples
The answers below were taken from a processor with one location, "Room 101", and its connection online, and are trimmed where they say so. The state example shows the shape of the answer: its values are illustrative.
List locations
curl --cacert box-ca.crt -H "X-Api-Key: $CENTRO_API_KEY" \
https://192.168.10.22:8443/api/locations
[
{
"id": "823c7cc2-7a3b-4131-8524-ea452b6b815d",
"name": "Room 101",
"supportsGrms": true,
"tags": ["suite"],
"group": "Floor 1",
"reference": null,
"grmsCapabilities": ["Climate", "Lighting"],
"connectivity": {
"online": true,
"lastSeenUtc": "2026-10-15T09:29:33.338053Z",
"connectorCount": 1,
"onlineConnectorCount": 1,
"connectors": [
{
"id": "ac32d061-a436-4974-9c3d-0389395573a9",
"name": "Room 101 driver",
"uiType": "CrestronHome",
"clientVersion": "1.0.0",
"ipAddress": "192.168.10.41",
"macAddress": "00:1C:01:01:A1:01",
"lastSeenUtc": "2026-10-15T09:29:33.338053Z",
"online": true,
"hardwareType": "CrestronHome",
"hardwareIdentifier": "CP4-0001",
"kind": "connector"
}
]
},
"capabilities": [
{ "name": "Climate", "reported": true, "shown": true },
{ "name": "Lighting", "reported": true, "shown": true }
]
}
]
The answer is trimmed: each location also lists its User App settings, the commands still waiting for a connection and whether it takes part in schedules for all locations. Some field names, such as connectorCount, use the processor's older word for a connection.
Read a location's state
curl --cacert box-ca.crt -H "X-Api-Key: $CENTRO_API_KEY" \
https://192.168.10.22:8443/api/dashboardLayout/823c7cc2-7a3b-4131-8524-ea452b6b815d/grms
{
"occupancy": null,
"service": null,
"scene": null,
"climate": {
"currentTemp": { "value": 22.5, "lastUpdatedUtc": "2026-10-15T09:30:00Z", "source": "Device", "health": "Synced" },
"targetTemp": { "value": 21, "lastUpdatedUtc": "2026-10-15T09:29:10Z", "source": "Command", "health": "Synced" },
"mode": { "value": "Heat", "lastUpdatedUtc": "2026-10-15T09:29:10Z", "source": "Command", "health": "Synced" },
"fanSpeed": null
},
"lighting": null,
"shades": null,
"door": null,
"media": null,
"currentStay": null
}
A section is null until a connection has reported it. Every value says where it came from (source) and whether it's current (health: Synced, Pending, Stale, Offline or Unknown). The example shows only the climate section. Which sections a location has depends on what its connections report.
Send a command
curl --cacert box-ca.crt -X POST \
-H "X-Api-Key: $CENTRO_API_KEY" -H "Content-Type: application/json" \
https://192.168.10.22:8443/api/LocationsCommands/823c7cc2-7a3b-4131-8524-ea452b6b815d/grms \
-d '{
"capability": "Climate",
"action": "SetState",
"climate": { "targetTemp": { "value": 21 }, "mode": { "value": "Heat" } }
}'
{
"accepted": true,
"message": "Queued for 1 connection(s).",
"capability": "Climate",
"timestampUtc": "2026-10-15T09:42:12.281977Z",
"dispatched": false,
"commandIds": ["3ec2d19f-7299-4d52-9055-f87e8394c8cd"]
}
capability is Climate, Lighting, Media, Scene, Shades, Door, Occupancy, Housekeeping or Stay. Put the values in the section named for the capability: climate, lighting, housekeeping, scene or shades. Each value is written as { "value": … }. GET /api/LocationsCommands says what each location can do.
For climate, mode is Off, Heat, Cool, Auto, Fan or Dry, and fanSpeed is Auto, Low, Medium or High.
To send one command to several locations, post to /api/LocationsCommands/grms/execute with the same fields and a locationIds list. The answer lists, for each location, whether the command was queued, skipped or not found.
accepted: true means the processor has queued the command for the location's connection. It hasn't reached the location yet. A location answers 400 with "Location not found." for an id the processor doesn't have.
Check where a command has got to
curl --cacert box-ca.crt -X POST \
-H "X-Api-Key: $CENTRO_API_KEY" -H "Content-Type: application/json" \
https://192.168.10.22:8443/api/LocationsCommands/grms/commands/status \
-d '{ "commandIds": ["3ec2d19f-7299-4d52-9055-f87e8394c8cd"] }'
[
{
"commandId": "3ec2d19f-7299-4d52-9055-f87e8394c8cd",
"locationId": "823c7cc2-7a3b-4131-8524-ea452b6b815d",
"connectorId": "ac32d061-a436-4974-9c3d-0389395573a9",
"capability": "Climate",
"status": "Pending",
"detail": null,
"issuedUtc": "2026-10-15T09:42:12.281977",
"deliveredUtc": null,
"resolvedUtc": null
}
]
status is Pending, Delivered, Acked or Failed. You can ask about up to 2,000 commands at a time.
Make a schedule
curl --cacert box-ca.crt -X POST \
-H "X-Api-Key: $CENTRO_API_KEY" -H "Content-Type: application/json" \
https://192.168.10.22:8443/api/CommandScheduler \
-d '{
"name": "Morning warm-up",
"enabled": true,
"repeat": "Daily",
"time": "06:30",
"commands": [
{
"type": "GrmsCommand",
"locationId": "823c7cc2-7a3b-4131-8524-ea452b6b815d",
"capability": "Climate",
"action": "SetState",
"commandId": "grms:823c7cc2-7a3b-4131-8524-ea452b6b815d:Climate",
"grmsState": { "climate": { "temperatureSetpoint": 21.5, "mode": "Heat" } }
}
]
}'
The answer is the schedule as it was made, with the id to use later and nextFireUtc in UTC:
{
"id": "f1449daf-a922-4b49-a03b-68949d82b602",
"name": "Morning warm-up",
"enabled": true,
"time": "06:30",
"repeat": "Daily",
"days": null,
"cron": "0 30 06 * * ?",
"timeZoneId": "Europe/Vilnius",
"nextFireUtc": "2026-10-16T03:30:00Z",
"group": null,
"tags": [],
"commands": [
{
"id": "bbd7571b-2ff4-4954-b74c-983889595b7c",
"type": "GrmsCommand",
"locationId": "823c7cc2-7a3b-4131-8524-ea452b6b815d",
"action": "SetState",
"capability": "Climate",
"commandId": "grms:823c7cc2-7a3b-4131-8524-ea452b6b815d:Climate",
"delaySeconds": 0,
"parameters": {},
"grmsState": {
"climate": { "temperatureSetpoint": 21.5, "temperatureUnit": null, "mode": "Heat", "fanSpeed": null },
"lighting": null, "housekeeping": null, "scene": null
},
"target": { "kind": "Rooms", "locationIds": ["823c7cc2-7a3b-4131-8524-ea452b6b815d"], "groups": [], "tags": [] }
}
]
}
| Field | Values |
|---|---|
repeat | Hourly, Daily, Weekly, Monthly, Yearly, Cron style, or a whole number of seconds. |
time | HH:mm, in the schedule's time zone. For Hourly, only the minute is used. |
days | Weekly: the days, for example MON,WED,FRI. Monthly: the day of the month. Yearly: the day and month, for example 15 March. |
cron | With Cron style: a cron expression of five fields. |
timeZoneId | An IANA time zone, such as Europe/London. Leave it out for the processor's own. |
commands[].target | Which locations. kind is Rooms (the ids in locationIds), AllRooms, Groups (the names in groups) or Tags (the names in tags). Groups, tags and all locations are worked out each time the schedule runs. Leave it out to use locationId. |
A schedule the processor can't accept is refused with 400 or 422 and a sentence in errorDetails.title. For example, a time zone it doesn't know answers "This system doesn't know the time zone "Mars/Base". Pick one from the list, such as Europe/London."
Change a schedule's time with a PATCH that names only that field:
curl --cacert box-ca.crt -X PATCH \
-H "X-Api-Key: $CENTRO_API_KEY" -H "Content-Type: application/json" \
https://192.168.10.22:8443/api/CommandScheduler/f1449daf-a922-4b49-a03b-68949d82b602 \
-d '{ "time": "07:00" }'
Check connection status
curl --cacert box-ca.crt -H "X-Api-Key: $CENTRO_API_KEY" \
https://192.168.10.22:8443/api/box/system/connectors
{
"connectors": [
{
"connector": {
"id": "ac32d061-a436-4974-9c3d-0389395573a9",
"name": "Room 101 driver",
"uiType": "CrestronHome",
"isLinked": true,
"lastSeenUtc": "2026-10-15T09:29:33.338053Z",
"ipAddress": "192.168.10.41",
"macAddress": "00:1C:01:01:A1:01",
"clientVersion": "1.0.0",
"locationId": "823c7cc2-7a3b-4131-8524-ea452b6b815d",
"locationName": "Room 101",
"grmsCapabilities": ["Climate", "Lighting"]
},
"online": true,
"hardwareType": "CrestronHome",
"effectiveCapabilities": ["Climate", "Lighting"],
"cpuPercent": null,
"memoryPercent": 41,
"processorKey": "mac:001C0101A101",
"alerts": []
}
],
"processors": [
{
"key": "mac:001C0101A101",
"ipAddress": "192.168.10.41",
"macAddress": "00:1C:01:01:A1:01",
"isCrestronHome": true,
"scanned": false
}
],
"processorsNotAdopted": 0
}
The answer is trimmed: each connection also reports the state of its certificate, and each processor its model, firmware and hostname once they've been read.
Errors
| Status | When | Body |
|---|---|---|
| 400 | The request can't be read or isn't accepted, for example a command for a location that doesn't exist. Also a key sent to the plain HTTP port, the User App port or the connection port. | errorDetails.title says why. For the other ports it's "Use HTTPS. Keys are not taken over plain HTTP." |
| 401 | No key, a key the processor doesn't know, a revoked or expired key, a key in the address or in an Authorization header, or a route that doesn't take keys. | Empty. |
| 403 | The key doesn't have the permission the route needs, or the key is for another API. | Empty. |
| 404 | There is no such location or schedule. | Empty. |
| 405 | A GET or DELETE on /mcp. See MCP. | Empty, with Allow: POST. |
| 422 | A schedule the processor can't accept, for example with a time zone it doesn't have. | errorDetails.title says why. |
| 423 | The processor lacks a licence the request needs. See Licences and limits. | feature names what is missing: api or rooms. errorDetails.title says it in a sentence. |
| 429 | Too many requests with this key, or too many requests with a key the processor doesn't know. | reason is rateLimited. The Retry-After header says how many seconds to wait. |
An error body looks like this:
{
"success": false,
"errorDetails": {
"title": "This processor has no API Licence. Add one on the licence page to use the API.",
"messages": []
},
"feature": "api"
}
Rate limit
A key can make 300 requests a minute, counted across the REST routes and /mcp together. After that every request with that key is refused with 429 and Retry-After: 60 until the minute is up. Other keys aren't affected.
A key the processor doesn't know is held to a tighter limit: about 10 a minute from one address, then 429 to that address for the rest of the minute. A key that is known still gets in from that address, and a request with no key at all isn't counted.
A refused request still counts. If a program retries a refused request in a loop, fix the cause first.
Good habits
- Use one key for each system, with the permissions it needs. A report only needs Read only.
- Check
acceptedand the command's status, not just the 200. A command the location's connection doesn't carry is not accepted, and an accepted one hasn't reached the location yet. - Don't poll faster than you need to. Read state when you need it. 300 requests a minute is the limit for each key.
- Keep keys out of code and logs. Read the key from your system's secret store. See Keep keys secret.