# HTTP API (/docs/api)

import { Callout } from "fumadocs-ui/components/callout";

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](/docs/scripting#no-install-at-all).

## Base URL and auth

```
https://api.bowmark.ai
```

**Not `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_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](https://bowmark.ai/sign-up), create a key at
[bowmark.ai/dashboard/keys](https://bowmark.ai/dashboard/keys). The first $10 of usage a
month is free with no card; add one at
[bowmark.ai/dashboard/billing](https://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 `return`s its result.                                                                           |

```sh
curl -sS "https://api.bowmark.ai/v1/library?query=flights" \
  -H "Authorization: Bearer $BOWMARK_API_KEY"
```

```sh
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](/docs/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](/docs/scripting#no-provider-for-a-site-use-readpage).

<Callout type="info" title="`/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.
</Callout>

## 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](/docs/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](/docs/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`](/docs/scripting#no-provider-for-a-site-use-readpage).

**`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](/docs/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](https://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](/docs/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](/docs/scripting#from-your-own-code) are generated from it and ship the types
already.

## What is not here

The routes above, plus [Recipes](/docs/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](mailto: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](/docs/quickstart)** — one `curl`, a real answer off three live retailers.
* **[Scripting § No install at all](/docs/scripting#no-install-at-all)** — the same two
  endpoints with worked Node, Python and `curl` examples, and the Python `urllib`
  User-Agent caveat that costs a **403** if you miss it.
* **[Installation](/docs/installation)** — if your client *does* speak MCP, wire it up once
  and skip the HTTP layer entirely.
* **[Browser Agent](/docs/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.
