Skip to content
Last updated

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

StepOpen APILocal API
Base URLhttps://api.morelogin.comhttp://127.0.0.1:40000
AuthenticationAuthorization: Bearer <token>Local API token when enabled
Request upload URLPOST /cloudphone/uploadUrlPOST /api/cloudphone/upload/file/signedUrl
Register uploaded objectPOST /cloudphone/uploadFilePOST /api/cloudphone/upload/file
Query upload resultPOST /cloudphone/uploadFileResultPOST /api/cloudphone/upload/file/result
Request file exportPOST /cloudphone/downloadPOST /api/cloudphone/download
Query export resultPOST /cloudphone/download/resultPOST /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:

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

Cloud PhoneTemporary storageMoreLogin APIClientCloud PhoneTemporary storageMoreLogin APIClientloop[Until terminal status]1. Request pre-signed URLpresignedUrl2. PUT file bytes2xx3. Register object URLfileId, status=04. Query upload resultnull / status 0, 1, or 2Deliver file
Cloud PhoneTemporary storageMoreLogin APIClientCloud PhoneTemporary storageMoreLogin APIClientloop[Until terminal status]1. Request pre-signed URLpresignedUrl2. PUT file bytes2xx3. Register object URLfileId, status=04. Query upload resultnull / status 0, 1, or 2Deliver file

1. Request a pre-signed URL

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.

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.

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

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.statusMeaningAction
nullThe task is not visible yetWait and poll again
0UploadingWait and poll again
1SuccessfulStop polling
2FailedStop 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.

Temporary storageCloud PhoneMoreLogin APIClientTemporary storageCloud PhoneMoreLogin APIClientloop[While status=10]1. Request file export (id, filePath)Read and export filedownId2. Query export result (id, downId)status=10Write exported filestatus=20, downUrl3. GET downUrlFile bytes
Temporary storageCloud PhoneMoreLogin APIClientTemporary storageCloud PhoneMoreLogin APIClientloop[While status=10]1. Request file export (id, filePath)Read and export filedownId2. Query export result (id, downId)status=10Write exported filestatus=20, downUrl3. GET downUrlFile 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.

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

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.statusMeaningAction
10RunningWait and poll again
20SuccessfulDownload from data.downUrl
30FailedStop and report requestId
40CancelledStop; create a new task only if still needed

When the status is 20, download the file promptly:

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.

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.
  • 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 and Cloud Phone Local API. For general polling and retry rules, see Asynchronous Operations.