> For an index of all Botscent documentation, see https://botscent.nibnalin.me/llms.txt.

# Page API

Every function, attribute, event and framework binding of the page half.

The page half runs in the browser and is about 5 KB gzipped. Its functions come from `botscent`, or from `window.botscent` when the page loads the script build. Every framework binding on this page reads the same page verdict. For the fields of a verdict, see [The verdict](/docs/verdict).

## `start`

Starts observation of the document. Returns a function that stops it.

```ts title="Signature"
function start(options?: StartOptions): () => void

type StartOptions = { debug?: boolean | undefined }
```

```ts title="src/main.ts"
import { start } from 'botscent'

const stop = start({ debug: true })
```

### Parameters

| Name            | Type      | Default | Description                                                                  |
| --------------- | --------- | ------- | ---------------------------------------------------------------------------- |
| `options.debug` | `boolean` | `false` | Logs one line per decision to `console.debug`, with the prefix `[botscent]`. |

### Returns

`() => void`. The function removes the library's listeners and observers. The evidence and the page verdict stay.

### Notes

* An import of `botscent` does nothing by itself. Only `botscent/auto` and the script build call `start` on import.
* `start` is idempotent. A second call does nothing more, also when it comes from a second copy of the library on the page.
* One instance runs per page. A second copy of the library uses the running instance.
* Debug output shows each probe status, each reason held, the transport outcome, and each verdict change with its time since navigation start.

## `verdict`

Returns the current page verdict.

```ts title="Signature"
function verdict(): Verdict
```

```ts title="src/analytics.ts"
import { verdict } from 'botscent'

const current = verdict()
// => { type: 'human', reasons: [] }
```

### Returns

`Verdict`, frozen. See [The verdict](/docs/verdict).

### Notes

* An unchanged state returns the same object.
* Before `start`, and during server rendering, the value is `{ type: 'human', reasons: [] }`.
* Detection is monotonic within a document. Once `type` is `'agent'`, it stays `'agent'`.
* The `agent_name` can change when a stronger source arrives.

## `subscribe`

Calls a listener after every change of the page verdict. Returns a function that removes the listener.

```ts title="Signature"
function subscribe(listener: (verdict: Verdict) => void): () => void
```

```ts title="src/analytics.ts"
import { subscribe, verdict } from 'botscent'

function track(name: string) {
  void fetch('/api/events', { method: 'POST', body: JSON.stringify({ name, botscent: verdict() }) })
}

let sent = verdict().type === 'agent'
const stop = subscribe((v) => {
  if (v.type !== 'agent' || sent) return
  sent = true
  track('agent_detected')
})
```

### Parameters

| Name       | Type                         | Default    | Description                                      |
| ---------- | ---------------------------- | ---------- | ------------------------------------------------ |
| `listener` | `(verdict: Verdict) => void` | (required) | Receives the new page verdict after each change. |

### Returns

`() => void`. The function removes the listener.

### Notes

* `subscribe` does not call the listener on subscription. Read the current value with `verdict`.
* Each change also dispatches the [`botscent` event](#botscent-event) on `window`.

## `reportHeaders`

Returns the header that carries the page verdict to the page's own server.

```ts title="Signature"
function reportHeaders(url: string | URL): Record<string, string>
```

```ts title="src/send.ts"
import { reportHeaders } from 'botscent'

const response = await fetch('/api/visit', {
  method: 'POST',
  headers: { 'content-type': 'application/json', ...reportHeaders('/api/visit') },
  body: '{}',
})
```

### Parameters

| Name  | Type            | Default    | Description                                     |
| ----- | --------------- | ---------- | ----------------------------------------------- |
| `url` | `string \| URL` | (required) | The URL of the request that carries the report. |

### Returns

`{ 'Botscent-Report': entry }` when the page verdict is an agent and `url` resolves to the page's own origin. Otherwise `{}`.

### Notes

* The entry holds a version, the agent name when there is one, and the reason ids. It holds no observed value.
* On the server, [`readReport`](/docs/server-api#readreport) parses the header. Its reasons get the `page.` prefix.
* Nothing leaves the page unless code uses `reportHeaders` or [`data-botscent-field`](#data-botscent-field).

> **Warning:** Do not grant access on a page report. Scripts on the page can forge it. Use [`isVerified`](/docs/server-api#isverified) on the request's own verdict.

## `diagnostics`

Returns the status of the page half: start-up, each probe and the transport. Never contains an observed value.

```ts title="Signature"
function diagnostics(): Diagnostics

type Diagnostics = {
  version: string
  started: boolean
  startedAt: number | null
  probes: Record<string, 'pending' | 'ok' | 'unsupported' | 'failed'>
  transport: 'pending' | 'received' | 'absent' | 'unsupported' | 'rejected'
}
```

```ts title="src/debug.ts"
import { diagnostics } from 'botscent'

console.log(diagnostics().transport)
// => 'absent'
```

### Returns

| Field       | Type                           | Description                                                                      |
| ----------- | ------------------------------ | -------------------------------------------------------------------------------- |
| `version`   | `string`                       | The version of the running instance.                                             |
| `started`   | `boolean`                      | `true` while observation runs.                                                   |
| `startedAt` | `number \| null`               | The time of `start` in milliseconds after navigation start, or `null` before it. |
| `probes`    | `Record<string, …>`            | The status of each probe: `pending`, `ok`, `unsupported` or `failed`.            |
| `transport` | `'pending' \| 'received' \| …` | The outcome of the server's `Server-Timing` entry.                               |

### Notes

* `transport` is `pending` before `start`, `received` for an accepted entry, and `absent` when the response had none.
* `transport` is `rejected` for more than one entry, a malformed entry or a stale entry. It is `unsupported` when the browser exposes no `Server-Timing` to the page.
* An `unsupported` or `failed` probe gives no evidence. It never makes a verdict more human.
* The top-level fields and their values are stable. The keys of `probes` name the current probes and can change in any release.
* [`npx botscent check`](/docs/check) reads `diagnostics` in a local Chrome.

## `VERSION`

Holds the version of this copy of the library.

```ts title="Signature"
const VERSION: string
```

```ts title="src/debug.ts"
import { VERSION, diagnostics } from 'botscent'

console.log(VERSION, diagnostics().version)
```

### Notes

* A second copy of the library on a page uses the running instance. In that case, `VERSION` and `diagnostics().version` can differ.

## `botscent.js`

Loads the page half with a script tag, without a bundler.

```html title="index.html"
<script defer src="/botscent.js"></script>
```

### Notes

* The file is `node_modules/botscent/dist/botscent.js`. Serve it from the site's own origin.
* The script sets [`window.botscent`](#windowbotscent), then starts itself.
* The script has no inline code and no `eval`. Under a strict Content Security Policy, `script-src` must allow the script's origin, or the tag must have a nonce with `'strict-dynamic'`.
* `'self'` allows the script when the site serves it from its own origin.
* If the script comes from another origin, the `script` check of `npx botscent check` reports `unknown`.

## `data-debug`

Turns on debug output for the script build.

```html title="index.html"
<script defer src="/botscent.js" data-debug></script>
```

### Notes

* The attribute needs no value. The script reads it from its own tag and calls `start({ debug: true })`.
* Each line goes to `console.debug`, with the prefix `[botscent]`.

## `window.botscent`

Gives the page API to code that does not import modules. Only the script build sets it.

```ts title="Shape"
{
  verdict(): Verdict
  subscribe(listener: (verdict: Verdict) => void): () => void
  reportHeaders(url: string | URL): Record<string, string>
  diagnostics(): Diagnostics
  start(options?: { debug?: boolean }): () => void
  VERSION: string
}
```

```html title="index.html"
<script defer src="/botscent.js"></script>
<pre id="verdict"></pre>
<script>
  const show = () => (document.getElementById('verdict').textContent = JSON.stringify(botscent.verdict()))
  addEventListener('DOMContentLoaded', show)
  addEventListener('botscent', show)
</script>
```

### Notes

* The members are the functions of the same names above.
* The script sets `window.botscent` before it calls `start`. A listener for the first `botscent` event can therefore call these functions.
* Deferred scripts run before `DOMContentLoaded`. A `DOMContentLoaded` handler can therefore call `botscent.verdict`.

## `botscent` event

Fires on `window` after every change of the page verdict. The event's `detail` is the new verdict.

```ts title="Shape"
CustomEvent<Verdict> // type 'botscent', dispatched on window
```

```html title="index.html"
<script>
  addEventListener('botscent', (event) => console.log(event.detail))
  // => { type: 'agent', reasons: ['browser.webdriver-flag'] }
</script>
<script defer src="/botscent.js"></script>
```

### Notes

* The event fires with every entry of the page half, not only with the script build.
* Code that runs before the library loads can listen for the event.
* The event fires only on a change. A page whose verdict stays `human` gets no event.

## `data-botscent-field`

Adds the page report to a form's own POST submission, in a field named `botscent`.

```html title="index.html"
<form method="post" action="/checkout" data-botscent-field>
  <input name="cart" value="1" />
  <button>Submit form</button>
</form>
```

### Notes

* The attribute can be on the form or on an element inside the form.
* The form gets the field only when all of these conditions hold:
  * The `submit` event finished without being canceled.
  * The page verdict is an agent.
  * The effective method is POST, and the effective action is same-origin. A submitter's `formmethod` and `formaction` count.
* A click, Enter and `requestSubmit()` fire a `submit` event. `form.submit()` and `new FormData(form)` fire none, so their data never gets the field.
* A form that a `submit` listener serializes while the event still dispatches does not get the field. Code that sends a form's data itself uses [`reportHeaders`](#reportheaders).
* The library writes nothing to the DOM. On the server, `readReport` parses the field's value.

## `useBotscent` for React

Returns the page verdict in a React component, or the part that a selector picks. Re-renders the component when that value changes.

```ts title="Signature"
// botscent/react
function useBotscent(): Verdict
function useBotscent<T>(select: (verdict: Verdict) => T): T
```

```tsx title="app/verdict.tsx"
'use client'
import { useBotscent } from 'botscent/react'

export function VerdictView() {
  const verdict = useBotscent()
  return <pre>{JSON.stringify(verdict)}</pre>
}
```

### Parameters

| Name     | Type                      | Default | Description                                      |
| -------- | ------------------------- | ------- | ------------------------------------------------ |
| `select` | `(verdict: Verdict) => T` | (none)  | Picks the value to return from the page verdict. |

### Returns

`Verdict`, or the value that `select` returns.

### Notes

* `select` must return a part of the verdict or a primitive. Then an unchanged verdict gives an unchanged value.
* The server render and the first hydration render see `{ type: 'human', reasons: [] }`. Hydration therefore always matches.
* `useBotscent` does not start observation. Start it with `botscent/auto` or [`<Botscent />`](#botscent-for-react).

## `Botscent` for React

Starts observation when it mounts. Renders nothing.

```ts title="Signature"
// botscent/react
function Botscent(props?: { debug?: boolean }): null
```

```tsx title="src/main.tsx"
import { createRoot } from 'react-dom/client'
import { Botscent } from 'botscent/react'
import { App } from './App.tsx'

createRoot(document.getElementById('root')!).render(
  <>
    <Botscent />
    <App />
  </>,
)
```

### Parameters

| Name    | Type      | Default | Description                                              |
| ------- | --------- | ------- | -------------------------------------------------------- |
| `debug` | `boolean` | `false` | Turns on debug output, as `start({ debug: true })` does. |

### Notes

* `<Botscent />` is for React apps without Next.js's `instrumentation-client.ts`.
* Before Next.js 15.3, render `<Botscent />` once in the root layout.

## `Botscent` plugin for Vue

Starts observation in the browser when the app installs the plugin.

```ts title="Signature"
// botscent/vue
const Botscent: Plugin<[StartOptions?]>
```

```ts title="src/main.ts"
import { createApp } from 'vue'
import { Botscent } from 'botscent/vue'
import App from './App.vue'

createApp(App).use(Botscent).mount('#app')
```

### Parameters

| Name            | Type      | Default | Description                                                  |
| --------------- | --------- | ------- | ------------------------------------------------------------ |
| `options.debug` | `boolean` | `false` | Turns on debug output: `app.use(Botscent, { debug: true })`. |

### Notes

* During server rendering, the plugin starts nothing.

## `useBotscent` for Vue

Returns a read-only ref that follows the page verdict, or the part that a selector picks.

```ts title="Signature"
// botscent/vue
function useBotscent(): Readonly<Ref<Verdict>>
function useBotscent<T>(select: (verdict: Verdict) => T): Readonly<Ref<T>>
```

```vue title="src/App.vue"
<script setup lang="ts">
import { useBotscent } from 'botscent/vue'

const verdict = useBotscent()
</script>

<template>
  <pre>{{ JSON.stringify(verdict) }}</pre>
</template>
```

### Parameters

| Name     | Type                      | Default | Description                                       |
| -------- | ------------------------- | ------- | ------------------------------------------------- |
| `select` | `(verdict: Verdict) => T` | (none)  | Picks the value of the ref from the page verdict. |

### Returns

`Readonly<Ref<Verdict>>`, or `Readonly<Ref<T>>` with `select`.

### Notes

* In a component, the ref starts at `{ type: 'human', reasons: [] }` and follows the page verdict after mount. Hydration therefore matches.
* `useBotscent` does not start observation. The [`Botscent` plugin](#botscent-plugin-for-vue) starts it.

## `botscent` store for Svelte

Holds the page verdict as a readable Svelte store.

```ts title="Signature"
// botscent/svelte
const botscent: Readable<Verdict>
```

```ts title="src/hooks.client.ts"
import 'botscent/auto'
```

```svelte title="src/routes/+page.svelte"
<script lang="ts">
  import { botscent } from 'botscent/svelte'
</script>

<pre>{JSON.stringify($botscent)}</pre>
```

### Notes

* The store does not start observation. In SvelteKit, import `botscent/auto` once, in `src/hooks.client.ts`.
* `$botscent` renders `{ type: 'human', reasons: [] }` on the server and follows the page verdict after hydration.

## `botscent/nuxt` module

Adds the page half to a Nuxt app. A client plugin calls `start`, and `useBotscent` is auto-imported.

```ts title="Signature"
// botscent/nuxt
type BotscentNuxtOptions = { debug?: boolean }
```

```ts title="nuxt.config.ts"
export default defineNuxtConfig({
  modules: ['botscent/nuxt'],
})
```

```vue title="app/app.vue"
<script setup lang="ts">
const verdict = useBotscent()
</script>

<template>
  <pre>{{ JSON.stringify(verdict) }}</pre>
</template>
```

### Parameters

| Name            | Type      | Default | Description                                           |
| --------------- | --------- | ------- | ----------------------------------------------------- |
| `options.debug` | `boolean` | `false` | Passes `debug: true` to `start` in the client plugin. |

### Notes

* The auto-imported `useBotscent` is [`useBotscent` from `botscent/vue`](#usebotscent-for-vue).
* The module has no server adapter. A server route calls [`inspect`](/docs/server-api#inspect) from `botscent/server` with `event.node.req`.
