Devices
A device is a smart lock, thermostat, sensor, hub, light, camera, sound system, repeater, voice assistant or valve that SuiteOp reaches through a connected provider. This guide covers reading devices, sending commands to them and linking them to properties. Every field and filter is listed on the operation’s page in the API Reference. Access codes on locks have their own guide: Lock codes.
| Operation | Request | Permission |
|---|---|---|
listDevices | GET /devices | view_devices |
getDevice | GET /devices/{id} | view_devices |
lockDevice | POST /devices/{id}/lock | control_device |
unlockDevice | POST /devices/{id}/unlock | control_device |
setDeviceTemperature | POST /devices/{id}/temperature | control_device |
setDeviceMode | POST /devices/{id}/mode | control_device |
refreshDeviceStatus | POST /devices/{id}/refresh | control_device |
assignDeviceToProperty | POST /devices/{deviceId}/assign-property | modify_devices |
Paths are relative to your region’s base URL, for example https://api-us.suiteop.com/api/v1. See Authentication for permissions. No operation creates or archives a device, or removes a device’s link to a property.
Listing devices
Section titled “Listing devices”GET /devices is paginated (default limit 20, maximum 100). Only active devices are returned unless you pass isArchived.
curl "https://api-us.suiteop.com/api/v1/devices?types=lock&needsAttention=true" \ -H "Authorization: Bearer sk_live_your_key_here"{ "data": [ { "id": "c1f0a6d2-…", "name": "Front door", "type": "lock", "ecosystem": "seam", "provider": "seam", "online": false, "batteryLevel": 22, "isArchived": false, "healthCheck": "2026-07-04T09:12:00.000Z", "deviceState": { … }, "createdAt": "2026-03-01T10:00:00.000Z", "linkedProperties": [{ "propertyId": "9a3d7e40-…", "propertyName": "Unit 4B" }], "isProvisioned": true, "provisioningAt": null, "hasOnlineCodes": true, "hasOfflineCodes": false } ], "meta": { "requestId": "…", "pagination": { "total": 1, "limit": 20, "offset": 0 } }}Useful filters:
| Filter | Notes |
|---|---|
types | lock, thermostat, sensor, door_window_sensor, sound_system, hub, light, camera, repeater, voice_assistant, valve. Repeat for several |
ecosystem, providers | ecosystem takes one vendor platform, such as seam or smartthings. providers takes a repeated list of integrations |
propertyIds, propertyGroupIds | Devices linked to these properties, or to properties in these groups |
onlineStatuses | online or offline. Both together is the same as no filter |
needsAttention | Devices that are offline, under 30% battery, or whose provider refused SuiteOp’s authorization, in one page |
flagReasons | One of low_battery, offline, auth_refused. Only one value is accepted; use needsAttention for all three |
lowBattery, unassigned, isArchived | lowBattery skips devices that report no battery. unassigned keeps devices linked to no property |
nameContains | Case-insensitive substring of the name. % and _ act as wildcards |
Field notes:
deviceStateholds the values as last reported by the provider: lock state, temperature, thermostat mode and setpoints. It isn’t read live. Temperatures in it are in your organization’s unit.- Code readiness:
isProvisioned,provisioningAt,hasOnlineCodesandhasOfflineCodeswarn you beforecreateCodeon a lock:isProvisioned: false, aprovisioningAtunder 15 minutes ago, orfalsefor the kind of code you want means the create will be refused.trueisn’t a guarantee of success. GET /devices/{id}returns the same fields plusenableRemoteControlsandupdatedAt.- Property scope: an API key sees every device. An OAuth token limited to some properties sees only devices linked to those properties, and
unassigneddoesn’t apply to it.
Sending commands
Section titled “Sending commands”Lock and thermostat commands go to the device through its provider:
curl -X POST https://api-us.suiteop.com/api/v1/devices/c1f0a6d2-…/lock \ -H "Authorization: Bearer sk_live_your_key_here" \ -H "Idempotency-Key: 550e8400-e29b-41d4-a716-446655440000" \ -H "Content-Type: application/json" \ -d '{}'{ "data": { "success": true }, "meta": { "requestId": "…" }}data is { success, commandId?, state? }; commandId and state are optional and can be absent.
- Success means the provider accepted the command, not that the bolt moved or the thermostat changed. Some providers finish later, and an offline device can fail after the response. Confirm with
getDeviceorrefreshDeviceStatus. unlockDeviceopens a real door straight away, with no schedule or duration. To give someone timed or repeated entry, create an access code instead.setDeviceTemperaturetakes{"temperature": 72}in your organization’s unit, not the device’s: 40 to 100 for Fahrenheit, 5 to 37 for Celsius. ReadtemperatureUnitfromgetOrganizationbefore sending a number. Where an operator has set temperature limits, the value is checked against the limits for the mode the thermostat last reported.setDeviceModetakes{"mode": "heat"}, one ofheat,cool,heat_cooloroff. It changes the mode only; a setpoint in the body is rejected with400.- Commands for an archived device, or a device with remote control switched off, are refused with
403 FORBIDDENbefore anything reaches the provider.error.messagesays which. Retrying unchanged gets the same answer. - A command the device’s provider doesn’t offer is refused with
400 VALIDATION_ERROR, anderror.details.operationnames it (lock,unlock,set_temperatureorthermostat). Retrying cannot succeed. setDeviceTemperatureis also refused with400 VALIDATION_ERRORwhen the thermostat is off, in Eco mode, or doesn’t allow manual overrides.setDeviceModeis refused with400 VALIDATION_ERRORwhen the thermostat doesn’t support the requested mode or doesn’t allow manual overrides; a thermostat that is off or in Eco mode still accepts a mode change, which is how you turn it back on.error.messagesays which.
If the provider refuses a command, the API returns 502 provider_error, and a retry with the same Idempotency-Key replays the refusal. See Provider refusals.
A command can also be refused with 409 before it reaches the provider. error.code says why:
error.code | Meaning |
|---|---|
CONFLICT | The device has no provider, provider device ID or integration account to send the command through; or, for lockDevice and unlockDevice, the provider last reported that remote lock or unlock is unavailable on this device (error.details.operation is lock or unlock). Also returned while a request with the same Idempotency-Key is still in flight; retry after it completes |
INTEGRATION_PUSH_DISABLED | Outbound writes are off. error.details.reason is org_muted (the organization’s outbound communications are paused), migration_held (the organization’s migration into SuiteOp has paused it) or push_disabled (the integration account’s own switch) |
CREDENTIAL_HELD_BY_MIGRATION | The integration account came over in the organization’s migration into SuiteOp and isn’t used until that migration finishes. error.details.reason is migration_held, the same as INTEGRATION_PUSH_DISABLED for a paused migration |
RECONNECT_REQUIRED | The integration account is expired, disconnected or needs re-authorization. Reconnect it in SuiteOp |
Apart from the in-flight CONFLICT, retrying unchanged gets the same answer. To recognise a paused organization, branch on error.details.reason rather than on the code.
Refreshing status
Section titled “Refreshing status”POST /devices/{id}/refresh asks the provider for the device’s status now, stores it, and returns the device in the same shape as getDevice. It needs control_device because it contacts the provider, and fails when the device is archived, has remote control off, or can’t be reached. It returns the same 409 codes as the commands, except CREDENTIAL_HELD_BY_MIGRATION: while the organization is paused it’s refused with INTEGRATION_PUSH_DISABLED and error.details.reason org_muted or migration_held, but the integration account’s own push switch doesn’t block it. Use getDevice when last-reported values are enough.
Linking a device to a property
Section titled “Linking a device to a property”curl -X POST https://api-us.suiteop.com/api/v1/devices/c1f0a6d2-…/assign-property \ -H "Authorization: Bearer sk_live_your_key_here" \ -H "Idempotency-Key: 7c9e6679-7425-40de-944b-e07fc1f90ae7" \ -H "Content-Type: application/json" \ -d '{"propertyId": "9a3d7e40-…", "provisioning": {"deleteUnknownCodes": false}}'The response is 201 with the link: id, deviceId, propertyId, elementInstanceId and createdAt.
- A device can serve several properties, so this adds a link rather than moving one. Linking the same pair again is rejected, and there is no unlink operation.
- The first link of an unprovisioned lock that supports codes also takes the lock over from its provider. That call needs
provisioningand themanage_lock_codepermission as well.deleteUnknownCodes: truewipes every code on the lock that SuiteOp didn’t issue;falseleaves them. There is no default.provisioningis ignored for every other device. - Archived devices are rejected, and the property must be one you can reach.