Overview & Quickstart

What is aimlib?

aimlib is a mobile-proxy and managed-browser service. When you rent a device through aimlib, you get:

  1. Raw mobile proxy: A proxy running on the device's real mobile IP, usable immediately with any HTTP client.
  2. Optional managed stealth browser: An on-demand, containerized Chrome browser that egresses through the same device mobile IP, providing a stealth browsing environment with a resolved device fingerprint.

Both the proxy and the browser share the same device identity — same mobile IP, same carrier, same region, same resolved fingerprint.

Authentication

aimlib uses a single API key:

  • Format: ak_live_... (provided by aimlib)
  • Transmission: HTTP header Authorization: Bearer <your-api-key>
  • Base URL: The regional gateway tied to your lease, e.g. https://uswest1.aimlib.com. This gateway serves both the /v1 REST surface and the browser CDP relay.
  • Override the base URL if your lease is in a different region.

The SDK sets the auth header automatically; you only supply the API key and (optionally) the base URL.

Installation

The SDK targets Python 3.10+. The proxy surface needs only httpx; the managed browser additionally needs Playwright.

# Proxy only:
pip install "aimlib @ https://docs.aimlib.com/sdk/aimlib-0.2.0-py3-none-any.whl"

# Proxy + managed browser:
pip install "aimlib[browser] @ https://docs.aimlib.com/sdk/aimlib-0.2.0-py3-none-any.whl"
playwright install chromium   # only needed to drive the browser

# Configure
export AIMLIB_API_KEY=ak_live_...
export AIMLIB_BASE_URL=https://uswest1.aimlib.com

Quick Start: Raw Proxy + Managed Browser

import asyncio
from aimlib import Aimlib

async def main():
    # Reads AIMLIB_API_KEY (required) and AIMLIB_BASE_URL (optional) from env.
    ai = Aimlib()

    # Your leased devices
    devices = await ai.devices.list()
    device = devices[0]

    # === Raw mobile proxy (ready immediately, any HTTP client) ===
    print(f"Mobile proxy: {device.proxy.url}")
    # import requests
    # resp = requests.get("https://example.com",
    #                     proxies={"http": device.proxy.url, "https": device.proxy.url})

    # === Managed stealth browser ===
    # device.browser(...) is a coroutine: await it to start (provision) a session.
    # Using it as an async context manager connects on entry and stops on exit.
    async with await device.browser(profile="pixel-8-pro", ttl="10m") as session:
        # Entering the context waits for the container to boot (~55-60s on first
        # connect), then connects over the CDP relay. `session` is a BrowserSession.
        page = await session.new_page()
        await page.goto("https://example.com")
        print(await page.title())

        # Additional tabs share the same session (same IP, same fingerprint),
        # up to session.max_tabs (default 5).
        page2 = await session.new_page()

    # Context exit calls session.stop() (closes the browser + DELETEs the session).
    await ai.aclose()

asyncio.run(main())

Device & Proxy Objects

device = await ai.device("device-id")  # fetch a specific leased device

device.id                # Device ID
device.region            # Region (may be None)
device.carrier           # Mobile carrier (may be None)
device.current_egress_ip # Current egress IP (may be None)

device.proxy             # Proxy object, or None if no proxy is attached
device.proxy.url         # Raw proxy URL
device.proxy.protocol    # Proxy protocol
device.proxy.status      # Proxy status
device.proxy.id          # Proxy ID

ai.device(id) resolves by listing your leased devices and matching the ID; it raises AimlibError if the device is not leased to your account.

Browser Sessions

device.browser(...) is an async method that provisions a session and returns a BrowserSession. All keyword arguments are optional; any you omit are left to the server's defaults.

async with await device.browser(
    profile="pixel-8-pro",   # device name (str), a Profile object, or a dict
    ttl="10m",               # session time-to-live: int seconds or "10m"/"1h"/"30s"
    idle_timeout="5m",       # auto-stop after this idle period
    sticky=True,             # request a sticky session
) as session:
    page = await session.new_page()
    await page.goto("https://example.com")

# Or manage the session manually:
session = await device.browser(profile="random", ttl="1h")
await session.connect()       # waits until ready, returns the Playwright Browser
page = await session.new_page()
# ...
await session.stop()          # closes the browser and DELETEs the session

Notes: - ttl and idle_timeout accept an integer number of seconds, or a string with a unit suffix s/m/h (e.g. "30s", "10m", "1h"). A bare number string is treated as seconds. - await session.connect() returns the live Playwright Browser. Entering the async context manager (async with) returns the BrowserSession itself — call session.new_page() on it.

profile parameter: - profile="pixel-8-pro" (string): an aimlib standardized device name, e.g. "pixel-8-pro", "galaxy-s24-ultra", or "random". - profile=Profile(...) (object): override individual fields (see Device Profiles). - profile={...} (dict): a raw override dict, passed through as-is.

Core Concepts

One Session = One Identity

All tabs opened in a single browser session share: - The same mobile IP — egressing through the device's real mobile carrier. - The same resolved fingerprint — user agent, screen size, timezone, locale, etc.

The per-container tab count is bounded by session.max_tabs (server-configured, default 5). Calling new_page() past the limit raises TabLimitError. For parallel work under different identities, open separate sessions.

On-Demand Container Boot

The first connect (entering the async context manager, or calling connect()) waits for the container to provision and Chrome to come up — typically 55-60 seconds. The SDK polls readiness with a default timeout of 180 seconds (3 minutes); exceeding it raises SessionTimeout. Subsequent new_page() calls are fast.

Browser Runs on aimlib Infrastructure

The managed browser runs in aimlib's container infrastructure, but all outbound traffic egresses through your device's real mobile IP: - Websites see the device's real mobile IP and a mobile fingerprint. - The fingerprint (screen, timezone, OS, etc.) matches the requested device profile.

No Billing in Beta

During beta there is no usage billing. Test freely.

Device Profiles

Customize device characteristics with the Profile object. Omit any field to keep the profile's default.

from aimlib import Profile

profile = Profile(
    device="pixel-8-pro",            # standardized device name
    timezone="America/Los_Angeles",
    locale="en-US",
    cores=4,
    ram_gb=8,
    screen=(1440, 3120),             # (width, height)
    dpi=512,
)

async with await device.browser(profile=profile) as session:
    page = await session.new_page()

Profile fields: device, timezone, locale, cores, ram_gb, screen, dpi.

Error Handling

The SDK raises typed exceptions (all subclass AimlibError):

from aimlib import (
    AimlibError,
    CapacityError,
    LeaseInactiveError,
    DataCapError,
    SessionExpiredError,
    SessionTimeout,
    TabLimitError,
)

try:
    async with await device.browser(ttl="10m") as session:
        page = await session.new_page()
except CapacityError:
    print("No container capacity available; retry later")
except SessionTimeout:
    print("Session was not ready within the timeout (~3 minutes)")
except SessionExpiredError:
    print("Session not found / expired")
except TabLimitError:
    print("Hit the per-session tab limit; open a separate session")
except LeaseInactiveError:
    print("Device lease is inactive")
except DataCapError:
    print("Data cap exceeded")
except AimlibError as e:
    print(f"API error: {e}")
Exception Raised when (server error code)
CapacityError capacity_unavailable
LeaseInactiveError lease_inactive
DataCapError data_cap_exceeded
SessionExpiredError session_expired (also raised locally if the session 404s while waiting)
SessionTimeout session_provisioning, or the readiness wait exceeds its timeout
TabLimitError opening more tabs than session.max_tabs
AimlibError base class / any other API error

Environment Variables

Variable Default Description
AIMLIB_API_KEY (required) Your API key (ak_live_...)
AIMLIB_BASE_URL https://uswest1.aimlib.com Regional gateway URL; override for other regions

REST API

The customer-facing REST API is served at the regional gateway (e.g., https://uswest1.aimlib.com) and uses bearer token authentication with customer API keys.

Authentication

All /v1 endpoints require the Authorization header carrying the customer API key:

Authorization: Bearer ak_live_...
  • A missing or malformed token, or one that is unknown/revoked/expired or not a customer key, returns 401 Unauthorized with {"error": "unauthorized"}.
  • A token whose customer account is not active returns 403 Forbidden with {"error": "lease_inactive", "message": "customer not active"}.
  • Per-endpoint scopes control access. A missing scope returns 403 Forbidden with {"error": "forbidden", "message": "missing scope <scope>"}.

Endpoints

GET /v1/devices

List the devices the caller holds an active lease on, each with its raw proxy URL and a browser-availability flag.

Authentication: Authorization: Bearer <api_key> (scope: proxies.read)

Request: (none)

Response: 200 OK — array of device objects (empty array if the caller has no active leases)

[
  {
    "device_id": "550e8400-e29b-41d4-a716-446655440000",
    "region": "uswest1",
    "carrier": "t-mobile",
    "lease": {
      "id": "660e8400-e29b-41d4-a716-446655440000",
      "ends_at": "2026-12-31T23:59:59Z"
    },
    "current_egress_ip": "203.0.113.45",
    "proxy": {
      "id": "770e8400-e29b-41d4-a716-446655440000",
      "protocol": "socks5",
      "url": "socks5h://user:pass@uswest1.aimlib.com:12345",
      "status": "active"
    },
    "browser": {
      "available": true
    }
  }
]
Field Type Notes
device_id UUID (string) The physical device
region string Gateway region (e.g., "uswest1")
carrier string Active carrier (e.g., "t-mobile")
lease.id UUID (string) Active lease ID
lease.ends_at ISO 8601 timestamp or null Lease expiration; null if open-ended
current_egress_ip string Device's current public IP. Present only when the lease has a proxy
proxy object Present only when the lease has a fully provisioned proxy (auth + gateway)
proxy.id UUID (string) Proxy ID
proxy.protocol string "http" or "socks5"
proxy.url string Full proxy URL with credentials. Scheme is http:// for HTTP proxies and socks5h:// for SOCKS proxies (host-side DNS resolution)
proxy.status string Proxy status (e.g., "active")
browser.available boolean Always true (every leased device offers an on-demand browser)

Error Responses:

  • 500 Internal Server Error: {"error": "server error"} — failure listing the caller's leases.

POST /v1/devices/{id}/browser

Create (or adopt a pre-bound) browser session on a device. The browser egresses through the device's mobile proxy IP. The response returns immediately with status: "provisioning"; the container takes roughly 55-60s to boot.

Authentication: Authorization: Bearer <api_key> (scope: sessions.write)

Path Parameters: - id (UUID): device ID from /v1/devices

Request Body: (all fields optional; an empty body is valid)

{
  "ttl": 300,
  "idle_timeout": 120,
  "sticky": false,
  "profile": {
    "android_version": "14",
    "chrome_version": "126",
    "device": "Pixel 8"
  }
}
Field Type Default Notes
ttl int (seconds) 1800 (30 min) Max session lifetime. Values <= 0 or above the 30-minute ceiling are set to the ceiling; then clamped down to the remaining lease duration
idle_timeout int (seconds) 120 Release the session after this many seconds of inactivity. Values <= 0 use the default
sticky boolean (unused) Accepted but not currently acted on
profile JSON object {} Transparent spoof overrides; max 4 KiB; must be a JSON object; unknown keys are stripped

Allowed profile keys (any other key is silently dropped): device, timezone, locale, cores, ram_gb, screen, dpi, model, brand, manufacturer, android_version, build_fingerprint, security_patch, webgl, touch_points, chrome_version, tls_randomize, webrtc_block, ipv6_block, dns_leak_block, fonts, battery, audio_48khz, storage_quota

Response: 201 Created

{
  "session_id": "880e8400-e29b-41d4-a716-446655440000",
  "status": "provisioning",
  "connect_url": "wss://uswest1.aimlib.com/cdp/880e8400-e29b-41d4-a716-446655440000",
  "connect_token": "abcdef1234567890abcdef1234567890",
  "region": "uswest1",
  "ready_in_s": 60,
  "egress_ip": null,
  "expires_at": "2026-06-07T15:30:00Z",
  "max_tabs": 5
}
Field Type Notes
session_id UUID (string) Browser session ID
status string Always "provisioning" on creation (becomes "ready" once booted)
connect_url string WebSocket (wss://) CDP-relay URL at the device's regional gateway, of the form <gateway>/cdp/<session_id>
connect_token string Bearer token for the /cdp/{session_id} WebSocket connection (24-byte random token; returned only here)
region string Gateway region where the session is booted
ready_in_s int Estimated cold-to-driveable boot time in seconds (currently 60; measured ~55s on the baked image)
egress_ip null Always null on creation; populated once the session reaches ready (query via GET)
expires_at ISO 8601 timestamp Hard deadline; the requested TTL clamped to the 30-minute ceiling and the lease end
max_tabs int Max concurrent tabs per session, enforced by the SDK (default 5; server-configurable)

Error Responses:

Status Body Notes
400 Bad Request {"error": "bad id"} Path id is not a valid UUID
400 Bad Request {"error": "profile_invalid", "message": "..."} Profile is not a JSON object or exceeds 4 KiB
403 Forbidden {"error": "lease_inactive", "message": "no active lease with a proxy on this device"} Caller has no active lease on this device, or the lease has no proxy
404 Not Found {"error": "device not found"} Device ID not found
409 Conflict {"error": "data_cap_exceeded", "message": "device monthly cellular cap reached"} Device has exhausted its monthly cellular allowance
429 Too Many Requests {"error": "capacity_unavailable", "message": "device session cap reached"} Device is at its per-device concurrent-session limit
429 Too Many Requests {"error": "capacity_unavailable", "message": "customer session cap reached"} Account is at its concurrent-session limit
503 Service Unavailable {"error": "capacity_unavailable", "message": "no browser host capacity in region"} No free browser host in the device's region
503 Service Unavailable {"error": "capacity_unavailable", "message": "session changed during provisioning, retry"} Lost a provisioning race; retry
500 Internal Server Error {"error": "server error"} Database or internal fault

GET /v1/devices/{id}/browser

Retrieve the active browser session on a device (or 404 if there is none).

Authentication: Authorization: Bearer <api_key> (scope: proxies.read)

Path Parameters: - id (UUID): device ID from /v1/devices

Request: (none)

Response: 200 OK

{
  "session_id": "880e8400-e29b-41d4-a716-446655440000",
  "status": "ready",
  "connect_url": "wss://uswest1.aimlib.com/cdp/880e8400-e29b-41d4-a716-446655440000",
  "egress_ip": "203.0.113.45",
  "expires_at": "2026-06-07T15:30:00Z",
  "started_at": "2026-06-07T14:55:00Z",
  "resolved_profile": {
    "android_version": "14",
    "chrome_version": "126"
  }
}
Field Type Notes
session_id UUID (string) Browser session ID
status string Current session state (e.g., "idle", "provisioning", "ready", "expiring")
connect_url string WebSocket CDP-relay URL; connect only once status is "ready"
egress_ip string or null Device public IP; null until the session is booted
expires_at ISO 8601 timestamp Hard deadline for the session
started_at ISO 8601 timestamp Present only once the session has started
resolved_profile JSON object Present only once a resolved profile exists; the effective profile after cloud processing

Error Responses:

Status Body Notes
400 Bad Request {"error": "bad id"} Path id is not a valid UUID
404 Not Found {"error": "session_not_found"} No active session on this device

DELETE /v1/devices/{id}/browser

Stop the active browser session on a device. The session is moved to expiring and torn down asynchronously.

Authentication: Authorization: Bearer <api_key> (scope: sessions.write)

Path Parameters: - id (UUID): device ID from /v1/devices

Request: (none)

Response: 200 OK

{
  "status": "stopping"
}

Error Responses:

Status Body Notes
400 Bad Request {"error": "bad id"} Path id is not a valid UUID
404 Not Found {"error": "session_not_found"} No active session on this device

Rate Limiting

All /v1 requests share one rate-limit bucket per customer API key:

  • Limit: 1000 requests per hour
  • Key: the customer API key (not the source IP), so one customer's traffic never throttles another

Exceeding the limit returns 429 Too Many Requests with a Retry-After header; the body is produced by the rate-limit middleware.

Error Envelope

Most error responses use a two-field envelope:

{
  "error": "<error_code>",
  "message": "<human_readable_detail>"
}

Some basic errors use the short form (no message):

{
  "error": "<error_message>"
}

Error codes are lowercase snake_case (e.g., unauthorized, forbidden, lease_inactive, profile_invalid, data_cap_exceeded, capacity_unavailable, session_not_found). Note that some short-form responses put a human-readable phrase directly in error (e.g., "bad id", "device not found", "server error").

Python SDK

The aimlib Python SDK is an async-only, typed wrapper over the customer REST surface (/v1) plus stock Playwright for driving a remote managed browser. It depends on httpx and playwright.

Installation

pip install "aimlib @ https://docs.aimlib.com/sdk/aimlib-0.2.0-py3-none-any.whl"

Quick Start

import asyncio
from aimlib import Aimlib

async def main():
    async with Aimlib() as ai:
        devices = await ai.devices.list()
        device = devices[0]
        print(f"Device {device.id}: {device.proxy.url}")

        # Browser: ~55-60s cold-boot wait, then drive Chrome over CDP
        async with await device.browser(profile="pixel-8-pro", ttl="10m") as b:
            page = await b.new_page()
            await page.goto("https://example.com")
            print(f"Page title: {await page.title()}")

asyncio.run(main())

Aimlib Client

Constructor

ai = Aimlib(
    api_key: Optional[str] = None,
    base_url: Optional[str] = None,
    timeout: float = 60,
)

Parameters

Param Type Default Notes
api_key str env AIMLIB_API_KEY Required. Format ak_live_*. Falls back to the environment variable. Sent as Authorization: Bearer <key>.
base_url str env AIMLIB_BASE_URL, else https://uswest1.aimlib.com The customer regional gateway base URL.
timeout float 60 Default HTTP request timeout, in seconds.

Raises - AimlibError if no API key is passed and AIMLIB_API_KEY is unset.

Environment variables - AIMLIB_API_KEY — API key (used when api_key is not passed). - AIMLIB_BASE_URL — regional gateway base URL (used when base_url is not passed).

Methods

async devices.list() -> list[Device]

List devices the caller holds an active lease on (GET /v1/devices).

devices = await ai.devices.list()
for device in devices:
    print(f"{device.id}: {device.carrier} in {device.region}")

async devices.get(device_id: str) -> Device

Return a single leased device by ID. Implemented client-side over devices.list(). Raises AimlibError if the device is not leased to this account.

device = await ai.devices.get("my-device-id")

async device(device_id: str) -> Device

Shorthand for devices.get().

device = await ai.device("my-device-id")

async aclose()

Close the underlying HTTP client. Prefer the async context manager so this is called for you.

ai = Aimlib()
# ... use ai ...
await ai.aclose()

Async Context Manager

async with Aimlib() as ai:
    devices = await ai.devices.list()
    # ai.aclose() is called automatically on exit

Device Object

Represents a leased mobile device.

Attributes

Attribute Type Notes
id str Device identifier (UUID).
region str | None Region (e.g. "uswest1").
carrier str | None Active mobile carrier.
current_egress_ip str | None Current public IP of the device's mobile connection.
proxy Proxy | None The raw mobile proxy endpoint. Present for an active lease with a provisioned proxy.

Methods

async browser(profile=None, ttl=None, idle_timeout=None, sticky=None) -> BrowserSession

Provision a managed stealth browser on this device (POST /v1/devices/{id}/browser). Returns an unconnected BrowserSession. Use async with await device.browser(...) as b to auto-connect and auto-stop.

Parameters

Param Type Default Notes
profile str | Profile | dict | None None Device profile and spoof overrides. A bare string is sent as {"device": <str>} (e.g. "pixel-8-pro", "galaxy-s24-ultra", "random"). A Profile dataclass or a raw dict is passed through (see below).
ttl int | float | str | None None Session lifetime. Accepts seconds (int/float) or a duration string ("30s", "5m", "1h"). Server clamps to 30 min max and to the lease end.
idle_timeout int | float | str | None None Idle release threshold; same formats as ttl. Server default when omitted is 120s.
sticky bool | None None Sent through as a boolean to the server when provided.

ReturnsBrowserSession (unconnected; call .connect() or use the async context manager to establish the CDP relay).

Raises

Exception When
LeaseInactiveError No active lease with a proxy on this device (HTTP 403, lease_inactive).
CapacityError Device/customer session cap or regional browser-host capacity reached (HTTP 429 / 503, capacity_unavailable).
DataCapError Device monthly cellular cap reached (HTTP 409, data_cap_exceeded).
AimlibError Invalid profile (HTTP 400, profile_invalid) or any other server/transport error.

Examples

# Bare string -> {"device": "pixel-8-pro"}
session = await device.browser(profile="pixel-8-pro", ttl="10m")

# Profile dataclass
from aimlib import Profile
profile = Profile(
    device="pixel-8-pro",
    timezone="America/Los_Angeles",
    locale="en-US",
    cores=8,
    ram_gb=6,
    screen=(1440, 3120),
    dpi=512,
)
session = await device.browser(profile=profile, ttl="5m")

# Raw dict (passed through; unknown keys are dropped server-side)
profile_dict = {
    "device": "pixel-8-pro",
    "model": "Pixel 8 Pro",
    "brand": "Google",
    "android_version": "15",
    "chrome_version": "132",
    "webgl": True,
    "webrtc_block": True,
    "battery": 85,
}
session = await device.browser(profile=profile_dict)

# Auto-connect and auto-stop
async with await device.browser(profile="pixel-8-pro", ttl="10m") as b:
    page = await b.new_page()
    # session auto-stops on __aexit__

Profile Dataclass

Typed device-profile overrides. A field left as None is omitted from the request and takes the profile default.

from aimlib import Profile

profile = Profile(
    device="pixel-8-pro",
    timezone="America/Los_Angeles",
    locale="en-US",
    cores=8,
    ram_gb=6,
    screen=(1440, 3120),
    dpi=512,
)

Fields

Field Type Default Notes
device str | None None Standardized device name (e.g. "pixel-8-pro", "galaxy-s24-ultra", "random").
timezone str | None None IANA timezone (e.g. "America/Los_Angeles").
locale str | None None Locale (e.g. "en-US").
cores int | None None Virtual CPU cores.
ram_gb int | None None Virtual RAM in GiB.
screen tuple[int, int] | None None Resolution (width, height); serialized as a JSON array.
dpi int | None None Display DPI.

The Profile dataclass exposes the seven typed fields above. To set additional spoof knobs, pass a raw dict to device.browser(profile={...}).

Raw Dict Passthrough

When you pass a plain dict, the SDK forwards it as-is. The server bounds it (max 4 KiB, must be a JSON object) and strips any key not on the allow-list before it reaches the host. Allowed keys:

Spoof fields device, timezone, locale, cores, ram_gb, screen, dpi, model, brand, manufacturer, android_version, build_fingerprint, security_patch, chrome_version

Behavioral flags webgl, touch_points, tls_randomize, webrtc_block, ipv6_block, dns_leak_block, fonts, battery, audio_48khz, storage_quota

profile_dict = {
    "device": "pixel-8-pro",
    "manufacturer": "Google",
    "build_fingerprint": "...",
    "webrtc_block": True,
    "dns_leak_block": True,
    "battery": 75,
}
await device.browser(profile=profile_dict)

Proxy Object

The raw mobile proxy endpoint, available immediately for an active lease.

Attributes

Attribute Type Notes
id str Proxy identifier.
url str Full proxy URL (<scheme>://username:password@host:port). Ready to use now.
protocol str Protocol (e.g. "http", "socks5").
status str Status (e.g. "active").
device = (await ai.devices.list())[0]
proxy_url = device.proxy.url   # use with httpx, requests, curl, etc.

BrowserSession Object

A managed browser session driving real Chrome over a Playwright CDP relay. It egresses through the same device mobile IP as the proxy.

Attributes

Attribute Type Notes
id str Session identifier (UUID).
device Device The device this session runs on.
status str Server-reported state (e.g. "provisioning", "ready", "failed", "gone").
connect_url str ws(s):// CDP relay endpoint. .connect() rewrites it to the HTTP discovery URL for Playwright.
connect_token str | None Bearer token for the CDP relay (sent as an Authorization header on connect).
max_tabs int Per-session tab cap (server-configured, default 5).
egress_ip str | None Device public IP for this session. Populated after the session is ready.
fingerprint dict Resolved profile (from the server resolved_profile). Populated after the session is ready; {} until then.
browser Browser | None Live Playwright Browser once connected; None before .connect().

Methods

async wait_until_ready(timeout: float = 180)

Poll until the session reaches "ready" (GET /v1/devices/{id}/browser). The container is booting; expect ~55-60s. Populates egress_ip and fingerprint.

Raises - SessionExpiredError — session not found / expired (HTTP 404). - AimlibError — session reported "failed" or "gone". - SessionTimeout — not ready within timeout.

session = await device.browser(ttl="10m")
await session.wait_until_ready(timeout=180)
print(f"Ready at IP: {session.egress_ip}")

async connect(timeout: float = 180) -> Browser

Wait until ready, then connect over CDP. Starts Playwright and returns the live Browser (chromium.connect_over_cdp(...)).

Raises — same as wait_until_ready().

b = await session.connect(timeout=180)
page = await b.new_page()
await page.goto("https://example.com")

async new_page() -> Page

Open a tab. Auto-calls .connect() if not yet connected. Reuses the initial tab on the first call, then opens new tabs up to max_tabs.

Raises - TabLimitError — open tabs would exceed max_tabs (raised client-side). Close a tab or start a separate session. Tabs share one IP and fingerprint.

async with await device.browser() as b:
    page1 = await b.new_page()  # reuses the initial tab
    page2 = await b.new_page()  # opens a 2nd tab
    page3 = await b.new_page()  # up to max_tabs
    # await page.close() frees a slot

async stop()

Close the browser, stop Playwright, and DELETE /v1/devices/{id}/browser (marks the session for teardown). Best-effort: cleanup errors are swallowed.

await session.stop()

Async Context Manager

Auto-connects on entry, auto-stops on exit.

async with await device.browser(profile="pixel-8-pro", ttl="10m") as b:
    page = await b.new_page()
    await page.goto("https://example.com")
    print(await page.title())
# session.stop() runs on __aexit__

Exceptions

All exceptions inherit from AimlibError. The SDK maps a typed server error code to its exception class; unmapped codes (and transport errors) raise the base AimlibError.

Exception Cause Server error
AimlibError Generic server/transport error, missing API key, device not leased, or invalid profile (profile_invalid). base class / unmapped
CapacityError Device/customer session cap or regional browser-host capacity reached. capacity_unavailable
LeaseInactiveError No active lease with a proxy on the device. lease_inactive
DataCapError Device monthly cellular cap reached. data_cap_exceeded
SessionExpiredError Session not found / already expired. session_expired
SessionTimeout Session not "ready" within the timeout. session_provisioning
TabLimitError Opening a tab would exceed max_tabs. raised client-side

End-to-End Example

import asyncio
from aimlib import Aimlib, Profile

async def scrape_with_browser():
    """Pick a leased device, boot a stealth browser, scrape a page."""
    async with Aimlib() as ai:
        devices = await ai.devices.list()
        if not devices:
            print("No devices leased")
            return
        device = devices[0]
        print(f"Using device {device.id} in {device.region}")
        print(f"Proxy: {device.proxy.url}")

        profile = Profile(
            device="pixel-8-pro",
            timezone="America/New_York",
            locale="en-US",
            cores=8,
            ram_gb=8,
            screen=(1440, 3120),
            dpi=512,
        )

        print("Starting browser (~55-60s boot)...")
        async with await device.browser(profile=profile, ttl="15m", idle_timeout="5m") as session:
            print(f"Browser ready at IP: {session.egress_ip}")
            print(f"Resolved profile: {session.fingerprint}")

            page = await session.new_page()
            await page.goto("https://example.com", timeout=30000)
            print(f"Page title: {await page.title()}")

            page2 = await session.new_page()
            await page2.goto("https://httpbin.org/ip")
            print(f"External IP (via browser): {await page2.text_content('body')}")
            # session auto-stops on exit

        print("Session stopped")

if __name__ == "__main__":
    asyncio.run(scrape_with_browser())

Reference — Device Profiles, Limits & Errors

Device Profiles

A device profile is a set of transparent spoof knobs that customize the appearance of a browser session. Profiles are passed at session-creation time and are optional. The server sanitizes every profile before it reaches the host: it must be a JSON object, unknown keys are silently dropped, and oversized profiles are rejected.

Profile Keys

The following keys are on the server allow-list. Any other key is dropped (never forwarded to the host daemon):

Category Keys
Identity device, model, brand, manufacturer, build_fingerprint
Display & Locale timezone, locale, screen, dpi
Graphics webgl, touch_points, chrome_version
System cores, ram_gb, android_version, security_patch, storage_quota, battery
Network & Leak Protection webrtc_block, ipv6_block, dns_leak_block, tls_randomize
Audio audio_48khz
Typography fonts

Maximum profile size: 4 KiB (4096 bytes). Oversized profiles, non-JSON-object input, and unparseable JSON are all rejected with profile_invalid. Empty input defaults to {}.

Passing Profiles

Profiles are passed to device.browser(profile=...) in three forms:

  1. Device name string (simplest) — sent as {"device": "<name>"}: python session = await device.browser(profile="pixel-8-pro") # or "galaxy-s24-ultra", "random", etc. — aimlib standardized device names

  2. Profile object / dict — any allow-listed keys: python session = await device.browser(profile={ "device": "pixel-8-pro", "timezone": "US/Pacific", "locale": "en-US", "cores": 8, "ram_gb": 12, "screen": [1440, 3120], "dpi": 512 })

  3. SDK Profile class (typed): python from aimlib import Profile p = Profile(device="pixel-8-pro", timezone="US/Pacific", locale="en-US") session = await device.browser(profile=p)

The typed Profile dataclass currently serializes only device, timezone, locale, cores, ram_gb, dpi, and screen. To set any other allow-listed key (e.g. webgl, build_fingerprint, battery), pass a plain dict.


Limits & Lifecycle

Session Limits

Limit Value Notes
Max concurrent sessions per device 1 (default) Single-tenant-per-device guard; enforced at session creation. The per-device value is device.max_browser_sessions.
Max concurrent sessions per customer 5 (default) Tenant-wide cap; enforced at session creation and at auto-bind time. The per-customer value is customer.max_browser_sessions.
Max tabs per session 5 (default) Server-configured via the FARMCTL_MAX_TABS_PER_SESSION environment variable; returned to the SDK as max_tabs on session create. The SDK enforces it client-side (TabLimitError). Tabs in one session share the same IP and device fingerprint.
Session TTL ceiling 30 minutes Maximum session lifetime. A requested ttl is clamped to this ceiling and further clamped so a session never outlives its lease.
Default idle timeout 120 seconds Inactivity window before the session is auto-released. Override with idle_timeout.
Per-session data cap 2 GB (default) Cellular budget protecting SIM usage. Clamped down to the device's remaining monthly headroom; creation is refused with data_cap_exceeded if that headroom is already exhausted.
Cold-start estimate ~55–60 s Returned as ready_in_s on session create (measured ~55 s on the baked image; the API constant is 60).

Lifecycle States

The status field on a session moves through:

  • requested — initial state; host port slot claimed, awaiting provisioning.
  • provisioning — container booting; not yet driveable (~55–60 s cold-to-driveable). The relay returns session_provisioning for requested and provisioning.
  • ready — container booted and driveable via the CDP relay.
  • active — session in active use.
  • idle — bound but not in use; eligible for adoption on the next device.browser() call for the same (customer, device) pair. Still connectable via the relay.
  • expiring — marked for termination; the host has been signaled to tear down the container.
  • gone — container released; no longer driveable.
  • failed — provisioning failed; not recoverable.

The relay treats ready, active, and idle as connectable; requested/provisioning return session_provisioning; any other state returns session_expired (HTTP 410).

Adoption (Lazy Provisioning)

When the deployment has AutoBrowserSession enabled (FARMCTL_AUTO_BROWSER_SESSION, default on), a zero-cost idle session is pre-bound to each lease that has both a device and a proxy — no host, no container. On the customer's first device.browser() call for that device, the server adopts the idle row in place: it binds a host and port, mints the real connect token, and boots the container, instead of creating a second session. This is transparent to the SDK; the customer still just calls device.browser().


Errors

Typed errors are returned as a JSON body {"error": "<code>", "message": "..."}. The same code can map to more than one HTTP status depending on where it is raised (session create vs. the CDP relay).

Error Code HTTP Status Cause What to Do
lease_inactive 403 No active lease with a proxy on this device, or the customer/lease is not active (checked at create and at relay connect). Confirm the account is active and the device is currently leased with a live proxy.
capacity_unavailable 429 (device or customer cap) / 503 (no host capacity, or adopt race) Per-device session cap reached, per-customer session cap reached, no browser-host capacity in the region, or the idle row changed during adoption. Cap reached: stop a session or request another device / upgrade. No host capacity or adopt race: retry shortly, or try another region.
data_cap_exceeded 409 The device's monthly cellular data cap is exhausted (SIM protection). Wait for the monthly window to roll over or raise the device's monthly cap.
session_expired 403 (expired at relay) / 410 (session torn down — expiring/gone/failed) Session TTL exceeded or the session is no longer connectable. Create a new session.
session_provisioning 425 Session still provisioning (state requested/provisioning). Response includes Retry-After: 5 and retry_after: "5". Retry after Retry-After seconds. wait_until_ready() / connect() handle this automatically.
profile_invalid 400 Profile is not a JSON object, is unparseable JSON, or exceeds 4 KiB. Send a valid JSON object within the 4 KiB limit.
session_not_found 404 No matching active session for this (customer, device), or the session id was not found at the relay. Verify the device id; create a new session if needed.
host_unavailable 503 The session has no host mesh IP or CDP port yet (host still starting / port not bound). Retry; this is typically transient during host startup.

Note: during wait_until_ready(), the SDK also raises SessionExpiredError locally if a GET .../browser poll returns 404 — this is a client-side mapping, not the API session_expired code.

SDK Exception Mapping

The Python SDK maps error codes to typed exceptions via _ERROR_BY_CODE in aimlib/__init__.py:

Error Code SDK Exception
capacity_unavailable CapacityError
lease_inactive LeaseInactiveError
data_cap_exceeded DataCapError
session_expired SessionExpiredError
session_provisioning SessionTimeout

Any unrecognized error code raises the base AimlibError. A 4xx/5xx response with no parseable error body raises AimlibError carrying the response text, or HTTP <status_code> if the body is empty. TabLimitError is raised by the SDK locally (it is not an API error code) when new_page() exceeds max_tabs.

Support

Submit issues, ask questions, and track them to resolution. A ticket is only complete once both you and the aimlib team have closed it — nothing is marked done while either side still considers it open. Posting any new message reopens a ticket.

Via the SDK

async with Aimlib() as ai:
    # File a ticket
    t = await ai.tickets.create(
        subject="device.browser() times out",
        body="On my device the session never reaches ready. Repro: ...",
    )
    print(t.id, t.status)                     # 'open'

    # List your tickets
    for t in await ai.tickets.list():
        print(t.subject, t.status, "awaiting:", t.awaiting_close_from)

    # View the full thread
    t = await ai.tickets.get(t.id)
    for m in t.messages:
        print(f"[{m['author']}] {m['body']}")

    # Reply (reopens the ticket if it was closed)
    await ai.tickets.reply(t.id, "Tried a fresh key, same result.")

    # Close from your side
    t = await ai.tickets.close(t.id)
    print(t.status, t.awaiting_close_from)    # 'open', awaiting 'operator' until we close too
    print(t.complete)                         # True only once BOTH sides have closed

Ticket object

Attribute Notes
id Ticket ID
subject Short title
status "open" or "closed" (closed = both sides have closed)
customer_closed / operator_closed Per-side close flags
awaiting_close_from "customer", "operator", or "both" while open
messages The thread: a list of {author, body, created_at} (author is "customer" or "operator")
complete True only when status == "closed"

REST

Method Path Body Notes
POST /v1/tickets {"subject", "body"} File a ticket
GET /v1/tickets Your tickets
GET /v1/tickets/{id} Ticket + full message thread
POST /v1/tickets/{id}/messages {"body"} Reply (reopens)
POST /v1/tickets/{id}/close Close from your side

All endpoints require Authorization: Bearer <api_key>.