---
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 <base64(clientId:clientSecret)>` header, and the form body
   `username=<afero email>&password=<afero password>&grant_type=password`.
3. Read `access_token` from the response and send it as `Authorization: Bearer <access_token>`
   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": "<target deviceId>"}`. 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/
