Phonebox / Docs
API reference

Uploads

Upload your own APK, such as the debug build your coding agent just made, so you can install it on any of your phones.

View as Markdown

An upload holds one APK of yours. You create it, send the file to the one-off upload_url it gives you, and complete it. Phonebox then reads the APK's manifest and stores it, and once its status is ready, Install an app with {"upload": "upl_…"} puts it on a phone. The file never passes through /v1, so the API's 4 MB body limit doesn't apply.

Uploads belong to the project, and every key of the project can use them, except a key limited to certain phones: it sees and uses only the uploads it created itself, so the customers a shared project serves never see each other's builds.

The CLI and the SDK do all of it in one call: phonebox apps "$PHONE" install ./app-debug.apk, or phone.installApk("./app-debug.apk").

LimitValue
File size524,288,000 bytes (500 MiB), sent within 2 minutes
A project's uploads524,288,000 bytes (500 MiB) in all, counting uploads that are processing or ready
New uploads50 a project each UTC day, for a project with credits that isn't frozen
Upload URLFor one file, sent within an hour
Kept7 days after the upload's last install, or after its file arrived if it was never installed. Once a newer build of the same package is ready, an older upload is kept only 24 hours after its last install.
The same file againCompleting an upload with a file the project already has ready answers with that ready upload, which is kept 7 more days. The new upload becomes a duplicate that names it.

Create an upload

POST /v1/apps/uploads needs the phones:control scope.

Takes no body, or an empty JSON object. It answers 201 Created with the upload and where to send its file:

curl -X POST https://phonebox.dev/v1/apps/uploads \
  -H "Authorization: Bearer $PHONEBOX_API_KEY"
Response: new upload
{
  "object": "upload",
  "id": "upl_q3m7x2k6v4ta",
  "status": "awaiting_file",
  "package": null,
  "version_code": null,
  "version_name": null,
  "min_sdk": null,
  "size_bytes": null,
  "sha256": null,
  "created_at": "2026-09-29T10:00:00.000Z",
  "expires_at": "2026-09-29T11:00:00.000Z",
  "error": null,
  "upload_url": "https://phonebox-files.example/api/storage/upload?token=…",
  "upload_method": "POST",
  "upload_headers": { "Content-Type": "application/vnd.android.package-archive" },
  "max_bytes": 524288000
}
FieldTypeMeaning
upload_urlstringWhere the file goes: send it one file, within an hour. It needs no API key, so keep it to yourself.
upload_methodstringPOST.
upload_headersobjectHeaders to send with the file.
max_bytesintegerThe largest file it takes.

The rest is the upload object.

ErrorWhen
402 insufficient_creditsThe project has no credits. Uploads need a funded project.
402 project_frozenThe project is frozen.
409 storage_limit_reachedThe project's uploads already hold 500 MiB. Delete ones you no longer need. details.limit_bytes and details.used_bytes say how much.
429 rate_limitedThe project created 50 uploads today (UTC). Retry-After and details.retry_after_seconds say when it can create more.
503 storage_fullPhonebox's app storage is full right now. Try again later.

Send the file

Send the APK's bytes as the request body to upload_url, with upload_method and upload_headers. It answers with the file's storageId:

curl -X POST "$UPLOAD_URL" \
  -H "Content-Type: application/vnd.android.package-archive" \
  --data-binary @app/build/outputs/apk/debug/app-debug.apk
{ "storageId": "kg28ph47rkkczabaeeqf536a5n8faqa5" }

Complete an upload

POST /v1/apps/uploads/{id}/complete needs the phones:control scope.

Tells Phonebox the file has arrived. It answers 202 Accepted with the upload processing: Phonebox reads the APK's manifest and stores the APK, which takes a few seconds, and up to about a minute for a large one. Wait with Get an upload and ?wait=ready.

FieldTypeDefaultRules
storage_idstringNoneThe storageId that upload_url answered with. The answer itself, {"storageId": "…"}, is taken as it is too.
POST /v1/apps/uploads/{id}/complete
{
  "storage_id": "kg28ph47rkkczabaeeqf536a5n8faqa5"
}
curl https://phonebox.dev/v1/apps/uploads/upl_q3m7x2k6v4ta/complete \
  -H "Authorization: Bearer $PHONEBOX_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{"storage_id": "kg28ph47rkkczabaeeqf536a5n8faqa5"}'

Since complete takes the upload URL's answer as it is, one pipeline sends the file and completes the upload:

curl -sS -X POST "$UPLOAD_URL" -H "Content-Type: application/vnd.android.package-archive" \
  --data-binary @app/build/outputs/apk/debug/app-debug.apk |
curl -sS https://phonebox.dev/v1/apps/uploads/upl_q3m7x2k6v4ta/complete \
  -H "Authorization: Bearer $PHONEBOX_API_KEY" -H "Content-Type: application/json" -d @-

A file the project already has ready answers 200 with that ready upload, not this one: install the upload in the answer. This upload becomes a duplicate that names it, in its error.next, and in upload_not_ready if you install it. A file over the size limit ends the upload at once as too_large, answered 200. Completing the same upload again with the same storage_id answers with the upload as it is.

ErrorWhen
400 validation_failedNo file has this storage_id, it wasn't sent to this upload's upload_url, another upload has it, or this upload already has its file. details.issues says which.
404 upload_not_foundNo upload with this ID exists in the project.
409 storage_limit_reachedThis file would take the project's uploads past 500 MiB. The upload keeps its file: delete other uploads, then complete it again.
503 storage_fullPhonebox's app storage is full right now. The upload keeps its file for the rest of its hour: complete it again later.

Get an upload

GET /v1/apps/uploads/{id} needs the phones:read scope.

ParameterTypeDefaultRules
waitstringNoneready: wait while the upload is processing. It answers as soon as the upload is ready or has ended, and at once for an upload in any other status.
timeoutinteger501 to 55 seconds of waiting.
curl "https://phonebox.dev/v1/apps/uploads/upl_q3m7x2k6v4ta?wait=ready" \
  -H "Authorization: Bearer $PHONEBOX_API_KEY"
Response: upload
{
  "object": "upload",
  "id": "upl_q3m7x2k6v4ta",
  "status": "ready",
  "package": "com.example.app",
  "version_code": 42,
  "version_name": "1.4.2",
  "min_sdk": 26,
  "size_bytes": 8388608,
  "sha256": "3b6f0c1f3e0d8b2f2c9a8e1f7d4c5b6a9e8d7c6b5a4f3e2d1c0b9a8f7e6d5c4b",
  "created_at": "2026-09-29T10:00:00.000Z",
  "expires_at": "2026-10-06T10:00:07.000Z",
  "error": null
}

Poll again only while status is processing.

The upload object

FieldTypeMeaning
objectstringupload.
idstringThe upload's ID, upl_ and 12 characters.
statusstringawaiting_file, processing, ready, invalid_apk, too_large, storage_full, failed, duplicate or deleted.
packagestring or nullThe package the APK's manifest declares, once it has been read. An install by this upload installs this package.
version_codeinteger or nullThe APK's versionCode.
version_namestring or nullThe APK's versionName.
min_sdkinteger or nullThe lowest Android API level the APK supports, when it says.
size_bytesinteger or nullThe file's size.
sha256string or nullThe file's SHA-256, in hex.
created_attimestampWhen the upload was created.
expires_attimestampWhen Phonebox deletes the upload: an hour after it was created while it waits for its file; then 7 days after its file arrived, and 7 days after each install; once a newer build of the same package is ready, 24 hours after its last install.
errorobject or nullWhy the upload ended without becoming ready: {code, message, next}.
StatusMeaningNext
awaiting_fileWaiting for its file and a complete.Send the file, then complete it.
processingPhonebox is reading and storing it.Wait with ?wait=ready.
readyIt can be installed.Install it with {"upload": "upl_…"}.
invalid_apkThe file isn't an APK Phonebox can install: error.next says why.Build an APK and upload it.
too_largeThe file is over 500 MiB. error.code is payload_too_large.Upload a smaller build.
storage_fullPhonebox's app storage is full.Try again later with a new upload.
failedPhonebox couldn't store the file. error.code is upload_failed, or platform_not_configured when Phonebox can't store apps at all right now.Create a new upload and send the file again, later for platform_not_configured.
duplicateIts file is another upload of the project, which is ready. error.code is duplicate_upload, and error.next names that upload.Install the upload error.next names.
deletedDeleted, or expired.Upload the APK again.
ErrorWhen
400 validation_failedwait or timeout is out of range.
404 upload_not_foundNo upload with this ID exists in the project.

List uploads

GET /v1/apps/uploads needs the phones:read scope.

Lists the project's uploads, newest first, leaving out deleted ones. It takes limit (1 to 100, 50 by default) and cursor, as every list does.

curl https://phonebox.dev/v1/apps/uploads \
  -H "Authorization: Bearer $PHONEBOX_API_KEY"
Response: uploads
{
  "data": [
    {
      "object": "upload", "id": "upl_q3m7x2k6v4ta", "status": "ready", "package": "com.example.app", "version_code": 42,
      "version_name": "1.4.2", "min_sdk": 26, "size_bytes": 8388608,
      "sha256": "3b6f0c1f3e0d8b2f2c9a8e1f7d4c5b6a9e8d7c6b5a4f3e2d1c0b9a8f7e6d5c4b",
      "created_at": "2026-09-29T10:00:00.000Z", "expires_at": "2026-10-06T10:00:07.000Z", "error": null
    }
  ],
  "next_cursor": null
}

Delete an upload

DELETE /v1/apps/uploads/{id} needs the phones:control scope.

Deletes the upload and the APK Phonebox stored for it. Apps already installed from it stay on their phones. It answers with the upload, status: "deleted"; deleting it again answers the same.

curl -X DELETE https://phonebox.dev/v1/apps/uploads/upl_q3m7x2k6v4ta \
  -H "Authorization: Bearer $PHONEBOX_API_KEY"
ErrorWhen
404 upload_not_foundNo upload with this ID exists in the project.

On this page