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.
start
Starts observation of the document. Returns a function that stops it.
function start(options?: StartOptions): () => void
type StartOptions = { debug?: boolean | undefined }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
botscentdoes nothing by itself. Onlybotscent/autoand the script build callstarton import. startis 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.
function verdict(): Verdictimport { verdict } from 'botscent'
const current = verdict()
// => { type: 'human', reasons: [] }Returns
Verdict, frozen. See The 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
typeis'agent', it stays'agent'. - The
agent_namecan change when a stronger source arrives.
subscribe
Calls a listener after every change of the page verdict. Returns a function that removes the listener.
function subscribe(listener: (verdict: Verdict) => void): () => voidimport { 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
subscribedoes not call the listener on subscription. Read the current value withverdict.- Each change also dispatches the
botscentevent onwindow.
reportHeaders
Returns the header that carries the page verdict to the page's own server.
function reportHeaders(url: string | URL): Record<string, string>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,
readReportparses the header. Its reasons get thepage.prefix. - Nothing leaves the page unless code uses
reportHeadersordata-botscent-field.
Warning: Do not grant access on a page report. Scripts on the page can forge it. Use
isVerifiedon 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.
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'
}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
transportispendingbeforestart,receivedfor an accepted entry, andabsentwhen the response had none.transportisrejectedfor more than one entry, a malformed entry or a stale entry. It isunsupportedwhen the browser exposes noServer-Timingto the page.- An
unsupportedorfailedprobe gives no evidence. It never makes a verdict more human. - The top-level fields and their values are stable. The keys of
probesname the current probes and can change in any release. npx botscent checkreadsdiagnosticsin a local Chrome.
VERSION
Holds the version of this copy of the library.
const VERSION: stringimport { 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,
VERSIONanddiagnostics().versioncan differ.
botscent.js
Loads the page half with a script tag, without a bundler.
<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, then starts itself. - The script has no inline code and no
eval. Under a strict Content Security Policy,script-srcmust 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
scriptcheck ofnpx botscent checkreportsunknown.
data-debug
Turns on debug output for the script build.
<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.
{
verdict(): Verdict
subscribe(listener: (verdict: Verdict) => void): () => void
reportHeaders(url: string | URL): Record<string, string>
diagnostics(): Diagnostics
start(options?: { debug?: boolean }): () => void
VERSION: string
}<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.botscentbefore it callsstart. A listener for the firstbotscentevent can therefore call these functions. - Deferred scripts run before
DOMContentLoaded. ADOMContentLoadedhandler can therefore callbotscent.verdict.
botscent event
Fires on window after every change of the page verdict. The event's detail is the new verdict.
CustomEvent<Verdict> // type 'botscent', dispatched on window<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
humangets no event.
data-botscent-field
Adds the page report to a form's own POST submission, in a field named botscent.
<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
submitevent 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
formmethodandformactioncount.
- The
- A click, Enter and
requestSubmit()fire asubmitevent.form.submit()andnew FormData(form)fire none, so their data never gets the field. - A form that a
submitlistener serializes while the event still dispatches does not get the field. Code that sends a form's data itself usesreportHeaders. - The library writes nothing to the DOM. On the server,
readReportparses 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.
// botscent/react
function useBotscent(): Verdict
function useBotscent<T>(select: (verdict: Verdict) => T): T'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
selectmust 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. useBotscentdoes not start observation. Start it withbotscent/autoor<Botscent />.
Botscent for React
Starts observation when it mounts. Renders nothing.
// botscent/react
function Botscent(props?: { debug?: boolean }): nullimport { 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'sinstrumentation-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.
// botscent/vue
const Botscent: Plugin<[StartOptions?]>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.
// botscent/vue
function useBotscent(): Readonly<Ref<Verdict>>
function useBotscent<T>(select: (verdict: Verdict) => T): Readonly<Ref<T>><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. useBotscentdoes not start observation. TheBotscentplugin starts it.
botscent store for Svelte
Holds the page verdict as a readable Svelte store.
// botscent/svelte
const botscent: Readable<Verdict>import 'botscent/auto'<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/autoonce, insrc/hooks.client.ts. $botscentrenders{ 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.
// botscent/nuxt
type BotscentNuxtOptions = { debug?: boolean }export default defineNuxtConfig({
modules: ['botscent/nuxt'],
})<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
useBotscentisuseBotscentfrombotscent/vue. - The module has no server adapter. A server route calls
inspectfrombotscent/serverwithevent.node.req.