# Uploads API

> Upload an APK straight to storage with a presigned PUT URL, confirm it, then install it on any device for 7 days.

Source: https://akaphone-docs.pages.dev/api/uploads/

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](https://akaphone-docs.pages.dev/guides/install-apk/).

## Create an upload

`POST /v1/uploads`

Declares 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. |

```bash title="curl"
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}'
```
```typescript title="TypeScript"
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());
```
```python title="Python"
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`

```json
{
  "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.

```bash title="curl"
curl -X PUT "$PUT_URL" \
  -H "Content-Type: application/vnd.android.package-archive" \
  --data-binary @app-release.apk
```
```typescript title="TypeScript"
import { 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}`);
```
```python title="Python"
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

`POST /v1/uploads/:id/complete`

Confirms the object is in storage and marks the upload `uploaded`. Idempotent.

```bash title="curl"
curl -X POST https://akaphone.abhgnx-417.workers.dev/v1/uploads/upl_01m5vqsywgw57haqkjnexz/complete \
  -H "Authorization: Bearer $AKAPHONE_API_KEY"
```
```typescript title="TypeScript"
await fetch(`https://akaphone.abhgnx-417.workers.dev/v1/uploads/${upload.upload.id}/complete`, {
  method: "POST",
  headers: { Authorization: `Bearer ${process.env.AKAPHONE_API_KEY}` },
});
```
```python title="Python"
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`

```json
{ "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`](https://akaphone-docs.pages.dev/api/devices/#install-an-app). One upload can be installed on any number of devices. Uploads are deleted after **7 days**.
