Publicar versões de APK
Envie um APK Android assinado diretamente do seu pipeline de release e acompanhe-o até que ele apareça na biblioteca da sua empresa.
Como funciona a publicação
Crie uma sessão de upload multipart com a API do Nomid, envie os bytes do APK diretamente para o S3 usando URLs de partes de curta duração, conclua a sessão e consulte o resultado final do processamento.
LIBRARY_MANAGE agora cobre uploads de APK e o gerenciamento de aplicativos, incluindo leitura, edição, ocultação e restauração de aplicativos e versões. As APIs públicas para arquivos, contatos e outros recursos da biblioteca ainda não estão disponíveis. Essa permissão não concede atribuição de políticas, comandos de dispositivo, provisionamento nem administração da conta.
Leia e gerencie os aplicativos da biblioteca da empresa e publique versões de APK assinadas. LIBRARY_MANAGE é uma permissão sensível, válida para toda a empresa, para uploads de APK e gerenciamento de aplicativos. Ela não concede atribuição de políticas, comandos de dispositivo, provisionamento nem operações da conta.
Uma publicação concluída coloca a versão na biblioteca e pode acionar a distribuição existente para as políticas que usam esse aplicativo e essa faixa. Ela 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 usuário do portal que conceda essa permissão precisa de API_KEY_MANAGE, escopo de política da empresa sem restrições, LIBRARY_VIEW e LIBRARY_EDIT. A permissão sensível vem desmarcada por padrão; as chaves existentes não a recebem automaticamente.
- Conceda a chave apenas a automações confiáveis que precisem dessas operações de biblioteca em toda a empresa. LIBRARY_MANAGE permite uploads de APK e o gerenciamento de aplicativos, incluindo o acesso a sessões de upload iniciadas por outras chaves ou usuários autorizados da empresa; mantenha a chave no cofre de segredos do seu CI e gire ou revogue a chave quando o acesso mudar.
- Use Python 3.11 ou mais recente para o uploader de exemplo. Compile e assine o APK com a identidade de assinatura pretendida e um código de versão Android adequado antes do upload. A API não assina APKs nem publica no Google Play.
- Envie X-API-Key para a API do Nomid. A chave validada define a empresa; as requisições não têm parâmetro de tenant ou de empresa. Nenhuma permissão DEVICES_READ ou POLICIES_READ é necessária 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 upload 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. Uma duplicata é apenas informativa. |
| POST | /library/apps/uploads/start | Inicie um upload e receba sessionId, partSize e expiresAt. |
| POST | /library/apps/uploads/parts/sign | Obtenha uma URL PUT direta para o S3, de curta duração, e requiredHeaders para uma parte. |
| GET | /library/apps/uploads/parts/{sessionId} | Liste as partes já enviadas que podem ser reutilizadas e seus ETags exatos para a mesma sessão. |
| POST | /library/apps/uploads/complete | Envie a lista ordenada de partes e, opcionalmente, as tracks para montar e processar o APK. |
| GET | /library/apps/uploads/status/{sessionId} | Consulte a sessão até que status seja completed ou failed. |
| DELETE | /library/apps/uploads/abort/{sessionId} | Cancele uma sessão não finalizada; 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 upload mais recente de uma versão. Ele exige packageName e versionCode da versão e o ambiente dela, quando este não estiver vazio. |
Sequência de upload
- POST /check com filename, o tamanho real em bytes (positivo), o mime aceito e o SHA-256 do APK. Inspecione allowed e reason; duplicateOf e existingSessionId podem estar presentes.
- POST /start com os mesmos campos. Guarde com segurança o sessionId e o partSize retornados, para que esta execução possa ser retomada se for interrompida.
- Para cada parte ausente, faça POST /parts/sign com sessionId e um partNumber de 1 a 10000.
- Divida o arquivo em blocos sequenciais de partSize bytes; somente o último bloco pode ser menor. Faça PUT de cada bloco na respectiva URL assinada usando requiredHeaders e guarde o cabeçalho de resposta ETag exatamente como retornado.
- POST /complete com sessionId, cada par 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 no endpoint de status com o ID da sessão até que o status, em minúsculas, seja completed ou failed. As respostas concluídas incluem app; as respostas com falha incluem um errorMessage. Os outros estados não indicam sucesso na publicação.
O tamanho deve ser maior que zero e estar dentro do limite de upload configurado. mime é application/vnd.android.package-archive ou application/octet-stream; sha256 é o digest hexadecimal em minúsculas, com 64 caracteres, dos bytes do APK. Aplicam-se as expirações da sessão e da URL assinada.
A URL assinada é do S3, não da API do Nomid: envie apenas requiredHeaders com o PUT, nunca X-API-Key. Não registre a URL assinada em logs. Se ela expirar, assine essa parte novamente.
Exemplos de requisição e resposta
Estes exemplos mostram os nomes dos campos e os valores de status em minúsculas. Identificadores, digest, horário, tamanho de parte, ETags e URL são ilustrativos; use os valores retornados para a sua própria sessão.
Requisição POST /check ou /start
{
"filename": "app-release.apk",
"size": 33554432,
"mime": "application/vnd.android.package-archive",
"sha256": "0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef"
} Resposta de POST /check (elegível)
{
"allowed": true,
"recommendedPartSize": 16777216
} Resposta de POST /start
{
"sessionId": "123e4567-e89b-12d3-a456-426614174000",
"partSize": 16777216,
"expiresAt": "2026-10-28T12:00:00Z"
} Requisição POST /parts/sign
{
"sessionId": "123e4567-e89b-12d3-a456-426614174000",
"partNumber": 1
} Resposta de POST /parts/sign
{
"url": "https://<presigned-part-url>",
"requiredHeaders": {}
} Resposta de GET das partes enviadas
{
"status": "uploading",
"partSize": 16777216,
"parts": [{ "partNumber": 1, "etag": "\"example-etag\"" }]
} Requisição 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 POST /complete em processamento
{
"status": "processing",
"sessionId": "123e4567-e89b-12d3-a456-426614174000"
} requiredHeaders pode conter cabeçalhos exigidos pela assinatura do S3. Aplique o mapa retornado exatamente como está, inclusive os valores, e capture o cabeçalho de resposta ETag de cada PUT bem-sucedido.
Verificações de upload
Todo upload é verificado antes de a sua versão ser criada: a assinatura e o manifesto do APK, as versões anteriores do mesmo pacote na sua empresa e os dispositivos da sua empresa (seus níveis de API do Android e os aplicativos instalados neles, com seus certificados de assinatura). O resultado é um relatório de verificação que você pode ler pela API.
As verificações apenas relatam descobertas. As descobertas são salvas e retornadas, e o upload ainda é concluído; somente VERSION_CODE_CONFLICT rejeita um upload. Uma descoberta com severidade ERROR descreve um APK que os dispositivos podem se recusar a instalar ou atualizar; uma WARNING descreve um APK que é instalado com um risco que vale a pena conhecer. Nenhuma das duas interrompe o upload.
Um upload concluído inclui checkReport na resposta de GET /status e, na resposta de POST /complete, quando ela retorna uma sessão finalizada (uma chamada repetida, por exemplo). Um upload rejeitado por uma verificação tem status failed e também inclui checkReport. O relatório não está presente enquanto o upload está em processamento, nem quando o upload falhou por outro motivo. O errorMessage de um upload com falha é genérico; por isso, leia as descobertas com blocking true em checkReport para saber por que uma verificação o rejeitou.
As versões retornadas pelas rotas de versões da biblioteca (a lista de versões e uma versão individual, documentadas no guia de aplicativos da biblioteca) incluem checkSummary: o status do relatório mais recente, errorCount (descobertas ERROR) e warningCount (descobertas WARNING). Ele é null para versões enviadas antes de as verificações existirem. A rota check-report da tabela acima retorna o relatório completo de uma versão, no mesmo formato de checkReport, e responde 404 quando a versão não tem relatório. Assim como as rotas de upload, ela exige X-API-Key com LIBRARY_MANAGE.
Campos do relatório
- status: PASSED (sem descobertas), WARNINGS (a versão foi criada e tem descobertas, inclusive descobertas ERROR que não bloquearam) ou BLOCKED (uma descoberta interrompeu o upload e nenhuma versão foi criada).
- findings: as descobertas ERROR primeiro. Cada uma tem code, severity (ERROR ou WARNING), params, blocking e affectedDeviceCount quando a descoberta diz respeito a dispositivos. blocking é true somente para uma descoberta que interrompeu o upload.
- params: valores lidos do APK enviado ou das versões da própria empresa, como versões, níveis de SDK, nomes de permissões e de ABIs e impressões digitais de certificados. Suas chaves dependem do código.
- affectedDeviceCount: a quantos dispositivos da sua empresa a descoberta 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 ele conta todos os dispositivos da empresa.
- checkerVersion identifica o conjunto de verificações que gerou o relatório. createdAt é o horário em que o relatório foi salvo, em UTC.
Códigos de verificação
Os códigos são estáveis: novos podem ser adicionados, e os existentes não são renomeados. Leia severity e blocking em cada descoberta, e não nesta tabela.
| Código | Severidade | Significado |
|---|---|---|
| SIGNATURE_INVALID | ERROR | O APK não está assinado ou sua assinatura não é válida. params.issues lista os problemas encontrados. |
| SIGNATURE_SCHEME_V1_ONLY | ERROR | O APK tem como alvo 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 vincula (params.previousVersionCode, previousCertSha256, newCertSha256), ou é diferente dos certificados instalados em affectedDeviceCount dispositivos (params.installedCertSha1, newCertSha1, em hexadecimal SHA-1, do modo como os dispositivos os informam). Os dispositivos se recusam a atualizar um aplicativo para uma build assinada com outra chave. |
| TEST_ONLY | WARNING | O manifesto define android:testOnly="true", o que marca uma build de desenvolvimento. Sem params. |
| VERSION_CODE_CONFLICT | ERROR | Uma versão com o mesmo versionCode e o mesmo ambiente já foi enviada com um binário diferente. Esta é a única descoberta que rejeita um upload. params.versionCode. |
| VERSION_DOWNGRADE | WARNING | O versionCode é menor que o da sua versão publicada mais recente (params.latestVersionCode) ou menor que o instalado em affectedDeviceCount dispositivos (params.installedVersionCode, o mais alto instalado). Normalmente, os dispositivos não são atualizados para um código de versão menor. |
| MIN_SDK_ABOVE_FLEET | WARNING | O minSdk do APK está acima do nível de API do Android de affectedDeviceCount dispositivos, que não conseguem instalá-lo. params.minSdk, scope. |
| TARGET_SDK_TOO_LOW | WARNING | O targetSdk do APK está abaixo de 23, e o Android 14 e posteriores se recusam a instalá-lo. affectedDeviceCount conta os dispositivos no escopo 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 ele tem. |
| DEBUGGABLE | WARNING | O manifesto define android:debuggable="true". Sem params. |
| NEW_DANGEROUS_PERMISSIONS | WARNING | O APK solicita permissões de tempo de execução (perigosas) que a versão anterior não solicitava. params.permissions, previousVersionCode. |
Exemplos de relatório de verificação
O primeiro exemplo é a resposta de GET status de um upload concluído que tem descobertas; outros campos, como app, foram omitidos. O segundo é um upload com falha rejeitado por uma verificação. Identificadores, contagens, impressões digitais e horários são ilustrativos.
Upload concluído com descobertas
{
"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"
}
} Upload 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 em uma 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
Omitir tracks usa prod. A primeira versão de um novo aplicativo sempre inclui prod, mesmo que outra track seja solicitada. As versões seguintes podem ter como alvo nomes de track explícitos de no máximo 10 caracteres. Esta API não tem etapa de rascunho nem de aprovação.
Um checksum correspondente pode identificar um aplicativo/versão existente, mas isso não atribui essa versão à track solicitada por esta execução. Não trate um resultado de duplicata de /check como sucesso de publicação para uma nova track.
Retome apenas o ID de sessão salvo desta mesma execução de publicação, com o mesmo APK e as mesmas tracks. Faça GET no endpoint de partes enviadas com esse ID de sessão para reutilizar as partes. Após um resultado incerto de /complete, consulte essa sessão antes de tentar a conclusão novamente. Uma sessão cancelada ou expirada exige um novo upload.
Publicar a partir do CI
Em um runner de CI Linux com Python 3.11 ou mais recente e sha256sum, baixe o uploader em Python que usa apenas a biblioteca padrão e verifique o checksum fixado antes de executá-lo. Revise e versione a cópia verificada no repositório do seu pipeline para as execuções futuras. Forneça NOMID_API_KEY por meio da injeção de variáveis de ambiente secretas da sua plataforma de CI. O uploader encerra com código diferente de zero em caso de falha ou timeout.
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, informe explicitamente o ID de sessão salvo, com o mesmo APK e as mesmas 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 rastreamento do shell. Mantenha as URLs pré-assinadas e seus 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 release for publicada
Este workflow completo baixa um APK já assinado, anexado à release do GitHub que o acionou, verifica o uploader do Nomid e aguarda a publicação na biblioteca da sua empresa. Ele usa o asset da release, não um artifact de build do Actions, e não compila nem assina o seu aplicativo.
- Crie uma chave de API dedicada do Nomid com LIBRARY_MANAGE. No seu repositório do GitHub, abra Settings → Secrets and variables → Actions e adicione um segredo de repositório chamado NOMID_API_KEY contendo a chave. Nenhuma permissão de dispositivo ou de política é necessária.
- Salve o YAML abaixo como .github/workflows/publish-to-nomid.yml no seu repositório antes de criar a tag da release, para que o commit com a tag contenha o workflow. Altere APK_ASSET_NAME para o nome exato do APK assinado que você anexa a cada release.
- Compile e assine o APK no seu pipeline existente. Anexe-o a uma release do GitHub em rascunho como app-release.apk (ou o nome de asset que você configurou) e depois publique a release. O exemplo também oferece suporte a repositórios privados por meio do token integrado do GitHub com contents: read. O APK já deve estar anexado quando a release for publicada; caso contrário, a etapa de download 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 releases estáveis são publicadas em prod; os prereleases do GitHub são publicados em beta. Para um novo aplicativo, o Nomid sempre inclui prod na primeira publicação, mesmo quando beta é solicitado. Um prerelease de um novo pacote, portanto, não fica isolado de prod. As releases seguintes preservam a seleção de track explícita. Escolha o tipo de release e a track antes de publicar.
A chave do Nomid é fornecida somente à etapa de publicação, por meio do ambiente dela. Uma chave ausente, um APK ausente, uma divergência de checksum, uma falha de publicação ou um timeout de publicação fazem o job falhar. Um job bem-sucedido confirma a publicação na loja, não a instalação nos dispositivos. Se houver interrupção, use o sessionId salvo com o mesmo APK e a mesma track, como descrito acima; executar o workflow inteiro novamente inicia um novo upload.
Se outro workflow publicar a release do GitHub usando o GITHUB_TOKEN integrado, o GitHub não aciona este workflow de release. Nesse caso, execute o uploader verificado diretamente no job de release existente após anexar o APK, ou publique a release usando um token de GitHub App ou um token de acesso pessoal com o escopo adequado. Mantenha o GH_TOKEN da etapa de download 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 está ausente, é inválida, expirou ou foi revogada. Verifique a injeção de segredos do CI sem imprimir o valor.
- 403: a chave não tem LIBRARY_MANAGE. Crie uma chave com a autorização adequada; 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 vinculada à empresa; não troque para outra empresa para procurá-lo.
- 400 para campos de requisição inválidos, inclusive tamanho em bytes ou MIME. Corrija a requisição; não repita sem alteração uma entrada inválida.
- 410: a sessão de upload expirou. Inicie um novo upload; as URLs de partes assinadas anteriormente não podem ser reutilizadas.
- 409 em /abort significa que o upload já está em processamento ou concluído. Verifique o status dele em vez de tentar cancelá-lo.
- 422 em /complete significa que as partes enviadas não conseguem formar o arquivo. Compare a lista completa de partes e os ETags exatos com o endpoint de partes enviadas.
- Em caso de erro transitório de rede ou do S3, assine novamente a URL de uma parte expirada e reutilize as partes bem-sucedidas. Em caso de resultado incerto de /complete, consulte primeiro a mesma sessão. Um upload finalizado com falha pode ser repetido pelo mesmo caminho de conclusão enquanto a sessão for válida.