Skip to main content

Invi Labs Python SDK

Automate real iPhones through the Invi Labs API using your API key and phone ID. Non-jailbroken iPhones require Invi Dongle.

Install

python -m pip install invilabs

Requires Python 3.10 or newer. No third-party runtime dependencies.

Connect and run an action

Set INVI_API_KEY and INVI_PHONE_ID in your environment. Keep your API key private. INVI_PHONE_ID is the phone's code as shown in the panel (for example SE001); its name or id also work.

import os
from invilabs import Client

client = Client(os.environ["INVI_API_KEY"])
phone = client.phone(os.environ["INVI_PHONE_ID"])

print(phone.details())
phone.tap(100, 200)
phone.save_screenshot("screen.png")

Coordinates use full-resolution screenshot pixels. Calls block until completion; the SDK waits and retries when the phone reports device_busy. A definite device_offline response waits for reconnect with delays of 1, 2, and 3 seconds, up to three retries (six seconds total by default). Each offline rejection uses a new operation key because the phone received no input. Uncertain execution is never automatically repeated; use the operation key to look up its status. Set Client(..., offline_retries=0) to disable reconnect waiting, or choose an integer from 0 to 30 for longer outages.

AI generation

result = client.generate(
    "Generate an email address using example.com",
    result_type="email",
)
print(result["data"]["text"])
print(client.balance())

Generation supports text, usernames and email address strings, optionally using a local PNG/JPEG image. It does not create mailboxes. OCR, AI prompts and generation use your shared Account AI balance. No AI-provider key is required.

Features

  • Tap, swipe, scroll, type, explicit key presses and opening installed apps.
  • Screenshots as PNG bytes or saved to your computer.
  • Grouped OCR with text and bounding boxes.
  • Single-action AI prompts and structured screen answers.
  • Image/video import through Invi Helper on supported dongle phones.
  • Device details, balance and operation recovery.
  • An MCP server (invilabs-mcp) so AI agents can drive your phones.

All network operations use the public HTTPS API with your API key. The package contains client code and response contracts; phone control and AI processing run on Invi Labs services. API keys remain on your computer and are sent only as an authentication header to the configured API. HTTP redirects are not followed.

This is an initial release. Device support and feature verification limits are listed in the release notes.

Documentation · Quickstart · Generation and outputs · Errors and recovery

Typing completion

Version 0.1.1 waits for the dongle's final typing acknowledgment. Long typing and AI actions remain synchronous to Python while the SDK waits for the same server operation to finish. It never starts a second copy while waiting. Call phone.press("enter") after phone.type(text) returns to submit a search. A lost connection can still produce ExecutionUncertainError; inspect the recorded operation and phone state before deciding on another action.

Tap text with OCR (0.1.2)

result = phone.tap_text("Add a comment")
result = phone.tap_text(r"^(?:add|write) (?:a )?comment[.…]*$", regex=True)
print(result["point"], result["ocr"]["credits_charged"])

Matches the full trimmed OCR block case-insensitively by default; regex uses re.search. The tap lands randomly inside an inset of the block's bounds. Use randomize=False for its center. Multiple matches raise AmbiguousTextError unless a zero-based occurrence is supplied; no match raises TargetNotFoundError. Neither error taps. Regex substring matches use the whole block's box. Keep the screen stable between OCR and tap: these are two operations.

Returns match (OCR block), point (x, y pixels), ocr (blocks, credit charge, balance), and tap (execution status and coordinates). Only the normal OCR credit charge applies, including when no match is found. A tap failure retains the OCR result in error.details["ocr"]. This helper makes no VLM request.

Check text presence with OCR (0.1.2)

check = phone.text_exists("Continue", region="middle")
if check["exists"]:
    print(check["matches"])

check = phone.text_exists(r"^(?:add|write) (?:a )?comment[.…]*$", regex=True, region="bottom")
check = phone.text_exists("Comments", region=(0, 900, 1170, 600))

Omit region for the full screen. Named regions are full-width horizontal thirds; custom tuples are (x,y,width,height) in original screenshot pixels. A block's center determines region membership. Literal and regex rules match tap_text. Returns exists (bool), matches (OCR blocks), region (resolved pixel box or None), and ocr (full result with charge and balance). Check result["exists"], not the dictionary's truthiness. No match is False; OCR failures raise errors. One billed full-screen OCR request is made regardless of region or match count. Named regions also read dimensions before and after OCR and reject geometry changes. This method never taps or calls a VLM.

Jailbroken-only extensions (0.1.5+)

The phone.jailbroken namespace is available only for jailbroken iPhones with our current daemon, UI tweak and container service. Dongle phones reject these operations; their existing controls and OCR helpers are unchanged.

jb = phone.jailbroken
print(jb.apps())
print(jb.containers.list("com.example.app"))
created = jb.containers.create("com.example.app", "testing")
jb.open_app("com.example.app", cid=created["cid"])
print(jb.containers.active("com.example.app"))
jb.containers.set_active("com.example.app", "default")
jb.containers.delete("com.example.app", created["cid"])
print(jb.ui_dump())
jb.tap_text("Settings")

Containers use the account's existing slot pool and appear in the panel. Container names contain 1–64 letters, digits, underscores or hyphens. The default container cannot be deleted. Switching stops the app; open_app(..., cid=...) switches and launches under one lease. A lost acknowledgment is never automatically replayed.

jb.tap_text uses exact trimmed native UI text and converts point bounds to pixel coordinates. Matching is case insensitive by default. Duplicate matches require occurrence=0 (or another zero-based occurrence, in screen reading order). Disabled and offscreen elements are excluded. UI dump targeting is unbilled and has no OCR fallback. Native dumps require the UI tweak in the foreground app; some custom-rendered content is unavailable. phone.tap_text retains its existing OCR behavior on all phones.

Jailbroken phone controls (0.1.9)

jb = phone.jailbroken
jb.tap(375, 667)                      # same as phone.tap
jb.swipe(375, 1100, 375, 300)         # same as phone.swipe
jb.long_press(375, 667, duration_ms=800)
jb.double_tap(375, 667)               # 4.7-inch screens
jb.home()
jb.app_switcher()
print(jb.foreground_app())            # {"bundle_id": ..., "home_screen": ...}
jb.close_app()                        # the app on screen
jb.close_app("com.burbn.instagram")   # one app by bundle ID
print(jb.close_all_apps())            # {"closed": ["Instagram", ...], "failed": 0}
print(jb.screen_state())              # {"screen_on": True, ...}
jb.lock(); jb.wake(); jb.unlock()

close_app and close_all_apps never close system UI, Invi apps, jailbreak tools or VPN/proxy apps (app_protected). close_app() on the Home screen returns no_app_open. unlock() works on phones without a passcode. The jb_* operations are refused on dongle phones before anything is sent.

Use the common phone.upload_media(path) and phone.prompt(...) methods on both phone types. Jailbroken uploads use an exact native Photos transaction and wait for its asset acknowledgment before deleting the temporary server copy. Image/video limits, permissions, shared credits for AI prompts and operation recovery remain the same.

MCP server for AI agents (0.1.12)

Invi Labs hosts a Model Context Protocol server at https://api.invilabs.io/mcp. It lets Claude, Codex, Cursor and other MCP clients control your phones with your API key, with nothing to install: add it as a remote (HTTP) server and send the key as Authorization: Bearer YOUR_API_KEY.

claude mcp add --transport http --scope user invilabs https://api.invilabs.io/mcp \
  --header "Authorization: Bearer YOUR_API_KEY"

Cursor and most other clients:

{
  "mcpServers": {
    "invilabs": {
      "url": "https://api.invilabs.io/mcp",
      "headers": {"Authorization": "Bearer YOUR_API_KEY"}
    }
  }
}

Your agent does the looking and deciding: it reads screenshots with its own model and chooses where to tap. The server has no OCR or AI of its own and never spends your AI credits, so phone_ocr, phone_prompt, phone_tap_text, phone_text_exists, generate, balance and operation are not MCP tools (they remain in the SDK). Every other SDK method is a tool, generated from the SDK: client.X is X, phone.X is phone_X, phone.jailbroken.X is jb_X and phone.jailbroken.containers.X is jb_containers_X.

Phones go by their code in the panel (for example SE001); agents pass it as phone_id. Narrow the tools in the URL: ?phone=SE001 (repeatable; the first is the default), read_only=1, tools=phone_*,devices or exclude_tools=jb_*. The hosted server cannot reach your files, so phone_upload_media and phone_save_screenshot are only offered by the local server.

Run it locally

The same server ships in this package as invilabs-mcp (stdio). Claude Code, with uv installed:

claude mcp add invilabs --scope user --env INVILABS_API_KEY=YOUR_API_KEY \
  -- uvx --from "invilabs[mcp]@latest" invilabs-mcp

Other clients use the same command and environment, for example:

{
  "mcpServers": {
    "invilabs": {
      "command": "uvx",
      "args": ["--from", "invilabs[mcp]@latest", "invilabs-mcp"],
      "env": {"INVILABS_API_KEY": "YOUR_API_KEY"}
    }
  }
}

@latest makes uv check for a new release each time the client starts the server. The [mcp] extra adds Pillow for smaller JPEG screenshots; without it the server sends PNG. Pass the API key in the client's env block, because most clients do not forward your shell environment (INVI_API_KEY also works). GUI apps such as Claude Desktop may need the full path to uvx.

Screenshots are scaled to fit vision models (1170×2532 becomes 725×1568), and every coordinate the server accepts or returns uses that image's pixels. The server converts to native pixels for you. Jailbroken jb_ui_dump frames come back in the same pixels.

Real phones take time to react, so the server handles delays for the agent:

  • Screen actions (taps, swipes, typing, keys, app launches) accept screenshot_after: true and settle_ms (1000 ms after a tap up to 4000 ms after an app launch by default) and return the resulting screen in the same call. phone_open_app does this by default: on dongle phones a dropped keystroke in Spotlight can open a different app.
  • A wait tool pauses between steps, for example while a video plays.
  • Read-only calls such as screenshots are repeated after transient failures (--read-retries, default 2). Inputs are never repeated automatically.
  • When a phone does not confirm an input (execution_uncertain), the server attaches the current screen and says why: ack_timeout (the phone's link stalled and the input may still land, so an identical repeat is refused for 45 s unless allow_repeat is set) or not_confirmed (the phone or its link reported an error, but the input may still have run, fully or partly). A lost response is looked up, so a completed action is reported as completed.
  • phone_health checks a phone before a session: online, busy, whether its screen video is streaming, and any unconfirmed action. One slow capture on weak Wi-Fi is only a suspicion (no_frame_once); a phone whose screen is not streaming fails fast with screen_not_streaming.
  • On dongle phones, swipes and scrolls are limited to 3000 ms, and vertical ones last at least 480 ms (shorter ones are raised, and the result says so).
  • The App Switcher sends no new video frames until something moves, so a screenshot there fails after about 12 s; the server says so instead of retrying.

Codex (~/.codex/config.toml) needs longer timeouts than its defaults, because typing and busy phones can take more than a minute:

[mcp_servers.invilabs]
command = "uvx"
args = ["--from", "invilabs[mcp]@latest", "invilabs-mcp"]
env_vars = ["INVILABS_API_KEY"]
startup_timeout_sec = 60
tool_timeout_sec = 300

Options go after invilabs-mcp:

Option Effect
--phone SE001 Only allow this phone: its code in the panel, name or id (repeatable). The first one is the default phone.
--read-only Only tools that change nothing.
--tools "phone_*,devices" Only matching tools. --exclude-tools "jb_*" hides tools.
--native-coordinates Full-resolution screenshots and native pixels.
--image-format png PNG instead of JPEG.
--busy-timeout 45 Seconds to wait for a phone another automation holds (default 45).
--list-tools Print the tool definitions and exit.

INVILABS_PHONE_ID sets a default phone without restricting access; with --phone it must be one of the allowed phones. The server tells the model to take a screenshot after each action and not to repeat an execution_uncertain action blindly. Cancelling a tool call stops the server waiting for a busy or offline phone, so the input is not sent later. phone_save_screenshot only writes .png files and never replaces a file that is not a PNG. Run invilabs-mcp --help for every option, including the screenshot size limits. invilabs-mcp --http --port 8000 serves the hosted variant yourself: every request brings its own key, and file tools are off.

Cloud phone proxy and VPN

SDK 0.1.7 adds connectivity controls for phones with an Invi Labs managed router. Your API key needs device.edit_proxy and access to the phone.

phone = client.phone(phone_id)
print(phone.connectivity(timeout=30))
locations = phone.vpn_locations(timeout=30)
print(locations)

phone.set_proxy("proxy.example", 1080, "username", "password", timeout=60)
phone.use_vpn(timeout=60)  # Restore the currently configured VPN
if locations:
    phone.use_vpn(locations[0], timeout=60)
# Equivalent explicit method:
# phone.set_vpn_location(locations[0], timeout=60)

Server-enforced cooldown

A phone accepts one network-change attempt every 60 seconds. The server shares this cooldown across API keys, server instances, proxy changes, VPN changes, and managed-proxy changes. Switching keys or endpoints cannot bypass it. A failed router attempt also consumes the interval. Status and VPN-location reads remain available during the cooldown.

Requests during the cooldown return HTTP 429, error="rate_limited", a Retry-After header, and details.retry_after_seconds. The SDK raises InviError with these details and does not retry automatically:

from invilabs import InviError

try:
    phone.use_vpn("lax")
except InviError as error:
    if error.code == "rate_limited":
        print("Wait", error.details["retry_after_seconds"], "seconds")
    else:
        raise

The optional client timeout argument only bounds how long the request waits; it cannot shorten or disable the server cooldown.

Each call blocks and accepts a positive timeout in seconds (default 60). A timed-out change raises ExecutionUncertainError: the server may still finish it. Read phone.connectivity() before deciding what to do next. Network changes are never automatically retried, including busy responses. A timeout is a client waiting limit, not a scheduled switch-back or rollback.

connectivity() reports mode, healthy, available_vpn_locations, and when available phone_exit_ip, proxy_host, and proxy_port. Locations come from this phone's assigned router; never assume a fixed global list. Status probes the router's egress and does not prove every app's traffic path.

Proxy support is SOCKS5. Changes affect the entire phone and may interrupt its connections. Selecting a VPN location removes the active proxy. Switching to your own proxy or VPN also stops an active managed-proxy subscription's renewal. SIM-rented phones keep their managed SIM connection; they report mode="sim" with no VPN locations and reject network changes. Customer-owned phones without a managed router cannot use these controls. Viewer sessions can remain open, though a network switch can temporarily interrupt them.

No separate owner connectivity helper, router URL, or phone MAC is needed.

AI credit pricing

AI usage is metered to 0.000001 credit. Your plan price and monthly credit allowance determine the dollar value of a credit; all plans use the same usage rates.

Processing Credits charged
1,000 input tokens Approximately 0.011111
1,000 output tokens Approximately 0.155556
One fresh OCR request 0.05
Direct taps, swipes, typing, screenshots and media transfers 0 AI credits

The exact token formula is (input_tokens + 14 * output_tokens) / 90000, rounded up once per metered invocation to the nearest millionth of a credit. Input includes the system instructions and any image tokens reported by inference. These rates apply to AI prompts, generation and agent model calls on both jailbroken and non-jailbroken phones. Agent flows add the work performed across their model calls and OCR scans.

For example, 2,000 input tokens and 100 output tokens cost 0.037778 credits. On the $45 / 1,000-credit plan, that is approximately $0.00170. A fresh OCR scan costs $0.00225 on that plan. OCR helpers such as tap_text() and text_exists() follow the same OCR metering; each fresh scan is charged, while eligible cache hits and operation replays do not charge again.

Metadata

Release files for invilabs 0.1.13

For a detailed explanation of source distributions (sdists) and built distributions (wheels), please see the package formats documentation.

Source distribution (sdist)

Source distribution for invilabs 0.1.13
File Size Uploaded Provenance
invilabs-0.1.13.tar.gz 92.0 kB With provenance Details

Built distribution (wheel)

Table of built distributions (wheels) for invilabs 0.1.13
File Interpreter ABI Platform
invilabs-0.1.13-py3-none-any.whl Python 3 none any Details

Total release size: 183.4 kB

Release files / invilabs-0.1.13.tar.gz

Download URL invilabs-0.1.13.tar.gz
Size 92.0 kB
Tags Source
SHA-256 checksum
How to use checksums
5d945a62c1e2d02ff4f7063ac6dac18dfb858b5f769259218bad3266fda5f600
BLAKE2b-256 checksum
How to use checksums
ec03cf6e9bd1e42b6af4c1297f22f12212f14a20e80a62b9a41d2ed611715535
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
Yes
Uploaded via twine/7.0.0 CPython/3.13.14

Provenance

Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.

PyPI Publish Attestation

PyPI verified that this artifact, at this checksum, originated from the publisher listed below.

Signed by GitHub Actions, verified by PyPI on Oct 19, 2026.

Transparency log

Release files / invilabs-0.1.13-py3-none-any.whl

Download URL invilabs-0.1.13-py3-none-any.whl
Size 91.4 kB
Tags Python 3
SHA-256 checksum
How to use checksums
31abff8ea74f1a6152c6b8183accbd6e453deafbd2e58190a045e7f99d6cad9a
BLAKE2b-256 checksum
How to use checksums
a8742e350eb567dfdb0ad8038754d1ae8c808c0447d5e06e8288d2e8b0956fcb
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
Yes
Uploaded via twine/7.0.0 CPython/3.13.14

Provenance

Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.

PyPI Publish Attestation

PyPI verified that this artifact, at this checksum, originated from the publisher listed below.

Signed by GitHub Actions, verified by PyPI on Oct 19, 2026.

Transparency log

Security for invilabs 0.1.13

Provenance

Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.

Full provenance

All 2 files in this release have been uploaded with provenance metadata.

Select from 2 files

Attestation for invilabs-0.1.13.tar.gz

PyPI Publish Attestation

PyPI verified that this artifact, at this checksum, originated from the publisher listed below.

Signed by GitHub Actions, verified by PyPI on Oct 19, 2026.

Transparency log

A note on package security

PyPI is a repository for distributing packages, not a service for auditing them. While third-party security researchers and our staff do their best to detect and remove malicious packages, PyPI cannot guarantee the quality, security or suitability of any given package.

To learn more about protecting yourself from malicious actors, visit our package security documentation.

Anthropic, PBC Visionary sponsor Bloomberg Visionary sponsor Hudson River Trading Visionary sponsor Meta Visionary sponsor NVIDIA Visionary sponsor Microsoft Sustainability sponsor Depot Continuous Integration AWS Cloud computing and Security Sponsor Datadog Monitoring Fastly CDN Google Download Analytics Sentry Error logging StatusPage Status page