Signed binary uploads, staged rollouts, and Ed25519 verification — push firmware to your fleet from the dashboard or API.
.bin files). The signing key is optional — SHA-256 integrity checking always applies.
jettyd OTA has three layers of safety:
params. The device verifies the signature against the tenant public key before accepting the update.Upload your binary via the dashboard (Firmware → Upload) or the API:
curl -X POST https://api.jettyd.com/v1/firmware \
-H "Authorization: Bearer $API_KEY" \
-F "file=@build/firmware.bin" \
-F "version=1.2.0" \
-F "device_type=sensor-v2" \
-F "notes=Adds BLE power-save mode"
The response includes the sha256 and, if a signing key is configured, the signature and signature_alg:
{
"id": "3fa85f64-5717-4562-b3fc-2c963f66afa6",
"version": "1.2.0",
"sha256": "e3b0c44298fc1c149afb4c8996fb92427ae41e4649b934ca495991b7852b855",
"size": 524288,
"signature": "a1b2c3d4…",
"signature_alg": "ed25519"
}
Pick a target (all devices, a device type, tag, individual device, or device group) and an initial batch percentage:
curl -X POST https://api.jettyd.com/v1/firmware/{firmware_id}/rollout \
-H "Authorization: Bearer $API_KEY" \
-H "Content-Type: application/json" \
-d '{
"target_type": "group",
"target_id": "your-device-group-uuid",
"rollout_percentage": 20
}'
Target types:
| target_type | target_id | Description |
|---|---|---|
| all | omit | All devices in the tenant |
| device_type | slug string | All devices of a given type |
| tag | tag string | All devices with a given tag |
| device | device UUID | A single device |
| group | group UUID | All devices in a device group |
When rollout_percentage is less than 100, the first batch size is ceil(total × pct / 100). The platform's background worker (runs every 30 seconds) advances to remaining devices once the current batch is fully terminal (confirmed, failed, or timed out).
Check overall rollout status:
curl https://api.jettyd.com/v1/firmware/rollouts \
-H "Authorization: Bearer $API_KEY"
Check per-device OTA state:
curl https://api.jettyd.com/v1/devices/{device_id}/firmware-status \
-H "Authorization: Bearer $API_KEY"
The update_state field is derived from the most recent firmware_update command for the device:
| update_state | Meaning |
|---|---|
| idle | No OTA command has been sent to this device |
| updating | Command is pending or delivered, awaiting device response |
| up_to_date | Device confirmed the update (command.status = confirmed) |
| failed | Command failed or timed out |
Cancel a rolling rollout to stop dispatching to remaining devices. Commands already sent are not recalled.
curl -X POST https://api.jettyd.com/v1/firmware/rollouts/{rollout_id}/cancel \
-H "Authorization: Bearer $API_KEY"
Group devices logically for targeted rollouts — for example, a staging-fleet group for canary testing before rolling out to all production devices.
curl -X POST https://api.jettyd.com/v1/groups \
-H "Authorization: Bearer $API_KEY" \
-H "Content-Type: application/json" \
-d '{"name": "staging-fleet"}'
curl -X POST https://api.jettyd.com/v1/groups/{group_id}/members \
-H "Authorization: Bearer $API_KEY" \
-H "Content-Type: application/json" \
-d '{"device_id": "your-device-uuid"}'
You can also manage groups from the dashboard at Device Groups in the left nav.
When a firmware signing key is configured in the platform, every upload generates an Ed25519 signature over the SHA-256 digest of the binary. The signature is included in the OTA command params as a hex-encoded 64-byte string.
curl https://api.jettyd.com/v1/firmware/public-key \
-H "Authorization: Bearer $API_KEY"
{
"algorithm": "ed25519",
"public_key": "abc123…"
}
Bake this 64-hex-character public key into your firmware at build time. It never changes for a given tenant.
On-device verification pseudocode using mbedTLS or a minimal Ed25519 library:
/* 1. Download binary and compute SHA-256 */
mbedtls_sha256(firmware_buf, firmware_len, digest, 0); /* digest = 32 bytes */
/* 2. Decode hex fields from the OTA command params */
uint8_t pub_key[32]; hex_decode(pub_key_hex, pub_key, 32);
uint8_t sig[64]; hex_decode(sig_hex, sig, 64);
/* 3. Verify Ed25519 signature over the digest (NOT the full binary) */
if (ed25519_verify(sig, digest, 32, pub_key) != 0) {
ESP_LOGE(TAG, "OTA signature verification FAILED — rejecting update");
return ESP_FAIL;
}
/* 4. Flash the verified binary */
esp_ota_write(ota_handle, firmware_buf, firmware_len);
from cryptography.hazmat.primitives.asymmetric.ed25519 import Ed25519PublicKey
from cryptography.hazmat.primitives.serialization import Encoding, PublicFormat
import hashlib, requests
pub_key_hex = "abc123…" # from /v1/firmware/public-key
sig_hex = "a1b2c3d4…" # from OTA command params
firmware = open("firmware.bin", "rb").read()
digest = hashlib.sha256(firmware).digest()
pub_key = Ed25519PublicKey.from_public_bytes(bytes.fromhex(pub_key_hex))
sig = bytes.fromhex(sig_hex)
pub_key.verify(sig, digest) # raises InvalidSignature on failure
print("Signature valid")
Generate a random 32-byte seed and set it as the FIRMWARE_SIGNING_KEY environment variable on the platform. The platform derives the Ed25519 signing key and verifying key from this seed.
# Generate a secure random seed (32 bytes = 64 hex chars)
openssl rand -hex 32
Set FIRMWARE_SIGNING_KEY=<output> in your platform environment variables. Treat this like a private key — rotate it only if compromised, and never expose it outside the server. The corresponding public key is available via GET /v1/firmware/public-key at any time.
FIRMWARE_SIGNING_KEY is not set, the OTA command params omits signature and signature_alg. SHA-256 integrity checking still applies. Your device firmware should check for the presence of the signature field before attempting verification.
| Endpoint | Method | Description |
|---|---|---|
| /v1/firmware | POST | Upload a firmware binary (multipart) |
| /v1/firmware | GET | List firmware images |
| /v1/firmware/{id} | DELETE | Delete a firmware image |
| /v1/firmware/public-key | GET | Get the tenant's Ed25519 public key |
| /v1/firmware/{id}/rollout | POST | Start a rollout |
| /v1/firmware/rollouts | GET | List all rollouts |
| /v1/firmware/rollouts/{id}/cancel | POST | Cancel a rolling rollout |
| /v1/devices/{id}/firmware-status | GET | Per-device OTA state |
| /v1/groups | POST / GET | Create / list device groups |
| /v1/groups/{id} | GET / PATCH / DELETE | Get, rename, or delete a group |
| /v1/groups/{id}/members | GET / POST | List / add group members |
| /v1/groups/{id}/members/{device_id} | DELETE | Remove a device from a group |