Uploads API
Upload an APK straight to storage with a presigned PUT URL, confirm it, then install it on any device for 7 days.
Uploads move APK files from you to AkaPhone storage without passing through the API. The full walk-through, including the install step and a one-file script, is in Install an APK.
Create an upload#
/v1/uploadsDeclares a file and returns a presigned URL to PUT it to.
| Body field | Type | Default | Description |
|---|---|---|---|
filename |
string | required | 5–128 characters: letters, digits, ., _, -, spaces; must start with a letter or digit and end in .apk. Spaces become _. Android App Bundles (.aab) are rejected; build a universal APK. |
size_bytes |
integer | required | Exact file size in bytes; at most 314,572,800 (300 MB). |
content_type |
string | "application/vnd.android.package-archive" |
The Content-Type you will PUT with. |
curl -X POST https://akaphone.abhgnx-417.workers.dev/v1/uploads \
-H "Authorization: Bearer $AKAPHONE_API_KEY" \
-H "Content-Type: application/json" \
-d '{"filename": "app-release.apk", "size_bytes": 48211337}'import { stat } from "node:fs/promises";
const { size } = await stat("app-release.apk");
const upload = await fetch("https://akaphone.abhgnx-417.workers.dev/v1/uploads", {
method: "POST",
headers: { Authorization: `Bearer ${process.env.AKAPHONE_API_KEY}`, "Content-Type": "application/json" },
body: JSON.stringify({ filename: "app-release.apk", size_bytes: size }),
}).then((r) => r.json());import os, requests
upload = requests.post(
"https://akaphone.abhgnx-417.workers.dev/v1/uploads",
headers={"Authorization": f"Bearer {os.environ['AKAPHONE_API_KEY']}"},
json={"filename": "app-release.apk", "size_bytes": os.path.getsize("app-release.apk")},
).json()Response 201 Created
{
"upload": {
"id": "upl_01m5vqsywgw57haqkjnexz",
"filename": "app-release.apk",
"size_bytes": 48211337,
"status": "pending"
},
"put_url": "https://<account>.r2.cloudflarestorage.com/akaphone-artifacts/apks/usr_01m5vqsywg3mxj7dyxv9q9/upl_01m5vqsywgw57haqkjnexz/app-release.apk?X-Amz-Algorithm=AWS4-HMAC-SHA256&X-Amz-Expires=900&...",
"headers": { "content-type": "application/vnd.android.package-archive" },
"expires_in": 900
}| Error | When |
|---|---|
400 invalid_request |
Bad filename, including .aab bundles (filename must be a plain .apk file name …), or size_bytes missing, not positive, or over 300 MB (APK must be at most 300 MB). |
Upload the file#
PUT the bytes to put_url within expires_in seconds, sending each header in headers and exactly size_bytes bytes. This request goes to storage, not to the API: do not add your Authorization header.
curl -X PUT "$PUT_URL" \
-H "Content-Type: application/vnd.android.package-archive" \
--data-binary @app-release.apkimport { readFile } from "node:fs/promises";
const put = await fetch(upload.put_url, {
method: "PUT",
headers: upload.headers,
body: await readFile("app-release.apk"),
});
if (!put.ok) throw new Error(`upload failed: ${put.status}`);with open("app-release.apk", "rb") as f:
requests.put(upload["put_url"], headers=upload["headers"], data=f).raise_for_status()Storage answers 200 on success. A 403 usually means the URL expired, or the Content-Type or length differs from what you declared.
Complete an upload#
/v1/uploads/:id/completeConfirms the object is in storage and marks the upload uploaded. Idempotent.
curl -X POST https://akaphone.abhgnx-417.workers.dev/v1/uploads/upl_01m5vqsywgw57haqkjnexz/complete \
-H "Authorization: Bearer $AKAPHONE_API_KEY"await fetch(`https://akaphone.abhgnx-417.workers.dev/v1/uploads/${upload.upload.id}/complete`, {
method: "POST",
headers: { Authorization: `Bearer ${process.env.AKAPHONE_API_KEY}` },
});requests.post(
f"https://akaphone.abhgnx-417.workers.dev/v1/uploads/{upload['upload']['id']}/complete",
headers={"Authorization": f"Bearer {os.environ['AKAPHONE_API_KEY']}"},
).raise_for_status()Response 200 OK
{ "upload": { "id": "upl_01m5vqsywgw57haqkjnexz", "status": "uploaded" } }| Error | When |
|---|---|
404 not_found |
Unknown upload. |
409 conflict |
The file is not in storage yet (Object not found in storage: upload the file to put_url first). |
400 invalid_request |
The stored file is over 300 MB; it is deleted. |
Install it#
Pass the upload id to POST /v1/devices/:id/apps. One upload can be installed on any number of devices. Uploads are deleted after 7 days.