If you're managing more than ten cloud phone profiles, doing anything by hand is a waste of your day.

I've been running automation against Geelark for the better part of a year. Mostly content distribution and account warm-up across TikTok and Reddit.

The platform gives you three ways to skip the manual work. Most guides only cover the easy one.

This walks through all three, ordered by how much control you want: Synchronizer, RPA, and the API. Working Python code where it matters.

What is Geelark automation?

Geelark automation is the system for running tasks on cloud phones without touching them. It works through three tools: Synchronizer (mirror clicks across phones), RPA (no-code drag-and-drop workflows), and the API (programmatic control over everything). Pick Synchronizer for ad-hoc work, RPA for repeatable flows, API for scale.

The 3 ways to automate Geelark

Each tool solves a different problem. Picking the wrong one costs you days.

Method When to use Coding required Best for
Synchronizer Same action across many phones, right now None Ad-hoc bulk work
RPA Repeatable, multi-step workflows None (visual builder) Posting, warm-up, engagement
API Integrating Geelark into your own system Yes (Python, Node, Go, anything) Scheduled jobs, custom logic, scale

I use Synchronizer for one-off tasks like changing a setting on 30 phones. RPA for content posting that runs nightly. The API for everything that talks to my own database or scheduler.

Method 1: Synchronizer (the quick fix)

Synchronizer is the laziest way to automate. You set one phone as the "main" window, pick which other phones follow it, and every action gets mirrored.

Click on the main phone, the same click happens on every controlled phone at the same pixel.

Type a message, all of them type it. Open an app, all of them open it.

It's not really automation. It's bulk manual work.

But it's perfect when you need to do something once across many accounts and don't want to build a whole flow.

Setting up Synchronizer

Open the Geelark app, launch all the cloud phones you want to control, and click the Synchronizer icon in the toolbar.

The interface lets you mark one window as the controller. Everything else becomes a follower. You can ungroup individual phones if one needs special treatment.

Main phone (controller)
├── Phone 02 (follower)
├── Phone 03 (follower)
├── Phone 04 (follower)
└── Phone 05 (follower)

This works because Geelark cloud phones share the same screen layout when you use identical Android versions and screen sizes. Mismatched specs and the click coordinates drift.

Where Synchronizer breaks

Try to run it on hundreds of phones and your local network and CPU choke. Actions also get mechanically identical, the opposite of what you want for accounts you're keeping separate.

For more than 20 phones or anything needing randomization, move to RPA.

Method 2: RPA builder (no-code workflows)

RPA is where most people live. It's a visual drag-and-drop tool with around 40 modules: taps, swipes, text input, OCR, conditionals, loops, waits, random delays.

You build a flow once, then run it across as many phones as you want.

The flow runs in the cloud, not on your laptop. You can shut your laptop and walk away.

Starting with a template

Don't build from scratch your first time. Open the Automation Marketplace and pick a template close to what you want.

There are templates for TikTok posting, Instagram warm-up, Reddit publishing, Facebook commenting, and dozens more. Each one is editable.

A typical TikTok post template looks like this in pseudo-code:

1. Open TikTok app
2. Wait random 3-7 seconds
3. Tap "+" upload button
4. Select video from gallery (file uploaded via API beforehand)
5. Wait for upload
6. Tap "Next"
7. Type caption (from variable)
8. Wait random 5-10 seconds
9. Tap "Post"
10. Verify post via OCR check

The random waits matter. Without them, your accounts move at machine speed and the platform notices.

Building a custom RPA flow

When templates don't fit, the RPA Builder lets you compose your own. The two pieces that matter most are element location and randomization.

For element location, Geelark gives you three options: coordinate-based clicks (fragile), OCR text matching (decent), and image recognition (best).

I default to OCR. It survives small UI changes and works across Android versions.

Module: Find Text
  - Search: "Sign in"
  - Region: full screen
  - Timeout: 10s
  - On success: Tap [found element]
  - On fail: Take screenshot, end flow with error

The conditional branch matters. If your flow assumes the button is there and it isn't, the next 30 steps fire blind and trash the account.

For randomization, every "Wait" module should use a min/max range, not a fixed value. Set 3-7 seconds, not 5. Set 1500-2200 ms, not 1800.

Pattern detection is statistical. Break the pattern.

Running RPA at scale

Once a flow works, you assign it to phones and schedule it. The Geelark dashboard lets you set:

  • Which cloud phones run it
  • Start time (immediate or scheduled)
  • Recurrence (one-time, daily, custom cron-ish)
  • Concurrency (how many run at once)

Tasks run in the cloud and bill per successful execution. Check the Logs tab to see what each phone did.

When a flow fails, the log shows you the exact step it died on, often with a screenshot.

Method 3: The Geelark API (full developer control)

The API is where serious automation lives. Synchronizer and RPA are great until you need Geelark to talk to your own database, your own scheduler, or your own content pipeline.

The API gives you everything: create profiles, start and stop phones, install apps, push files, trigger RPA tasks, register webhook callbacks, and run raw ADB commands. All over standard JSON-over-HTTPS.

What you can build

Real things I've shipped using the Geelark API:

A nightly job that pulls 50 video files from S3, uploads each to a different cloud phone's gallery, then triggers a TikTok post template against all 50 in parallel.

A Slack bot that lets the content team type /post-reels [drive_link] and have it auto-distribute to 30 Instagram accounts on a randomized schedule over the next 6 hours.

A health-check service that pings the API every 15 minutes, verifies each phone is running, and pages me if any go offline.

You can't do any of these from inside the Geelark UI alone.

Authentication

Generate an API key from the Geelark dashboard under Settings → API. You'll get an appId and an apiKey. Treat the apiKey like a password.

The API expects four headers on every request: your appId, a unique trace ID, a timestamp, a nonce, and a signature derived from those plus your apiKey.

All requests are POST with JSON bodies.

Here's the auth helper in Python. Drop it in geelark_auth.py:

# geelark_auth.py
import hashlib
import time
import uuid

def build_headers(app_id: str, api_key: str) -> dict:
    """Build Geelark API auth headers for a single request."""
    ts = str(int(time.time() * 1000))   # milliseconds
    nonce = uuid.uuid4().hex[:6]         # 6-char random
    raw = f"{app_id}{ts}{nonce}{api_key}"
    sign = hashlib.sha256(raw.encode()).hexdigest().upper()
    return {
        "Content-Type": "application/json",
        "appId": app_id,
        "traceId": uuid.uuid4().hex,
        "ts": ts,
        "nonce": nonce,
        "sign": sign,
    }

Two things to notice: the timestamp is in milliseconds, not seconds. The signature is uppercase hex.

Get either wrong and you get a generic auth error with no hint what's wrong. Ask me how I know.

Your first request: list phones

Test your auth by listing your cloud phones. The endpoint is /open/v1/phone/list.

# list_phones.py
import requests
from geelark_auth import build_headers

APP_ID = "your-app-id"
API_KEY = "your-api-key"
BASE = "https://openapi.geelark.com"

def list_phones(page=1, page_size=20):
    url = f"{BASE}/open/v1/phone/list"
    headers = build_headers(APP_ID, API_KEY)
    body = {"page": page, "pageSize": page_size}
    r = requests.post(url, headers=headers, json=body, timeout=10)
    r.raise_for_status()
    return r.json()

if __name__ == "__main__":
    data = list_phones()
    for phone in data["data"]["items"]:
        print(f"{phone['id']}  {phone['serialName']}  {phone['status']}")

A 200 response with your phone list means auth works. If you get 41001 or similar, your signature is off.

Most often it's a timestamp drift or a typo in the apiKey. Re-check both before assuming the API is broken.

Starting and stopping phones

Phones bill per minute, so you want them off when nothing's running. Start them right before a task and stop them after.

# phone_control.py
import requests
from geelark_auth import build_headers

APP_ID, API_KEY = "...", "..."
BASE = "https://openapi.geelark.com"

def start_phone(phone_id: str):
    url = f"{BASE}/open/v1/phone/start"
    headers = build_headers(APP_ID, API_KEY)
    r = requests.post(url, headers=headers,
                      json={"ids": [phone_id]}, timeout=15)
    return r.json()

def stop_phone(phone_id: str):
    url = f"{BASE}/open/v1/phone/stop"
    headers = build_headers(APP_ID, API_KEY)
    r = requests.post(url, headers=headers,
                      json={"ids": [phone_id]}, timeout=15)
    return r.json()

start is asynchronous. The phone takes 30-60 seconds to boot.

Don't fire your RPA task immediately after the start call returns. Poll /phone/list and wait for status == "started" first, or wire up a webhook (see below).

Triggering an RPA task via API

This is the magic move. Build a flow once in the visual builder, then fire it from code against any phone with any input variables you want.

# run_task.py
import requests
from geelark_auth import build_headers

APP_ID, API_KEY = "...", "..."
BASE = "https://openapi.geelark.com"

def run_task(phone_id: str, flow_id: str, params: dict):
    url = f"{BASE}/open/v1/task/run"
    headers = build_headers(APP_ID, API_KEY)
    body = {
        "envId": phone_id,        # cloud phone ID
        "flowId": flow_id,        # RPA flow ID from dashboard
        "params": params,         # variables your flow expects
    }
    r = requests.post(url, headers=headers, json=body, timeout=15)
    return r.json()["data"]["taskId"]

# Example: post a TikTok video with custom caption
task_id = run_task(
    phone_id="phone_abc123",
    flow_id="flow_tiktok_post",
    params={
        "video_url": "https://my-cdn.com/clip42.mp4",
        "caption": "POV: you finally automated your stack",
    },
)
print("Task started:", task_id)

The params dict is passed into the RPA flow as variables. In your flow's text-input modules, reference them as ${video_url} and ${caption}.

This is how you avoid building 100 nearly-identical flows for 100 captions.

Handling webhooks

Polling the API every 30 seconds to check task status works but burns rate limit. Webhooks are better.

Configure a webhook URL in Settings → API → Callback. Geelark POSTs to your endpoint when phones change state, tasks finish, or errors fire. Here's a minimal Flask receiver:

# webhook_server.py
from flask import Flask, request, jsonify

app = Flask(__name__)

@app.route("/geelark/webhook", methods=["POST"])
def webhook():
    event = request.json
    event_type = event.get("type")

    if event_type == "task.finished":
        task_id = event["data"]["taskId"]
        status = event["data"]["status"]   # "success" or "failed"
        handle_task_done(task_id, status)
    elif event_type == "phone.status":
        phone_id = event["data"]["id"]
        new_status = event["data"]["status"]
        update_phone_state(phone_id, new_status)

    return jsonify({"received": True}), 200

def handle_task_done(task_id, status):
    print(f"Task {task_id}: {status}")

def update_phone_state(phone_id, status):
    print(f"Phone {phone_id} -> {status}")

if __name__ == "__main__":
    app.run(port=8080)

Run that behind ngrok or a real domain, set the webhook URL in Geelark, and your code reacts to events instead of polling.

Make sure to return a 200 quickly. Geelark retries on non-2xx responses, and you don't want duplicate processing.

Putting it together: a full automation script

Here's the actual pattern I run nightly. It boots a phone, waits for it to be ready, runs a posting flow, and shuts the phone down.

All from one Python file.

# nightly_post.py
import time
from phone_control import start_phone, stop_phone, list_phones
from run_task import run_task

def wait_until_ready(phone_id, timeout=120):
    """Poll until the phone is fully started or timeout."""
    deadline = time.time() + timeout
    while time.time() < deadline:
        phones = list_phones()["data"]["items"]
        match = next((p for p in phones if p["id"] == phone_id), None)
        if match and match["status"] == "started":
            return True
        time.sleep(5)
    raise TimeoutError(f"Phone {phone_id} never started")

def post_video(phone_id, video_url, caption):
    start_phone(phone_id)
    wait_until_ready(phone_id)
    task_id = run_task(phone_id, "flow_tiktok_post",
                      {"video_url": video_url, "caption": caption})
    print(f"Posted from {phone_id}, task {task_id}")
    # In production: wait for webhook before stopping
    time.sleep(180)  # crude wait for posting to finish
    stop_phone(phone_id)

That's roughly 20 lines doing what would take half an hour of clicking. Multiply by 50 phones and the time savings get real.

Going deeper: ADB shell access

When the API and RPA aren't enough, you can drop down to ADB and run shell commands directly on the cloud phone.

This is the escape hatch for anything Geelark's higher-level tools don't expose.

ADB works on Android 9, 11, 12, 13, 14, and 15. The phone has to be started first.

Enabling ADB is asynchronous. Wait about 3 seconds after the call returns before grabbing connection details.

Enabling and using ADB

# adb_setup.py
import requests
import time
from geelark_auth import build_headers

APP_ID, API_KEY = "...", "..."
BASE = "https://openapi.geelark.com"

def enable_adb(phone_id):
    url = f"{BASE}/open/v1/adb/setStatus"
    headers = build_headers(APP_ID, API_KEY)
    r = requests.post(url, headers=headers,
                      json={"id": phone_id, "open": True})
    time.sleep(3)  # async — wait before fetching details
    return r.json()

def get_adb_info(phone_id):
    url = f"{BASE}/open/v1/adb/getData"
    headers = build_headers(APP_ID, API_KEY)
    r = requests.post(url, headers=headers, json={"ids": [phone_id]})
    return r.json()["data"]["items"][0]

The getData response gives you ip, port, and pwd. From there, connect with the standard adb client:

# Connect and run a shell command
adb connect <ip>:<port>
adb -s <ip>:<port> shell pm list packages
adb -s <ip>:<port> install ./my-app.apk

I use this for installing apps that aren't in Geelark's app market and for grabbing logcat when an RPA flow keeps failing on a specific phone.

It's also the cleanest way to push specific files to non-standard paths.

Production patterns that save you

Three things turn a working script into a working system. Skip them and your nightly job dies the first time something flickers.

Retry with exponential backoff

The Geelark API is reliable but not infallible. Wrap calls in a retry helper:

# retry.py
import time
import requests

def with_retry(fn, max_attempts=3, base_delay=2):
    """Run fn() with exponential backoff on transient errors."""
    for attempt in range(max_attempts):
        try:
            return fn()
        except (requests.Timeout, requests.ConnectionError) as e:
            if attempt == max_attempts - 1:
                raise
            wait = base_delay * (2 ** attempt)
            print(f"Retry {attempt+1}/{max_attempts} after {wait}s: {e}")
            time.sleep(wait)

Wrap any API call: with_retry(lambda: start_phone(phone_id)). Don't retry on 4xx responses — those mean your request is wrong, not that the network blinked.

Scheduling with cron

Skip the temptation to use time.sleep(86400) and a while True loop. Use cron or systemd timers.

A simple crontab entry that runs the posting job every night at 9pm:

# crontab -e
0 21 * * * cd /opt/geelark && /usr/bin/python3 nightly_post.py >> /var/log/geelark.log 2>&1

The >> /var/log/geelark.log 2>&1 matters. Without it you have no idea why your job stopped working three weeks ago.

Concurrency limits

Don't fire 200 RPA tasks at once. Geelark accepts them, but each running phone uses a minute of your account quota, and you'll burn through your plan fast.

Cap parallelism with a semaphore:

import concurrent.futures

PHONES = ["phone_001", "phone_002", ...]   # 50 phones

def post_to_phone(phone_id):
    return post_video(phone_id, video_url, caption)

# Run at most 10 phones at the same time
with concurrent.futures.ThreadPoolExecutor(max_workers=10) as ex:
    results = list(ex.map(post_to_phone, PHONES))

Ten concurrent phones is a sane starting point. Watch your minute usage in the dashboard and adjust.

Common errors and fixes

These are the ones I hit most often. Save yourself the debug time.

41001 invalid signature

The signature header is wrong. Most likely causes: timestamp in seconds instead of milliseconds, lowercase hex instead of uppercase, or your system clock is more than 5 minutes off.

Run ntpdate pool.ntp.org if you're on a server with drift. Verify with print(int(time.time() * 1000)) and check it matches a current epoch milliseconds value.

phone not started when triggering a task

You called task/run before the phone finished booting. Use the polling loop in the example above, or wire up the phone.status webhook and only fire tasks after started.

Don't paper over this with a static time.sleep(60) — phones boot in 20-90 seconds depending on Android version and load.

RPA flow runs but does nothing

Almost always an OCR or element-find failure. The app updated, the screen layout changed, your "Sign in" button is now "Log in".

Open the flow logs, find the failing step, look at the screenshot. Update the OCR target text or rebuild that step with image recognition.

This is why every step needs a timeout and a fail-handler. Silent failures are the worst.

Webhook never fires

Your URL isn't reachable from Geelark's servers. Localhost won't work. Use ngrok for testing, a real domain with a valid HTTPS cert for production.

Hit your webhook URL with curl -X POST from another machine to confirm it's externally reachable before blaming Geelark.

A note on proxies

Every Geelark cloud phone needs a proxy. Without one, the phone's IP gives away that it's a datacenter device.

Your fingerprint stops being unique and your accounts link together. The whole reason for using cloud phones evaporates.

For social platforms like TikTok and Instagram, you want residential or mobile proxies. The kind that come from real consumer ISPs and look like home internet.

Datacenter proxies work for some tasks but get flagged faster on the platforms that matter.

If you don't already have a provider, Roundproxies offers residential, ISP, and mobile pools that drop into Geelark's proxy manager cleanly. Import once, assign one per profile.

The non-negotiable rule: one proxy per profile. Reusing a single proxy across 20 cloud phones defeats the entire point.

Wrapping up

The three methods aren't competing. They're a ladder.

Start with Synchronizer when you need something done now. Move to RPA when the same flow runs twice. Drop down to the API when Geelark needs to talk to your own systems.

Most of the value comes from picking correctly.

I've watched teams build elaborate RPA flows for tasks that needed two minutes of Synchronizer. I've watched others click manually for weeks on something that should have been a 30-line Python script.

What to build next: pick one repetitive task you do at least three times a week. Decide which tool fits. Build the smallest version that works.

Run it for a week before adding any features.

For more on the API surface, check the official Geelark API documentation.

For why cloud phones differ from emulators (it matters when fingerprints decide whether your accounts survive), see Geelark's comparison with Android emulators.

The GeeLark GitHub org hosts their open API repo and example skills.