--- name: Publish a firmware image and push it over the air description: 'Drive the Afero OTA pipeline from CI: create a firmware type, upload a binary, move it into the firmware repository, create the pool record, associate it with a device type, and push it to a device.' api: openapi/afero-ota-api-openapi.yml operations: - createAccessToken - getCurrentUser - listFirmwareTypes - createFirmwareType - uploadFirmwareBinary - moveBinaryToRepository - poolFirmwareImageExists - createPoolFirmwareImage - listPoolFirmwareImages - createDeviceTypeFirmwareImage - listDeviceTypeFirmwareImages - pushFirmwareImageToDevice provider: Afero base_url: https://api.afero.io generated: '2026-08-02' method: generated --- # Publish a firmware image and push it over the air ## Authenticate first (every skill step depends on this) 1. Obtain the partner **OAuth Client ID** and **OAuth Client Secret** from the Afero Profile Editor under `VIEW > ACCOUNT INFO`. Never use developer credentials in production. 2. Call `createAccessToken` — `POST https://api.afero.io/oauth/token` with `Content-Type: application/x-www-form-urlencoded`, an `Authorization: Basic ` header, and the form body `username=&password=&grant_type=password`. 3. Read `access_token` from the response and send it as `Authorization: Bearer ` on every subsequent call. Tokens expire (about four hours; check `expires_in`, in seconds) — on any `401 unauthorized` re-run step 2 and retry once. > **This skill flashes firmware onto deployed hardware.** `pushFirmwareImageToDevice` is > safety-critical and irreversible in practice — the Afero OTA service refuses any downgrade > below the version already installed on a device. Require explicit human approval of the exact > `firmwareImageId` and target `deviceId` before the final step. ## Steps 1. **Resolve the partner.** Call `getCurrentUser` (`GET /v1/users/me`) and read `partnerAccess[0].partner.partnerId`. That is the `partnerId` path parameter for every OTA call. Require `partnerAccess[].privileges.manageDeviceProfiles` before proceeding. 2. **Reuse a firmware type; do not invent one.** Call `listFirmwareTypes` (`GET /v1/ota/partners/{partnerId}/types`) and look for an existing type that describes the purpose of this artefact. Afero is explicit: do not create a new firmware type for every new device. Platform types occupy 1-100, MCU types 101-200. Only if nothing fits, call `createFirmwareType` with `{"name", "description", "type"}` (`201`). The resulting `versionAttributeId` is `type + 2000` — the device attribute that will report this firmware's version. 3. **Upload the binary.** Call `uploadFirmwareBinary` (`POST /v1/ota/partners/{partnerId}/binaries`) with `Content-Type: application/octet-stream` (or `multipart/form-data`). The response `value` is the temporary file identifier and is the SHA-256 hash of the file — verify it against your build artefact's hash. 4. **Move it into the repository.** Call `moveBinaryToRepository` (`POST /v1/ota/partners/{partnerId}/binaries/moveToRepository`) passing the upload response body unchanged. The response `value` is the permanent repository URL. You MUST carry this URL into the pool record — otherwise the OTA service cannot deliver the update. 5. **Check for a collision.** Call `poolFirmwareImageExists` (`GET /v1/ota/partners/{partnerId}/pool/types/{type}/names/{name}/versions/{version}/exists`). When updating an existing record, pass `excludeFirmwareImageId` so the record being updated is not counted against itself. 6. **Create the pool record.** Call `createPoolFirmwareImage` (`POST /v1/ota/partners/{partnerId}/pool`) with `{"name", "description", "type", "version", "url", "tags", "associations"}` (`201`). `version` is your human-friendly string; the globally unique numeric `versionNumber` is generated by the platform and cannot be changed. `associations` maps a partner ID to an array of device type IDs and may be set later instead. 7. **Associate the image with a device type.** Call `createDeviceTypeFirmwareImage` (`POST /v1/ota/partners/{partnerId}/deviceTypes/{deviceTypeId}/firmwareImages`) (`201`). Take the object returned by `listPoolFirmwareImages` and send it back UNMODIFIED plus the required `versionNumber` — Afero warns that modifying the pool payload causes errors and prevents OTAs from succeeding. 8. **Confirm it is eligible.** Call `listDeviceTypeFirmwareImages` (`GET /v1/ota/partners/{partnerId}/deviceTypes/{deviceTypeId}/firmwareImages`) and find your record. Note its `id` — that is the `firmwareImageId`. 9. **Push to a device (human-approved).** Call `pushFirmwareImageToDevice` (`PUT /v1/ota/partners/{partnerId}/deviceTypes/{deviceTypeId}/firmwareImages/{firmwareImageId}/push`) with `{"value": ""}`. A `202 Accepted` means the OTA service has accepted the request, not that the device has updated. 10. **Verify on the device.** Read the device's version attribute (`firmware type + 2000`) via the Devices API to confirm the new version landed. ## Conventions that apply to every step - Base URL is `https://api.afero.io`; every resource except the token endpoint is under `/v1/`. - Errors return `{timestamp, status, error, error_description, service_name, region}` as `application/json` — this is NOT RFC 9457 problem+json. See `errors/afero-problem-types.yml`. - `id`, `versionNumber` and `firmwareImageId` are integers returned as STRINGS and may exceed 53-bit precision — parse them with BigInt, not Number. - Timestamps are epoch milliseconds. - **There is no idempotency key.** Afero documents no deduplication contract, so never blind-retry a write; re-read state and decide. See `conventions/afero-conventions.yml`. - OTA list operations are paged: `page` (zero-based), `size` (default 50), `sort`; the envelope carries `number`, `size`, `totalPages`, `numberOfElements`, `totalElements`, `content`. ## Reference - OTA background — firmware types, version numbers, pools and associations: https://afero-docs.readthedocs.io/en/latest/API-OTAEndpoints/ - OTA endpoint reference: https://afero-docs.readthedocs.io/en/latest/API-OTAEndpoints-Funcs/