ceki-sdk
Using Playwright / Puppeteer / Selenium? Ceki speaks the same CDP protocol — see migration guide
Python SDK for browser.ceki.me — rent real browsers from real people for AI agent automation.
Install
pip install ceki-sdk
Quickstart
import 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.
Using Playwright / Puppeteer / Selenium? Ceki speaks the same CDP protocol — see migration guide
Playwright / Puppeteer / Selenium Compatibility
Ceki speaks raw CDP (Chrome DevTools Protocol) — the same protocol Playwright and Puppeteer use internally. Most automation scripts port with just an import and connection change.
Why Ceki instead of your current stack?
| Feature | Playwright | Puppeteer | Selenium | Ceki |
|---|---|---|---|---|
| Real residential IPs | ❌ | ❌ | ❌ | ✅ |
| Human behavioral mimicry | ❌ | ❌ | ❌ | ✅ |
| Captcha solving via chat | ❌ | ❌ | ❌ | ✅ |
| Raw CDP access | ✅ | ✅ | Partial | ✅ native |
| MCP-native | ❌ | ❌ | ❌ | ✅ |
| Real human browser provider | ❌ | ❌ | ❌ | ✅ |
| Zero bot detection risk | ❌ | ❌ | ❌ | ✅ |
Per-framework migration
- Playwright —
page.context.newCDPSession(page)→client.rent(), then replacecdp_session.send('DOM.getDocument')withbrowser.send({"method": "DOM.getDocument"}). Navigation, clicking, typing, and DOM queries work identically. - Puppeteer / Pyppeteer —
page._client().send('DOM.getDocument')→browser.send({"method": "DOM.getDocument"}). Same CDP method names and params. - Selenium —
driver.execute_cdp_cmd('DOM.getDocument', {})→browser.send({"method": "DOM.getDocument"}). Same method names, same parameter shapes. - undetected-chromedriver — no longer needed. Ceki provides a real human browser fingerprint + residential IP. No WebDriver patches required.
- selenium-wire — request interception:
browser.send({"method": "Network.enable"})+ subscribe toNetwork.responseReceivedevents. - MCP — Ceki is MCP-native. Connect it as an MCP server (
ceki mcp), and any MCP client (Claude Code, Cline, Cursor) gets browser tools — navigate, click, type, screenshot, captcha chat — without writing a single line of CDP code.
Migration example: Playwright → Ceki (Python)
| Playwright (raw CDP) | Ceki |
|---|---|
from playwright.async_api import async_playwright
async with async_playwright() as p:
browser = await p.chromium.launch()
page = await browser.new_page()
cdp = await page.context.new_cdp_session(page)
await cdp.send("Page.navigate",
{"url": "https://x.com"})
await page.click("text=Log in")
data = await cdp.send(
"Page.captureScreenshot")
await browser.close()
|
from ceki_sdk import connect
async def main():
client = await connect(API_KEY)
br = await client.rent(sid)
await br.send({
"method": "Page.navigate",
"params": {"url": "https://x.com"},
})
await br.click(320, 480)
snap = await br.screenshot()
await br.close()
await client.close()
|
The browser.send({"method": ..., "params": ...}) method mirrors Playwright's CDPSession.send() — same CDP method names, same parameter shapes. Only the connection setup changes.
Known Differences
Ceki's CDP-over-WSS relay is not a direct local CDP pipe — a few differences to keep in mind when migrating from Playwright / Puppeteer / Selenium:
- No CSS selectors on interaction methods —
click,type, andscrolltake viewport coordinates, not CSS selectors. For element-based selection, useRuntime.evaluateto get bounding rects, then pass coordinates. - Speed — each command travels over the WebSocket relay with ~50–200ms round-trip (internet-latency dependent, unlike local CDP which is sub-millisecond). Batch operations in a single
Runtime.evaluatewhere possible, rather than chaining many small CDP calls. - 120s grace window — after each command there's a 120s window before the session auto-closes. This is by design for workflow chaining, but long-running idle sessions will need a keepalive.
- Runtime.evaluate serialization — CDP's
Runtime.evaluatereturns JSON-serializable values only. Functions, symbols, DOM nodes, and circular references are not transferable. Use--returnByValueexplicitly.
Environment Variables
| Variable | Description |
|---|---|
CEKI_API_KEY |
Your API key (required) |
API
connect(api_key, options: ConnectOptions | None = None) -> Client
Establish a WebSocket connection to the relay. Returns a Client instance.
ConnectOptions
| Field | Default | Description |
|---|---|---|
reconnect |
True |
Auto-reconnect on disconnect |
client.search(filters=None, limit=20) -> list[BrowserOption]
Search for available browsers. Filters: geo, language, etc.
client.rent(schedule_id) -> Browser
Rent a browser by schedule ID. Waits up to 60s for a match.
client.close()
Close all sessions and the connection.
Error Codes
| 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 |
Session profile (cookies + storage)
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 in
Notes:
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).
CloudBrowser subscriptions ($5/mo)
A CloudBrowser subscription is a paid browser-group subscription (type 3) that grants the owner's agents rent access to that group's real browsers at $0/min (the owner already paid the monthly fee). Sessions under a subscription are always incognito — subscription browsers have no "main" profile.
client.my_browsers() returns both your pre-arranged rent contracts
(browsers) and the owner's subscriptions (subscriptions), each with its
group and member browsers:
result = await client.my_browsers()
for sub in result.subscriptions:
print(sub.subscription_id, sub.group.label, sub.geo_key, sub.expires_at)
for b in sub.browsers:
print(" ", b.browser_id, b.name, "online" if b.online else "offline")
client.rent(subscription_id=...) rents a free browser from the
subscription's group — the backend picks the currently free browser:
browser = await client.rent(subscription_id=709)
client.subscription — discover groups and buy a subscription
(requires an agent ag_ key; same MCP endpoint as ceki contract):
groups = await client.subscription.list_groups() # {groups: [{group_id, plans, geo}]}
result = await client.subscription.subscribe(1, plan_key="30days", geo_key="ru")
print(result["subscription_id"], result["payment_mode"]) # invoice → pay to activate
The same flows are exposed on the CLI (ceki my-browsers, ceki rent --subscription ID, ceki subscribe [GROUP_ID] --plan KEY --geo KEY).
Browser Vault (server-stored sessions)
The vault stores a browser snapshot (cookies + per-origin localStorage/sessionStorage + fingerprint) on the Ceki API (/api/vault/sessions, encrypted server-side) so you can restore it on a different browser later — the same session profile across machines.
Two surfaces:
client.vault — HTTP CRUD on vault sessions (no live browser needed):
# List your vault sessions
sessions = await client.vault.list()
for s in sessions:
print(s.id, s.label, s.urls)
# Fetch one session with its decrypted profile envelope
session = await client.vault.get(8)
data = session.data # {cookies, localStorage, sessionStorage, fingerprint, urls, collectedAt}
browser.vault — snapshot save / restore from a rented browser:
# 1. On browser A — export the current state into a NEW vault session
vault_id = await browser.vault.save(label="vc.ru session")
# or overwrite the session you rented with (browser was rented with vault=8)
vault_id = await browser.vault.save() # PUT onto the bound id
# 2. On browser B (or the same one later) — rent WITH the vault profile
async with await client.rent(schedule_id, vault=vault_id) as browser:
# cookies applied immediately, localStorage/sessionStorage buffered by the
# extension and flushed on first navigation to each origin
await browser.send({"method": "Page.navigate", "params": {"url": "https://vc.ru"}})
# or restore the profile mid-session
await browser.vault.restore(vault_id)
Notes:
- Vault restore uses
session.configure(profile=...)— the extension applies cookies first, then buffers localStorage/sessionStorage until the first navigation to each origin (Vault 3+ extension required). - When
vault=<id>is passed torent(), the browser is bound to that vault session: a laterbrowser.vault.save()overwrites it (PUT). - The vault endpoints are guarded by Sanctum and resolve to a user; use a user token as
api_keyfor vault operations. browser.profile.export()(local blob) andbrowser.vault.save()(server) are complementary: the first keeps the blob agent-side, the second stores it encrypted on the API.
CDP Lifecycle
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.
Real-signup examples
See examples/SMOKE.md for full runbook.
Quick:
pip install -e ".[dev]"
export CEKI_API_KEY=...
export SCHEDULE_ID=...
python examples/reddit_signup.py
These 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.
Human Mode
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}})
Per-call disable
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".
High-level methods
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()
Runtime control
prev = browser.set_human("careful") # Switch profile, returns previous
browser.set_human(None) # Disable session-wide humanization
Environment variables
CEKI_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
CLI
The SDK installs a ceki CLI binary on your PATH.
Install
pip install ceki-sdk
Environment variables
| Variable | Required | Purpose |
|---|---|---|
CEKI_API_KEY |
yes | Agent token (ag_...) |
Quick start
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 $SID
The CLI persists session state locally — after rent it saves the session ID so subsequent commands resume it by SID without re-renting.
Commands
Discovery and lifecycle
| Command | Description |
|---|---|
search [--limit N] [--filter K=V]… |
List available browsers |
my-browsers |
List pre-arranged rent contracts + the owner's CloudBrowser subscriptions |
rent --schedule ID [--mode incognito|main] [--fingerprint-from FILE] [--vault SESSION_ID] |
Rent a browser |
rent --subscription SUB_ID |
Rent a free browser ($0/min) from a CloudBrowser subscription's group |
subscribe [GROUP_ID] [--plan PLAN_KEY] [--geo GEO_KEY] |
List browser groups with plans/geo, or buy a CloudBrowser subscription |
sessions [--all] [--limit N] [--json] |
List your sessions |
stop SID |
End a session |
wait SID |
Block until the session ends |
Browser control
| 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"> |
Chat with host
| 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 |
Advanced
| 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 |
Vault sessions
| Command | Description |
|---|---|
vault list [--json] [--per-page N] |
List vault sessions (id, label, urls, updated) |
vault get ID [--json] [-o FILE] |
Show a session; --json prints the decrypted profile, -o dumps it to a file |
vault save FILE [--id ID] [--label L] |
Create (or PUT-update with --id) a session from a profile JSON |
vault save --session SID [--id ID] [--label L] [--no-session-storage] |
Snapshot a live rental session into the vault |
vault apply ID --session SID |
Apply a vault profile into an existing rental (resume + restore) |
vault apply ID --schedule N |
Rent a fresh browser with the vault profile restored |
vault delete ID |
Delete a vault session |
Vault commands run over plain HTTP (no relay session) and are user/Sanctum-scoped — the same token caveat as client.vault applies (use a user token).
Output and errors
Successful commands write a single JSON line to stdout. Errors go to stderr as {"error": "...", "code": "..."}. Pipe stdout through jq to chain commands.
Exit codes
| 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
ceki provider — rent out your browser
Turn a machine you control into a provider: it runs a real Chromium with the Ceki extension, injects your browser token and brings the browser online so it can be rented out as a public browser.
The provider itself is not reimplemented in this SDK. It lives in the
public repo Ceki-me/docker-browser
and ships as the Docker Hub image ceki/provider. ceki provider run pulls
and runs that image — the launcher stays a single source of truth.
export CEKI_PROVIDER_TOKEN=<one-time browser token from your dashboard>
ceki provider run # pull + run, stays online until stopped
ceki provider run --timeout 600 # stop after 10 minutes
ceki provider run --build ~/docker-browser # build from a local checkout instead of pulling
The token is issued for one specific browser and cannot be reused for another.
Provider environment variables
| Variable | Required | Purpose |
|---|---|---|
CEKI_PROVIDER_TOKEN |
yes | Extension token issued for this browser |
CEKI_PROVIDER_IMAGE |
no | Image tag (default ceki/provider:latest) |
CEKI_PROVIDER_VIEWPORT |
no | Browser viewport / resolution WxH (default 1920x1080) |
CEKI_PROVIDER_LOG_LEVEL |
no | Container log verbosity (DEBUG/INFO/WARNING/ERROR) |
TZ |
no | Browser timezone (kept consistent with your location) |
DISPLAY |
no | X display (the container starts its own virtual screen if unset) |
docker stop (or Ctrl-C) sends a clean shutdown signal: the rented browser is
closed and your browser goes offline.
ceki contract — participate in contracts via /mcp/agent
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 ".."] [--file PATH]...
ceki contract comment <eid> --label ".." [--status N] [--duration N] \
[--amount N] [--currency USD] [--benefitable agent:8] [--desc ".."] \
[--file PATH]...
ceki contract propose <eid> [--status N] [--label ..] [--desc ..] \
[--duration N] [--amount N] [--currency USD] [--benefitable agent:8] \
[--file PATH]...
ceki contract edit <eid> [--status N] [--label ..] [--desc ..] [--file PATH]...
ceki contract progress <eid> [--status N] --desc ".." [--file PATH]...
ceki contract upload-file PATH [--filename NAME] [--mime TYPE]
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
--file PATH (repeatable) attaches a file to the task / comment / correction:
the SDK uploads it to the backend (MCP upload-file) and attaches the returned
user_files.id to the event. ceki contract upload-file PATH uploads a single
file and prints its {id, url} record. Files also render in markdown as
 — the url appears in the event payload under files[].url.
Environment
| 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.
ceki timelog — event time tracking via /mcp/agent
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.
Development
pip install -e ".[dev]"
pytest
ruff check ceki_sdk/
mypy ceki_sdk/
Metadata
Release files for ceki-sdk 2.38.0
For a detailed explanation of source distributions (sdists) and built distributions (wheels), please see the package formats documentation.
Source distribution (sdist)
| File | Size | Uploaded | Provenance | Actions |
|---|---|---|---|---|
| ceki_sdk-2.38.0.tar.gz | 157.5 kB | Without provenance | Details for ceki_sdk-2.38.0.tar.gz |
Built distribution (wheel)
| File | Interpreter | ABI | Platform | Actions Reset |
|---|---|---|---|---|
| ceki_sdk-2.38.0-py3-none-any.whl | Python 3 | none | any | Details for ceki_sdk-2.38.0-py3-none-any.whl |
Total release size: 252.3 kB
Release files / ceki_sdk-2.38.0.tar.gz
| Download URL | ceki_sdk-2.38.0.tar.gz |
|---|---|
| Size | 157.5 kB |
| Tags | Source |
|
SHA-256 checksum How to use checksums |
2124fc6b77b8697997de48beca49f1a62bdd2bce52ffbf0bc60bc51e36af0f5f
|
|
BLAKE2b-256 checksum How to use checksums |
852632132cb12ef6566c0624cc03e5e2aad06e83067971bc23d9f030ff32b207
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
Yes |
| Uploaded via |
twine/7.0.0 CPython/3.13.16
|
Release files / ceki_sdk-2.38.0-py3-none-any.whl
| Download URL | ceki_sdk-2.38.0-py3-none-any.whl |
|---|---|
| Size | 94.8 kB |
| Tags | Python 3 |
|
SHA-256 checksum How to use checksums |
b1b88ddee314afc80cf5c9f63db7b9629b2d82f1951d00f2d9b9110c4102a99b
|
|
BLAKE2b-256 checksum How to use checksums |
4f8ba6e4a98ebfa72177975bb2f033d9bad7308d64508cbc21ebad37896107c4
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
Yes |
| Uploaded via |
twine/7.0.0 CPython/3.13.16
|
Security for ceki-sdk 2.38.0
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.
No provenance found
None of the files in this release were uploaded with provenance metadata.
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.