Skip to content
Dashboard

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.

OperationRequestPermission
listDevicesGET /devicesview_devices
getDeviceGET /devices/{id}view_devices
lockDevicePOST /devices/{id}/lockcontrol_device
unlockDevicePOST /devices/{id}/unlockcontrol_device
setDeviceTemperaturePOST /devices/{id}/temperaturecontrol_device
setDeviceModePOST /devices/{id}/modecontrol_device
refreshDeviceStatusPOST /devices/{id}/refreshcontrol_device
assignDeviceToPropertyPOST /devices/{deviceId}/assign-propertymodify_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.

GET /devices is paginated (default limit 20, maximum 100). Only active devices are returned unless you pass isArchived.

Terminal window
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:

FilterNotes
typeslock, thermostat, sensor, door_window_sensor, sound_system, hub, light, camera, repeater, voice_assistant, valve. Repeat for several
ecosystem, providersecosystem takes one vendor platform, such as seam or smartthings. providers takes a repeated list of integrations
propertyIds, propertyGroupIdsDevices linked to these properties, or to properties in these groups
onlineStatusesonline or offline. Both together is the same as no filter
needsAttentionDevices that are offline, under 30% battery, or whose provider refused SuiteOp’s authorization, in one page
flagReasonsOne of low_battery, offline, auth_refused. Only one value is accepted; use needsAttention for all three
lowBattery, unassigned, isArchivedlowBattery skips devices that report no battery. unassigned keeps devices linked to no property
nameContainsCase-insensitive substring of the name. % and _ act as wildcards

Field notes:

  • deviceState holds 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, hasOnlineCodes and hasOfflineCodes warn you before createCode on a lock: isProvisioned: false, a provisioningAt under 15 minutes ago, or false for the kind of code you want means the create will be refused. true isn’t a guarantee of success.
  • GET /devices/{id} returns the same fields plus enableRemoteControls and updatedAt.
  • Property scope: an API key sees every device. An OAuth token limited to some properties sees only devices linked to those properties, and unassigned doesn’t apply to it.

Lock and thermostat commands go to the device through its provider:

Terminal window
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 getDevice or refreshDeviceStatus.
  • unlockDevice opens a real door straight away, with no schedule or duration. To give someone timed or repeated entry, create an access code instead.
  • setDeviceTemperature takes {"temperature": 72} in your organization’s unit, not the device’s: 40 to 100 for Fahrenheit, 5 to 37 for Celsius. Read temperatureUnit from getOrganization before sending a number. Where an operator has set temperature limits, the value is checked against the limits for the mode the thermostat last reported.
  • setDeviceMode takes {"mode": "heat"}, one of heat, cool, heat_cool or off. It changes the mode only; a setpoint in the body is rejected with 400.
  • Commands for an archived device, or a device with remote control switched off, are refused with 403 FORBIDDEN before anything reaches the provider. error.message says which. Retrying unchanged gets the same answer.
  • A command the device’s provider doesn’t offer is refused with 400 VALIDATION_ERROR, and error.details.operation names it (lock, unlock, set_temperature or thermostat). Retrying cannot succeed.
  • setDeviceTemperature is also refused with 400 VALIDATION_ERROR when the thermostat is off, in Eco mode, or doesn’t allow manual overrides. setDeviceMode is refused with 400 VALIDATION_ERROR when 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.message says 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.codeMeaning
CONFLICTThe 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_DISABLEDOutbound 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_MIGRATIONThe 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_REQUIREDThe 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.

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.

Terminal window
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 provisioning and the manage_lock_code permission as well. deleteUnknownCodes: true wipes every code on the lock that SuiteOp didn’t issue; false leaves them. There is no default. provisioning is ignored for every other device.
  • Archived devices are rejected, and the property must be one you can reach.