Publish APK releases

Upload a signed Android APK directly from your release pipeline and follow it until it appears in your company library.

How publication works

Create a multipart upload session with the Nomid API, send APK bytes directly to S3 using short-lived part URLs, complete the session, and poll for the final processing result.

The first public LIBRARY_MANAGE API covers APK uploads. The permission is company-wide and intentionally covers future library management operations too; it does not yet provide public endpoints to edit or delete apps, files, contacts or other library content. It does not grant policy editing, device commands, provisioning or account administration.

Publish signed APK releases to the company library. LIBRARY_MANAGE is a sensitive, company-wide grant intended for future library actions too; currently the public API exposes APK uploads only. It does not grant device, policy, provisioning or account operations.

A completed publication places the release in the library and may trigger existing distribution for policies using its app and track. It does not confirm that any device has installed the release.

Before you begin

  • Create a dedicated API key with LIBRARY_MANAGE. A Portal user creating that grant needs API_KEY_MANAGE, unrestricted company policy scope, LIBRARY_VIEW and LIBRARY_EDIT. The sensitive grant is unchecked by default; existing keys do not gain it automatically.
  • Grant the key only to the pipeline that publishes releases. LIBRARY_MANAGE authorizes company-wide library uploads, including access to upload sessions started by other authorized company keys or users; keep the key in your CI secret store and rotate or revoke it when access changes.
  • Use Python 3.11 or newer for the example uploader. Build and sign the APK with the intended signing identity and a suitable Android version code before upload. The API does not sign APKs or publish to Google Play.
  • Send X-API-Key to the Nomid API. The validated key selects the company; requests have no tenant or company parameter. No DEVICES_READ or POLICIES_READ grant is needed for APK publication.

Endpoints

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

All seven routes below require X-API-Key with LIBRARY_MANAGE; an upload session belongs to the key’s company.

MethodEndpointDescription
POST/library/apps/uploads/checkCheck eligibility, duplicate content and an existing resumable session. A duplicate is informational.
POST/library/apps/uploads/startStart an upload and receive sessionId, partSize and expiresAt.
POST/library/apps/uploads/parts/signGet a short-lived direct S3 PUT URL and requiredHeaders for one part.
GET/library/apps/uploads/parts/{sessionId}List reusable uploaded parts and their exact ETags for the same session.
POST/library/apps/uploads/completeSubmit the ordered part list and optional tracks to assemble and process the APK.
GET/library/apps/uploads/status/{sessionId}Poll the session until status is completed or failed.
DELETE/library/apps/uploads/abort/{sessionId}Abort an unfinished session; processing or completed sessions cannot be aborted.

Upload sequence

  1. POST /check with filename, actual positive byte size, accepted mime and the APK SHA-256. Inspect allowed and reason; duplicateOf and existingSessionId may be present.
  2. POST /start with the same fields. Save the returned sessionId and partSize securely so this run can resume if interrupted.
  3. For each missing part, POST /parts/sign with sessionId and a partNumber from 1 to 10000.
  4. Slice the file into sequential chunks of partSize bytes; only the final chunk may be shorter. PUT each chunk to its signed URL using requiredHeaders, then retain the ETag response header exactly as returned.
  5. POST /complete with sessionId, every partNumber/etag pair, and optional tracks. Completion starts background APK processing; its immediate response can still say processing.
  6. GET the status endpoint with the session ID until the lowercase status is completed or failed. Completed responses include app; failed responses include an errorMessage. Other states are not publication success.

The size must be greater than zero and within the configured upload limit. mime is application/vnd.android.package-archive or application/octet-stream; sha256 is the lowercase 64-character hex digest of the APK bytes. Session and signed URL expiries apply.

The signed URL is for S3, not the Nomid API: send only requiredHeaders with the PUT, never X-API-Key. Do not log the signed URL. If it expires, sign that part again.

Request and response examples

These examples show field names and lowercase status values. Identifiers, digest, time, part size, ETags and URL are illustrative; use values returned for your own session.

POST /check or /start request

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

POST /check eligible response

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

POST /start response

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

POST /parts/sign request

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

POST /parts/sign response

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

GET uploaded parts response

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

POST /complete request

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

POST /complete processing response

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

requiredHeaders can contain headers required by the S3 signature. Apply the returned map exactly, including any values, and capture the ETag response header from each successful PUT.

Tracks and retries

Omitting tracks uses prod. The first release of a new app always includes prod even if another track is requested. Later releases can target explicit track names of at most 10 characters. There is no draft or approval stage in this API.

A matching checksum can identify an existing app/version, but it does not assign that release to the track requested by this run. Do not treat a /check duplicate result as publication success for a new track.

Resume only the session ID saved from this same publication run, with the same APK and tracks. GET the uploaded parts endpoint with that session ID to reuse parts. After an uncertain /complete result, poll that session before retrying completion. An aborted or expired session requires a new upload.

Publish from CI

On a Linux CI runner with Python 3.11 or newer and sha256sum, download the Python standard-library uploader and verify its pinned checksum before running it. Review and version the verified copy in your pipeline repository for future runs. Supply NOMID_API_KEY through your CI platform’s secret environment injection. The uploader exits nonzero on failure or timeout.

Download the Python uploader

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

To resume an interrupted run, pass its saved session ID explicitly with the same APK and tracks:

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

Do not put the API key in the command, repository or logs; do not enable shell tracing. Keep presigned URLs and their required headers out of CI logs. Publication success and device installation are separate checks.

Errors and recovery

  • 401: the key is missing, invalid, expired or revoked. Check CI secret injection without printing the value.
  • 403: the key lacks LIBRARY_MANAGE. Create a properly authorized key; unrelated read or write permissions are insufficient.
  • 404 for a session ID that is unknown in the key’s company. Confirm the ID and the company-bound key; do not switch to another company to search for it.
  • 400 for invalid request fields, including byte size or MIME. Correct the request; do not retry unchanged invalid input.
  • 410: the upload session has expired. Start a new upload; previously signed part URLs cannot be reused.
  • 409 from /abort means the upload is already processing or completed. Check its status instead of trying to cancel it.
  • 422 from /complete means the supplied parts cannot form the file. Compare the complete part list and exact ETags with the uploaded parts endpoint.
  • For a transient network or S3 error, re-sign an expired part URL and reuse successful parts. For an uncertain /complete result, poll the same session first. A failed finalized upload can be retried through the same completion path while its session is valid.

Choose Your Time

Loading...
Open booking calendar