Skip to content
Botscent

Server API (Python)

Every function and adapter of the Python server half, with its settings.

The Python package is the server half for Python 3.11 or later. Its only dependency is cryptography. The functions come from botscent, and the adapters come from botscent.asgi, botscent.django and botscent.flask. The verdicts are identical in JSON to those of the TypeScript server half.

botscent.inspect

Returns the request's own verdict. Reads headers only, and makes no network request.

Signature
def inspect(request, *, cf=None, now=None) -> Verdict: ...
app.py
import botscent
from fastapi import FastAPI, Request

app = FastAPI()


@app.post("/checkout")
async def checkout(request: Request):
    verdict = botscent.inspect(request)
    # => {"type": "agent", "agent_name": "curl", "reasons": ["ua.declared-agent-token"]}
    return verdict

Parameters

NameTypeDefaultDescription
requestany(required)A Django, Starlette, Flask or Werkzeug request, an object with a headers mapping, or a mapping of headers.
cfMappingNoneCloudflare's request.cf as a mapping, read for its verified-bot field.
nowfloatNoneThe request time in milliseconds since the epoch, for tests and replays.

Returns

Verdict, a dict with the keys type, agent_name and reasons. The agent_name key is present only when there is a name. See The verdict.

Notes

  • inspect is synchronous. The TypeScript inspect returns a promise.
  • The method comes from request.method when it is present.
  • inspect reads no body and no page report. It never raises: at worst, it returns {"type": "human", "reasons": []}.
  • inspect verifies signatures against signer keys bundled in the release. It makes no network request.
  • Debug output goes to the botscent logger at DEBUG, one line per decision.

Examples

Turn on debug output

Set the level of the botscent logger:

settings.py
import logging

logging.basicConfig()
logging.getLogger("botscent").setLevel(logging.DEBUG)

botscent.read_report

Parses a page report. Returns a verdict whose reasons carry the page. prefix, or None.

Signature
def read_report(value: str | Mapping[str, object] | None) -> Verdict | None: ...
app.py
import botscent
from fastapi import FastAPI, Request
from botscent.asgi import BotscentMiddleware

app = FastAPI()
app.add_middleware(BotscentMiddleware)


@app.post("/checkout")
async def checkout(request: Request):
    own = request.state.botscent
    report = botscent.read_report(request.headers.get("botscent-report"))
    return {"request": own, "report": report}

Parameters

NameTypeDefaultDescription
valueany(required)The Botscent-Report header, the form's botscent field, or a verdict mapping.

Returns

Verdict | None. The value is None for anything malformed, longer than 256 bytes, or of an unknown major version.

Notes

  • Every reason of a parsed report gets the page. prefix. A report therefore never passes is_verified.
  • A verdict mapping must follow the same grammar as the wire string.
  • For a form with data-botscent-field, read the field: botscent.read_report(fields.get("botscent")).

Warning: Do not grant access on a page report. Scripts on the page can forge it. Use is_verified on the verdict from inspect.

botscent.combine

Adds a page report's evidence to the request's own verdict.

Signature
def combine(request: Verdict, report: Verdict | None) -> Verdict: ...
app.py
import botscent
from fastapi import FastAPI, Request
from botscent.asgi import BotscentMiddleware

app = FastAPI()
app.add_middleware(BotscentMiddleware)


@app.post("/checkout")
async def checkout(request: Request):
    own = request.state.botscent
    report = botscent.read_report(request.headers.get("botscent-report"))
    return {"request": own, "report": report, "combined": botscent.combine(own, report)}

Parameters

NameTypeDefaultDescription
requestVerdict(required)The request's own verdict from inspect.
reportVerdict | None(required)The page report from read_report.

Returns

Verdict. The combined verdict is an agent when either verdict is an agent. Its reasons are the request's reasons, then the report's reasons.

Notes

  • The agent_name is the request's name when it has one. Otherwise it is the report's name.
  • A missing or empty report returns the request verdict unchanged.
  • A combined verdict is for measurement and for changes to the interface. It is never for access.

botscent.is_verified

Returns True when the request itself was verified. With a name, returns True only when a verified signature names that agent.

Signature
def is_verified(verdict: Verdict, name=None) -> bool: ...
app.py
import botscent
from flask import Flask, abort, g
from botscent.flask import Botscent

app = Flask(__name__)
Botscent(app)


@app.post("/checkout")
def checkout():
    if not botscent.is_verified(g.botscent, "chatgpt"):
        abort(403)
    return {"ok": True}

Parameters

NameTypeDefaultDescription
verdictVerdict(required)The request's own verdict from inspect.
namestrNoneThe agent name that a verified signature must give.

Returns

bool. Without name, the value is True when reasons contains signer.web-bot-auth.verified or signer.edge-verified-bot without a prefix. With name, it is True when reasons contains signer.web-bot-auth.verified, agent_name is name, and no reason has the page. prefix.

Notes

  • Only inspect produces the two verified reasons.
  • The platform's verified-bot field never satisfies a check with a name. The field says that some bot was verified, not which bot.
  • To let one agent through, use is_verified(verdict, "chatgpt"). A check of agent_name after is_verified(verdict) can pair a verified bot with a name that it only declared.
  • Signer keys are pinned in each release. A key that a signer removes still verifies until the installation upgrades. See Versions and stability.

Warning: Do not use agent_name alone to allow access. Anyone can send the headers that produce a name. Use is_verified(verdict, name).

botscent.VERSION

Holds the version of the package. The npm package and the Python package of one release have the same version.

Signature
VERSION: str

botscent.Verdict

Gives the type of a verdict, for type checkers.

Signature
class Verdict(TypedDict):
    type: Literal["agent", "human"]
    agent_name: NotRequired[str]
    reasons: list[str]

What every adapter does

Every adapter below runs inspect for each request and keeps the request's own verdict on the request. An adapter also does these things:

  • It reads no body, and passes request bodies and streaming responses through unchanged.
  • It removes every inherited botscent entry from Server-Timing, and keeps all other entries.
  • If its transport is on, it adds the botscent entry and Cache-Control: no-store to an agent's document navigation.
  • On its own failure, it passes the response on unchanged.

The transport is off by default. An origin cannot see whether a CDN in front of it stores HTML. Turn it on only when no shared cache stores the site's HTML. See From the server to the page.

botscent.asgi.BotscentMiddleware

Runs the server half as ASGI middleware, for FastAPI, Starlette and other ASGI frameworks.

Signature
class BotscentMiddleware:
    def __init__(self, app, *, transport: bool = False): ...
app.py
from fastapi import FastAPI, Request
from botscent.asgi import BotscentMiddleware

app = FastAPI()
app.add_middleware(BotscentMiddleware)


@app.get("/verdict")
async def verdict(request: Request):
    return request.state.botscent

Parameters

NameTypeDefaultDescription
appASGI app(required)The application that the middleware wraps.
transportboolFalseSends the entry to the page when True: app.add_middleware(BotscentMiddleware, transport=True).

Notes

  • The request's verdict is request.state.botscent, which is scope["state"]["botscent"].
  • The middleware handles HTTP requests only. Other scopes pass through unchanged.

botscent.django.BotscentMiddleware

Runs the server half as Django middleware.

Signature
class BotscentMiddleware:
    def __init__(self, get_response): ...
settings.py
MIDDLEWARE = ["botscent.django.BotscentMiddleware"]

Settings

NameTypeDefaultDescription
BOTSCENT_TRANSPORTboolFalseSends the entry to the page when True.

Notes

  • The request's verdict is request.botscent.
  • The middleware works in sync and async middleware stacks.
  • Add "botscent.django.BotscentMiddleware" next to the existing entries in MIDDLEWARE.

botscent.flask.Botscent

Runs the server half as a Flask extension.

Signature
class Botscent:
    def __init__(self, app=None, *, transport: bool = False): ...
    def init_app(self, app): ...
app.py
from flask import Flask, g, jsonify
from botscent.flask import Botscent

app = Flask(__name__)
Botscent(app)


@app.route("/verdict")
def verdict():
    return jsonify(verdict=g.botscent)

Parameters

NameTypeDefaultDescription
appFlaskNoneThe application. Without it, call init_app(app) later.
transportboolFalseSends the entry to the page when True: Botscent(app, transport=True).

Notes

  • The request's verdict is flask.g.botscent in every view.