New: the AI firewall watches every module and explains each incident in plain words. See how it works

Firmware and device settings API

Upload signed firmware, assign it, run and steer rollouts in waves, and read and change licensed devices' settings over REST – routes, bodies and errors.

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

MethodPathPermissionDoes
GET/api/iot/ota/packagesiot_ota.readList packages (status, title)
POST/api/iot/ota/packagesiot_ota.packages.manageUpload a package (multipart)
GET/api/iot/ota/packages/{packageId}iot_ota.readOne package with its update counts
PATCH/api/iot/ota/packages/{packageId}iot_ota.packages.manageRelease notes, or status DEPRECATED, READY again, REVOKED
DELETE/api/iot/ota/packages/{packageId}iot_ota.packages.manageDelete an unused package
GET, POST/api/iot/ota/assignmentsread: iot_ota.read; assign: iot_ota.assignList or create assignments
DELETE/api/iot/ota/assignments/{assignmentId}iot_ota.assignRemove an assignment
GET/api/iot/ota/rolloutsiot_ota.readRollouts in waves (packageId, status)
GET, POST/api/iot/ota/rollouts/{rolloutId}read; iot_ota.assignRead; { "action": "pause" \| "resume" \| "abort" }
GET/api/iot/ota/statesiot_ota.readUpdate attempts (packageId, state, limit up to 500)
GET, POST/api/iot/ota/devices/{deviceId}read; iot_ota.assignA device’s update; { "action": "retry" }
GET, PUT/api/iot/ota/settings/{profile\|device}/{id}read; iot_ota.assignOTA settings of a profile, or a device’s metered-link override
GET, POST/api/iot/ota/signing-keysread; iot_ota.keys.manageList or add signing public keys
DELETE/api/iot/ota/signing-keys/{keyId}iot_ota.keys.manageRevoke 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 fieldRules
title1 to 128; letters, digits, ., _, +, -
version1 to 64; letters, digits, ., _, +, ~, -
checksumSHA-256 of the file, 64 hexadecimal characters
signatureSchemeusign-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)
typeFIRMWARE or SOFTWARE
profileId, hardwareModels (up to 50), minFromVersionWhich devices it fits
releaseNotesup to 4,000 characters

The default maximum size is 64 MiB. The answer is 201 { "package": {...} }.

RefusalMessage
400 file_missingAttach the firmware file.
413 too_largeThe package is larger than allowed.
413 quotaNot enough storage left for this package.
422 checksum_mismatchThe SHA-256 of the uploaded file does not match the one given.
422The signature of the image does not verify against a registered key.
409 package_existsA 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.

MethodPathPermissionDoes
GET/api/iot/devices/{deviceId}/settingsiot_device_settings.readEvery section: desired and reported values, state, drift
GET/api/iot/devices/{deviceId}/settings/schema.readThe manifest of the device’s model and firmware
GET/api/iot/devices/{deviceId}/settings/history.readPast changes (section, page from 0, sort=at:desc)
GET/api/iot/devices/{deviceId}/settings/{section}.readOne section; ETag is its revision
PUT/api/iot/devices/{deviceId}/settings/{section}.operate and the classes changedReplace a section: { values, confirm? }
PATCH/api/iot/devices/{deviceId}/settings/{section}.operate and the classes changedMerge patch: { patch, confirm? }
POST/api/iot/devices/{deviceId}/settings/{section}/reapply.operateSend the desired section again
GET/api/iot/device-settings/models, /model-schema?model=.readYour models and one model’s sections
GET, POST/api/iot/device-settings/baselines.read; .baselines.manageList or create baselines
GET, PUT, DELETE/api/iot/device-settings/baselines/{baselineId}.read; .baselines.manageRead, replace, delete
GET, POST/api/iot/device-settings/jobs.read; .jobs.manageList or start bulk jobs
GET/api/iot/device-settings/jobs/{jobId}, .../targets.readA job, its waves and its devices
POST/api/iot/device-settings/jobs/{jobId}/pause, /resume, /cancel.jobs.manageSteer 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-Match is 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=true validates and lists what would change, and stores nothing.
  • A merge patch merges objects, deletes with null and 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.