# Cloud Phone File Upload and Download

This guide shows the complete file-transfer workflow: upload a local file to a Cloud Phone, export a file from the Cloud Phone, poll asynchronous tasks, and recover safely from timeouts.

## Choose an API surface

| Step | Open API | Local API |
|  --- | --- | --- |
| Base URL | `https://api.morelogin.com` | `http://127.0.0.1:40000` |
| Authentication | `Authorization: Bearer <token>` | Local API token when enabled |
| Request upload URL | `POST /cloudphone/uploadUrl` | `POST /api/cloudphone/upload/file/signedUrl` |
| Register uploaded object | `POST /cloudphone/uploadFile` | `POST /api/cloudphone/upload/file` |
| Query upload result | `POST /cloudphone/uploadFileResult` | `POST /api/cloudphone/upload/file/result` |
| Request file export | `POST /cloudphone/download` | `POST /api/cloudphone/download` |
| Query export result | `POST /cloudphone/download/result` | `POST /api/cloudphone/download/result` |


The examples below use Open API. To use Local API, change the base URL and paths according to the table. Keep Cloud Phone IDs and task IDs as strings; JavaScript clients must not convert them through `Number`.

### Local API multipart shortcut

When the file is on the same machine as the MoreLogin client, Local API also provides `POST /api/cloudphone/uploadFile` with `multipart/form-data`:

```bash
curl --request POST "http://127.0.0.1:40000/api/cloudphone/uploadFile" \
  --form 'id=1661515884160372' \
  --form 'uploadDest=/Download' \
  --form 'file=@./report.csv'
```

Use the signed-URL workflow below when the caller runs remotely, when the file is large, or when you need a `fileId` for explicit status polling.

## Upload workflow

```mermaid
sequenceDiagram
    participant Client
    participant API as MoreLogin API
    participant Storage as Temporary storage
    participant Phone as Cloud Phone
    Client->>API: 1. Request pre-signed URL
    API-->>Client: presignedUrl
    Client->>Storage: 2. PUT file bytes
    Storage-->>Client: 2xx
    Client->>API: 3. Register object URL
    API-->>Client: fileId, status=0
    loop Until terminal status
        Client->>API: 4. Query upload result
        API-->>Client: null / status 0, 1, or 2
    end
    API->>Phone: Deliver file
```

### 1. Request a pre-signed URL

```bash
curl --request POST "https://api.morelogin.com/cloudphone/uploadUrl" \
  --header "Authorization: Bearer $MORELOGIN_TOKEN" \
  --header "Content-Type: application/json" \
  --data '{
    "id": "1661515884160372",
    "fileName": "report.csv"
  }'
```

The response contains `data.presignedUrl`. The temporary object is automatically deleted after 7 days; this does not delete a file that has already been delivered to the Cloud Phone.

### 2. Upload the file bytes

Send the file directly to the returned URL. Do not add the MoreLogin Bearer token to this storage request.

```bash
curl --location --request PUT \
  --upload-file "./report.csv" \
  "$PRESIGNED_URL"
```

Only continue after the storage service returns a successful HTTP status. If this request fails or times out, retry the `PUT` to the same URL while it remains valid.

### 3. Register the object for delivery

Use the uploaded object URL. The query string on a pre-signed URL contains credentials, so remove it before storing or logging the URL when your object-store configuration permits it.

```bash
FILE_URL="${PRESIGNED_URL%%\?*}"

curl --request POST "https://api.morelogin.com/cloudphone/uploadFile" \
  --header "Authorization: Bearer $MORELOGIN_TOKEN" \
  --header "Content-Type: application/json" \
  --data "$(jq -n \
    --arg id '1661515884160372' \
    --arg url "$FILE_URL" \
    --arg dest '/Download' \
    '{id: $id, url: $url, uploadDest: $dest, uploadType: 1}')"
```

Use `uploadType: 1` for regular files. Use `uploadType: 2` only for supported MP4 live-streaming files. Save `data.fileId` from the response.

### 4. Poll the upload result

```bash
curl --request POST "https://api.morelogin.com/cloudphone/uploadFileResult" \
  --header "Authorization: Bearer $MORELOGIN_TOKEN" \
  --header "Content-Type: application/json" \
  --data '{
    "id": "1661515884160372",
    "fileId": "1661515884160111"
  }'
```

| `data` / `data.status` | Meaning | Action |
|  --- | --- | --- |
| `null` | The task is not visible yet | Wait and poll again |
| `0` | Uploading | Wait and poll again |
| `1` | Successful | Stop polling |
| `2` | Failed | Stop polling and report `requestId` |


Poll every 2 seconds and set a client-side deadline appropriate for the file size. Do not create another upload task merely because a status request timed out; query the existing `fileId` first.

## Download workflow

“Download” is an export from the Cloud Phone to a temporary URL. It is a two-stage asynchronous workflow.

```mermaid
sequenceDiagram
    participant Client
    participant API as MoreLogin API
    participant Phone as Cloud Phone
    participant Storage as Temporary storage
    Client->>API: 1. Request file export (id, filePath)
    API->>Phone: Read and export file
    API-->>Client: downId
    loop While status=10
        Client->>API: 2. Query export result (id, downId)
        API-->>Client: status=10
    end
    Phone->>Storage: Write exported file
    API-->>Client: status=20, downUrl
    Client->>Storage: 3. GET downUrl
    Storage-->>Client: File bytes
```

If polling returns `30` (failed) or `40` (cancelled), stop the flow and do not request `downUrl`.

### 1. Request a file export

Use the absolute Android file path, including the file name.

```bash
curl --request POST "https://api.morelogin.com/cloudphone/download" \
  --header "Authorization: Bearer $MORELOGIN_TOKEN" \
  --header "Content-Type: application/json" \
  --data '{
    "id": "1661515884160372",
    "filePath": "/sdcard/Download/report.csv"
  }'
```

Save `data.downId`. The request only creates the export task; it does not return the file bytes.

### 2. Poll until the URL is ready

```bash
curl --request POST "https://api.morelogin.com/cloudphone/download/result" \
  --header "Authorization: Bearer $MORELOGIN_TOKEN" \
  --header "Content-Type: application/json" \
  --data '{
    "id": "1661515884160372",
    "downId": "1661515884160999"
  }'
```

| `data.status` | Meaning | Action |
|  --- | --- | --- |
| `10` | Running | Wait and poll again |
| `20` | Successful | Download from `data.downUrl` |
| `30` | Failed | Stop and report `requestId` |
| `40` | Cancelled | Stop; create a new task only if still needed |


When the status is `20`, download the file promptly:

```bash
curl --location "$DOWN_URL" --output "./report.csv"
```

Do not send the MoreLogin Bearer token to `downUrl`. Treat both pre-signed upload URLs and download URLs as secrets, and never persist them in application logs.

## Complete Python example

This example requires `requests`. It checks both HTTP status and the MoreLogin business `code`, polls with a deadline, and reuses the returned task IDs.

```python
import os
import time
from pathlib import Path

import requests

BASE_URL = "https://api.morelogin.com"
TOKEN = os.environ["MORELOGIN_TOKEN"]
PHONE_ID = "1661515884160372"
HEADERS = {"Authorization": f"Bearer {TOKEN}"}


def api_post(path, payload):
    response = requests.post(
        f"{BASE_URL}{path}", json=payload, headers=HEADERS, timeout=30
    )
    response.raise_for_status()
    body = response.json()
    if body.get("code") != 0:
        raise RuntimeError(
            f"API error code={body.get('code')} msg={body.get('msg')} "
            f"requestId={body.get('requestId')}"
        )
    return body


def wait_for(path, payload, terminal, timeout=300):
    deadline = time.monotonic() + timeout
    while time.monotonic() < deadline:
        body = api_post(path, payload)
        data = body.get("data")
        if data is not None and data.get("status") in terminal:
            return body
        time.sleep(2)
    raise TimeoutError(f"Timed out while polling {path}; keep the task ID")


def upload_to_phone(local_path, destination="/Download"):
    path = Path(local_path)
    signed = api_post(
        "/cloudphone/uploadUrl", {"id": PHONE_ID, "fileName": path.name}
    )
    presigned_url = signed["data"]["presignedUrl"]
    with path.open("rb") as stream:
        put_response = requests.put(presigned_url, data=stream, timeout=120)
    put_response.raise_for_status()

    object_url = presigned_url.split("?", 1)[0]
    created = api_post(
        "/cloudphone/uploadFile",
        {
            "id": PHONE_ID,
            "url": object_url,
            "uploadDest": destination,
            "uploadType": 1,
        },
    )
    file_id = created["data"]["fileId"]
    result = wait_for(
        "/cloudphone/uploadFileResult",
        {"id": PHONE_ID, "fileId": file_id},
        terminal={1, 2},
    )
    if result["data"]["status"] != 1:
        raise RuntimeError(f"Upload failed; requestId={result.get('requestId')}")
    return file_id


def download_from_phone(remote_path, local_path):
    created = api_post(
        "/cloudphone/download", {"id": PHONE_ID, "filePath": remote_path}
    )
    down_id = created["data"]["downId"]
    result = wait_for(
        "/cloudphone/download/result",
        {"id": PHONE_ID, "downId": down_id},
        terminal={20, 30, 40},
    )
    if result["data"]["status"] != 20:
        raise RuntimeError(f"Download export failed; requestId={result.get('requestId')}")

    with requests.get(result["data"]["downUrl"], stream=True, timeout=120) as response:
        response.raise_for_status()
        with open(local_path, "wb") as output:
            for chunk in response.iter_content(1024 * 1024):
                if chunk:
                    output.write(chunk)
    return down_id


upload_to_phone("./report.csv")
download_from_phone("/sdcard/Download/report.csv", "./downloaded-report.csv")
```

## Production checklist

- Confirm `code == 0`; HTTP `200` alone does not mean the business operation succeeded.
- Keep `id`, `fileId`, and `downId` as strings.
- Retry status queries with backoff. Before repeating a task-creation request after a timeout, query the task ID when one was received.
- Limit concurrent transfers and follow the documented [rate limits](/api-reference/getting-started/rate-limits).
- Validate the local file size and available Cloud Phone storage before large transfers.
- Record `requestId`, endpoint, Cloud Phone ID, task ID, and terminal status for support, but redact tokens and signed URLs.


For field-level schemas, see the [Cloud Phone Open API](/api-reference/cloud-phone/open-api) and [Cloud Phone Local API](/api-reference/cloud-phone/local-api). For general polling and retry rules, see [Asynchronous Operations](/api-reference/getting-started/async-operations).