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

# 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](/docs/server-api).

## `botscent.inspect`

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

```python title="Signature"
def inspect(request, *, cf=None, now=None) -> Verdict: ...
```

```python title="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

| 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](/docs/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:

```python title="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`.

```python title="Signature"
def read_report(value: str | Mapping[str, object] | None) -> Verdict | None: ...
```

```python title="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

| 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 passes `is_verified`.
* A verdict mapping must follow the same grammar as the wire string.
* For a form with [`data-botscent-field`](/docs/page-api#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`](#botscentis_verified) on the verdict from `inspect`.

## `botscent.combine`

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

```python title="Signature"
def combine(request: Verdict, report: Verdict | None) -> Verdict: ...
```

```python title="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

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

```python title="Signature"
def is_verified(verdict: Verdict, name=None) -> bool: ...
```

```python title="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

| 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 `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](/docs/versions).

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

```python title="Signature"
VERSION: str
```

## `botscent.Verdict`

Gives the type of a verdict, for type checkers.

```python title="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](/docs/server-to-page).

## `botscent.asgi.BotscentMiddleware`

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

```python title="Signature"
class BotscentMiddleware:
    def __init__(self, app, *, transport: bool = False): ...
```

```python title="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

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

```python title="Signature"
class BotscentMiddleware:
    def __init__(self, get_response): ...
```

```python title="settings.py"
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 in `MIDDLEWARE`.

## `botscent.flask.Botscent`

Runs the server half as a Flask extension.

```python title="Signature"
class Botscent:
    def __init__(self, app=None, *, transport: bool = False): ...
    def init_app(self, app): ...
```

```python title="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

| 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.botscent` in every view.
