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).
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);
Use the uploadUrl (which already contains the SAS) to PUT the APK bytes. Set blob headers as needed.
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;
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.
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 -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
uploadUrlis valid for 60 minutes. Make sure to complete the upload within this time window. Remember to save thefileSizevalue for Step 4.
Note: If your environment blocks setting
x-ms-blob-typeon content headers, set it on the request message instead.
POST /app/devices/{deviceId}/perm lets you enable marketplace/unknown source install and uninstall permissions.
Body (all booleans): marketplaceInstall, unknownInstall, uninstall.
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
POST /app/devices/{deviceId}/install confirms the upload and starts installation.
Body:
guidFileName: from Step 1fileSize: APK size in bytes (from the stream)verifyKey: from Step 1var 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
GET /app/status returns { operationType: "INSTALL|UNINSTALL", status: "IN_PROGRESS|SUCCESS|FAILED" }.
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);
GET /app/devices/{deviceId} returns a paged list of installed apps (name, packageName, version, isSystem).
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 }} ] }}
POST /app/devices/{deviceId}/uninstall (body may include package info depending on device policy).
var uninstallBody = new {
// e.g., packageName = "com.example.app"
};
var uninstallResp = await http.PostAsJsonAsync($"{baseUrl}/app/devices/{deviceId}/uninstall", uninstallBody);
uninstallResp.EnsureSuccessStatusCode();
400/422: invalid body/constraints; check guidFileName, verifyKey, fileSize.
401: invalid API key.
403/409: permission or conflicting state; ensure Step 3 succeeded and the device is online.
404: wrong deviceId or resource expired (SAS).
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);