Browser

Browsers: a browser an agent drives, and profiles that keep a person's logins.

profile = noclick.BrowserProfile.from_name("ana-work", create_if_missing=True, user="user_42")
profile.login("https://portal.example.com", reason="to download your invoices").wait()

with noclick.Browser.create(profile=profile, timeout_s=900) as browser:
    browser.goto("https://portal.example.com/invoices")
    result = browser.act("Download every invoice from September")
    print(result.summary)
# closed on exit

A browser is Chromium in a sandbox, driven by a model that sees the page and acts on it. NoClick solves the CAPTCHAs it can; when a page still needs a person (a CAPTCHA it couldn't solve, a two-factor code), the browser parks, unbilled, while that person finishes the step on a page NoClick sends them. A profile keeps the sign-ins a person made once; when a site signs it out, the next browser asks that person to sign in again the same way, and saves the new sign-in. A profile counts as a credential, so it is never exported and only its own user's browsers and agents use it.

class Browser

A running browser. with noclick.Browser.create() as browser: closes it on exit; otherwise it closes at expires_at. Billed per minute while it runs, plus the model tokens act and extract use; time it waits on a person is neither billed nor counted against timeout_s.

Attributes
idstr
statusstr | None

starting, running, parked (waiting on a person, or an agent's browser waiting for its conversation's next turn), closed or failed.

regionstr | None

Where it runs: its profile's region, else the one it was created in.

profile_idstr | None

The profile it is signed in with (None if none).

agent_idstr | None

The agent driving it, for an agent's browser.

live_urlstr | None

A NoClick page where you watch it live and can take over; it works while the browser runs.

urlstr | None

The page it is on.

titlestr | None
timeout_sint | None

How long it may run; parked time doesn't count.

solve_captchasbool | None

Whether NoClick solves CAPTCHAs before asking a person.

parked_sint | None

Seconds it has waited parked: not billed, not in timeout_s.

cost_usdfloat | None

What it has cost so far: minutes and model tokens.

user_idstr | None
created_atAny
expires_atAny
closed_atAny

createclassmethodasync

noclick.Browser.create(
    *, profile=None, timeout_s=None, region=None, solve_captchas=True, human=None,
    user=..., client=None
) -> Browser

Start a browser that runs at most timeout_s seconds (300 by default, up to 3600; time it waits on a person doesn't count). With profile (a BrowserProfile or id) it is signed in to the profile's sites, acts for the profile's user, runs in the profile's region and saves new sign-ins into it when it closes: one such browser per profile at a time, so a second raises noclick.Conflict naming the first (browser_id). Without one, region is where it runs (noclick.regions(kind="browser"); the account's default when omitted). NoClick solves the CAPTCHAs it can before asking a person; solve_captchas=False turns that off for sites whose terms forbid it. human is who is asked when a page needs a person (the user by default, you for an account browser).

Parameters
profileAnydefault None
timeout_sint | Nonedefault None
regionstr | Nonedefault None
solve_captchasbooldefault True
humanHuman | Nonedefault None
userAny

Who the call acts for. Defaults to the user set by noclick.as_user (none: the account itself).

(defaults to the current user)
clientClient | None

The client to send with; defaults to the module client built from NOCLICK_API_KEY.

(default None)
Returns
Browser

The browser, running on a blank page, with its live_url.

Usage

Use it as a context manager, so it closes when the block ends:

profile = noclick.BrowserProfile.from_name("ana-work", user="user_42")
with noclick.Browser.create(profile=profile, timeout_s=900) as browser:
    browser.goto("https://portal.example.com/invoices")
    print(browser.act("Download every invoice from September").summary)

Without with, close it yourself or let it run out:

browser = noclick.Browser.create(timeout_s=600)
print(browser.live_url)   # watch it, or take over
browser.close()

from_idclassmethod

noclick.Browser.from_id(browser_id, *, client=None, user=...) -> Browser

A lazy reference to a browser by id, for one you started earlier.

Parameters
browser_idstrrequired
clientClient | None

The client to send with; defaults to the module client built from NOCLICK_API_KEY.

(default None)
userAny

Who the call acts for. Defaults to the user set by noclick.as_user (none: the account itself).

(defaults to the current user)
Returns
Browser

A browser that loads the first time you read a field. Nothing is sent yet.

Usage
browser = noclick.Browser.from_id("e2c9a4f7-1b3d-4e8a-9c6f-5d0b7a2e1f83")
print(browser.content(format="text"))

getclassmethodasync

noclick.Browser.get(browser_id, *, user=..., client=None) -> Browser

Load a browser by id now; an unknown id raises noclick.NotFound.

Parameters
browser_idstrrequired
userAny

Who the call acts for. Defaults to the user set by noclick.as_user (none: the account itself).

(defaults to the current user)
clientClient | None

The client to send with; defaults to the module client built from NOCLICK_API_KEY.

(default None)
Returns
Browser

The browser, loaded: its status, the page it is on and its live_url.

Usage
browser = noclick.Browser.get("e2c9a4f7-1b3d-4e8a-9c6f-5d0b7a2e1f83")
print(browser.status, browser.url, browser.live_url)

listclassmethodasync

noclick.Browser.list(
    *, profile=None, status=None, limit=None, user_filter=None, user=..., client=None
) -> list[Browser]

The current user's browsers (the account's own outside as_user, or those user_filter covers), optionally one profile's; status="live" lists those starting, running or parked.

Parameters
profileAnydefault None
statusstr | Nonedefault None
limitint | None

Return at most this many.

(default None)
user_filterAny

Which users a read covers: noclick.ALL_USERS, noclick.NO_USER, a User, or your id for one. It filters without acting as that user.

(default None)
userAny

Who the call acts for. Defaults to the user set by noclick.as_user (none: the account itself).

(defaults to the current user)
clientClient | None

The client to send with; defaults to the module client built from NOCLICK_API_KEY.

(default None)
Returns
list[Browser]

Every matching browser, newest first, fetched page by page. Agents' browsers are listed too.

Usage
for browser in noclick.Browser.list(status="live", user_filter=noclick.ALL_USERS):
    print(browser.id, browser.url, browser.live_url)

profile = noclick.BrowserProfile.from_name("ana-work", user="user_42")
recent = noclick.Browser.list(profile=profile, limit=10)

list_pageclassmethodasync

noclick.Browser.list_page(
    *, cursor=None, limit=None, profile=None, status=None, user_filter=None, user=...,
    client=None
) -> Page

One page of browsers; pass next_cursor back as cursor.

Parameters
cursorstr | None

The next_cursor of the page before; omit it for the first page.

(default None)
limitint | None

Return at most this many.

(default None)
profileAnydefault None
statusstr | Nonedefault None
user_filterAny

Which users a read covers: noclick.ALL_USERS, noclick.NO_USER, a User, or your id for one. It filters without acting as that user.

(default None)
userAny

Who the call acts for. Defaults to the user set by noclick.as_user (none: the account itself).

(defaults to the current user)
clientClient | None

The client to send with; defaults to the module client built from NOCLICK_API_KEY.

(default None)
Returns
Page

A page whose data holds the browsers and whose next_cursor is None on the last page.

Usage
page = noclick.Browser.list_page(status="closed", limit=50)
while True:
    for browser in page.data:
        print(browser.id, browser.cost_usd)
    if page.next_cursor is None:
        break
    page = noclick.Browser.list_page(status="closed", limit=50, cursor=page.next_cursor)

gotoasync

browser.goto(url) -> PageState

Open url and wait for the page to load.

Parameters
urlstrrequired
Returns
PageState

The page it landed on: its url after redirects and its title.

Usage
with noclick.Browser.create() as browser:
    page = browser.goto("https://example.com")
    print(page.url, page.title)

actasync

browser.act(
    instruction, *, background=False, max_steps=None, files=None, timeout=None
) -> ActResult | Operation

Have the model carry out instruction on the page: clicking, typing, scrolling, uploading, downloading, at most max_steps actions (50 by default, up to 200). files are https URLs (a bucket's signed_url works) NoClick downloads into the browser first, for the task to put in a form, up to 5. NoClick solves the CAPTCHAs it can, giving one half a minute; one still there, or a two-factor prompt, parks the browser (not billed, not counted against its timeout) and sends a person a page carrying the live view, and the step waits until they have dealt with it. So does a sign-in page for a site whose saved sign-in expired: it asks the person who signed in (SiteLogin.signed_in_by) to sign in again, saves the new sign-in to the profile, and carries on.

timeout is how many seconds to wait here; the step keeps going after it. background=True returns at once.

Parameters
instructionstrrequired
backgroundbooldefault False
max_stepsint | Nonedefault None
fileslist[str] | Nonedefault None
timeoutfloat | Nonedefault None
Returns
ActResult | Operation

How it went: whether it's done, a summary, the steps it took, the url it ended on and any downloads. With background=True, an Operation at once, whose wait() gives that result.

Usage

Describe the task, not the clicks:

with noclick.Browser.create(profile=noclick.BrowserProfile.from_name("ana-work", user="user_42")) as browser:
    browser.goto("https://portal.example.com/invoices")
    result = browser.act("Download every invoice from September")
    print(result.done, result.summary)
    for file in result.downloads:
        print(file["name"], file["url"])

Run a long task in the background and pick it up later:

browser = noclick.Browser.create(timeout_s=1800)
task = browser.act("Compare the three cheapest flights from SFO to JFK on Oct 12", background=True)
print(task.id)                                   # save it; then, later, anywhere:
result = noclick.Operation.from_id(task.id).wait()

Hand it the files a form needs:

w9 = noclick.Bucket.from_name("forms").signed_url("w9-2026.pdf")
with noclick.Browser.create(profile=noclick.BrowserProfile.from_name("ana-work", user="user_42")) as browser:
    browser.goto("https://supplier.example.com/onboarding")
    print(browser.act("Fill in the onboarding form and attach the W-9", files=[w9]).summary)

extractasync

browser.extract(query, *, schema=None) -> Any

Read what query describes off the current page, shaped by schema (a JSON Schema) when you pass one.

Parameters
querystrrequired
schemadict[str, Any] | Nonedefault None
Returns
Any

The data as JSON: an object or a list matching schema, or the model's own shape without one.

Usage
with noclick.Browser.create() as browser:
    browser.goto("https://portal.example.com/invoices")
    invoices = browser.extract(
        "the invoice numbers and totals",
        schema={"type": "array", "items": {"type": "object", "properties": {
            "number": {"type": "string"}, "total_usd": {"type": "number"}}}},
    )
    print(invoices[0]["number"], invoices[0]["total_usd"])

screenshotasync

browser.screenshot(*, full_page=False) -> bytes

A PNG of what the browser shows: the viewport, or the whole page with full_page=True.

Parameters
full_pagebooldefault False
Returns
bytes

The PNG's bytes.

Usage
with noclick.Browser.create() as browser:
    browser.goto("https://example.com")
    with open("page.png", "wb") as f:
        f.write(browser.screenshot(full_page=True))

contentasync

browser.content(*, format='markdown') -> str

The current page's content as "markdown", "text" or "html".

Parameters
formatstrdefault 'markdown'
Returns
str

The page's content in that format; markdown keeps headings, links and tables.

Usage
with noclick.Browser.create() as browser:
    browser.goto("https://example.com/pricing")
    print(browser.content(format="markdown"))

closeasync

browser.close() -> None

Close the browser now (billing stops with it). The profile keeps any new sign-ins.

Usage
browser = noclick.Browser.create()
try:
    print(browser.goto("https://example.com").title)
finally:
    browser.close()

BrowserProfile

class BrowserProfile

Saved sign-ins for one person. BrowserProfile.from_name("ana-work", create_if_missing=True, user="user_42") is lazy; profile.login(url) asks the person to sign in once, and browsers started with the profile are signed in. Its region is where its browsers run; its sign-ins are kept in the US whatever the region.

Attributes
idstr
namestr
regionstr | None

Where its browsers run, fixed when it is created.

user_idstr | None
last_used_atAny

When a browser last used it (None if never).

created_atAny
siteslist[SiteLogin]

The sites it is signed in to, most recently signed in first (loads the profile if needed).

createclassmethodasync

noclick.BrowserProfile.create(
    name, *, region=None, user=..., client=None
) -> BrowserProfile

Create an empty profile called name (unique per user; a duplicate raises noclick.Conflict with existing_id). region is where its browsers run (noclick.regions(kind="browser"); the account's default region when omitted), fixed for its life; its sign-ins are kept in the US.

Parameters
namestrrequired
regionstr | Nonedefault None
userAny

Who the call acts for. Defaults to the user set by noclick.as_user (none: the account itself).

(defaults to the current user)
clientClient | None

The client to send with; defaults to the module client built from NOCLICK_API_KEY.

(default None)
Returns
BrowserProfile

The new profile, signed in nowhere yet.

Usage
profile = noclick.BrowserProfile.create("ana-work", user="user_42")
login = profile.login("https://portal.example.com", reason="to download your invoices")

from_nameclassmethod

noclick.BrowserProfile.from_name(
    name, *, create_if_missing=False, region=None, user=..., client=None
) -> BrowserProfile

A lazy reference to the profile called name in the current (or given) user; with create_if_missing=True it's created, empty, on first use, in region when you pass one.

Parameters
namestrrequired
create_if_missingbooldefault False
regionstr | Nonedefault None
userAny

Who the call acts for. Defaults to the user set by noclick.as_user (none: the account itself).

(defaults to the current user)
clientClient | None

The client to send with; defaults to the module client built from NOCLICK_API_KEY.

(default None)
Returns
BrowserProfile

A profile that loads, or is created, the first time it's used. Nothing is sent yet.

Usage
profile = noclick.BrowserProfile.from_name("ana-work", create_if_missing=True, user="user_42")
if not any(site.domain == "portal.example.com" for site in profile.sites):
    profile.login("https://portal.example.com", reason="to download your invoices").wait()

from_idclassmethod

noclick.BrowserProfile.from_id(profile_id, *, client=None, user=...) -> BrowserProfile

A lazy reference to a profile by id.

Parameters
profile_idstrrequired
clientClient | None

The client to send with; defaults to the module client built from NOCLICK_API_KEY.

(default None)
userAny

Who the call acts for. Defaults to the user set by noclick.as_user (none: the account itself).

(defaults to the current user)
Returns
BrowserProfile

A profile that loads the first time you read a field. Nothing is sent yet.

Usage
profile = noclick.BrowserProfile.from_id("b7e1d3a9-4c2f-4e6b-8a5d-1f9c0e3b7a24")
print([site.domain for site in profile.sites])

getclassmethodasync

noclick.BrowserProfile.get(profile_id, *, user=..., client=None) -> BrowserProfile

Load a profile by id now; an unknown id raises noclick.NotFound.

Parameters
profile_idstrrequired
userAny

Who the call acts for. Defaults to the user set by noclick.as_user (none: the account itself).

(defaults to the current user)
clientClient | None

The client to send with; defaults to the module client built from NOCLICK_API_KEY.

(default None)
Returns
BrowserProfile

The profile, loaded, with the sites it is signed in to.

Usage
profile = noclick.BrowserProfile.get("b7e1d3a9-4c2f-4e6b-8a5d-1f9c0e3b7a24")
for site in profile.sites:
    print(site.domain, site.signed_in_at, site.last_used_at)

listclassmethodasync

noclick.BrowserProfile.list(
    *, limit=None, user_filter=None, user=..., client=None
) -> list[BrowserProfile]

The current user's profiles (the account's own outside as_user, or those user_filter covers).

Parameters
limitint | None

Return at most this many.

(default None)
user_filterAny

Which users a read covers: noclick.ALL_USERS, noclick.NO_USER, a User, or your id for one. It filters without acting as that user.

(default None)
userAny

Who the call acts for. Defaults to the user set by noclick.as_user (none: the account itself).

(defaults to the current user)
clientClient | None

The client to send with; defaults to the module client built from NOCLICK_API_KEY.

(default None)
Returns
list[BrowserProfile]

Every matching profile, newest first, fetched page by page, each with its sites.

Usage
for profile in noclick.BrowserProfile.list(user_filter=noclick.ALL_USERS):
    print(profile.user_id, profile.name, [site.domain for site in profile.sites])

list_pageclassmethodasync

noclick.BrowserProfile.list_page(
    *, cursor=None, limit=None, user_filter=None, user=..., client=None
) -> Page

One page of profiles; pass next_cursor back as cursor.

Parameters
cursorstr | None

The next_cursor of the page before; omit it for the first page.

(default None)
limitint | None

Return at most this many.

(default None)
user_filterAny

Which users a read covers: noclick.ALL_USERS, noclick.NO_USER, a User, or your id for one. It filters without acting as that user.

(default None)
userAny

Who the call acts for. Defaults to the user set by noclick.as_user (none: the account itself).

(defaults to the current user)
clientClient | None

The client to send with; defaults to the module client built from NOCLICK_API_KEY.

(default None)
Returns
Page

A page whose data holds the profiles and whose next_cursor is None on the last page.

Usage
page = noclick.BrowserProfile.list_page(limit=50)
while True:
    for profile in page.data:
        print(profile.name, profile.last_used_at)
    if page.next_cursor is None:
        break
    page = noclick.BrowserProfile.list_page(limit=50, cursor=page.next_cursor)

loginasync

profile.login(url, *, human=None, reason=None) -> BrowserLogin

Ask a person to sign in to the site at url in a live browser; the sign-in is saved to this profile. human is who is asked (a noclick.Human): the profile's user when omitted. An account profile with no human sends nothing, so pass the link's url on yourself. You ask once: when the site signs the profile out later, the browser that meets its sign-in page asks the same person again (SiteLogin.signed_in_by) and saves the new sign-in. The link's browser starts (and is billed) only once the person opens it, so a link nobody opens costs nothing.

Parameters
urlstrrequired
humanHuman | Nonedefault None
reasonstr | Nonedefault None
Returns
BrowserLogin

The sign-in link, already sent: its url, when it expires_at (in 24 hours) and where it went. wait() returns the SiteLogin once the person has signed in.

Usage

Ask the user once, then use the sign-in whenever you need it:

profile = noclick.BrowserProfile.from_name("ana-work", create_if_missing=True, user="user_42")
login = profile.login("https://portal.example.com", reason="to download your invoices")
site = login.wait()
print(site.domain, site.signed_in_at)

Or, for your account's own profile, ask someone by email:

profile = noclick.BrowserProfile.from_name("acme-ops", create_if_missing=True)
login = profile.login("https://vendor.example.com", human=noclick.Human(email="ops@acme.test"))
print(login.delivered_via)   # ['email']

forgetasync

profile.forget(domain) -> None

Sign the profile out of domain and drop what it and its subdomains stored. A short browser (about a minute, billed like any) clears them and saves the profile; this returns once it has. It refuses (noclick.Conflict naming the browser) while another browser saves into the profile, and a browser already using the profile keeps the old state until it closes.

Parameters
domainstrrequired
Usage
profile = noclick.BrowserProfile.from_name("ana-work", user="user_42")
profile.forget("portal.example.com")

deleteasync

profile.delete() -> None

Delete the profile and every sign-in in it.

Usage
noclick.BrowserProfile.from_name("ana-work", user="user_42").delete()

BrowserLogin

class BrowserLogin(Operation)

A live-browser link where a person signs in to a site, and the operation that finishes once they have: wait() returns the SiteLogin.

Attributes
urlstr

The page where the person signs in, in a live browser.

expires_atAny
domainstr | None

The site's domain.

delivered_vialist[str]

Channels the link went out on (["email"]); empty when nobody was sent it.

undeliveredlist[dict[str, Any]]

Channels that failed, each with a reason: send the link yourself.

listclassmethodasync

noclick.BrowserLogin.list(*, limit=None, **filters) -> list[Operation]

Sign-in operations (kind browser.login), newest first; takes Operation.list's filters except kind.

Parameters
limitint | None

Return at most this many.

(default None)
**filtersAny
Returns
list[Operation]

Every matching operation (or the first limit); wait() on one returns the SiteLogin.

Usage
for op in noclick.BrowserLogin.list(status="awaiting_human", user_filter=noclick.ALL_USERS):
    print(op.user_id, op.summary_text, op.created_at)   # ... Sign in to portal.example.com ...

list_pageclassmethodasync

noclick.BrowserLogin.list_page(*, cursor=None, limit=None, **filters) -> Page

One page of sign-in operations; pass next_cursor back as cursor.

Parameters
cursorstr | None

The next_cursor of the page before; omit it for the first page.

(default None)
limitint | None

Return at most this many.

(default None)
**filtersAny
Returns
Page

A page whose data holds the operations and whose next_cursor is None on the last page.

Usage
page = noclick.BrowserLogin.list_page(status="expired", limit=50)
while True:
    for op in page.data:
        print(op.summary_text, op.updated_at)
    if page.next_cursor is None:
        break
    page = noclick.BrowserLogin.list_page(status="expired", limit=50, cursor=page.next_cursor)

summaryclassmethodasync

noclick.BrowserLogin.summary(**filters) -> OperationSummary

Count sign-in operations; takes Operation.summary's filters except kind.

Parameters
**filtersAny
Returns
OperationSummary

The total that match and a count for every status in by_status.

Usage
counts = noclick.BrowserLogin.summary(user_filter=noclick.ALL_USERS)
print(counts.by_status["awaiting_human"], counts.by_status["succeeded"])

PageState

class PageState

Where the browser is: the page's url (after redirects) and title.

Attributes
urlstr | None
titlestr | None
rawdict[str, Any]

The response JSON exactly as the API sent it.

ActResult

class ActResult

How an act went: done says whether the task was finished (False: it ran out of steps or couldn't go on, and summary says why). steps are the actions it took (action, detail, url); downloads the files it saved (name, size_bytes, and a signed url valid for an hour).

Attributes
donebool
summarystr
stepslist[dict[str, Any]]
urlstr | None
downloadslist[dict[str, Any]]
cost_usdfloat | None
rawdict[str, Any]

The response JSON exactly as the API sent it.

SiteLogin

class SiteLogin

A site a profile is signed in to: its domain, when the person signed in and who did, and when a browser last used the sign-in (None if never). When the site signs the profile out, the next browser that meets its sign-in page asks signed_in_by to sign in again in the live view, as it asks for a two-factor code, and saves the new sign-in; signed_in_at then moves.

Attributes
domainstr
signed_in_atAny
signed_in_bystr | None

The email of the person who signed in (the one login asked), and who is asked again when the sign-in expires; None for a sign-in made through a link you passed on yourself, and then the browser's human is asked.

last_used_atAny
rawdict[str, Any]

The response JSON exactly as the API sent it.