Bowmark AIdocs

Quickstart

Get one real result out of Bowmark before you install anything, then wire it into your agent.

Most integrations ask you to install first and trust that something happened. Bowmark is a plain HTTP endpoint behind an API key, so you can do it the other way round: get a real answer out of it first, decide it is worth installing second.

1 — See it work. Nothing installed.

Every call needs a Bowmark account. Sign up at bowmark.ai/sign-up, create a key at bowmark.ai/dashboard/keys, and export it as BOWMARK_API_KEY. Every account gets $10 of usage free each month (Pricing). Then paste this into any terminal:

curl -s https://api.bowmark.ai/v1/run \
  -H "Authorization: Bearer $BOWMARK_API_KEY" \
  -H 'Content-Type: application/json' \
  -d '{"script":"const { offers } = await bowmark.pcparts.search(\"RTX 5080\"); return offers.slice(0, 3);"}'

It sits there for about ten seconds, because in that time Bowmark is searching Newegg, Micro Center and B&H at once and price-sorting what comes back. This is what it printed, recorded 2026-08-24:

{
  "ok": true,
  "status": "ok",
  "ms": 9953,
  "result": [
    {
      "store": "newegg",
      "price": 1449.99,
      "title": "MSI INSPIRE GeForce RTX 5080 16GB GDDR7 PCI Express 5.0 Graphics Card"
    },
    {
      "store": "bhphoto",
      "price": 1499.99,
      "title": "Gigabyte GeForce RTX 5080 MASTER Graphics Card"
    },
    {
      "store": "newegg",
      "price": 1549.99,
      "title": "PNY GeForce RTX 5080 OC 16GB 256-Bit GDDR7 DLSS 4.0 Graphics Card"
    }
  ]
}

Your prices will differ, and that is the point: those numbers came off the retailers' live pages while the request was open, not out of an index.

Using Python? Set the User-Agent header

The curl example above works out of the box. If you translate it to Python using stdlib urllib.request, you'll get a 403 Forbidden (error code 1010) because Python's default User-Agent header is blocked by our WAF. Fix it with one line:

headers = {
  "Authorization": f"Bearer {os.environ['BOWMARK_API_KEY']}",
  "Content-Type": "application/json",
  "User-Agent": "my-app/1.0",  # ← add this line
}

If you use requests, httpx, aiohttp, or the bowmark-web package, you're not affected — their User-Agent strings are fine. Full details and a working example are in Scripting.

What the ten seconds were spent on

Every response also carries a trace. It is the receipt — one row per site Bowmark actually reached, with what each one gave back. Summarised, from the same run:

newegg       ok      3 offers   4100ms
bhphoto      ok      3 offers   5426ms
microcenter  empty   0 offers   9943ms
-> pcparts   ok                 9946ms

Three retailers were searched in parallel and Micro Center had nothing matching, so the answer is the cheapest of two. Bowmark reports that rather than hiding it — which is the difference between a result you can quote and one you can only hope about.

That was the whole product

You just wrote a script against the capability library and had it executed against live retail sites. No browser was opened, no page was rendered, and nothing was installed. Everything below is about getting your agent to do that on its own.

2 — Install it into your agent

One line, one time. Pick your host. Where a plugin exists, take it — it wires the MCP and installs the skill that teaches your agent when to reach for Bowmark, and it lands in the host's own plugin list. Installation has the config for every host, including the double-click bundle for Claude Desktop.

claude mcp add bowmark --transport http https://api.bowmark.ai/mcp \
  --header "Authorization: Bearer $BOWMARK_API_KEY"

Run it from any terminal. Every project picks Bowmark up — there is no per-project step, and nothing to name in a prompt. Claude reaches for it on its own when a task needs the live web.

Want the skill bundled in too? One install does both:

claude plugin marketplace add bowmark-ai/plugin
claude plugin install bowmark@bowmark-ai

Four clicks, about a minute. Full screenshots are on Installation.

Go to chatgpt.com/plugins and click + (top right).

Name it Bowmark, choose Server URL, paste https://api.bowmark.ai/mcp/chatgpt-app, set OAuth, tick the box, hit Create.

Hit Connect on the next screen and sign in to Bowmark.

In a chat, type @ and pick Bowmark. You must do this every turn — see the warning below.

Settings → MCP → Add new MCP server, then paste:

{
  "mcpServers": {
    "bowmark": {
      "url": "https://api.bowmark.ai/mcp",
      "headers": { "Authorization": "Bearer <your Bowmark API key>" }
    }
  }
}
codex plugin marketplace add bowmark-ai/plugin

Then codex /plugins and install Bowmark from the list. Or edit ~/.codex/config.toml directly:

[mcp_servers.bowmark]
url = "https://api.bowmark.ai/mcp"
bearer_token_env_var = "BOWMARK_API_KEY"

Install the SDK into your Node or Python project:

Node.js:

npm install @bowmark/web

Python:

pip install bowmark-web

Then use the typed SDK in your code:

Node.js:

import { Bowmark } from "@bowmark/web";

const bowmark = new Bowmark({ apiKey: process.env.BOWMARK_API_KEY });
const { offers } = await bowmark.pcparts.search("RTX 5080");
console.log(offers);

Python:

from bowmark import Bowmark

bowmark = Bowmark(api_key=os.environ["BOWMARK_API_KEY"])
offers = bowmark.pcparts.search("RTX 5080")
print(offers)

For more details and examples, see Installation and Scripting.

Same URL, same key in the Authorization header. Windsurf, Claude Desktop, LM Studio, browser-use, your own agent — every path is on Installation.

If your host speaks stdio rather than HTTP, bridge it:

{
  "mcpServers": {
    "bowmark": {
      "command": "npx",
      "args": ["@bowmark/mcp"],
      "env": { "BOWMARK_API_KEY": "<your Bowmark API key>" }
    }
  }
}

3 — Give your agent its first task

Paste this into a fresh chat, verbatim:

Price an RTX 5080 across every retailer you can reach, and tell me
which store is cheapest and what it costs right now.

In ChatGPT, type @ and pick Bowmark first. Everywhere else, just send it.

A working agent does three things, in this order, and you can watch each one:

  1. Calls get_library with something like "pc parts". No site is touched.
  2. Calls run with a few lines of JavaScript it wrote against what came back.
  3. Answers with a store name, a price, and a link you can click.

If it answers without calling a tool, it did not use Bowmark — it answered from memory or browsed. In ChatGPT that is almost always the missing @.

4 — When it doesn't work

Three failures account for nearly all of them.

The agent never called Bowmark

In ChatGPT, a custom connector is inert until you attach it for that turn. This is documented OpenAI behaviour and no amount of prompt wording changes it. Type @, pick Bowmark, then ask. Everywhere else, the tools are in scope on every turn.

TypeError: Cannot read properties of undefined

The script called a function with the wrong argument shape and then read a field off undefined. Signatures are not uniform on purpose — bowmark.pcparts.search takes a plain string, bowmark.flights.search takes an object.

// wrong — search takes a string here
const { offers } = await bowmark.pcparts.search({ query: "RTX 5080" });

// right
const { offers } = await bowmark.pcparts.search("RTX 5080");

The fix is always the same: read the library for that capability and check the signature. It costs nothing and touches no site.

How you read it depends on which channel you are on. In an agent it is the get_library tool. Over HTTP — including if you are POSTing to /v1/run as above — it is a second endpoint, never something you can call from inside the script:

curl -s "https://api.bowmark.ai/v1/library?query=pc%20parts" \
  -H "Authorization: Bearer $BOWMARK_API_KEY"

It answered, but the answer looks thin

Read warnings and the trace together — they say different things, and reading only one of them is how a thin answer gets quoted as a complete one.

  • warnings names every site dropped from the fan-out: it failed, or it timed out. A populated warnings means a site was never heard from at all.
  • trace has one row per site regardless, with what each one returned. A row reading empty is a site that answered and had nothing matching.

So an empty warnings does not mean every site had a result — it means none were dropped. The run above is exactly that case: warnings was [], and Micro Center still contributed nothing. Read both before you call an answer complete.

Check status before ok — partial means the script ran and the result is real but narrower than you asked for. Full rules in Scripting.

The site I need isn't supported

If the library comes back empty for the site or capability you're looking for, there's no dedicated provider for it yet. That doesn't mean Bowmark can't reach it.

Call bowmark.read.page(url) (or read.pages for several URLs at once) through run() — it fetches any page as a plain GET and transparently escalates to a real browser only if the page needs one. This is Bowmark's own answer to "the site isn't in the list", and it works for most pages with no dedicated provider at all. session() and the top-level bowmark client both refuse it (code: "rung_undeclared" — its rung is decided per call, not statically), so run() is not optional here. See How it works § No provider? Read any page directly for the full example.

If a page genuinely needs more than that — it's behind a login, needs a click or a date picked on a widget before the data shows up, or you want it turned into a typed capability rather than raw HTML:

  • Reach for bowmark.browser_agent if the blocker is interaction, not access — a form to fill, a calendar widget to click through, a wizard to drive. It's Bowmark's own hosted browser agent, built for exactly the case read.page can't handle. It always pauses for a human at a watch link when a login is needed, and that is true on every run — there is no way to store or replay a login across runs, even against the same site twice. See Scripting § When nothing else works: the browser agent.
  • Email support@bowmark.ai with the site name and what you were trying to do. Bowmark prioritizes sites by actual usage and by direct request. Your request is tracked as demand — every time an agent reaches for a site we don't cover, we see it and add it to the priority queue.
  • Keep going anyway. Your agent can still reach the site via a browser or a direct API call if one exists. Bowmark works alongside whatever your agent does, not instead of it.

Write actions and multi-step flows with browser_agent

Everything above reads data. Bowmark also runs a hosted browser agent for sites that need interaction — filling forms, clicking through wizards, signing in, any action a person would perform in their browser.

When to use browser_agent

  • The site needs interaction you can't script: a form to fill, a date to pick on a calendar widget, a wizard to step through
  • You're writing data: form submissions, order placement, profile updates, content uploads
  • The site needs multiple steps in sequence: login, navigate, fill form, verify, submit
  • Standard reads won't work because the data only appears after you interact with the page

If the site just needs a login and then serves you regular pages, you don't need browser_agent — use the SDK or a capability directly with a caller-supplied login. browser_agent is for the interaction itself.

The async polling pattern

browser_agent runs asynchronously. You start it in one run(), then poll for status from later runs. You cannot start it and immediately wait for the result in the same run — that would block forever and hit the 90-second timeout.

// Step 1: Start the browser agent (in one run)
const { result: started } = await run(
  `return bowmark.browser_agent.start({
    task: "Fill out the form on athenahealth.com with patient ID 12345, add clinical note, and submit"
  })`
);

const { id, watchUrl } = started;
console.log("Browser agent started. Watch at:", watchUrl);
// Step 2: Poll from a LATER run (or multiple later runs)
// You can do this immediately, or wait and try again later
for (let i = 0; i < 10; i++) {
  const { result: status } = await run(
    `return bowmark.browser_agent.status(${JSON.stringify(id)}, { waitMs: 60000 })`
  );
  
  console.log("Status:", status.status);
  if (status.status === "needs_input") {
    // The browser agent needs you to answer something (login, captcha, etc)
    // Use browser_agent.send() to provide input
    break;
  }
  if (status.status === "idle") {
    // Done! The result is in status.result
    console.log("Completed with result:", status.result);
    await run(`return bowmark.browser_agent.stop(${JSON.stringify(id)})`);
    break;
  }
  // status.status === "running" means keep polling
}

Understanding timeouts and polling

What waitMs means: status() blocks for up to that many milliseconds waiting for the agent to finish. If it doesn't finish in that time, it returns status: "running" and you can poll again.

  • waitMs: 60000 (60 seconds) — recommended for most tasks. The polling call itself will wait this long before returning.
  • waitMs: 5000 (5 seconds) — faster feedback loop if you want to check on progress frequently
  • Default if you omit it — the endpoint waits as long as it can

The whole run() call times out at 90 seconds. This is the ceiling for run(), not specific to browser_agent. A script that starts the agent and then immediately calls status() in a loop will hit this ceiling on the second poll.

Typical task takes 1-4 polls (about 1-3 minutes total). A repeated step name showing up on back-to-back polls is normal — multi-step sites need several attempts before they resolve. Don't stop on a status plateau; keep polling until you see idle or needs_input.

// ❌ DON'T do this — the second run() call will fail
const start = await run(`return bowmark.browser_agent.start({task})`);
const status = await run(`return bowmark.browser_agent.status(start.result.id)`);
// This throws because status() inside a loop hits the 90s ceiling

// ✓ DO this — separate runs, so each gets its own 90s budget
const start = await run(`return bowmark.browser_agent.start({task})`);
// ... wait, then in a DIFFERENT call ...
const status = await run(`return bowmark.browser_agent.status(start.result.id)`);

Success verification

The result field tells you what the browser agent found or did:

  • For reads (checking information): result contains the extracted data — text, values, confirmations
  • For writes (form submissions, uploads): result contains a summary of what was written and any confirmation text from the site
  • Field-by-field verification: The summary describes what was completed. If you need to verify individual fields were filled correctly, include that in your original task description ("After filling the form, take a screenshot and list each field you filled")

status.result is a plain object; the structure depends on what you asked the agent to do:

const status = await run(`return bowmark.browser_agent.status(id)`);
// status.result might be:
// { message: "Form submitted successfully", confirmation_number: "ATH-123456" }
// or
// { order_placed: true, order_id: "12345", estimated_delivery: "2026-09-28" }

Cost estimation

Each call to browser_agent costs $0.02–$0.25 per run, depending on task complexity and how long it takes. Typical tasks run 1-3 minutes and cost $0.02–$0.10.

See Pricing § Hosted browser agents for the full cost breakdown and how to estimate your costs at scale.

Unattended logins are not supported here

bowmark.secret() stores a credential for a typed provider's own signIn() call — it is not a way to log in through browser_agent. start() takes a plain-language task string and nothing else, and interpolating bowmark.secret("…") into that string sends the browser agent the literal placeholder text, never the password, with no error. When a task hits a login, browser_agent returns needs_input with kind: "takeover" and waits for a person to act at watchUrl — every run, not just the first.

If the site has a typed provider, sign in once with providers.<site>.signIn() (using a stored bowmark.secret()) and make repeated calls inside that session() instead. See Scripting § When nothing else works: the browser agent for the full explanation.

Stop when done

An open browser_agent session costs money until you stop it. Always call stop() when the task is done, even if the status was idle.

await run(`return bowmark.browser_agent.stop(${JSON.stringify(id)})`);

Where to go next

  • How it works — the two calls, drawn, and why the surface is a language rather than a menu of tools.
  • Scripting — the globals, the sandbox rules, and how to read the response envelope. Includes a full reference for browser_agent.
  • Installation — every host, with screenshots, plus API keys and what they unlock.

Reading this with an agent?

This page as plain text: /docs/quickstart.md. The whole site as one file: https://bowmark.ai/llms-full.txt. Index of every page: https://bowmark.ai/llms.txt.