HTTP API
The base URL, the auth header, and the endpoints — for a caller with no MCP client and nothing installed.
Bowmark is an HTTP API behind an API key. Nothing needs to be installed and no MCP client is required — if your code can send a request, it can use Bowmark.
This page is the reference: the base URL, the header, the endpoints. For worked
examples in Node, Python and curl, go to
Scripting § No install at all.
Base URL and auth
https://api.bowmark.aiNot bowmark.ai — that host serves the marketing site and these docs, and will 404 an
API path. Every endpoint below takes the same header:
Authorization: Bearer $BOWMARK_API_KEYOpenAPI spec
The endpoints on this page as an OpenAPI 3.1 spec, for Postman, Swagger UI or an SDK generator. It needs no key:
https://api.bowmark.ai/openapi.jsonGetting a key
There is no anonymous tier and no public browsing: every call — GET /v1/library
included — is refused with 401 without a key. Sign up at
bowmark.ai/sign-up, create a key at
bowmark.ai/dashboard/keys. The first $10 of usage a
month is free with no card; add one at
bowmark.ai/dashboard/billing to go past it.
The two calls
Everything a caller does is one of these, in this order — read the library, then send a script written against it.
GET /v1/library?query=… | Read the vocabulary. Returns the typed function library for a task or a site. Read-only, touches no site, costs nothing, and an unrecognized query returns the whole index rather than an error — so the call cannot dead-end. |
POST /v1/run | Execute a script written against that library, on the live sites. Body is {"script":"…"}; the script is async JavaScript and returns its result. |
curl -sS "https://api.bowmark.ai/v1/library?query=flights" \
-H "Authorization: Bearer $BOWMARK_API_KEY"curl -sS -X POST 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);"}'run answers an envelope: ok, status, result, logs, error and ms are always
present, plus runId, queuedMs, incomplete, emptyHanded, notes, needs, meta
and notice when they are relevant to what happened. Full field-by-field reference,
including exactly when each optional field shows up: Scripting § Reading the
response. What you may write inside script — the
bowmark.* globals (including bowmark.read.page for sites with no provider), log, talk,
and pay — is Scripting.
`/v1/library` returns markdown by default
It is a document meant to go straight into a model's context, so the default response is
text/markdown. Send Accept: application/json to get the machine shape instead.
Recipes
For a common job you can skip writing a script. POST /v1/recipes/:id runs a ready-made
one with the inputs you send, and returns the same envelope as run. GET /v1/recipes
lists them, and needs no key. Full reference: Recipes.
read.page and browser_agent — the two fallbacks
These are globals your script calls inside run(), not separate endpoints — the two calls
above are still the whole HTTP surface. This is the reference for their parameters and
return shape; Scripting has the full walkthrough, error codes and worked
examples for both.
bowmark.read.page(url, options?) — reads one page directly when GET /v1/library
returns no provider for the site.
| Param | |
|---|---|
url | required |
format | "markdown" (default) or "html" |
maxChars | cap on returned content, default 200,000 |
Returns { ok, title, content, format, chars, truncated, warnings, url, requestedUrl, status, error, needsLogin, escalationReason, wall, json?, headers? } — ok: false on a
wall, a login page, or an empty shell, not only on a network failure.
bowmark.read.pages(urls: string[], options?: ReadOptions): Promise<ReadResult[]> —
the same call over several URLs at once, returning one result object per page in the same
shape as read.page. One dead URL in the batch does not lose the others; each page's own
error carries its own failure.
Field-by-field reference for every field above, and when each one is set: Scripting §
No provider for a site? Use read.page.
bowmark.browser_agent.start({ task, outputSchema? }) — hands a transactional task (a
booking, a form, a login walkthrough) to a hosted browser agent. One-off only; never for a
scheduled or repeating check.
| Call | |
|---|---|
start({ task, outputSchema? }) | returns a session id; outputSchema (JSON Schema) gets validated JSON back instead of free text |
status(id, { waitMs? }) | idle | running | needs_input |
send(id, answer) | answers a needs_input pause |
stop(id) / list() | release a session / see what you're holding — max 3 open at once |
Callable only through run() — session() and the bare top-level client refuse it with
code: "run_only", which is never an API-key problem. Full reference, costs and the
merchant_verification_failed safety check: Scripting § When nothing else works: the
browser agent.
Errors and rate limits
POST /v1/run answers with an HTTP status that tells you, without parsing the body,
whether the call itself was accepted — status inside the envelope is a separate,
narrower thing: whether your script succeeded. A failed script, or one that paused on
a login, is still a 200.
| HTTP status | When | What to do |
|---|---|---|
200 | The call was accepted and run — including a script that threw (status: "error") or paused for a login (status: "needs_user") | Read status in the body |
401 | No key, a bad key, or (anonymous callers only) past the free run limit | Create or fix the key; www-authenticate names the next step for an anonymous caller |
402 | The account is out of free usage, at its spend cap, or its card was declined | Fix billing at bowmark.ai/dashboard/billing — waiting does not clear this |
429 | Too many runs in flight at once (currently 16 concurrent) | Read Retry-After and meta.concurrency (limit, inFlight, retryAfterSeconds); nothing was started and nothing was charged |
503 | The queue or an identity lookup failed on our side | Retry; this is never your script's fault |
A code on a thrown error (from the typed client, or inside your own script's own
reasoning) is a second, finer-grained vocabulary — no_api_key, wire_refused,
bad_argument, unknown_function, rung_undeclared, run_only,
merchant_verification_failed — one run() call can throw any of these from your own
process. Full table, and what to do for each: Scripting § What it
throws.
The only hard numeric ceiling on /v1/run itself is 16 concurrent runs per account.
Past it you get the 429 above. There is no separate per-minute request limit on calling
run or library. A vendor behind a provider — a site a capability or provider calls —
may have its own limit; that shows up in incomplete.failures on that call, not as an
HTTP status on your own request.
Generating types
GET /v1/manifest?units=… | The same library as a machine-readable manifest, for a code generator rather than a model. units is required and comma-separated (?units=flights,providers.kayak) — there is deliberately no whole-catalog form, so call /v1/library first to find the ids. |
You usually do not need this: the @bowmark/web and bowmark-web
packages are generated from it and ship the types
already.
What is not here
The routes above, plus Recipes, are the whole caller-facing surface. Everything else under /v1/ —
the login handoff, secrets, files, saved connections, the per-company MCP servers — is
machinery the SDK and the sandbox drive on your behalf, documented where it is used rather
than as a route list. If you are reaching for one of those directly, email
support@bowmark.ai and tell us what you are building; that is
a gap in this page, not in the API.
Where to go next
- Quickstart — one
curl, a real answer off three live retailers. - Scripting § No install at all — the same two
endpoints with worked Node, Python and
curlexamples, and the PythonurllibUser-Agent caveat that costs a 403 if you miss it. - Installation — if your client does speak MCP, wire it up once and skip the HTTP layer entirely.
- Browser Agent — the fallback for a page that needs a form filled,
a login walked through, or a wizard driven; a plain
run()call, not a separate surface.
Reading this with an agent?