This page covers two families of routes: firmware updates (OTA) and remote device settings. How both work for people is in Firmware updates (OTA), Firmware rollout waves and Remote device settings. Paths are under https://<your host>/tenant/<tenantId>.
Firmware routes
| Method | Path | Permission | Does |
|---|---|---|---|
| GET | /api/iot/ota/packages | iot_ota.read | List packages (status, title) |
| POST | /api/iot/ota/packages | iot_ota.packages.manage | Upload a package (multipart) |
| GET | /api/iot/ota/packages/{packageId} | iot_ota.read | One package with its update counts |
| PATCH | /api/iot/ota/packages/{packageId} | iot_ota.packages.manage | Release notes, or status DEPRECATED, READY again, REVOKED |
| DELETE | /api/iot/ota/packages/{packageId} | iot_ota.packages.manage | Delete an unused package |
| GET, POST | /api/iot/ota/assignments | read: iot_ota.read; assign: iot_ota.assign | List or create assignments |
| DELETE | /api/iot/ota/assignments/{assignmentId} | iot_ota.assign | Remove an assignment |
| GET | /api/iot/ota/rollouts | iot_ota.read | Rollouts in waves (packageId, status) |
| GET, POST | /api/iot/ota/rollouts/{rolloutId} | read; iot_ota.assign | Read; { "action": "pause" \| "resume" \| "abort" } |
| GET | /api/iot/ota/states | iot_ota.read | Update attempts (packageId, state, limit up to 500) |
| GET, POST | /api/iot/ota/devices/{deviceId} | read; iot_ota.assign | A device’s update; { "action": "retry" } |
| GET, PUT | /api/iot/ota/settings/{profile\|device}/{id} | read; iot_ota.assign | OTA settings of a profile, or a device’s metered-link override |
| GET, POST | /api/iot/ota/signing-keys | read; iot_ota.keys.manage | List or add signing public keys |
| DELETE | /api/iot/ota/signing-keys/{keyId} | iot_ota.keys.manage | Revoke a key |
Adding and revoking signing keys is an Owner permission, so an API key cannot do it.
Upload a package
POST /api/iot/ota/packages takes multipart/form-data with two parts: file (the image) and meta (JSON text). The checksum is checked before anything is stored, and the signature is verified against your registered keys.
curl -s -X POST "https://$HOST/tenant/$TENANT/api/iot/ota/packages" \
-H "Authorization: Bearer $HERMETRY_KEY" \
-F "file=@router-fw-2.5.0.bin" \
-F 'meta={"title":"router-fw","version":"2.5.0","checksum":"<sha256 of the file>","signatureScheme":"usign-ed25519","signature":"<text of the .sig file>","profileId":"a1b2…","releaseNotes":"Fixes LTE reconnect."}'
meta field | Rules |
|---|---|
title | 1 to 128; letters, digits, ., _, +, - |
version | 1 to 64; letters, digits, ., _, +, ~, - |
checksum | SHA-256 of the file, 64 hexadecimal characters |
signatureScheme | usign-ed25519 (a detached signature in signature), the secure-boot scheme of the device family the package is for, or none (only where the profile allows unsigned images) |
type | FIRMWARE or SOFTWARE |
profileId, hardwareModels (up to 50), minFromVersion | Which devices it fits |
releaseNotes | up to 4,000 characters |
The default maximum size is 64 MiB. The answer is 201 { "package": {...} }.
| Refusal | Message |
|---|---|
400 file_missing | Attach the firmware file. |
413 too_large | The package is larger than allowed. |
413 quota | Not enough storage left for this package. |
422 checksum_mismatch | The SHA-256 of the uploaded file does not match the one given. |
422 | The signature of the image does not verify against a registered key. |
409 package_exists | A package with this title and version already exists. |
Assign and roll out
POST/tenant/{tenantId}/api/iot/ota/assignments
Assign a READY package to a PROFILE or a DEVICE (in targetId) or to a GROUP (deviceIds, up to 5,000, or selector.profileId, both frozen now). dryRun: true answers the counts and writes nothing. waves (1 to 20) rolls a profile or group out in waves; one device is never rolled out in waves.
Request body application/json
{
"packageId": "f00d…",
"targetType": "PROFILE",
"targetId": "a1b2…",
"waves": [
{
"size": {
"percent": 5
},
"soakMin": 60,
"minSuccessPct": 95,
"maxFailurePct": 2,
"maxDurationMin": 1440
},
{
"size": {
"percent": 100
},
"soakMin": 0,
"minSuccessPct": 95,
"maxFailurePct": 2,
"maxDurationMin": 2880
}
]
}
Example curl
curl -X POST 'https://<your-host>/tenant/{tenantId}/api/iot/ota/assignments' \
-H 'Content-Type: application/json' \
-d '{"packageId":"f00d…","targetType":"PROFILE","targetId":"a1b2…","waves":[{"size":{"percent":5},"soakMin":60,"minSuccessPct":95,"maxFailurePct":2,"maxDurationMin":1440},{"size":{"percent":100},"soakMin":0,"minSuccessPct":95,"maxFailurePct":2,"maxDurationMin":2880}]}'
Responses
201Created
{
"assignment": {
"…": "…"
},
"preview": {
"…": "…"
},
"result": {
"…": "…"
}
}
200OK
{
"assignment": null,
"preview": {
"…": "…"
}
}
409Conflict
{
"message": "Only a ready package can be assigned.",
"code": "CONFLICT"
}
A wave’s size is { "percent": 0–100 } or { "count": 1–100000 }; the last wave takes the rest. soakMin 0 to 10,080, minSuccessPct and maxFailurePct 0 to 100, optional maxFailures, maxDurationMin 5 to 43,200.
Rollout statuses: RUNNING, PAUSED, COMPLETED, ABORTED. A failed gate pauses the rollout; resume continues, abort ends it.
Update states
A device’s update moves through QUEUED, INITIATED, WAITING, DOWNLOADING, DOWNLOADED, VERIFIED, UPDATING and ends UPDATED, FAILED, TIMEOUT, ROLLED_BACK, SKIPPED or CANCELLED. Only FAILED, TIMEOUT and ROLLED_BACK can be retried ({ "action": "retry" }), up to the attempt limit.
OTA settings
PUT /api/iot/ota/settings/profile/{profileId} takes otaEnabled, allowMetered, allowUnsigned, downloadTimeoutMin (5 to 1,440), updateTimeoutMin (2 to 240), maxAttempts (1 to 10); null goes back to the default. For a device (/settings/device/{deviceId}) only allowMetered is taken: “A device takes only the metered-link setting.”
Webhook events: ota.update.* and ota.rollout.*, see the Webhook event reference.
Device settings routes
Remote settings are for licensed devices whose model publishes a settings manifest.
| Method | Path | Permission | Does |
|---|---|---|---|
| GET | /api/iot/devices/{deviceId}/settings | iot_device_settings.read | Every section: desired and reported values, state, drift |
| GET | /api/iot/devices/{deviceId}/settings/schema | .read | The manifest of the device’s model and firmware |
| GET | /api/iot/devices/{deviceId}/settings/history | .read | Past changes (section, page from 0, sort=at:desc) |
| GET | /api/iot/devices/{deviceId}/settings/{section} | .read | One section; ETag is its revision |
| PUT | /api/iot/devices/{deviceId}/settings/{section} | .operate and the classes changed | Replace a section: { values, confirm? } |
| PATCH | /api/iot/devices/{deviceId}/settings/{section} | .operate and the classes changed | Merge patch: { patch, confirm? } |
| POST | /api/iot/devices/{deviceId}/settings/{section}/reapply | .operate | Send the desired section again |
| GET | /api/iot/device-settings/models, /model-schema?model= | .read | Your models and one model’s sections |
| GET, POST | /api/iot/device-settings/baselines | .read; .baselines.manage | List or create baselines |
| GET, PUT, DELETE | /api/iot/device-settings/baselines/{baselineId} | .read; .baselines.manage | Read, replace, delete |
| GET, POST | /api/iot/device-settings/jobs | .read; .jobs.manage | List or start bulk jobs |
| GET | /api/iot/device-settings/jobs/{jobId}, .../targets | .read | A job, its waves and its devices |
| POST | /api/iot/device-settings/jobs/{jobId}/pause, /resume, /cancel | .jobs.manage | Steer a job |
What a write may change depends on the class of each changed setting: operating settings need iot_device_settings.operate, configuration settings iot_device_settings.write, dangerous settings iot_device_settings.dangerous.write (Owner only, so never an API key) and "confirm": true.
Change a section
GET /api/iot/devices/{deviceId}/settings/network → ETag: "7"
PATCH /api/iot/devices/{deviceId}/settings/network
If-Match: "7"
{ "patch": { "lte": { "apn": "iot.example" } } }
If-Matchis required: without it,428“If-Match is required: send the rev you read.”- A section changed since you read it:
412“The section changed since it was read; read it again.” (rev_mismatch). ?dryRun=truevalidates and lists what would change, and stores nothing.- A merge patch merges objects, deletes with
nulland replaces lists whole. - Writes are limited to 600 per organisation per minute.
- Secrets are never returned: a secret setting reads as
{ "set": true }.
A device that does not take settings by section answers 409 “This device does not take settings by section.”; one whose manifest is not in the catalog yet, “The settings manifest of this device’s model and firmware is not in the catalog yet.”
Bulk jobs
POST /api/iot/device-settings/jobs with Idempotency-Key header: name (up to 80), baselineId or sections, selector (deviceIds, profileId or models), optional waves ({ "percent": [1, 10, 100] } or { "count": [...] }, up to 10), gates (minApplied, maxReverted as fractions 0 to 1), onConflict (queue or skip), offlineExpiryDays (1 to 90), waveTimeoutMin (1 to 1,440), confirm. A repeated Idempotency-Key answers the first job (200). ?dryRun=true answers the plan. Refusals: 409 too_many_jobs; 422 confirm_required, invalid_sections, no_devices, too_many_devices; 403 class_forbidden.

