Skip to content
unfenced

Documentation

Give your agent the whole web.

Everything you need to go from "I have an account" to "my agent just fetched a page": get a key, connect an agent, the tools it gets, how permissions and logins work, what a failure means, and worked examples you can run from a terminal. Signed in already? The same reference lives in the dashboard under Docs, with your own key spliced into every snippet.

What Unfenced is

Unfenced fetches any web page through a real browser on a real IP and hands your agent back clean, readable content instead of raw HTML - no headless-browser fleet to run, no anti-bot fighting, no HTML-to-text pipeline to maintain.

There are two ways to use it:

  • Reading - give it a URL, get back markdown (or JSON, text, HTML). This covers most of what an agent needs: articles, listings, docs, feeds, and PDFs read as text.
  • Live pages - for anything that needs a signed-in session, a click, or a form: open a real page, look at it, act on it, and read it back. The same engine can drive your own logged-in accounts, behind a permission model you control - it never acts on a site you have not allowed.

Reach both through one MCP connector (a small set of tools an agent calls directly), the zero-dependency TypeScript SDK, or the plain HTTP API the two share underneath.

Get an API key

Every connection to Unfenced - MCP, the SDK, or plain HTTP - authenticates with an API key.

  1. Sign in and open API keys in the dashboard.
  2. Create a key and name it after whatever will use it - an agent, a client, a script.
  3. The key is shown once. The same screen shows the exact setup for your platform - Claude, Cursor, VS Code, a CLI, a framework, or a config file - with that key already filled in.

Treat a key like a password: anyone holding it can act as your account, within whatever an attached agent is scoped to.

Sign in to get a key

Connect an agent

Remote MCP - works today

Point any MCP-capable client at this URL, with your key as a bearer token:

https://unfenced.ai/api/mcp
Authorization: Bearer <your key>

The API keys screen in the dashboard has exact configuration for 31 clients and frameworks - Claude, Cursor, VS Code, Claude Code, Codex, LangChain, the Vercel AI SDK, plain HTTP, and more - with your key already spliced into every snippet. Copy from there rather than typing this by hand.

Local stdio bridge - once @unfenced-ai/mcp is published

A process on your machine that forwards every tool to this service over HTTP. It runs no browser of its own: the fetch still happens on our worker, on your account's exit IP and its stored logins, and is metered to your account. It exists for clients that cannot speak MCP over HTTP:

npx -y -p @unfenced-ai/mcp unfenced-mcp

This package is not on npm yet, so the command above does not work today. Use the remote MCP URL above in the meantime - it is the path that works right now, for every client that speaks MCP over HTTP.

TypeScript SDK - same "once published" caveat

A zero-dependency client (@unfenced-ai/sdk, Node 22+) that speaks the same HTTP API, for building your own agent or script rather than wiring MCP:

import { Unfenced } from "@unfenced-ai/sdk";

const ac = new Unfenced({ baseUrl: "https://unfenced.ai/api", apiKey: process.env.UNFENCED_TOKEN });

const r = await ac.fetch("https://example.com");
if (r.outcome === "delivered") console.log(r.doc.markdown);

The same client drives live pages too - ac.open(url), then observe, act, extract, close. Branch on r.outcome ("delivered" · "offered" · "failed"); there is no r.ok.

Like the stdio bridge, @unfenced-ai/sdk is not on npm yet. The shape above is the shipping client; until it publishes, the remote MCP URL is the path that works today.

The tools

Every tool your agent gets, grouped by what it does.

Reading a page

No live browser, no login, always available:

  • fetch_page - fetch one URL, get back clean markdown (or JSON, text, HTML).
  • fetch_batch - fetch up to 20 URLs in one call.
  • get_page_links - list a page's links, for crawling one level out without paying for the whole page.

A live page

Opens a real browser tab your agent can look at and act on, for logins, cookie banners, and anything that needs a click:

  • open_page - open a URL and keep it open so you can act on it.
  • observe_page - re-read an open page, after a slow load or to find a control.
  • act - click, type, select, scroll, drag, wait, and more - one action at a time.
  • see_page - a screenshot of the page with every control outlined and numbered.
  • extract_page - pull clean markdown or JSON out of a live page exactly as it stands - after logging in, after paginating, after expanding a section.
  • fill_form - fill several fields in one call: a checkout, a booking, a sign-up.
  • park_page - hold a page open while something else finishes: an emailed code, an approval in another app.
  • read_download - read a file the page downloaded, including a PDF.
  • close_page - release an open page.
  • switch_account - switch a page to a different one of your sign-in accounts.
  • whoami - confirm which account a page is actually signed in as.

Credentials and permissions

The guardrails around acting - see Permissions & credentials for how they fit together:

  • list_permissions - the sites your agent may act on.
  • connect_site - a one-screen link to add a site, or a login, you are missing.
  • list_credentials - the names of stored logins, never their values.
  • pending_approvals - what is currently waiting on you to approve.

A small memory

remember, recall and forget hold short notes - "invoice #4471 downloaded, waiting on approval" - that survive across sessions, so a later session can pick up where an earlier one left off. It is for task state, never for secrets.

Permissions & credentials

Reading a page never needs permission. Acting on one - a click, a keystroke, a form submit - is gated by an allowlist your account owns, and an agent cannot add itself to it.

  • Reading is always allowed. fetch_page, fetch_batch, get_page_links and reading an open page work on any site, no grant required.
  • Acting is granted per site. A site is either allowed (your agent acts freely) or set to ask each time (every action waits for you to approve it). A site on neither footing cannot be acted on. list_permissions shows the current list.
  • An agent cannot grant itself a site. A key acts within an account; it does not administer one. So the grant is a signed-in human decision: when your agent reaches a site it may not act on, it gets a connect_site link you open to add it, and pending_approvals shows everything currently waiting on you.

Credentials never reach the agent or the model. A saved login lives in an encrypted vault the agent cannot read. It fills one by name, and the value is typed straight into the page:

  • act kind: "fill_secret" - a stored password, by credential name.
  • act kind: "fill_totp" - the current 2FA code, derived from a stored seed.
  • act kind: "fill_otp" - the one exception: a single-use code the user hands the agent in chat (an emailed or texted 2FA code, no stored seed). It is shape-checked, redacted in the log, and refused if it looks like a password.

list_credentials shows which names exist - never a value. There is deliberately no tool that stores or reveals a secret; you add logins in the dashboard.

How failures come back

A failed call answers with a structured {"error": "<code>", "detail": "..."} rather than just an HTTP status - the code is the instruction for what to do next, and the codes are not interchangeable:

CodeWhenMeans
needs-credential 401 The page wants a signed-in session. No retry reaches it - fill a stored login.
payment-required 402 The site quoted a price rather than refusing you. A purchasing decision, not an error.
legally-restricted 451 Withheld for legal reasons, usually by jurisdiction. No tier or retry changes that.
blocked 403, 408, 429, 430, 503, or a challenge page The site refused you, and a real browser already tried first. Try a different source.
transient 500, 502, 504 (and other 5xx) The server faulted rather than refusing you. Worth retrying shortly.
http-error 404, 410 (and other 4xx) The path is wrong or gone, and the site itself answered fine. Check the URL rather than retrying.

A successful result can still carry contentConfidence: "low" with contentWarnings - the text came back, but it may not be the page you asked for, often a login wall. Worth a glance before you rely on it.

Live-page actions fail differently

A refusal from act, fill_form or open_page comes back as {"ok": false, "code": "..."} - not an HTTP error - and the code says what is missing:

CodeMeansWhat to do
permission-required The site is not on your act-allowlist. Call connect_site; the owner allows it in the dashboard.
credential-required A login field needs a stored secret. act with kind: "fill_secret" (or fill_totp) by credential name.
otp-required A one-time 2FA code is needed and no seed is stored. Ask the user for the code, fill it with kind: "fill_otp".
approval-required The site is "ask each time"; the owner must clear it. It is queued in pending_approvals.
confirmation-required The action would submit. Retry the same action with confirm: true.

Worked examples

Three flows end to end. The tool-call blocks show the arguments your agent sends; your MCP client handles the wire.

Read one page

The plain HTTP API - no client required. fetch_page is a thin wrapper over exactly this:

curl -s -X POST https://unfenced.ai/api/jobs   -H "Authorization: Bearer YOUR_KEY"   -H "content-type: application/json"   -d '{"url":"https://example.com","format":"markdown"}'
# -> 202 {"jobId": "..."}

curl -s https://unfenced.ai/api/jobs/JOB_ID -H "Authorization: Bearer YOUR_KEY"
# -> {"status": "done", "result": {"doc": {"markdown": "..."}}}

Poll the second call until status is "done", then read result.doc.markdown.

Fetch a batch

One call, up to 20 URLs, sharing a 25,000-word budget; a URL that fails carries its own error in place, so one bad link never sinks the other nineteen:

{ "urls": ["https://a.example/1", "https://a.example/2"], "format": "markdown" }

Over the SDK: await ac.batch([url1, url2]) returns one result per URL, in order.

Log in and act

Acting needs the site on your allowlist and, for a password field, a stored credential - whose value never passes through the agent:

1. open_page    { "url": "https://app.example.com/login", "intent": "act" }
                -> { "sessionId": "k3n9x2", "page": { ... } }

2. observe_page { "sessionId": "k3n9x2" }
                -> controls, each with a ref: { "ref": "k3n9x2:e7", "name": "Email", ... }

3. act { "sessionId": "k3n9x2", "kind": "type",        "ref": "k3n9x2:e7", "text": "me@example.com" }
   act { "sessionId": "k3n9x2", "kind": "fill_secret", "ref": "k3n9x2:e9", "credential": "example-login" }
                // the password is typed into the field - never through the agent

4. act { "sessionId": "k3n9x2", "kind": "click", "on": "Sign in", "confirm": true }
                // confirm: true is required for anything that submits

5. act { "sessionId": "k3n9x2", "kind": "fill_totp", "ref": "k3n9x2:e12", "credential": "example-login" }
                // only if a 2FA prompt appears; derived from the saved seed

6. extract_page { "sessionId": "k3n9x2", "format": "markdown" }   // the signed-in page, clean
7. close_page   { "sessionId": "k3n9x2" }                         // release the browser

A live page closes itself after a few minutes idle; park_page holds one open - up to 30 minutes - while you wait on an emailed code or an approval elsewhere.

Sign in to get a key How a fetch works