Publicar versiones de APK

Cargue un APK de Android firmado directamente desde su pipeline de publicación y siga su estado hasta que aparezca en la biblioteca de su empresa.

Cómo funciona la publicación

Cree una sesión de carga multiparte con la API de Nomid, envíe los bytes del APK directamente a S3 mediante URL de partes de corta duración, complete la sesión y consulte periódicamente el resultado final del procesamiento.

LIBRARY_MANAGE ahora cubre las cargas de APK y la administración de aplicaciones, incluida la lectura, edición, ocultación y restauración de aplicaciones y versiones. Las API públicas para archivos, contactos y otros recursos de la biblioteca siguen sin estar disponibles. No concede la asignación de políticas, comandos de dispositivo, aprovisionamiento ni administración de la cuenta.

Lea y administre las aplicaciones de la biblioteca de la empresa y publique versiones firmadas de APK. LIBRARY_MANAGE es un permiso sensible, a nivel de toda la empresa, para cargas de APK y administración de aplicaciones. No concede la asignación de políticas, comandos de dispositivo, aprovisionamiento ni operaciones de la cuenta.

Una publicación completada coloca la versión en la biblioteca y puede activar la distribución existente para las políticas que usan su aplicación y canal. No confirma que ningún dispositivo haya instalado la versión.

Antes de empezar

  • Cree una clave de API dedicada con LIBRARY_MANAGE. Un usuario del Portal que cree ese permiso necesita API_KEY_MANAGE, alcance de políticas de la empresa sin restricciones, LIBRARY_VIEW y LIBRARY_EDIT. El permiso sensible viene desmarcado de forma predeterminada; las claves existentes no lo obtienen automáticamente.
  • Conceda la clave únicamente a automatizaciones de confianza que necesiten estas operaciones de biblioteca a nivel de toda la empresa. LIBRARY_MANAGE permite cargas de APK y administración de aplicaciones, incluido el acceso a sesiones de carga iniciadas por otras claves o usuarios autorizados de la empresa; guarde la clave en el almacén de secretos de su CI y rótela o revóquela cuando cambie el acceso.
  • Use Python 3.11 o posterior para el cargador de ejemplo. Compile y firme el APK con la identidad de firma prevista y un código de versión de Android adecuado antes de cargarlo. La API no firma APK ni publica en Google Play.
  • Envíe X-API-Key a la API de Nomid. La clave validada determina la empresa; las solicitudes no tienen ningún parámetro de inquilino ni de empresa. No se necesita ningún permiso DEVICES_READ ni POLICIES_READ para publicar APK.

Endpoints

https://api.nomid.tech/emm/api/v1

Las ocho rutas siguientes requieren X-API-Key con LIBRARY_MANAGE; una sesión de carga pertenece a la empresa de la clave.

MétodoEndpointDescripción
POST/library/apps/uploads/checkCompruebe la elegibilidad, el contenido duplicado y si existe una sesión reanudable. Un duplicado es meramente informativo.
POST/library/apps/uploads/startInicie una carga y reciba sessionId, partSize y expiresAt.
POST/library/apps/uploads/parts/signObtenga una URL PUT directa de S3 de corta duración y requiredHeaders para una parte.
GET/library/apps/uploads/parts/{sessionId}Liste las partes ya cargadas que se pueden reutilizar y sus ETags exactos para la misma sesión.
POST/library/apps/uploads/completeEnvíe la lista ordenada de partes y, opcionalmente, los canales para ensamblar y procesar el APK.
GET/library/apps/uploads/status/{sessionId}Consulte la sesión periódicamente hasta que status sea completed o failed.
DELETE/library/apps/uploads/abort/{sessionId}Cancele una sesión sin terminar; las sesiones en procesamiento o completadas no se pueden cancelar.
GET/library/apps/{packageName}/versions/{versionCode}/check-reportLea el último informe de comprobación de carga de una versión. Necesita el packageName y el versionCode de la versión y su entorno cuando este no esté vacío.

Secuencia de carga

  1. POST /check con el nombre de archivo, el tamaño real en bytes (positivo), el mime aceptado y el SHA-256 del APK. Revise allowed y reason; puede haber duplicateOf y existingSessionId.
  2. POST /start con los mismos campos. Guarde de forma segura el sessionId y el partSize devueltos para que esta ejecución pueda reanudarse si se interrumpe.
  3. Para cada parte que falte, POST /parts/sign con sessionId y un partNumber de 1 a 10000.
  4. Divida el archivo en fragmentos secuenciales de partSize bytes; solo el último fragmento puede ser más corto. Envíe cada fragmento con PUT a su URL firmada usando requiredHeaders y conserve el encabezado de respuesta ETag exactamente como se devuelve.
  5. POST /complete con sessionId, cada par partNumber/etag y, opcionalmente, tracks. La finalización inicia el procesamiento del APK en segundo plano; su respuesta inmediata aún puede indicar processing.
  6. Haga GET al endpoint de estado con el ID de la sesión hasta que el status, en minúsculas, sea completed o failed. Las respuestas completadas incluyen app; las fallidas incluyen un errorMessage. Los demás estados no equivalen a una publicación correcta.

El tamaño debe ser mayor que cero y estar dentro del límite de carga configurado. mime es application/vnd.android.package-archive o application/octet-stream; sha256 es el resumen hexadecimal en minúsculas de 64 caracteres de los bytes del APK. Se aplican las caducidades de la sesión y de la URL firmada.

La URL firmada es para S3, no para la API de Nomid: envíe con el PUT únicamente requiredHeaders, nunca X-API-Key. No registre la URL firmada en los logs. Si caduca, vuelva a firmar esa parte.

Ejemplos de solicitud y respuesta

Estos ejemplos muestran los nombres de los campos y los valores de status en minúsculas. Los identificadores, el resumen, la hora, el tamaño de parte, los ETags y la URL son ilustrativos; use los valores devueltos para su propia sesión.

Solicitud POST /check o /start

{
  "filename": "app-release.apk",
  "size": 33554432,
  "mime": "application/vnd.android.package-archive",
  "sha256": "0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef"
}

Respuesta POST /check de carga elegible

{
  "allowed": true,
  "recommendedPartSize": 16777216
}

Respuesta POST /start

{
  "sessionId": "123e4567-e89b-12d3-a456-426614174000",
  "partSize": 16777216,
  "expiresAt": "2026-10-28T12:00:00Z"
}

Solicitud POST /parts/sign

{
  "sessionId": "123e4567-e89b-12d3-a456-426614174000",
  "partNumber": 1
}

Respuesta POST /parts/sign

{
  "url": "https://<presigned-part-url>",
  "requiredHeaders": {}
}

Respuesta GET de partes cargadas

{
  "status": "uploading",
  "partSize": 16777216,
  "parts": [{ "partNumber": 1, "etag": "\"example-etag\"" }]
}

Solicitud POST /complete

{
  "sessionId": "123e4567-e89b-12d3-a456-426614174000",
  "parts": [
    { "partNumber": 1, "etag": "\"example-etag-1\"" },
    { "partNumber": 2, "etag": "\"example-etag-2\"" }
  ],
  "tracks": ["prod"]
}

Respuesta POST /complete en procesamiento

{
  "status": "processing",
  "sessionId": "123e4567-e89b-12d3-a456-426614174000"
}

requiredHeaders puede contener encabezados que exige la firma de S3. Aplique el mapa devuelto exactamente, incluidos sus valores, y capture el encabezado de respuesta ETag de cada PUT correcto.

Comprobaciones de carga

Cada carga se comprueba antes de crear su versión: la firma y el manifiesto del APK, las versiones anteriores del mismo paquete de su empresa y los dispositivos de su empresa (sus niveles de API de Android y las aplicaciones instaladas en ellos con sus certificados de firma). El resultado es un informe de comprobación que puede leer desde la API.

Las comprobaciones solo informan de hallazgos. Los hallazgos se guardan y se devuelven, y la carga se completa igualmente; únicamente VERSION_CODE_CONFLICT rechaza una carga. Un hallazgo con severidad ERROR describe un APK que los dispositivos pueden negarse a instalar o actualizar; un WARNING describe un APK que se instala con un riesgo que conviene conocer. Ninguno detiene la carga.

Una carga completada incluye checkReport en la respuesta de GET /status y en la respuesta de POST /complete cuando devuelve una sesión finalizada (por ejemplo, en una llamada repetida). Una carga rechazada por una comprobación tiene status failed e incluye también checkReport. El informe no está presente mientras la carga se procesa ni cuando la carga falló por otro motivo. El errorMessage de una carga fallida es genérico, así que lea en checkReport los hallazgos con blocking true para saber por qué una comprobación la rechazó.

Las versiones devueltas por las rutas de versiones de la biblioteca (la lista de versiones y una versión individual, documentadas en la guía de aplicaciones de la biblioteca) incluyen checkSummary: el status del último informe, errorCount (hallazgos ERROR) y warningCount (hallazgos WARNING). Es null para las versiones cargadas antes de que existieran las comprobaciones. La ruta check-report de la tabla anterior devuelve el informe completo de una versión, con la misma estructura que checkReport, y responde 404 cuando la versión no tiene ninguno. Igual que las rutas de carga, requiere X-API-Key con LIBRARY_MANAGE.

Campos del informe

  • status: PASSED (sin hallazgos), WARNINGS (la versión se creó y tiene hallazgos, incluidos hallazgos ERROR que no bloquearon) o BLOCKED (un hallazgo detuvo la carga y no se creó ninguna versión).
  • findings: primero los hallazgos ERROR. Cada uno tiene code, severity (ERROR o WARNING), params, blocking y affectedDeviceCount cuando el hallazgo se refiere a dispositivos. blocking es true solo para un hallazgo que detuvo la carga.
  • params: valores leídos del APK cargado o de las propias versiones de su empresa, como versiones, niveles de SDK, nombres de permisos y de ABI, y huellas de certificados. Sus claves dependen del código.
  • affectedDeviceCount: a cuántos dispositivos de su empresa se aplica el hallazgo. Para MIN_SDK_ABOVE_FLEET y TARGET_SDK_TOO_LOW, params.scope es INSTALLED cuando cuenta los dispositivos que tienen el paquete instalado, o COMPANY cuando ningún dispositivo lo tiene y cuenta todos los dispositivos de la empresa.
  • checkerVersion identifica el conjunto de comprobaciones que produjo el informe. createdAt es la hora en que se guardó el informe, en UTC.

Códigos de comprobación

Los códigos son estables: pueden añadirse nuevos y los existentes no se renombran. Lea severity y blocking de cada hallazgo en lugar de hacerlo de esta tabla.

CódigoSeveridadSignificado
SIGNATURE_INVALIDERROREl APK no está firmado o su firma no se verifica. params.issues enumera los problemas encontrados.
SIGNATURE_SCHEME_V1_ONLYERROREl APK tiene como objetivo Android 11 (API 30) o superior pero solo tiene una firma v1 (JAR), que Android 11 y posteriores rechazan. Fírmelo con APK Signature Scheme v2 o posterior. params.targetSdk.
SIGNING_KEY_CHANGEDERROREl certificado de firma difiere del de la versión anterior y ninguna prueba de rotación de clave v3 los vincula (params.previousVersionCode, previousCertSha256, newCertSha256), o difiere de los certificados instalados en affectedDeviceCount dispositivos (params.installedCertSha1, newCertSha1, como SHA-1 hexadecimal, tal como lo informan los dispositivos). Los dispositivos se niegan a actualizar una aplicación a una compilación firmada con otra clave.
TEST_ONLYWARNINGEl manifiesto establece android:testOnly="true", lo que marca una compilación de desarrollo. Sin params.
VERSION_CODE_CONFLICTERRORYa se cargó una versión con el mismo versionCode y entorno con un binario distinto. Es el único hallazgo que rechaza una carga. params.versionCode.
VERSION_DOWNGRADEWARNINGEl versionCode es inferior al de su última versión publicada (params.latestVersionCode) o inferior al que tienen instalado affectedDeviceCount dispositivos (params.installedVersionCode, el más alto instalado). Normalmente los dispositivos no se actualizan a un código de versión inferior.
MIN_SDK_ABOVE_FLEETWARNINGEl minSdk del APK es superior al nivel de API de Android de affectedDeviceCount dispositivos, que no pueden instalarlo. params.minSdk, scope.
TARGET_SDK_TOO_LOWWARNINGEl targetSdk del APK es inferior a 23, y Android 14 y posteriores se niegan a instalarlo. affectedDeviceCount cuenta los dispositivos del ámbito que ejecutan Android 14 o posterior. params.targetSdk, minimumTargetSdk, scope.
MISSING_ARM64WARNINGEl APK tiene bibliotecas nativas pero ninguna para arm64-v8a. params.abis enumera las ABI que tiene.
DEBUGGABLEWARNINGEl manifiesto establece android:debuggable="true". Sin params.
NEW_DANGEROUS_PERMISSIONSWARNINGEl APK solicita permisos de tiempo de ejecución (peligrosos) que la versión anterior no solicitaba. params.permissions, previousVersionCode.

Ejemplos de informes de comprobación

El primer ejemplo es la respuesta GET de estado de una carga completada que tiene hallazgos; se omiten otros campos como app. El segundo es una carga fallida que una comprobación rechazó. Los identificadores, recuentos, huellas y horas son ilustrativos.

Carga completada con hallazgos

{
  "status": "completed",
  "sessionId": "123e4567-e89b-12d3-a456-426614174000",
  "checkReport": {
    "status": "WARNINGS",
    "findings": [
      {
        "code": "SIGNING_KEY_CHANGED",
        "severity": "ERROR",
        "params": {
          "installedCertSha1": ["0123456789abcdef0123456789abcdef01234567"],
          "newCertSha1": ["89abcdef0123456789abcdef0123456789abcdef"]
        },
        "affectedDeviceCount": 2,
        "blocking": false
      },
      {
        "code": "MIN_SDK_ABOVE_FLEET",
        "severity": "WARNING",
        "params": { "minSdk": 28, "scope": "INSTALLED" },
        "affectedDeviceCount": 3,
        "blocking": false
      },
      {
        "code": "MISSING_ARM64",
        "severity": "WARNING",
        "params": { "abis": ["armeabi-v7a"] },
        "blocking": false
      }
    ],
    "checkerVersion": "2",
    "createdAt": "2026-11-03T09:30:00Z"
  }
}

Carga rechazada por una comprobación

{
  "status": "failed",
  "sessionId": "123e4567-e89b-12d3-a456-426614174000",
  "errorMessage": "Upload processing failed",
  "checkReport": {
    "status": "BLOCKED",
    "findings": [
      {
        "code": "VERSION_CODE_CONFLICT",
        "severity": "ERROR",
        "params": { "versionCode": 42 },
        "blocking": true
      }
    ],
    "checkerVersion": "2",
    "createdAt": "2026-11-03T09:30:00Z"
  }
}

checkSummary en una versión de una lista de versiones

{
  "versionCode": 42,
  "environment": "",
  "status": "PUBLISHED",
  "displayName": "Kiosk",
  "displayVersion": "1.4.2",
  "createdAt": "2026-11-03T09:30:00Z",
  "sha256": "0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef",
  "fileSizeBytes": 33554432,
  "tracks": ["prod"],
  "checkSummary": { "status": "WARNINGS", "errorCount": 1, "warningCount": 2 }
}

Canales y reintentos

Si se omite tracks, se usa prod. La primera versión de una aplicación nueva siempre incluye prod aunque se solicite otro canal. Las versiones posteriores pueden dirigirse a nombres de canal explícitos de un máximo de 10 caracteres. Esta API no tiene fase de borrador ni de aprobación.

Un checksum coincidente puede identificar una aplicación o versión existente, pero no asigna esa versión al canal solicitado en esta ejecución. No trate un resultado de duplicado de /check como una publicación correcta para un canal nuevo.

Reanude solo con el ID de sesión guardado de esta misma ejecución de publicación, con el mismo APK y los mismos canales. Haga GET al endpoint de partes cargadas con ese ID de sesión para reutilizar partes. Tras un resultado incierto de /complete, consulte esa sesión antes de reintentar la finalización. Una sesión cancelada o caducada requiere una carga nueva.

Publicar desde CI

En un runner de CI Linux con Python 3.11 o posterior y sha256sum, descargue el cargador de Python de biblioteca estándar y verifique su checksum fijado antes de ejecutarlo. Revise y versione la copia verificada en el repositorio de su pipeline para ejecuciones futuras. Proporcione NOMID_API_KEY mediante la inyección de entorno secreto de su plataforma de CI. El cargador termina con un código distinto de cero si falla o se agota el tiempo.

Descargar el cargador de Python

curl --fail --silent --show-error --output publish_apk.py \
  https://www.nomidmdm.com/developers/publish_apk.py &&
printf '%s  %s\n' '8d9ba2a4432e6e6db05ba2411bfb6bfa85c85a79d1ce480d97e0014f965f12d2' publish_apk.py | sha256sum --check --status &&
python3 publish_apk.py app-release.apk --track prod

Para reanudar una ejecución interrumpida, pase explícitamente su ID de sesión guardado con el mismo APK y los mismos canales:

python3 publish_apk.py app-release.apk --track prod \
  --session-id SESSION_ID_FROM_PREVIOUS_RUN

No ponga la clave de API en el comando, el repositorio ni los logs; no active el rastreo del shell. Mantenga las URL prefirmadas y sus encabezados requeridos fuera de los logs de CI. El éxito de la publicación y la instalación en dispositivos son comprobaciones distintas.

GitHub Actions: publicar un APK cuando se publica una release

Este workflow completo descarga un APK ya firmado adjunto a la release de GitHub que lo activó, verifica el cargador de Nomid y espera la publicación en la biblioteca de su empresa. Usa el recurso de la release, no un artefacto de compilación de Actions, y no compila ni firma su aplicación.

  1. Cree una clave de API de Nomid dedicada con LIBRARY_MANAGE. En su repositorio de GitHub, abra Settings → Secrets and variables → Actions y añada un secreto de repositorio llamado NOMID_API_KEY que contenga la clave. No se necesitan permisos de dispositivos ni de políticas.
  2. Guarde el YAML siguiente como .github/workflows/publish-to-nomid.yml en su repositorio antes de crear la etiqueta de la release, para que el commit etiquetado contenga el workflow. Cambie APK_ASSET_NAME por el nombre exacto del APK firmado que adjunte a cada release.
  3. Compile y firme el APK en su pipeline existente. Adjúntelo a una release de GitHub en borrador como app-release.apk (o el nombre de recurso que haya configurado) y después publique la release. El ejemplo también admite repositorios privados mediante el token integrado de GitHub con contents: read. El APK ya debe estar adjunto cuando se publique la release; de lo contrario, el paso de descarga falla.
name: Publish APK to Nomid

on:
  release:
    types: [published]

permissions:
  contents: read

concurrency:
  group: nomid-apk-${{ github.event.release.id }}
  cancel-in-progress: false

jobs:
  publish:
    runs-on: ubuntu-24.04
    timeout-minutes: 30
    env:
      APK_ASSET_NAME: app-release.apk
      NOMID_TRACK: ${{ github.event.release.prerelease && 'beta' || 'prod' }}
    steps:
      - name: Set up Python
        uses: actions/setup-python@ece7cb06caefa5fff74198d8649806c4678c61a1 # v6
        with:
          python-version: '3.12'

      - name: Download the signed APK from this release
        env:
          GH_TOKEN: ${{ github.token }}
          GH_REPO: ${{ github.repository }}
          RELEASE_TAG: ${{ github.event.release.tag_name }}
        shell: bash
        run: |
          set -euo pipefail
          gh release download --repo "$GH_REPO" \
            --pattern "$APK_ASSET_NAME" --output app-release.apk -- "$RELEASE_TAG"
          test -s app-release.apk

      - name: Download and verify the Nomid uploader
        shell: bash
        run: |
          set -euo pipefail
          curl --fail --silent --show-error --output publish_apk.py \
            https://www.nomidmdm.com/developers/publish_apk.py
          printf '%s  %s\n' '8d9ba2a4432e6e6db05ba2411bfb6bfa85c85a79d1ce480d97e0014f965f12d2' publish_apk.py \
            | sha256sum --check --status

      - name: Publish to the Nomid company library
        env:
          NOMID_API_KEY: ${{ secrets.NOMID_API_KEY }}
        shell: bash
        run: |
          set -euo pipefail
          if [[ -z "$NOMID_API_KEY" ]]; then
            echo "Configure the NOMID_API_KEY repository secret before publishing." >&2
            exit 1
          fi
          python3 publish_apk.py app-release.apk --track "$NOMID_TRACK" --timeout 1200

Las releases estables se publican en prod; las prereleases de GitHub se publican en beta. Para una aplicación nueva, Nomid siempre incluye prod en su primera publicación, incluso cuando se solicita beta. Por tanto, una prerelease de un paquete nuevo no queda aislada de prod. Las releases posteriores conservan la selección de canal explícita. Elija el tipo de release y el canal antes de publicar.

La clave de Nomid se proporciona solo al paso de publicación mediante su entorno. Una clave ausente, un APK ausente, una discrepancia de checksum, una publicación fallida o el agotamiento del tiempo de publicación hacen fallar el job. Un job correcto confirma la publicación en la tienda, no la instalación en dispositivos. Si se interrumpe, use el sessionId guardado con el mismo APK y canal como se describe arriba; volver a ejecutar todo el workflow inicia una carga nueva.

Si otro workflow publica la release de GitHub usando su GITHUB_TOKEN integrado, GitHub no activa este workflow de release. En ese caso, ejecute el cargador verificado directamente en el job de release existente después de adjuntar el APK, o publique la release usando un token de GitHub App o un token de acceso personal con el alcance adecuado. Mantenga el GH_TOKEN del paso de descarga separado de la clave de API de Nomid.

Documentación de GitHub: activar un workflow desde un workflow

Errores y recuperación

  • 401: la clave falta, no es válida, ha caducado o ha sido revocada. Compruebe la inyección de secretos del CI sin imprimir el valor.
  • 403: la clave no tiene LIBRARY_MANAGE. Cree una clave debidamente autorizada; los permisos de lectura o escritura no relacionados no bastan.
  • 404 para un ID de sesión desconocido en la empresa de la clave. Confirme el ID y la clave vinculada a la empresa; no cambie a otra empresa para buscarlo.
  • 400 por campos de solicitud no válidos, incluidos el tamaño en bytes o el MIME. Corrija la solicitud; no reintente sin cambios una entrada no válida.
  • 410: la sesión de carga ha caducado. Inicie una carga nueva; las URL de partes firmadas anteriormente no se pueden reutilizar.
  • 409 desde /abort significa que la carga ya se está procesando o está completada. Consulte su estado en lugar de intentar cancelarla.
  • 422 desde /complete significa que las partes suministradas no pueden formar el archivo. Compare la lista completa de partes y los ETags exactos con el endpoint de partes cargadas.
  • Ante un error transitorio de red o de S3, vuelva a firmar la URL de una parte caducada y reutilice las partes correctas. Ante un resultado incierto de /complete, consulte primero la misma sesión. Una carga finalizada que falló puede reintentarse por la misma vía de finalización mientras su sesión siga siendo válida.

Elija su horario

Cargando...
Abrir el calendario de reservas