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

# From the page to the server

How your code sends the page verdict to your server, and how the server keeps it apart.

A *page report* is the page verdict, sent by your code to your own server. The page half sends nothing on its own. Your code chooses a carrier, and the server reads the report with [`readReport`](/docs/server-api#readreport).

```ts title="app/checkout/client.ts"
import { reportHeaders } from 'botscent'

export async function checkout() {
  return fetch('/api/checkout', { method: 'POST', headers: { ...reportHeaders('/api/checkout') }, body: 'cart=1' })
}
```

## The carriers

Botscent has 3 carriers. Each carrier reads the page verdict at the moment your code sends it.

| Carrier               | Use it for                             | What the server gets       |
| --------------------- | -------------------------------------- | -------------------------- |
| `reportHeaders(url)`  | A `fetch` that your code sends         | A `Botscent-Report` header |
| `data-botscent-field` | A POST form that the browser submits   | A `botscent` form field    |
| An argument           | A server function that your code calls | The verdict object         |

### A header for `fetch`

[`reportHeaders`](/docs/page-api#reportheaders) returns `{ 'Botscent-Report': <entry> }` when two things are true. The page verdict is an agent, and `url` is on the page's own origin. In every other case, it returns `{}`. So a person's requests do not change, and no report goes to another origin.

Spread the headers into the request, as in the example above. With the script tag, use `window.botscent.reportHeaders`.

### A field for forms

Add [`data-botscent-field`](/docs/page-api#data-botscent-field) to a form, or to an element inside the form:

```html title="checkout.html"
<form method="post" action="/checkout" data-botscent-field>
  <input name="cart" value="1" />
  <button>Check out</button>
</form>
```

The form gets a `botscent` field only on its own submission: a click, Enter, or `requestSubmit`. All of these must also be true:

* No listener canceled the `submit` event.
* The page verdict is an agent.
* The method is `POST`. A submit button's `formmethod` counts.
* The action is on the same origin. A submit button's `formaction` counts.

A call to `form.submit` and a `new FormData(form)` never get the field, because their data can go anywhere. A form that a `submit` listener reads while the event is still in progress does not get the field either. If your code sends a form's data with `fetch`, use `reportHeaders(url)`. Botscent writes nothing to the DOM.

### An argument

Pass the object that `verdict` returns to a server function that your code calls. On the server, pass the object to `readReport`. `readReport` checks the object against the same format as the header.

## On the server

Read the report, and keep it apart from the request's own verdict:

```ts title="app/api/checkout/route.ts"
import { combine, inspect, readReport } from 'botscent/server'

export async function POST(request: Request) {
  const own = await inspect(request)
  const report = readReport(request.headers.get('botscent-report'))
  return Response.json({ request: own, report, combined: combine(own, report) })
}
```

The response holds 3 verdicts: what the request declared, what the page reported, and the two joined.

`readReport` returns `null` for a value that is malformed, longer than 256 bytes, or of an unknown major version. Each reason of a parsed report gets the `page.` prefix, for example `page.claude.marker.active`. So a report never passes `isVerified`.

For a form, your code parses the body and passes the `botscent` field to `readReport`. Botscent never reads a request body. In Python, use `botscent.read_report` and `botscent.combine`.

## How `combine` joins the two

`combine(own, report)` adds the report's evidence to the request's own verdict:

* The joined verdict is an agent when either verdict is an agent.
* Its reasons are the request's reasons, then the report's reasons.
* Its name is the request's name. If the request has no name, it is the report's name.
* A missing or empty report returns the request's own verdict unchanged.

Use the joined verdict to measure agent traffic and to adapt what you show.

> **Warning:** Do not use a page report or a joined verdict to allow access. Scripts on the page can forge a report. Use `isVerified` on the request's own verdict.

## What a report does not carry

* It carries no observed value. It holds only the format version, the agent name and the reason ids.
* It is not sent for a person. `reportHeaders` returns `{}`, and the form gets no field.
* It does not go to another origin.
* It does not say who performed the action that sent it.

When a report leaves the device, your site collects it. See [Privacy](/docs/privacy).

## Related

* [The trust model](/docs/trust-model): why a page report is not fit to decide access.
* [Page API](/docs/page-api#reportheaders): the signature of `reportHeaders` and the other page functions.
* [Server API (TypeScript)](/docs/server-api#readreport): `readReport`, `combine` and `isVerified`.
