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 = "<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)

Use the uploadUrl (which already contains the SAS) to PUT the APK bytes. Set blob headers as needed.

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;

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");
content.Headers.Add("x-ms-blob-type", "BlockBlob");

// Use PUT method to upload
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: The SAS token in the uploadUrl is valid for 60 minutes. Make sure to complete the upload within this time window. Remember to save the fileSize value for Step 4.

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);