AccountsThe logins the agent works through — one per platform
Your platforms
Click a platform to open its profile and sign in
Live now
What the agent is doing right now — open it to watch or step in
Each account is its own isolated profile — its own saved cookies, its own pinned exit route.
🧠Agent & AI
The model the agent thinks with, and whether it may act on its own. Keys are stored on this browser's own volume and never sent back out — only the last characters are shown.
🔑Account
This browser's API key grants everything the console can do — it is also the way back in if the password is lost. Keep it somewhere safe.
🗂️Per-login exit route
Override the default for one login. "Always my tailnet" refuses to open while the tailnet is down rather than quietly leaving from this server. Applies on the login's next session.
🚀Getting started
The Ghost Browser is a real logged-in browser behind an HTTP API. Everything the console does — open a session, drive a page, dispatch an autonomous agent job — is a call you can make yourself. This is the same interface the platform's master agent uses. All requests and responses are JSON.
Base URL. Every path is relative to the origin serving this console — for you right now that is this origin.
Authentication. Programs authenticate with a bearer key. Signed in here, you can read the key straight from the API — then send it on every call as an Authorization: Bearer <key> header.
/api/auth/keycookieReturn this console's API key(s). Requires the signed-in cookie — a key cannot fetch itself. Every returned key grants everything the console can do.
curl -b cookies.txt https://your-ghost-host/api/auth/key
{
"keys": [
{ "key": "gb_live_9f3c2a71b0e4…", "plan": "solo", "maxConcurrent": 1 }
]
}
Set these once in your shell and every example below just works:
BASE="https://your-ghost-host" # the origin serving this console KEY="gb_live_9f3c2a71b0e4…" # from GET /api/auth/key
A refusal comes back as an error object with the HTTP status — for example { "error": "not your session" } with 403.
🧠Two ways to use it
There are two ways to run the Ghost Browser. Pick either — or start with the built-in agent and move to your own later. Both run on your model and your costs.
1 · Your own LLM drives it (this reference). Your agent runs on your side and is the brain. For every step it calls the API below — open a session, read the page as numbered boxes, click / type / act. The Ghost Browser is purely the hands. Start at Sessions, then loop: analyze → decide in your LLM → click / type.
2 · The built-in agent, on your model. Put your own LLM key in Settings and the agent that ships with the Ghost Browser decides and acts by itself. You write no code — you configure roles and agent jobs / workflows as data.
🔌Bring your own model
Add your own key so nothing runs on someone else's account. Supported: OpenRouter and your own Ollama endpoint. Set it in Settings → Agent & AI.
/v1/agent/settingsbearerThe current agent configuration (provider + model), secrets redacted.
🔑Signing in
Two ways in. SSO from the platform — sign in on the platform and open the Ghost Browser. And a bearer key for scripts — read it with GET /api/auth/key and send it as Authorization: Bearer <key> on every call.
🪟Sessions
A session is one browser tab you hold. Open one, point it at a URL, read the page as numbered boxes (Set-of-Mark), and act on a box by its number.
/v1/sessionsbearerOpen a session. With reuse you get the one you already hold instead of an error; with profile it opens a named, cookie-keeping login.
| Field | Type | Req | Description |
|---|---|---|---|
| profile | string | no | Named profile to open — its own saved cookies and pinned exit route. |
| preset | string | no | A known site preset; configures the profile and lands on its login page. |
| reuse | boolean | no | Return an existing session rather than failing at the limit. |
| takeover | boolean | no | Close a conflicting session of yours and open here instead. |
curl -X POST "$BASE/v1/sessions" \
-H "Authorization: Bearer $KEY" \
-H "Content-Type: application/json" \
-d '{ "profile": "facebook", "reuse": true }'
{
"sessionId": "a1b2c3d4",
"profile": "facebook",
"expiresAt": 1756497200000,
"plan": "solo",
"reused": false
}
/v1/sessionsbearerList the sessions you hold, with your concurrency limit and plan.
curl "$BASE/v1/sessions" -H "Authorization: Bearer $KEY"
/v1/sessionsbearerClose every session you hold. Close a single one with DELETE /v1/sessions/:id.
{ "closed": 1 }
/v1/sessions/:id/navigatebearerGo to a URL. Only public addresses are allowed.
curl -X POST "$BASE/v1/sessions/a1b2c3d4/navigate" \
-H "Authorization: Bearer $KEY" \
-H "Content-Type: application/json" \
-d '{ "url": "https://www.facebook.com" }'
{ "url": "https://www.facebook.com/", "status": 200, "title": "Facebook" }
/v1/sessions/:id/analyzebearerThe page as numbered, clickable boxes — Set-of-Mark. Each element has an index you then click or type into.
curl "$BASE/v1/sessions/a1b2c3d4/analyze?screenshot=false" \ -H "Authorization: Bearer $KEY"
{
"url": "https://www.facebook.com/",
"title": "Facebook",
"elementCount": 42,
"elements": [
{ "index": 1, "tag": "input", "type": "text", "editable": true,
"placeholder": "Email or phone number", "text": "", "x": 512, "y": 288 },
{ "index": 2, "tag": "button", "role": "button", "text": "Log in", "x": 512, "y": 360 }
],
"summary": "[1] ✎ FIELD[text] — \"Email or phone number\" …\n[2] button — \"Log in\" @ (512,360)",
"screenshot": null
}
/v1/sessions/:id/clickbearerClick a box by its index from the last analyze, or by visible text.
curl -X POST "$BASE/v1/sessions/a1b2c3d4/click" \
-H "Authorization: Bearer $KEY" \
-H "Content-Type: application/json" \
-d '{ "index": 2 }'
{ "clicked": { "index": 2, "text": "Log in" }, "url": "https://www.facebook.com/" }
/v1/sessions/:id/typebearerType into a field box, human-paced. It re-reads the page if it moved (so the number still names the right field), scrolls the field on-screen first, and clears it before typing. For a rich / markdown / React editor, pass paste: true: it inserts the whole block so the editor actually registers it — plain typing into such a box lands in a hidden backing field and silently does nothing (this is what made a Bugcrowd biography or a GitHub comment look filled but submit empty). submit: true presses Enter after.
| Field | Type | Req | Description |
|---|---|---|---|
| index | number | yes | The field’s number from the last analyze. |
| text | string | yes | What to type. |
| paste | boolean | no | Insert the whole block at once, firing the input events a rich / markdown / contenteditable editor needs. Use for any editor that ignores plain typing. |
| clear | boolean | no | Select and replace the field’s existing content first. Default true. |
| submit | boolean | no | Press Enter after typing. |
{ "index": 14, "text": "Security researcher focused on web and API security...", "paste": true }
{ "typed": 371, "into": "biography", "mode": "paste", "url": "https://bugcrowd.com/h/onboarding" }
🤖Agent jobs
The important one. A job is "go and do this in the browser I am already logged into". You dispatch it with a goal and a role against an open session, then poll it.
/v1/agent/jobsbearerDispatch a job. It runs in the session you name (open one first). The loop outlives the request — this returns immediately with the job to poll.
| Field | Type | Req | Description |
|---|---|---|---|
| goal | string | yes | Plain-language description of what to do. |
| sessionId | string | yes | The open session to work in. |
| role | string | no | The specialist to run as. Defaults to general. |
| autoApprove | boolean | no | Run this job with acting-without-asking on. |
| unattended | boolean | no | Nobody is watching: conclude and hand the session back. |
curl -X POST "$BASE/v1/agent/jobs" \
-H "Authorization: Bearer $KEY" \
-H "Content-Type: application/json" \
-d '{
"sessionId": "a1b2c3d4",
"role": "reddit.demand",
"goal": "Find people in r/smallbusiness asking how to invoice EU clients"
}'
{
"jobId": "k7f2a9",
"status": "running",
"createdAt": 1756490500000,
"steps": [],
"leads": [],
"proposals": []
}
/v1/agent/jobs/:idbearerPoll one job — status, steps, leads, and any proposals waiting on you.
curl "$BASE/v1/agent/jobs/k7f2a9" -H "Authorization: Bearer $KEY"
/v1/agent/jobs/:id/proposals/:pidbearerApprove or reject one proposal. edit rewrites the text before it is sent.
curl -X POST "$BASE/v1/agent/jobs/k7f2a9/proposals/p-1-9f3c" \
-H "Authorization: Bearer $KEY" \
-H "Content-Type: application/json" \
-d '{ "approve": true, "edit": "We built exactly this — happy to show you." }'
/v1/agent/jobs/:id/saybearerSay something to a running job — steer it, or nudge an idle one back to work.
{ "text": "Only save people who mention VAT specifically." }
/v1/agent/jobs/:id/stopbearerStop a job. Any pending proposals are skipped and the session is handed back.
curl -X POST "$BASE/v1/agent/jobs/k7f2a9/stop" -H "Authorization: Bearer $KEY"
🎭Roles
The specialists a job can run as. A role fixes the tools and the prompt server-side — a scout cannot post, a poster cannot invent leads.
/v1/agent/rolesbearerEvery role, grouped for display. Use name as the role when dispatching a job.
{
"roles": [
{ "name": "general", "label": "General", "description": "Everything in reach, no site assumed.", "site": null, "group": "Anything" },
{ "name": "facebook.scout", "label": "Facebook · Lead scout", "description": "Reads Facebook groups for people who need what you sell. Cannot post.", "site": "facebook", "group": "Facebook" }
]
}
🗂️Profiles
The saved logins on disk — one isolated profile per platform. Open one by passing its name to POST /v1/sessions.
/v1/profilesbearerThe profile names that exist right now.
{ "profiles": ["facebook", "linkedin", "reddit"] }
📊Capacity & health
Two open endpoints — no key needed.
/v1/capacityopenHow full the worker is. accepting: false means open a session later.
{
"sessions": 2, "maxSessions": 8, "memoryPct": 41,
"draining": false, "accepting": true
}
/healthzopenLiveness. Always { "ok": true } while the process is up.
🧭How the master uses it
The whole loop the platform's master agent runs, end to end. Five calls: get a session, dispatch a job, poll it, approve what it drafts, read the structured result.
reuse.SID=$(curl -s -X POST "$BASE/v1/sessions" \
-H "Authorization: Bearer $KEY" -H "Content-Type: application/json" \
-d '{ "profile": "linkedin", "reuse": true }' | jq -r .sessionId)
jobId at once.JOB=$(curl -s -X POST "$BASE/v1/agent/jobs" \
-H "Authorization: Bearer $KEY" -H "Content-Type: application/json" \
-d "{ \"sessionId\": \"$SID\", \"role\": \"linkedin.buyers\",
\"goal\": \"Find heads of finance at 20-100 person agencies\" }" | jq -r .jobId)
status is running.while : ; do J=$(curl -s "$BASE/v1/agent/jobs/$JOB" -H "Authorization: Bearer $KEY") echo "$J" | jq -r .status [ "$(echo "$J" | jq -r .status)" = "running" ] || break sleep 3 done
state: "pending".curl -s -X POST "$BASE/v1/agent/jobs/$JOB/proposals/$PID" \
-H "Authorization: Bearer $KEY" -H "Content-Type: application/json" \
-d '{ "approve": true }'
status is done, the deliverable is in leads.curl -s "$BASE/v1/agent/jobs/$JOB" -H "Authorization: Bearer $KEY" \
| jq '.leads[] | { name, why, quote, url }'
Loading…