Publicar versões de APK
Carregue um APK Android assinado diretamente a partir do seu pipeline de lançamento e acompanhe-o até aparecer na biblioteca da sua empresa.
Como funciona a publicação
Crie uma sessão de carregamento em várias partes com a API do Nomid, envie os bytes do APK diretamente para o S3 através de URLs de partes de curta duração, conclua a sessão e consulte periodicamente o resultado final do processamento.
LIBRARY_MANAGE abrange agora os carregamentos de APK e a gestão de aplicações, incluindo ler, editar, ocultar e restaurar aplicações e versões. As APIs públicas para ficheiros, contactos e outros recursos da biblioteca continuam indisponíveis. Não concede atribuição de políticas, comandos de dispositivo, aprovisionamento nem administração da conta.
Leia e gira as aplicações da biblioteca da empresa e publique versões de APK assinadas. LIBRARY_MANAGE é uma permissão sensível, ao nível da empresa, para carregamentos de APK e gestão de aplicações. Não concede atribuição de políticas, comandos de dispositivo, aprovisionamento nem operações da conta.
Uma publicação concluída coloca a versão na biblioteca e pode acionar a distribuição já existente para as políticas que utilizam a sua aplicação e o seu track. Não confirma que algum dispositivo tenha instalado a versão.
Antes de começar
- Crie uma chave de API dedicada com LIBRARY_MANAGE. Um utilizador do Portal que crie essa permissão precisa de API_KEY_MANAGE, âmbito de políticas da empresa sem restrições, LIBRARY_VIEW e LIBRARY_EDIT. A permissão sensível vem desmarcada por predefinição; as chaves existentes não a recebem automaticamente.
- Atribua a chave apenas a automatizações de confiança que precisem destas operações da biblioteca ao nível da empresa. LIBRARY_MANAGE permite carregamentos de APK e gestão de aplicações, incluindo o acesso a sessões de carregamento iniciadas por outras chaves ou utilizadores autorizados da empresa; guarde a chave no cofre de segredos do seu CI e rode-a ou revogue-a quando o acesso mudar.
- Utilize Python 3.11 ou posterior para o carregador de exemplo. Compile e assine o APK com a identidade de assinatura pretendida e um código de versão Android adequado antes do carregamento. A API não assina APKs nem publica no Google Play.
- Envie X-API-Key para a API do Nomid. A chave validada determina a empresa; os pedidos não têm parâmetro de tenant nem de empresa. Não é necessária qualquer permissão DEVICES_READ ou POLICIES_READ para a publicação de APK.
Endpoints
https://api.nomid.tech/emm/api/v1
Todas as oito rotas abaixo exigem X-API-Key com LIBRARY_MANAGE; uma sessão de carregamento pertence à empresa da chave.
| Método | Endpoint | Descrição |
|---|---|---|
| POST | /library/apps/uploads/check | Verifique a elegibilidade, o conteúdo duplicado e uma sessão retomável existente. Um duplicado é meramente informativo. |
| POST | /library/apps/uploads/start | Inicie um carregamento e receba sessionId, partSize e expiresAt. |
| POST | /library/apps/uploads/parts/sign | Obtenha um URL PUT direto para o S3, de curta duração, e requiredHeaders para uma parte. |
| GET | /library/apps/uploads/parts/{sessionId} | Liste as partes já carregadas que podem ser reutilizadas e os respetivos ETags exatos da mesma sessão. |
| POST | /library/apps/uploads/complete | Envie a lista ordenada de partes e, opcionalmente, tracks para montar e processar o APK. |
| GET | /library/apps/uploads/status/{sessionId} | Consulte a sessão periodicamente até status ser completed ou failed. |
| DELETE | /library/apps/uploads/abort/{sessionId} | Cancele uma sessão por concluir; as sessões em processamento ou concluídas não podem ser canceladas. |
| GET | /library/apps/{packageName}/versions/{versionCode}/check-report | Leia o relatório de verificação de carregamento mais recente de uma versão. Requer o packageName e o versionCode da versão e, quando não estiver vazio, o respetivo ambiente. |
Sequência de carregamento
- POST /check com o nome do ficheiro, o tamanho real positivo em bytes, o mime aceite e o SHA-256 do APK. Analise allowed e reason; duplicateOf e existingSessionId podem estar presentes.
- POST /start com os mesmos campos. Guarde de forma segura o sessionId e o partSize devolvidos, para que esta execução possa ser retomada se for interrompida.
- Para cada parte em falta, POST /parts/sign com sessionId e um partNumber de 1 a 10000.
- Divida o ficheiro em blocos sequenciais de partSize bytes; apenas o último bloco pode ser mais curto. Envie cada bloco com PUT para o respetivo URL assinado, usando requiredHeaders, e conserve o cabeçalho de resposta ETag exatamente como foi devolvido.
- POST /complete com sessionId, todos os pares partNumber/etag e, opcionalmente, tracks. A conclusão inicia o processamento do APK em segundo plano; a resposta imediata ainda pode indicar processing.
- Faça GET ao endpoint de estado com o ID da sessão até o status em minúsculas ser completed ou failed. As respostas de sessões concluídas incluem app; as de sessões falhadas incluem um errorMessage. Os outros estados não são um sucesso de publicação.
O tamanho tem de ser superior a zero e estar dentro do limite de carregamento configurado. mime é application/vnd.android.package-archive ou application/octet-stream; sha256 é o resumo hexadecimal de 64 caracteres em minúsculas dos bytes do APK. Aplicam-se os prazos de expiração da sessão e dos URLs assinados.
O URL assinado destina-se ao S3, não à API do Nomid: envie apenas requiredHeaders com o PUT, nunca X-API-Key. Não registe o URL assinado em logs. Se expirar, assine essa parte novamente.
Exemplos de pedidos e respostas
Estes exemplos mostram os nomes dos campos e os valores de status em minúsculas. Os identificadores, o resumo, a hora, o tamanho das partes, os ETags e o URL são ilustrativos; utilize os valores devolvidos para a sua própria sessão.
Pedido POST /check ou /start
{
"filename": "app-release.apk",
"size": 33554432,
"mime": "application/vnd.android.package-archive",
"sha256": "0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef"
} Resposta elegível de POST /check
{
"allowed": true,
"recommendedPartSize": 16777216
} Resposta de POST /start
{
"sessionId": "123e4567-e89b-12d3-a456-426614174000",
"partSize": 16777216,
"expiresAt": "2026-10-28T12:00:00Z"
} Pedido POST /parts/sign
{
"sessionId": "123e4567-e89b-12d3-a456-426614174000",
"partNumber": 1
} Resposta de POST /parts/sign
{
"url": "https://<presigned-part-url>",
"requiredHeaders": {}
} Resposta GET das partes carregadas
{
"status": "uploading",
"partSize": 16777216,
"parts": [{ "partNumber": 1, "etag": "\"example-etag\"" }]
} Pedido POST /complete
{
"sessionId": "123e4567-e89b-12d3-a456-426614174000",
"parts": [
{ "partNumber": 1, "etag": "\"example-etag-1\"" },
{ "partNumber": 2, "etag": "\"example-etag-2\"" }
],
"tracks": ["prod"]
} Resposta de processamento de POST /complete
{
"status": "processing",
"sessionId": "123e4567-e89b-12d3-a456-426614174000"
} requiredHeaders pode conter cabeçalhos exigidos pela assinatura do S3. Aplique o mapa devolvido exatamente como está, incluindo quaisquer valores, e capture o cabeçalho de resposta ETag de cada PUT bem-sucedido.
Verificações de carregamento
Cada carregamento é verificado antes de a respetiva versão ser criada: a assinatura e o manifesto do APK, as versões anteriores do mesmo pacote da sua empresa e os dispositivos da sua empresa (os níveis de API Android e as aplicações instaladas, com os respetivos certificados de assinatura). O resultado é um relatório de verificação que pode ler através da API.
As verificações apenas reportam resultados. Os resultados são guardados e devolvidos, e o carregamento é concluído na mesma; só VERSION_CODE_CONFLICT rejeita um carregamento. Um resultado com severidade ERROR descreve um APK que os dispositivos podem recusar instalar ou atualizar; um WARNING descreve um APK que se instala com um risco que convém conhecer. Nenhum deles impede o carregamento.
Um carregamento concluído inclui checkReport na resposta de GET /status e na resposta de POST /complete quando esta devolve uma sessão terminada (uma chamada repetida, por exemplo). Um carregamento rejeitado por uma verificação tem status failed e também inclui checkReport. O relatório não existe enquanto o carregamento está em processamento, nem quando o carregamento falhou por outro motivo. O errorMessage de um carregamento falhado é genérico, por isso leia os resultados com blocking true em checkReport para saber por que motivo uma verificação o rejeitou.
As versões devolvidas pelas rotas de versões da biblioteca (a lista de versões e uma versão individual, documentadas no guia de aplicações da biblioteca) incluem checkSummary: o estado do relatório mais recente, errorCount (resultados ERROR) e warningCount (resultados WARNING). É null nas versões carregadas antes de as verificações existirem. A rota check-report da tabela acima devolve o relatório completo de uma versão, com o mesmo formato de checkReport, e responde 404 quando a versão não tem nenhum. Tal como as rotas de carregamento, exige X-API-Key com LIBRARY_MANAGE.
Campos do relatório
- status: PASSED (sem resultados), WARNINGS (a versão foi criada e tem resultados, incluindo resultados ERROR que não bloquearam) ou BLOCKED (um resultado impediu o carregamento e não foi criada nenhuma versão).
- findings: primeiro os resultados ERROR. Cada um tem code, severity (ERROR ou WARNING), params, blocking e affectedDeviceCount quando o resultado diz respeito a dispositivos. blocking é true apenas num resultado que impediu o carregamento.
- params: valores lidos do APK carregado ou das versões da sua própria empresa, como versões, níveis de SDK, nomes de permissões e de ABIs, e impressões digitais de certificados. As suas chaves dependem do código.
- affectedDeviceCount: a quantos dispositivos da sua empresa o resultado se aplica. Para MIN_SDK_ABOVE_FLEET e TARGET_SDK_TOO_LOW, params.scope é INSTALLED quando conta os dispositivos que têm o pacote instalado, ou COMPANY quando nenhum dispositivo o tem e conta todos os dispositivos da empresa.
- checkerVersion identifica o conjunto de verificações que produziu o relatório. createdAt é a hora a que o relatório foi guardado, em UTC.
Códigos de verificação
Os códigos são estáveis: podem ser acrescentados novos e os existentes não são renomeados. Leia severity e blocking em cada resultado, em vez de os ler nesta tabela.
| Código | Severidade | Significado |
|---|---|---|
| SIGNATURE_INVALID | ERROR | O APK não está assinado ou a sua assinatura não é validada. params.issues lista os problemas encontrados. |
| SIGNATURE_SCHEME_V1_ONLY | ERROR | O APK tem como destino o Android 11 (API 30) ou superior, mas tem apenas uma assinatura v1 (JAR), que o Android 11 e posteriores recusam. Assine-o com o APK Signature Scheme v2 ou posterior. params.targetSdk. |
| SIGNING_KEY_CHANGED | ERROR | O certificado de assinatura é diferente do da versão anterior e nenhuma prova de rotação de chave v3 os liga (params.previousVersionCode, previousCertSha256, newCertSha256), ou é diferente dos certificados instalados em affectedDeviceCount dispositivos (params.installedCertSha1, newCertSha1, em hexadecimal SHA-1, tal como os dispositivos o comunicam). Os dispositivos recusam atualizar uma aplicação para uma compilação assinada com outra chave. |
| TEST_ONLY | WARNING | O manifesto define android:testOnly="true", o que assinala uma compilação de desenvolvimento. Sem params. |
| VERSION_CODE_CONFLICT | ERROR | Já foi carregada uma versão com o mesmo versionCode e ambiente com um binário diferente. Este é o único resultado que rejeita um carregamento. params.versionCode. |
| VERSION_DOWNGRADE | WARNING | O versionCode é inferior ao da sua última versão publicada (params.latestVersionCode), ou inferior ao que affectedDeviceCount dispositivos têm instalado (params.installedVersionCode, o mais alto instalado). Normalmente, os dispositivos não atualizam para um código de versão inferior. |
| MIN_SDK_ABOVE_FLEET | WARNING | O minSdk do APK é superior ao nível de API Android de affectedDeviceCount dispositivos, que não o conseguem instalar. params.minSdk, scope. |
| TARGET_SDK_TOO_LOW | WARNING | O targetSdk do APK é inferior a 23, e o Android 14 e posteriores recusam instalá-lo. affectedDeviceCount conta os dispositivos abrangidos que executam o Android 14 ou posterior. params.targetSdk, minimumTargetSdk, scope. |
| MISSING_ARM64 | WARNING | O APK tem bibliotecas nativas, mas nenhuma para arm64-v8a. params.abis lista as ABIs que tem. |
| DEBUGGABLE | WARNING | O manifesto define android:debuggable="true". Sem params. |
| NEW_DANGEROUS_PERMISSIONS | WARNING | O APK pede permissões de tempo de execução (perigosas) que a versão anterior não pedia. params.permissions, previousVersionCode. |
Exemplos de relatório de verificação
O primeiro exemplo é a resposta GET de estado de um carregamento concluído que tem resultados; outros campos, como app, foram omitidos. O segundo é um carregamento falhado que uma verificação rejeitou. Os identificadores, as contagens, as impressões digitais e as horas são ilustrativos.
Carregamento concluído com resultados
{
"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"
}
} Carregamento rejeitado por uma verificação
{
"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 numa versão de uma lista de versões
{
"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 }
} Tracks e novas tentativas
Se omitir tracks, é utilizado prod. A primeira versão de uma nova aplicação inclui sempre prod, mesmo que seja pedido outro track. As versões seguintes podem ter como destino nomes de track explícitos com um máximo de 10 caracteres. Esta API não tem fase de rascunho nem de aprovação.
Um checksum coincidente pode identificar uma aplicação/versão existente, mas não atribui essa versão ao track pedido por esta execução. Não trate um resultado duplicado de /check como sucesso de publicação para um novo track.
Retome apenas o ID de sessão guardado desta mesma execução de publicação, com o mesmo APK e os mesmos tracks. Faça GET ao endpoint das partes carregadas com esse ID de sessão para reutilizar partes. Após um resultado incerto de /complete, consulte essa sessão antes de repetir a conclusão. Uma sessão cancelada ou expirada exige um novo carregamento.
Publicar a partir do CI
Num runner de CI Linux com Python 3.11 ou posterior e sha256sum, transfira o carregador Python que usa apenas a biblioteca padrão e verifique o respetivo checksum fixado antes de o executar. Reveja e versione a cópia verificada no repositório do seu pipeline para as execuções futuras. Forneça NOMID_API_KEY através da injeção de variáveis de ambiente secretas da sua plataforma de CI. O carregador termina com código diferente de zero em caso de falha ou de limite de tempo.
Transferir o carregador 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 retomar uma execução interrompida, indique explicitamente o respetivo ID de sessão guardado, com o mesmo APK e os mesmos tracks:
python3 publish_apk.py app-release.apk --track prod \
--session-id SESSION_ID_FROM_PREVIOUS_RUN Não coloque a chave de API no comando, no repositório nem nos logs; não ative o rastreio da shell. Mantenha os URLs pré-assinados e os respetivos cabeçalhos obrigatórios fora dos logs do CI. O sucesso da publicação e a instalação nos dispositivos são verificações distintas.
GitHub Actions: publicar um APK quando uma versão é publicada
Este workflow completo transfere um APK já assinado, anexado à versão do GitHub que o acionou, verifica o carregador do Nomid e aguarda pela publicação na biblioteca da sua empresa. Utiliza o ativo da versão, não um artefacto de compilação do Actions, e não compila nem assina a sua aplicação.
- Crie uma chave de API do Nomid dedicada com LIBRARY_MANAGE. No seu repositório do GitHub, abra Settings → Secrets and variables → Actions e adicione um segredo de repositório com o nome NOMID_API_KEY que contenha a chave. Não são necessárias permissões de dispositivos nem de políticas.
- Guarde o YAML abaixo como .github/workflows/publish-to-nomid.yml no seu repositório antes de criar a tag da versão, para que o commit marcado contenha o workflow. Altere APK_ASSET_NAME para o nome exato do APK assinado que anexa a cada versão.
- Compile e assine o APK no seu pipeline existente. Anexe-o a uma versão do GitHub em rascunho como app-release.apk (ou o nome de ativo que configurou) e depois publique a versão. O exemplo também suporta repositórios privados através do token integrado do GitHub com contents: read. O APK tem de estar já anexado quando a versão é publicada; caso contrário, o passo de transferência falha.
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 As versões estáveis são publicadas em prod; as pré-versões do GitHub são publicadas em beta. Numa nova aplicação, o Nomid inclui sempre prod na primeira publicação, mesmo quando é pedido beta. Por isso, uma pré-versão de um novo pacote não fica isolada de prod. As versões seguintes preservam a seleção explícita do track. Escolha o tipo de versão e o track antes de publicar.
A chave do Nomid é fornecida apenas ao passo de publicação, através do respetivo ambiente. Uma chave em falta, um APK em falta, uma divergência de checksum, uma publicação falhada ou um limite de tempo da publicação fazem o job falhar. Um job bem-sucedido confirma a publicação na loja, não a instalação nos dispositivos. Se for interrompido, utilize o sessionId guardado com o mesmo APK e track, conforme descrito acima; voltar a executar o workflow inteiro inicia um novo carregamento.
Se outro workflow publicar a versão do GitHub com o seu GITHUB_TOKEN integrado, o GitHub não aciona este workflow de versão. Nesse caso, execute o carregador verificado diretamente no job de versão existente depois de anexar o APK, ou publique a versão com um token de GitHub App ou um token de acesso pessoal com o âmbito adequado. Mantenha o GH_TOKEN do passo de transferência separado da chave de API do Nomid.
Documentação do GitHub: acionar um workflow a partir de um workflow
Erros e recuperação
- 401: a chave não existe, é inválida, expirou ou foi revogada. Verifique a injeção de segredos no CI sem imprimir o valor.
- 403: a chave não tem LIBRARY_MANAGE. Crie uma chave devidamente autorizada; permissões de leitura ou escrita não relacionadas não são suficientes.
- 404 para um ID de sessão desconhecido na empresa da chave. Confirme o ID e a chave associada à empresa; não mude para outra empresa para o procurar.
- 400 para campos de pedido inválidos, incluindo o tamanho em bytes ou o MIME. Corrija o pedido; não repita sem alterações um pedido inválido.
- 410: a sessão de carregamento expirou. Inicie um novo carregamento; os URLs de partes assinados anteriormente não podem ser reutilizados.
- 409 em /abort significa que o carregamento já está em processamento ou concluído. Consulte o respetivo estado em vez de tentar cancelá-lo.
- 422 em /complete significa que as partes fornecidas não conseguem formar o ficheiro. Compare a lista completa de partes e os ETags exatos com o endpoint das partes carregadas.
- Perante um erro transitório de rede ou do S3, assine novamente o URL de uma parte expirada e reutilize as partes bem-sucedidas. Perante um resultado incerto de /complete, consulte primeiro a mesma sessão. Um carregamento finalizado que falhou pode ser repetido pelo mesmo caminho de conclusão enquanto a sua sessão for válida.