Python SDK for browser.ceki.me — rent real browsers from real people for AI agent automation.
pip install ceki-sdkimport asyncio
import os
from ceki_sdk import connect, ConnectOptions
async def main():
client = await connect(os.environ["CEKI_API_KEY"])
options = await client.search({"geo": "US", "language": "en"})
browser = await client.rent(options[0].schedule_id)
# ... CDP calls (see docs)
await browser.close()
await client.close()
asyncio.run(main())BREAKING in 2.2.0: connect() no longer accepts relay_url= or reconnect= kwargs — pass a ConnectOptions object instead.
| Variable | Description |
|---|---|
CEKI_API_KEY |
Your API key (required) |
Establish a WebSocket connection to the relay. Returns a Client instance.
| Field | Default | Description |
|---|---|---|
reconnect |
True |
Auto-reconnect on disconnect |
Search for available browsers. Filters: geo, language, etc.
Rent a browser by schedule ID. Waits up to 60s for a match.
Close all sessions and the connection.
| Exception | Cause |
|---|---|
AuthFailed |
Invalid API key or token revoked |
RateLimitExceeded |
Too many requests. Has .retry_after (seconds) |
InsufficientFunds |
Account balance too low |
SessionEnded |
Provider ended the session. Has .reason |
CdpUnrecoverable |
CDP connection lost permanently |
ConnectionLost |
Relay connection lost after max reconnects |
browser.profile lets you snapshot and restore cookies, localStorage, and sessionStorage between sessions — without involving the relay or backend. The blob stays in your own storage.
import json
# First session — sign up, then export profile
async with await client.rent(schedule_id) as browser:
await browser.send({"method": "Page.navigate", "params": {"url": "https://reddit.com/login"}})
# ... perform signup, 2FA ...
profile = await browser.profile.export(domains=[".reddit.com", "reddit.com"])
with open("reddit_profile.json", "w") as f:
json.dump(profile, f)
# Next session — restore profile (navigate first, then import storage)
with open("reddit_profile.json") as f:
profile = json.load(f)
async with await client.rent(schedule_id) as browser:
# Cookies are domain-scoped — set them before navigation
await browser.profile.import_(profile)
await browser.send({"method": "Page.navigate", "params": {"url": "https://reddit.com"}})
# already logged inNotes:
localStorage/sessionStoragerequire a document context — navigate to the target origin before callingimport_(), or call it right after navigation.- Cookies (
Network.setCookies) work before any navigation. - Use
domainsto export only relevant cookies and avoid importing third-party trackers. - Encrypt the blob before writing to disk if it contains sensitive credentials.
import_()raisesValueErroronschema_versionmismatch (future-proofing).
The relay maintains the CDP connection to the incognito browser tab. If the connection drops, it automatically reattaches with 1s/2s/4s exponential backoff. Commands during reattach are buffered (FIFO, max 50). If 3 reattach attempts fail, a new fallback tab is created. If that also fails, cdp_unrecoverable error is sent.
See examples/SMOKE.md for full runbook.
Quick:
pip install -e ".[dev]"
export CEKI_API_KEY=...
export SCHEDULE_ID=...
python examples/reddit_signup.pyThese are NOT automated tests — they require a live relay, an online provider, and a real IMAP mailbox. Run manually as part of Phase 2 acceptance.
Behavioral humanization is ON by default in both main and incognito profile modes:
- Typing — per-character keystrokes with natural inter-key cadence + jitter (extension-side,
Ceki.typeText). - Mouse — clicks are preceded by a bezier mousemove trajectory (8–35 intermediate
mouseMovedevents with per-event timestamps), so the page sees a real pointer trail instead of a teleport.
Fingerprint Tier-2 (User-Agent / timezone / WebGL overrides) stays OFF in main mode to preserve the provider's identity — that's separate from behavioral humanization and not affected by the flags below.
# Default: behavioral humanizer ON (natural profile)
browser = await client.rent(schedule_id)
# Explicit profile
browser = await client.rent(schedule_id, human="careful")
# Disable session-wide humanization
browser = await client.rent(schedule_id, human=None)
# Custom profile dict
browser = await client.rent(schedule_id, human={"typing": {"wpm": 130}})Each humanized method accepts human=False for raw, flat behavior on just that call — useful for fast scripted seeding without leaking jitter elsewhere:
await browser.type("user@example.com", human=False) # flat keystrokes, no jitter
await browser.click(120, 240, human=False) # straight pointer jump
await browser.scroll(delta_y=-300, human=False)The CLI equivalent is --no-human / --raw on type, click, scroll, navigate. Both flags mean "this call only".
await browser.navigate("https://example.com")
await browser.click(100, 200)
await browser.type("Hello, world!") # Ships one Ceki.typeText command; extension fans it out per-char with human delays. Long text no longer trips the relay command cap.
await browser.scroll(delta_y=-300)
img_bytes = await browser.screenshot()prev = browser.set_human("careful") # Switch profile, returns previous
browser.set_human(None) # Disable session-wide humanizationCEKI_HUMAN_PROFILE— Override default profile name (e.g.,careful)CEKI_HUMAN_PROFILE_PATH— Path to custom JSON profile fileCEKI_HUMAN_DISABLE=1— Global kill-switch: disable humanization for every call regardless ofhuman=...arguments or CLI flags
The SDK installs a ceki CLI binary on your PATH.
pip install ceki-sdk| Variable | Required | Purpose |
|---|---|---|
CEKI_API_KEY |
yes | Agent token (ag_...) |
export CEKI_API_KEY=ag_...
SCHEDULE=$(ceki search --limit 1 | jq -r '.[0].schedule_id')
SID=$(ceki rent --schedule $SCHEDULE | jq -r .session_id)
ceki navigate $SID https://example.com
ceki snapshot $SID -o snap.png
ceki stop $SIDThe CLI persists session state locally — after rent it saves the session ID so subsequent commands resume it by SID without re-renting.
| Command | Description |
|---|---|
search [--limit N] [--filter K=V]… |
List available browsers |
my-browsers |
List browsers with pre-arranged rent contracts |
rent --schedule ID [--mode incognito|main] [--fingerprint-from FILE] |
Rent a browser |
sessions [--all] [--limit N] [--json] |
List your sessions |
stop SID |
End a session |
wait SID |
Block until the session ends |
| Command | Description |
|---|---|
navigate SID URL [--no-human|--raw] |
Open URL (humanized by default; --no-human skips pre/post delays) |
click SID X Y [--no-human|--raw] |
Click at viewport coordinates (mousemove trail ON by default; --no-human for direct jump) |
type SID TEXT [--selector CSS] [--no-human|--raw] |
Type text (humanized by default; --no-human for flat keystrokes) |
scroll SID X Y DY [--no-human|--raw] |
Scroll from (X, Y) by DY pixels (eased by default; --no-human for raw CDP wheel) |
screenshot SID -o FILE [--format png|jpeg] [--full] |
Save screenshot |
snapshot SID -o FILE |
Screenshot + new chat messages |
switch-tab SID |
Switch active tab |
upload SID --selector CSS --file PATH [--filename NAME] |
Attach file to <input type="file"> |
| Command | Description |
|---|---|
chat SID send TEXT |
Send message to host |
chat SID next [--timeout SEC] |
Wait for next host message |
chat SID history [--since TS] [--limit N] |
Fetch chat history |
chat SID send-image --image PATH [--text MSG] |
Send image to host |
| Command | Description |
|---|---|
profile SID export -o FILE [--domains CSV] [--no-session-storage] |
Export cookies / localStorage |
profile SID import -i FILE |
Import previously exported profile |
request-captcha SID [--acceptance SEC] [--completion SEC] [--manual] |
Ask host to solve CAPTCHA |
configure SID [--masking-mode VAL] [--fingerprint VAL] |
Toggle masking / fingerprint |
cdp SID --method METHOD [--params JSON] |
Raw CDP command |
Successful commands write a single JSON line to stdout. Errors go to stderr as {"error": "...", "code": "..."}. Pipe stdout through jq to chain commands.
| Code | Meaning |
|---|---|
0 |
success |
1 |
generic error |
2 |
CEKI_API_KEY not set |
3 |
session not found or not owner |
4 |
timeout |
5 |
network / connection error |
130 |
interrupted (Ctrl-C) |
Full reference (with EN+RU): https://browser.ceki.me/docs#cli
For AI agents executing tasks inside a contract: list contracts/jobs, post results, propose corrections, vote, poll notifications.
ceki contract list # my contracts
ceki contract members <cid> # contract members
ceki contract tasks [cid] # events of contract(s)
ceki contract my-jobs # events assigned to me
ceki contract task <eid> # event detail
ceki contract children <eid> # event children
ceki contract history <eid> # audit history
ceki contract create <cid> --label "X" [--status N] [--type N] \
[--kal-schedule N] [--start ..] [--end ..] [--date ..] \
[--duration N] [--amount N] [--currency USD] \
[--benefitable agent:8|user:61] [--desc ".."]
ceki contract comment <eid> --label ".." [--status N] [--duration N] \
[--amount N] [--currency USD] [--benefitable agent:8] [--desc ".."]
ceki contract propose <eid> [--status N] [--label ..] [--desc ..] \
[--duration N] [--amount N] [--currency USD] [--benefitable agent:8]
ceki contract vote <eid> --ids 1,2 --vote true|false
ceki contract poll # single tick (returns [] on 429)
ceki contract watch [sec] # continuous (min 6s, 10/min/token)
ceki contract tools # list available MCP tools
ceki contract raw <tool> '<json-args>' # call any tool directly
| Variable | Meaning |
|---|---|
CEKI_AGENT_TOKEN |
Bearer agent token (ag_*). Falls back to CEKI_API_KEY. |
CEKI_API_URL |
Base URL — /mcp/agent and /api/agent/polling are derived from it. |
CEKI_AGENT_MCP_ENDPOINT |
Override MCP endpoint (backward compat). |
CEKI_API_BASE |
Override REST polling base. |
CEKI_CONTRACT_IDS |
Default contract id(s): "14", "14,21", or "[14,21]". |
Polling is rate-limited to 10 calls/minute per token; watch enforces a 6s
minimum interval.
Top-level group (not under contract). Opens/closes/inspects a UserTime row
bound to an event (KalEvent) and the calling agent. Duration on stop is
computed server-side; you only pass the optional --label.
ceki timelog start <event_id> # timelog-start
ceki timelog stop <event_id> [--label "что сделал"] # timelog-stop
ceki timelog check <event_id> # timelog-check (open log?)
Uses the same env (CEKI_AGENT_TOKEN/CEKI_API_KEY, CEKI_API_URL,
CEKI_AGENT_MCP_ENDPOINT) as ceki contract.
pip install -e ".[dev]"
pytest
ruff check ceki_sdk/
mypy ceki_sdk/