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.
def inspect(request, *, cf=None, now=None) -> Verdict: ...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 verdictParameters
| Name | Type | Default | Description |
|---|---|---|---|
request | any | (required) | A Django, Starlette, Flask or Werkzeug request, an object with a headers mapping, or a mapping of headers. |
cf | Mapping | None | Cloudflare's request.cf as a mapping, read for its verified-bot field. |
now | float | None | The 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
inspectis synchronous. The TypeScriptinspectreturns a promise.- The method comes from
request.methodwhen it is present. inspectreads no body and no page report. It never raises: at worst, it returns{"type": "human", "reasons": []}.inspectverifies signatures against signer keys bundled in the release. It makes no network request.- Debug output goes to the
botscentlogger atDEBUG, one line per decision.
Examples
Turn on debug output
Set the level of the botscent logger:
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.
def read_report(value: str | Mapping[str, object] | None) -> Verdict | None: ...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
| Name | Type | Default | Description |
|---|---|---|---|
value | any | (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 passesis_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_verifiedon the verdict frominspect.
botscent.combine
Adds a page report's evidence to the request's own verdict.
def combine(request: Verdict, report: Verdict | None) -> Verdict: ...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
| Name | Type | Default | Description |
|---|---|---|---|
request | Verdict | (required) | The request's own verdict from inspect. |
report | Verdict | 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_nameis 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.
def is_verified(verdict: Verdict, name=None) -> bool: ...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
| Name | Type | Default | Description |
|---|---|---|---|
verdict | Verdict | (required) | The request's own verdict from inspect. |
name | str | None | The 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
inspectproduces 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 ofagent_nameafteris_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_namealone to allow access. Anyone can send the headers that produce a name. Useis_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.
VERSION: strbotscent.Verdict
Gives the type of a verdict, for type checkers.
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
botscententry fromServer-Timing, and keeps all other entries. - If its transport is on, it adds the
botscententry andCache-Control: no-storeto 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.
class BotscentMiddleware:
def __init__(self, app, *, transport: bool = False): ...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.botscentParameters
| Name | Type | Default | Description |
|---|---|---|---|
app | ASGI app | (required) | The application that the middleware wraps. |
transport | bool | False | Sends the entry to the page when True: app.add_middleware(BotscentMiddleware, transport=True). |
Notes
- The request's verdict is
request.state.botscent, which isscope["state"]["botscent"]. - The middleware handles HTTP requests only. Other scopes pass through unchanged.
botscent.django.BotscentMiddleware
Runs the server half as Django middleware.
class BotscentMiddleware:
def __init__(self, get_response): ...MIDDLEWARE = ["botscent.django.BotscentMiddleware"]Settings
| Name | Type | Default | Description |
|---|---|---|---|
BOTSCENT_TRANSPORT | bool | False | Sends 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 inMIDDLEWARE.
botscent.flask.Botscent
Runs the server half as a Flask extension.
class Botscent:
def __init__(self, app=None, *, transport: bool = False): ...
def init_app(self, app): ...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
| Name | Type | Default | Description |
|---|---|---|---|
app | Flask | None | The application. Without it, call init_app(app) later. |
transport | bool | False | Sends the entry to the page when True: Botscent(app, transport=True). |
Notes
- The request's verdict is
flask.g.botscentin every view.