Step 1 — Request an APK upload token

GET /app/upload returns a GUID filename, an uploadUrl (with SAS), and a verifyKey to be used in the install call.

Response fields: guidFileName, uploadUrl, verifyKey (SAS is valid 60 minutes).

C# example

using System.Net.Http;
using System.Net.Http.Headers;
using System.Text.Json;

var baseUrl = "<API_ENPDOINT_BASE>";
var apiKey = "<YOUR_API_KEY>";
using var http = new HttpClient();
http.DefaultRequestHeaders.Add("x-api-key", apiKey);

var uploadResp = await http.GetAsync($"{baseUrl}/app/upload");
uploadResp.EnsureSuccessStatusCode();
var uploadJson = await uploadResp.Content.ReadAsStringAsync();
var uploadInfo = JsonSerializer.Deserialize<UploadInfo>(uploadJson);

public record UploadInfo(string guidFileName, string uploadUrl, string verifyKey);

Step 2 — Upload the APK to Azure Blob Storage (using SAS)

Upload your APK file to Azure Blob Storage using the uploadUrl from Step 1. The URL contains a SAS token, so you just need to send a (PUT Method) with the proper headers.

C# example (HttpClient)

using var apkStream = File.OpenRead("/path/to/your.apk");
using var content = new StreamContent(apkStream);
content.Headers.ContentType = new MediaTypeHeaderValue("application/vnd.android.package-archive");
// Azure Blob generally expects this header for PUT when creating a block blob
content.Headers.Add("x-ms-blob-type", "BlockBlob");

var putResp = await http.PutAsync(uploadInfo.uploadUrl, content);
putResp.EnsureSuccessStatusCode();

long fileSize = apkStream.Length;

cURL Examples

curl -X PUT "$UPLOAD_URL" \
  -H "Content-Type: application/vnd.android.package-archive" \
  -H "x-ms-blob-type: BlockBlob" \
  --data-binary @your-app.apk

Note: If your environment blocks setting x-ms-blob-type on content headers, set it on the request message instead.

Step 3 — Ensure device permissions allow installation

POST /app/devices/{deviceId}/perm lets you enable marketplace/unknown source install and uninstall permissions.

Body (all booleans): marketplaceInstall, unknownInstall, uninstall.

C# example

var deviceId = "<DEVICE_ID>";
var permBody = new {
    marketplaceInstall = true,
    unknownInstall = true,
    uninstall = true
};
var permResp = await http.PostAsJsonAsync($"{baseUrl}/app/devices/{deviceId}/perm", permBody);
permResp.EnsureSuccessStatusCode(); // 204 No Content on success

Step 4 — Trigger install on the device

POST /app/devices/{deviceId}/install confirms the upload and starts installation.

Body:

C# example

var installBody = new {
    guidFileName = uploadInfo.guidFileName,
    fileSize = fileSize,
    verifyKey = uploadInfo.verifyKey
};
var installResp = await http.PostAsJsonAsync($"{baseUrl}/app/devices/{deviceId}/install", installBody);
installResp.EnsureSuccessStatusCode();
// 200 OK on accepted; installation proceeds asynchronously on the device

Step 5 — Check installation status

GET /app/status returns { operationType: "INSTALL|UNINSTALL", status: "IN_PROGRESS|SUCCESS|FAILED" }.

C# example

var statusResp = await http.GetAsync($"{baseUrl}/app/status");
statusResp.EnsureSuccessStatusCode();
var statusJson = await statusResp.Content.ReadAsStringAsync();
var status = JsonSerializer.Deserialize<AppStatus>(statusJson);

public record AppStatus(string operationType, string status);

Verify on device / enumerate installed apps

GET /app/devices/{deviceId} returns a paged list of installed apps (name, packageName, version, isSystem).

C# example

var appsResp = await http.GetAsync($"{baseUrl}/app/devices/{deviceId}");
appsResp.EnsureSuccessStatusCode();
var appsJson = await appsResp.Content.ReadAsStringAsync();
// shape: {{ pageInfo, data: [ {{ appName, packageName, versionCode, versionName, isSystem }} ] }}

Optional — Uninstall an app

POST /app/devices/{deviceId}/uninstall (body may include package info depending on device policy).

C# example

var uninstallBody = new {
    // e.g., packageName = "com.example.app"
};
var uninstallResp = await http.PostAsJsonAsync($"{baseUrl}/app/devices/{deviceId}/uninstall", uninstallBody);
uninstallResp.EnsureSuccessStatusCode();

Error handling

End-to-end C# helper (consolidated)

public static async Task InstallApkAsync(string baseUrl, string apiKey, string deviceId, string apkPath)
{
    using var http = new HttpClient();
    http.DefaultRequestHeaders.Add("x-api-key", apiKey);

    // 1) Get upload info
    var uploadResp = await http.GetAsync($"{baseUrl}/app/upload");
    uploadResp.EnsureSuccessStatusCode();
    var uploadInfo = JsonSerializer.Deserialize<UploadInfo>(await uploadResp.Content.ReadAsStringAsync());

    // 2) PUT APK
    using var apk = File.OpenRead(apkPath);
    var req = new HttpRequestMessage(HttpMethod.Put, uploadInfo.uploadUrl);
    req.Content = new StreamContent(apk);
    req.Content.Headers.ContentType = new MediaTypeHeaderValue("application/vnd.android.package-archive");
    req.Headers.TryAddWithoutValidation("x-ms-blob-type", "BlockBlob");
    var put = await http.SendAsync(req);
    put.EnsureSuccessStatusCode();

    // 3) Ensure permissions
    var permBody = new { marketplaceInstall = true, unknownInstall = true, uninstall = true };
    var perm = await http.PostAsJsonAsync($"{baseUrl}/app/devices/{deviceId}/perm", permBody);
    perm.EnsureSuccessStatusCode();

    // 4) Trigger install
    var installBody = new { guidFileName = uploadInfo.guidFileName, fileSize = apk.Length, verifyKey = uploadInfo.verifyKey };
    var install = await http.PostAsJsonAsync($"{baseUrl}/app/devices/{deviceId}/install", installBody);
    install.EnsureSuccessStatusCode();

    // 5) Poll status
    for (int i = 0; i < 30; i++)
    {
        await Task.Delay(TimeSpan.FromSeconds(4));
        var s = await http.GetAsync($"{baseUrl}/app/status");
        s.EnsureSuccessStatusCode();
        var status = JsonSerializer.Deserialize<AppStatus>(await s.Content.ReadAsStringAsync());
        Console.WriteLine($"{status.operationType}: {status.status}");
        if (status.status is "SUCCESS" or "FAILED") break;
    }
}

public record UploadInfo(string guidFileName, string uploadUrl, string verifyKey);
public record AppStatus(string operationType, string status);