Skip to content
Botscent

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.

Signature
function start(options?: StartOptions): () => void

type StartOptions = { debug?: boolean | undefined }
src/main.ts
import { start } from 'botscent'

const stop = start({ debug: true })

Parameters

NameTypeDefaultDescription
options.debugbooleanfalseLogs 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.

Signature
function verdict(): Verdict
src/analytics.ts
import { 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 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.

Signature
function subscribe(listener: (verdict: Verdict) => void): () => void
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

NameTypeDefaultDescription
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 on window.

reportHeaders

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

Signature
function reportHeaders(url: string | URL): Record<string, string>
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

NameTypeDefaultDescription
urlstring | 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 parses the header. Its reasons get the page. prefix.
  • Nothing leaves the page unless code uses reportHeaders or data-botscent-field.

Warning: Do not grant access on a page report. Scripts on the page can forge it. Use 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.

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'
}
src/debug.ts
import { diagnostics } from 'botscent'

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

Returns

FieldTypeDescription
versionstringThe version of the running instance.
startedbooleantrue while observation runs.
startedAtnumber | nullThe time of start in milliseconds after navigation start, or null before it.
probesRecord<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 reads diagnostics in a local Chrome.

VERSION

Holds the version of this copy of the library.

Signature
const VERSION: string
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.

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, 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.

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.

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
}
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.

Shape
CustomEvent<Verdict> // type 'botscent', dispatched on window
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.

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.
  • 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.

Signature
// botscent/react
function useBotscent(): Verdict
function useBotscent<T>(select: (verdict: Verdict) => T): T
app/verdict.tsx
'use client'
import { useBotscent } from 'botscent/react'

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

Parameters

NameTypeDefaultDescription
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

Starts observation when it mounts. Renders nothing.

Signature
// botscent/react
function Botscent(props?: { debug?: boolean }): null
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

NameTypeDefaultDescription
debugbooleanfalseTurns 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.

Signature
// botscent/vue
const Botscent: Plugin<[StartOptions?]>
src/main.ts
import { createApp } from 'vue'
import { Botscent } from 'botscent/vue'
import App from './App.vue'

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

Parameters

NameTypeDefaultDescription
options.debugbooleanfalseTurns 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.

Signature
// botscent/vue
function useBotscent(): Readonly<Ref<Verdict>>
function useBotscent<T>(select: (verdict: Verdict) => T): Readonly<Ref<T>>
src/App.vue
<script setup lang="ts">
import { useBotscent } from 'botscent/vue'

const verdict = useBotscent()
</script>

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

Parameters

NameTypeDefaultDescription
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 starts it.

botscent store for Svelte

Holds the page verdict as a readable Svelte store.

Signature
// botscent/svelte
const botscent: Readable<Verdict>
src/hooks.client.ts
import 'botscent/auto'
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.

Signature
// botscent/nuxt
type BotscentNuxtOptions = { debug?: boolean }
nuxt.config.ts
export default defineNuxtConfig({
  modules: ['botscent/nuxt'],
})
app/app.vue
<script setup lang="ts">
const verdict = useBotscent()
</script>

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

Parameters

NameTypeDefaultDescription
options.debugbooleanfalsePasses debug: true to start in the client plugin.

Notes