# Checkend docs, v0.1 (unreleased)
> Checkend is open-source, self-hosted web analytics for coding agents. Agents read data and record changes through MCP, the ce CLI and a REST API. Checkend measures the outcome of each change (14 days before and after, with a significance test) and keeps it as memory. It has no AI inside. Analytics is the first module; errors, uptime and logs are coming. Nothing is released yet: install commands use placeholders such as [INSTALL_URL].
---
# Checkend documentation
> Install Checkend, add a site, connect your agent, and measure each change that your agent makes.
Checkend is open-source web analytics that your agent can read, act on, and learn from. It runs on
your server: one binary, one data folder. It has no AI inside. You bring the agent.
> **Note: Not released yet**
> Checkend is not released yet. The install commands in these docs use placeholders, for example
> `[INSTALL_URL]`.
## Start here
- [Quickstart](/docs/start/quickstart.md): From an empty server to the first answer of your agent.
- [Install](/docs/start/install.md): The installer, Docker, automatic TLS, a reverse proxy, an offline install.
- [Add a site](/docs/start/add-a-site.md): The install prompt, the script tag and the install check.
- [Connect your agent](/docs/start/connect-your-agent.md): MCP with OAuth, API tokens, ce login and ce mcp.
## Understand Checkend
- [The closed loop](/docs/concepts/closed-loop.md): Findings, changes, measured outcomes and the brief.
- [Privacy modes](/docs/concepts/privacy.md): Strict, standard and extended: what Checkend keeps.
## Reference
- [CLI](/docs/reference/cli.md): Each ce command, with its flags and examples.
- [MCP tools](/docs/reference/mcp-tools.md): The tools and prompts of the MCP server.
- [REST API](/docs/reference/rest-api.md): Each endpoint, its access and its fields.
## For agents
The docs are also plain text for agents:
- Each page has a Markdown form at its address plus `.md`, for example
[/docs/start/install.md](/docs/start/install.md).
- [/llms.txt](/llms.txt) is the index of all pages. [/llms-full.txt](/llms-full.txt) has all
pages in one file.
- A task page has a **Copy as prompt** button. Paste the prompt into your coding agent.
---
# Quickstart
> Install Checkend, make the first admin, add a site, connect an agent and ask a first question.
> **Note**
> Checkend is not released yet. The install commands use placeholders.
This page shows the shortest path from an empty server to a first answer from your agent. Each step links to a full page with all options.
## What you need
- A Linux server (amd64 or arm64) with systemd and glibc 2.36 or newer, for example Debian 12 or Ubuntu 24.04. Docker also works.
- A DNS name, for example `stats.example.com`, that points at the server.
- Open ports 80 and 443 on the server.
- A website where you can add a script tag, for example `example.com`.
## 1. Install the server
Run the installer as root. Give your domain for automatic TLS:
```sh
curl -fsSL [INSTALL_URL] | sudo sh -s -- --domain stats.example.com
```
The installer puts `checkend` and `ce` in `/usr/local/bin`, makes a systemd service and starts it. At the end, it prints a one-time setup URL.
Full page: [Install Checkend](/docs/start/install.md).
## 2. Make the first admin
1. Open the setup URL in a browser. It looks like `https://stats.example.com/setup#cesetup_...`.
2. Type your email address.
3. Select **Create a passkey**.
You are now the first admin. The setup URL then does not work again.
Full page: [First boot](/docs/start/first-boot.md).
## 3. Add a site
The setup page asks for the first site. You can also add a site with `ce`. Log in first, then add the site:
```sh
ce login https://stats.example.com
ce sites add example.com
```
Checkend shows the script tag and an install prompt. Copy the install prompt into your coding agent. The agent adds the tag to your site and checks that events arrive. Or add the script tag yourself:
```html
```
Then check the install:
```sh
ce sites verify example.com
```
Full page: [Add a site](/docs/start/add-a-site.md).
## 4. Connect your agent
Give your MCP client the URL of the MCP server:
```
https://stats.example.com/mcp
```
For example, in Claude Code:
```sh
claude mcp add --transport http checkend https://stats.example.com/mcp
```
The client opens a consent page in your browser. Sign in with your passkey, check the access, and approve. The default access is read only.
Full page: [Connect your agent](/docs/start/connect-your-agent.md).
## 5. Ask a first question
Ask your agent a question in plain words, for example:
> What changed on example.com this week, and what should I fix first?
The agent calls the `get_brief` tool. The brief has the top changes, the open findings, the recent changes and the outcomes of earlier changes. Each top change has a significance test, and the brief shows the data date of each site.
You can read the same brief in a terminal:
```sh
ce brief example.com
```
## Next steps
- Learn how Checkend measures each change: [The closed loop](/docs/concepts/closed-loop.md).
- Add goals and revenue: [Goals and revenue](/docs/guides/goals-and-revenue.md).
- Select the privacy mode of each site: [Privacy](/docs/concepts/privacy.md).
---
# Install Checkend
> Install one Checkend server with the installer or Docker, with automatic TLS or behind a reverse proxy.
> **Note**
> Checkend is not released yet. The install commands use placeholders.
Checkend is one binary and one data folder on one server. This page installs the server with the installer or with Docker. It also shows automatic TLS, a reverse proxy and an offline install.
## Requirements
- **Operating system:** Linux on amd64 or arm64, with systemd and glibc 2.36 or newer. Debian 12 and Ubuntu 24.04 or newer work. An older glibc (for example Ubuntu 22.04) does not work. Docker works on every system.
- **DNS:** a name (A and AAAA records) that points at the server, for example `stats.example.com`. Automatic TLS and passkeys need it. Without a domain, see [Without a domain](#without-a-domain).
- **Ports:** 80 and 443 open, for automatic TLS.
- **Disk:** about 1 GB for the program files, plus the data. See [Disks, folders and memory](#disks-folders-and-memory).
### First downloads
At the first start, the server downloads two sets of files into the cache folder:
| File | Size | Where it goes |
|---|---|---|
| The DuckDB extensions `ducklake` and `sqlite` | About 85 MB | `/duckdb-extensions`, or the folder of `--duckdb-extensions` |
| The DB-IP City Lite file (IP location) | About 60 MB compressed, 127 MB on the disk | `/geo` |
The cache folder is `/cache` by default. See [Disks, folders and memory](#disks-folders-and-memory).
The GeoIP download runs in the background. For a server without network access, see [Offline install](#offline-install).
## The installer
1. Run the installer as root. Give your domain for automatic TLS:
```sh
curl -fsSL [INSTALL_URL] | sudo sh -s -- --domain stats.example.com
```
2. Read the setup URL at the end of the output. It looks like `https://stats.example.com/setup#cesetup_...`.
3. Open the setup URL in a browser, and make the first admin. See [First boot](/docs/start/first-boot.md).
> **Caution**
> Keep the setup URL private until you use it. Anybody with the URL can make the first admin. A restart of the server makes a new URL, and the old URL does not work after the restart.
### What the installer does
| Step | What it does |
|---|---|
| Platform | Finds the OS and the CPU. Linux amd64 and arm64 get the server. macOS gets the `ce` client only. |
| Download | Gets the release archive and `checksums.txt`. |
| Signature | Checks the minisign signature of `checksums.txt` with the release key in the script. It uses `minisign`, or OpenSSL 3 when `minisign` is not installed. A missing or bad signature stops the install. |
| Checksum | Compares the SHA-256 of the archive with `checksums.txt`. |
| Binaries | Puts `checkend` and `ce` in `/usr/local/bin`. |
| User | Makes the system user `checkend` without a login shell. |
| Data | Makes `/var/lib/checkend` (mode 0750, owner `checkend`). |
| Flags | Writes `/etc/checkend/checkend.env` one time (mode 0640) with `CHECKEND_FLAGS`. |
| Service | Writes `/etc/systemd/system/checkend.service`, then enables and starts it. |
| Setup URL | Prints the setup URL. |
The service runs as the user `checkend` with a hardened systemd unit. It can open ports 80 and 443 without root.
### Installer flags
| Flag | Meaning |
|---|---|
| `--domain ` | First install only: automatic TLS for this name (ports 80 and 443). |
| `--addr ` | First install only, without `--domain`: the listen address. The default is `0.0.0.0:8080`. |
| `--data-dir ` | The data folder on a mounted volume, for example `/mnt/HC_Volume_1234/checkend`. A later run keeps it. See [Disks, folders and memory](#disks-folders-and-memory). |
| `--version ` | Install this version. The default is the newest release. |
| `--ce-only` | Install only the `ce` client. |
| `--no-start` | Install, but do not start or restart the server. |
| `--uninstall` | Remove the binaries and the service. Keep the data and the flags file. |
| `--purge` | With `--uninstall`: also delete the data folder, `/var/cache/checkend`, `/etc/checkend` and the user `checkend`. |
| `--unsigned` | Do not check the signature. The checksum check stays. |
> **Danger**
> Do not use `--unsigned` for a release that you downloaded. The signature is the only proof that the files come from the Checkend project.
The installer reads these environment variables:
| Variable | Meaning |
|---|---|
| `CHECKEND_RELEASE_URL` | The URL of the release folder. |
| `CHECKEND_MINISIGN_PUBKEY` | Another public key, for a release that you signed yourself. |
| `CHECKEND_SIGNATURE_TOOL` | `minisign` or `openssl`. |
| `CHECKEND_SETUP_WAIT` | The seconds to wait for the setup URL. The default is 60. |
The signature check needs `minisign` or OpenSSL 3. Debian 12 and Ubuntu 24.04 have OpenSSL 3. The `openssl` of macOS is LibreSSL and does not work: install `minisign` with Homebrew.
### Change the server flags
The installer writes the flags file one time and never changes it again.
1. Edit `CHECKEND_FLAGS` in `/etc/checkend/checkend.env`.
2. Restart the service:
```sh
sudo systemctl restart checkend
```
### Without a domain
Without `--domain`, the server listens on `0.0.0.0:8080` with plain HTTP. The setup URL then starts with `http://localhost:8080`. Passkeys need HTTPS on a domain name, or `localhost`.
1. Open an SSH tunnel from your computer:
```sh
ssh -L 8080:localhost:8080 you@server
```
2. Open the setup URL on your computer. The passkey then belongs to `localhost`.
On a private network with an IP address only, the setup page can make the admin with an API token. See [First boot](/docs/start/first-boot.md).
> **Caution**
> Do not open plain HTTP to the internet. The session cookie and the tokens then go over the network without encryption. Use `--domain`, or a reverse proxy with TLS.
## Disks, folders and memory
Checkend uses two folders. You select them one time. Checkend checks your choice, but it never moves data by itself.
| Folder | Flag | What it holds | If you lose it |
|---|---|---|---|
| The data folder | `--data` | `app.sqlite` (sites, people, tokens, goals, findings), `lake.sqlite` (the lake catalog), the Parquet files of the lake (without a bucket), the event log, `secret.key`, the certificates. | You lose your data. Keep it safe, and make backups. |
| The cache folder | `--cache-dir` (default `/cache`) | The DuckDB temporary files, the DuckDB extensions, the GeoIP file. | Nothing. Checkend makes or downloads the files again. |
### A local disk or a volume
Most cloud servers have a local disk. You can also add a volume (a network block disk) to them.
- **Local disk only.** This is the default: `/var/lib/checkend` on the root disk. It is fast. When you delete or rebuild the server, the disk and the data go too. Make backups to another place.
- **A volume for the data.** A volume stays when you delete or rebuild the server, and you can attach it to a new server. It can be slower than the local disk. Put `--data` on the volume, and put `--cache-dir` on the local disk.
Example with a Hetzner Cloud server and a Hetzner Volume ([Hetzner docs: Volumes](https://docs.hetzner.com/cloud/volumes/getting-started/creating-a-volume/)):
1. Create the volume in the Hetzner Cloud Console. Select the option to format and mount it automatically.
2. Find the mount folder of the volume with `df -h`, for example `/mnt/HC_Volume_1234`.
3. Run the installer with the data folder on the volume:
```sh
curl -fsSL [INSTALL_URL] | sudo sh -s -- --domain stats.example.com \
--data-dir /mnt/HC_Volume_1234/checkend
```
4. Open `/etc/fstab` and find the line of the volume. Make sure that it has the option `nofail`.
> **Caution**
> Add `nofail` to the `/etc/fstab` line of the volume. Without it, the server does not boot when the volume is missing. The installer does not change `/etc/fstab`.
With `--data-dir`, the installer sets up the service in a different way:
- The installer refuses a `--data-dir` that is not on a mounted volume.
- The service waits for the mount at boot (`RequiresMountsFor=`).
- The server runs with `--require-mount`. It starts only when the data folder is on its own mounted file system.
- The cache folder is `/var/cache/checkend` on the local disk.
Thus Checkend never writes new data into an empty folder on the root disk when the volume is missing.
> **Caution**
> Do not use a network file system (NFS, SMB) for the data folder. SQLite and the event log need a local disk or a block volume. `checkend doctor` warns about it.
### What checkend doctor checks for disks
Run `checkend doctor --data `, and add `--cache-dir ` when the server uses one. For each folder, it shows:
- the file system,
- whether the folder is on its own mount,
- the free space,
- a short write test: 16 MiB with `fsync`, and 16 small writes. `--no-speed-test` turns the test off.
It warns in these cases:
- The data folder is on the root file system, and the server has a separate volume. This is likely wrong.
- A mount point of `/etc/fstab` that holds the data folder is not mounted. This is a failure.
- The data folder is on a network file system, or on the root file system of a container.
- The cache folder is less than half as fast as the data folder.
The health report of the running server (`ce health`, **Settings → Health**) shows the same disk checks without the write test. It also shows the DuckDB settings.
### Memory
DuckDB gets half of the memory of the server, and one thread for each CPU. Checkend reads the limits of its cgroup. Thus a Docker limit (`--memory`, `--cpus`) or a systemd `MemoryMax=` counts. The other half is for the Checkend process, the event log, SQLite, the GeoIP file and the file cache of the system. A query that needs more memory writes temporary files to `/duckdb-tmp`. These files use at most a quarter of the free space of the cache disk.
| Flag | Meaning |
|---|---|
| `--duckdb-memory` | A size (`1.5GiB`) or a percent of the memory (`40%`, at most 90 %). The default is 50 %. The value is never less than 256 MiB. |
| `--duckdb-threads` | The count of DuckDB threads. The default is one for each CPU. |
The server log line `duckdb settings` and `ce health` show the values at each start.
## Docker
The image has `checkend` and `ce`. It runs as the user 65532 (nonroot), keeps the data in the volume `/var/lib/checkend`, and has a health check. By default, the container listens on port 8080.
> **Note**
> No image is published yet. The name `ghcr.io/checkend/checkend` is the planned name.
1. Start the container:
```sh
docker run -d --name checkend -p 8080:8080 -v checkend:/var/lib/checkend ghcr.io/checkend/checkend
```
2. Find the setup URL in the log:
```sh
docker logs checkend 2>&1 | grep setup
```
Put a reverse proxy with TLS in front of port 8080. See [Behind a reverse proxy](#behind-a-reverse-proxy).
### Docker Compose with automatic TLS
This example publishes ports 80 and 443 and gets a certificate for `stats.example.com`:
```yaml
services:
checkend:
image: ghcr.io/checkend/checkend
restart: unless-stopped
command:
- serve
- --data
- /var/lib/checkend
- --duckdb-extensions
- /var/lib/checkend/duckdb-extensions
- --domain
- stats.example.com
# The health check of the image calls http://127.0.0.1:8080/health.
- --addr
- 127.0.0.1:8080
ports:
- "80:80"
- "443:443"
volumes:
- checkend:/var/lib/checkend
stop_grace_period: 60s
volumes:
checkend:
```
1. Point the DNS name at the server. Open ports 80 and 443.
2. Save the example as `compose.yaml`, and change `stats.example.com` to your name.
3. Start the service:
```sh
docker compose up -d
```
4. Find the setup URL:
```sh
docker compose logs checkend | grep setup
```
The certificates, the data, the DuckDB extensions and the GeoIP file stay in the volume.
## Automatic TLS
The flag `--domain stats.example.com` turns on automatic TLS with ACME. The default ACME server is Let's Encrypt. For more names, give a comma-separated list. The first name is the public URL.
With `--domain`, the server listens on two ports:
- Port 443 serves HTTPS and answers the TLS-ALPN-01 challenge.
- Port 80 answers the HTTP-01 challenge and sends all other requests to HTTPS.
The first HTTPS request for a name gets the certificate. The certificates and the ACME account key are in `/certs` (mode 0700). The renewal starts 30 days before the end date.
| Flag | Meaning |
|---|---|
| `--domain` | The names of the certificate. The first name is the public URL. |
| `--https-addr` | The HTTPS listen address. The default is `:443`. |
| `--http-addr` | The HTTP listen address. The default is `:80`. `--http-addr -` turns port 80 off: then only TLS-ALPN-01 works. |
| `--acme-email` | The contact address of the ACME account (optional). |
| `--acme-directory` | The ACME directory URL. The default is Let's Encrypt. |
| `--acme-ca-root` | A PEM file of extra root certificates for the ACME server (a private CA). |
| `--addr` | With `--domain`: a plain listener that serves only `/health`, for a health check. |
> **Caution**
> Test a new setup with the Let's Encrypt staging directory: `--acme-directory https://acme-staging-v02.api.letsencrypt.org/directory`. Let's Encrypt has rate limits for each name.
Each answer over TLS has the header `Strict-Transport-Security: max-age=31536000` (one year).
## Behind a reverse proxy
Run Checkend with plain HTTP on a local address, and let the proxy do TLS:
```sh
checkend serve --data /var/lib/checkend --addr 127.0.0.1:8080 \
--public-url https://stats.example.com --trusted-proxies 127.0.0.1
```
- `--public-url` is the URL that browsers use. The script tag, the setup URL, the passkeys, the MCP metadata and the Secure flag of the session cookie come from it.
- `--trusted-proxies` lists the proxy addresses or CIDR ranges. Checkend reads `X-Forwarded-For` only from these addresses. It reads the header from the right to the left, and takes the first address that is not a trusted proxy.
> **Caution**
> Set `--trusted-proxies` behind a proxy. Without it, each visitor has the address of the proxy. Standard mode then counts one visitor, and the countries are wrong.
Caddy:
```
stats.example.com {
reverse_proxy 127.0.0.1:8080
}
```
Nginx (with a certificate from your own tool):
```nginx
server {
listen 443 ssl;
server_name stats.example.com;
location / {
proxy_pass http://127.0.0.1:8080;
proxy_set_header Host $host;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
}
}
```
Behind a proxy with TLS, the proxy sets HSTS. Checkend does not, because its own answers are plain HTTP.
## Offline install
Use these steps for a server without network access.
1. Copy the release archive, `checksums.txt` and `checksums.txt.minisig` to a folder on the server.
2. Serve the folder, for example with `python3 -m http.server 8000`.
3. Run the installer with `CHECKEND_RELEASE_URL=http://127.0.0.1:8000`.
4. Copy the DuckDB extensions `ducklake` and `sqlite` for the platform of the server from a computer with network access. They are in `~/.duckdb/extensions///`. With `--lake-bucket`, also copy `httpfs`.
5. Put them in the folder of `--duckdb-extensions` in the service, under `//`. This folder is `/var/lib/checkend/duckdb-extensions`, or `/var/cache/checkend/duckdb-extensions` with `--data-dir`.
6. Copy the DB-IP City Lite file to the server.
7. Add `--geoip-file ` to `CHECKEND_FLAGS`.
To run without location data, add `--geoip-download=false` in place of step 7. Events then have no location.
The flag `--duckdb-extensions ` selects the folder of the DuckDB extensions. The installer sets it to `/var/lib/checkend/duckdb-extensions`. With `--data-dir`, it sets it to `/var/cache/checkend/duckdb-extensions`.
## Check the install
Run `checkend doctor` on the server:
```sh
checkend doctor --data /var/lib/checkend --public-url https://stats.example.com
```
It checks the disk, the data folder and `secret.key`, the data and cache disks, the lake files, Litestream, the GeoIP file, the DuckDB extensions, the backups, the jobs and the certificates. Give it the same `--cache-dir` and `--lake-bucket` as the server. With `--public-url`, it also gets `/health` as a browser does, and shows the end date of the certificate.
See [Troubleshooting](/docs/guides/troubleshooting.md) for the other checks.
## Uninstall
Remove the binaries and the service. The data and the flags file stay:
```sh
curl -fsSL [INSTALL_URL] | sudo sh -s -- --uninstall
```
Add `--purge` to also delete the data folder, `/var/cache/checkend`, `/etc/checkend` and the user `checkend`.
> **Danger**
> Make a backup before you use `--purge`. It deletes all data and `secret.key`.
## Next step
[First boot](/docs/start/first-boot.md): make the first admin.
## Prompt for a coding agent
```text
Install a Checkend server on this Linux machine.
1. Check the requirements on the page below: Linux amd64 or arm64 with systemd, glibc 2.36 or newer, ports 80 and 443 open.
2. Ask me for the domain name of the server. Do not guess it. Check that its DNS points at this machine.
3. Run the installer as root with --domain , as the page shows.
4. Show me the setup URL at the end of the output. Do not open it: I make the first admin with a passkey.
5. Run `checkend doctor --data /var/lib/checkend` and report each check that fails.
Stop and tell me when a step fails.
```
---
# First boot
> Make the first admin with the one-time setup URL and a passkey, or with an API token.
Checkend has no default password. At the first start, the server makes a one-time setup URL. The person who opens it becomes the first admin.
## The setup URL
The server prints the setup URL at the first start. It looks like this:
```
https://stats.example.com/setup#cesetup_...
```
The server also writes the URL to the file `/setup-url` (mode 0600), until the first admin exists. The installer prints the URL for you. With Docker, find it in the log:
```sh
docker logs checkend 2>&1 | grep setup
```
The token is in the URL fragment (after `#`), so it does not go into an access log.
> **Caution**
> Keep the setup URL private until you use it. Anybody with the URL can make the first admin. A restart of the server makes a new URL, and the old URL does not work after the restart.
## Make the first admin with a passkey
1. Open the setup URL in a browser.
2. Type your email address.
3. Select **Create a passkey**, and follow the steps of your browser or password manager.
4. Select the modules. In this version, only analytics is available.
5. Add the first site. The page shows the install prompt and waits for the first event. See [Add a site](/docs/start/add-a-site.md).
6. Connect an agent. The page shows the MCP URL, a form for an agent token, and the commands for Claude Code, any MCP client and `ce`. See [Connect your agent](/docs/start/connect-your-agent.md).
The page removes the token from the address bar. After this, sign in on `/signin` with **Sign in with a passkey**. You do not type an email address to sign in.
You can add more passkeys later in **Settings**.
## Passkeys need HTTPS or localhost
Passkeys work only on HTTPS with a domain name, or on `localhost`. They do not work on an IP address, or on plain HTTP with a domain name.
- On a server with `--domain`, or behind a proxy with TLS, passkeys work.
- On `127.0.0.1`, the server uses `localhost` for the passkeys. Open the setup URL with `localhost`, not with `127.0.0.1`.
- On a remote server without a domain, open an SSH tunnel, then open the setup URL on your computer:
```sh
ssh -L 8080:localhost:8080 you@server
```
## Make the first admin with an API token
Where passkeys do not work, the setup page tells you why. It then offers **Create the admin with an API token**. Use this on a private network with an IP address only.
1. Open the setup URL.
2. Type your email address.
3. Select **Create the admin with an API token**.
4. Copy the token. The page shows it one time.
5. Sign in to the web UI with the token.
You can also make the first admin from a terminal with `ce setup`. Use the token from the setup URL:
```sh
ce setup https://stats.example.com --email you@example.com --setup-token cesetup_...
```
`ce setup` makes the admin and stores the new admin token, as `ce login` does. A wrong or used setup token exits with code 5.
## Automation: no setup step
For automation and CI, make the first admin from environment variables at the first start. Set both variables:
| Variable | Value |
|---|---|
| `CHECKEND_ADMIN_EMAIL` | The email address of the first admin. |
| `CHECKEND_API_TOKEN` | The API token of the admin: `ce_` and at least 32 characters. |
```sh
CHECKEND_ADMIN_EMAIL=ops@example.com CHECKEND_API_TOKEN=ce_ \
checkend serve --data /var/lib/checkend
```
Then use the token with `ce`:
```sh
CHECKEND_URL=https://stats.example.com CHECKEND_TOKEN=ce_... ce sites list
```
> **Caution**
> Keep `CHECKEND_API_TOKEN` in a secret store. It is an admin token.
## Next step
[Add a site](/docs/start/add-a-site.md).
---
# Add a site
> Add a website to Checkend, install the tracking script with the install prompt, and check that events arrive.
A *site* is a website that sends events to Checkend. This page adds a site, installs the tracking script and checks that events arrive.
## Add the site
Add the site in the web UI with **Add site**, or with `ce`:
```sh
ce sites add example.com
```
You need write access to the project of the site. Checkend shows two things:
- **The script tag.** Put it in the `` of each page.
- **The install prompt.** Paste it into your coding agent. The agent installs the tag for you.
To select a privacy mode now, add `--privacy`:
```sh
ce sites add example.com --privacy strict
```
The default mode is `standard`. See [Privacy modes](#privacy-mode) below.
## The script tag
The tag loads the tracking script from your Checkend server:
```html
```
The script is smaller than 2 KB gzip. It records page views, route changes of single-page apps, outbound links, file downloads, 404 pages and web vitals. It sets no cookies.
Some frameworks need a small change:
| Framework | File | Note |
|---|---|---|
| Next.js | `app/layout.tsx` (App Router) or `pages/_document.tsx` (Pages Router) | Put the plain `
```
The web server of your site then sends two paths to Checkend:
| Path on your site | Goes to |
|---|---|
| `/ce/script.js` | `https://stats.example.com/js/script.js` |
| `/ce/e` | `https://stats.example.com/api/e` |
The install prompt has tested snippets for Next.js, Nginx and Caddy. Nginx example:
```nginx
location = /ce/script.js {
proxy_pass https://stats.example.com/js/script.js;
proxy_set_header Host stats.example.com;
proxy_ssl_server_name on;
}
location = /ce/e {
proxy_pass https://stats.example.com/api/e;
proxy_set_header Host stats.example.com;
proxy_ssl_server_name on;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
}
```
> **Caution**
> Add the address of the site's proxy to `--trusted-proxies` on the Checkend server. Without it, Checkend sees the proxy address for each visitor, and the countries are wrong.
> **Caution**
> Do not trust a Next.js server alone. Next.js rewrites pass the `X-Forwarded-For` header of the visitor through and do not add the visitor address, so a visitor can send a false address. Trust Next.js only when a proxy in front of it sets `X-Forwarded-For`, and visitors cannot reach Next.js directly.
## Privacy mode
Each site has one privacy mode: `strict`, `standard` (the default) or `extended`. The mode controls what Checkend keeps about each visitor. See [Privacy](/docs/concepts/privacy.md).
Change the mode of a site:
```sh
ce sites set example.com --privacy strict
```
The new mode applies to new events. Ingest keeps sites in memory for a short time (5 seconds by default), so a change takes effect within this time.
## Next step
[Connect your agent](/docs/start/connect-your-agent.md).
## Prompt for a coding agent
```text
Add Checkend analytics to this project.
1. Find the production domain of this project. Ask me if it is not clear.
2. Run `ce sites add `. If the site already exists, skip this step.
3. Run `ce sites prompt ` and read the `prompt` field of the JSON. It is the install prompt.
4. Follow the install prompt: add the script tag to the root layout, add the goal events for the signup and purchase forms, and add the goals.
5. After the deploy, run `ce sites verify --wait 5m`. Exit code 0 means that the first page view arrived.
If ce exits with code 2, tell me to run `ce login `.
```
---
# Connect your agent
> Connect an MCP client with OAuth, a script with an API token, or a terminal with ce login.
Checkend has no AI inside. You bring the agent. The agent reads data and records changes through MCP, the `ce` CLI or the REST API. All three give the same numbers.
| Way | For | How |
|---|---|---|
| MCP with OAuth | Interactive MCP clients, for example Claude Code or Cursor | Give the client the URL `/mcp`. A person approves the access in the browser. |
| MCP with an API token | Scripts, scheduled agents, CI | Send the header `Authorization: Bearer ce_...`. |
| `ce mcp` | MCP clients that support only stdio | `ce` runs a local stdio MCP server and sends each call to the server. |
| `ce` CLI | Coding agents in a terminal, people, CI | `ce login`, or the variables `CHECKEND_URL` and `CHECKEND_TOKEN`. |
## MCP with OAuth
The MCP server is at `/mcp`, for example `https://stats.example.com/mcp`. It uses Streamable HTTP.
1. Give the URL to your MCP client. For Claude Code:
```sh
claude mcp add --transport http checkend https://stats.example.com/mcp
```
2. The client opens the consent page of Checkend in your browser.
3. Sign in with your passkey, if you are not signed in.
4. Read the page. It shows the app name, your email address and role, and the host where the browser goes back to.
5. Select the access for the app.
6. Select **Approve**.
The client then gets an access token for one hour and a refresh token. It renews the access token by itself.
### The access
The consent page shows only the access of your role:
| Choice | Scope |
|---|---|
| Read only | `read:*` |
| Read and write | `write:*` |
| Admin | `admin` |
The page selects **Read only** (`read:*`) first, also when the app asks for more. You can select more access, up to the access of your role. An Admin can give an app the `admin` scope, but only with a deliberate choice.
> **Caution**
> Give an agent read-only access, unless it must write. Write access lets it claim findings, record changes and change goals.
> **Caution**
> Approve only a connection that you started. The app gives its name itself, and any program on your computer can listen on a localhost redirect URI.
> **Caution**
> Set `--public-url` on the server to the URL that MCP clients use. The OAuth metadata comes from it, and a client refuses metadata with another issuer.
The access token belongs to you. It shows in your token list with the app name and "(MCP)". It works only at `/mcp`: the REST API refuses it. To end the access, revoke the token in **Settings**, or with `ce tokens revoke `.
## MCP with an API token
Scripts and scheduled agents cannot open a browser. Give them an API token.
1. Make a read-only token:
```sh
ce tokens create --name "weekly agent" --scope 'read:*'
```
Or make the token in **Settings → API tokens**. Checkend shows the secret one time.
2. Give the token to the client as a bearer token. For Claude Code:
```sh
claude mcp add --transport http checkend https://stats.example.com/mcp \
--header "Authorization: Bearer $CHECKEND_TOKEN"
```
An API token works at `/mcp` and at the REST API. For the scopes, see [Roles and tokens](/docs/concepts/roles-and-tokens.md).
## ce mcp: a stdio bridge
Some MCP clients support only stdio. For them, run `ce mcp`. It connects to `/mcp` with the token of the current `ce` context, or with `CHECKEND_URL` and `CHECKEND_TOKEN`. It gives the same tools and prompts as the server. Each call goes to the server, so the scope checks and the audit log are the same.
Example client configuration:
```json
{
"mcpServers": {
"checkend": {
"command": "ce",
"args": ["mcp"],
"env": {
"CHECKEND_URL": "https://stats.example.com",
"CHECKEND_TOKEN": "ce_..."
}
}
}
}
```
`ce mcp` writes its logs to stderr. It stops at the end of stdin.
## ce login
`ce login` connects the `ce` CLI to a server. It uses the device flow in the browser.
1. Run the command:
```sh
ce login https://stats.example.com
```
2. `ce` prints a URL and a code, for example `BCDF-GHJK`, and opens the browser.
3. Sign in with your passkey.
4. Check that the page shows the same code as the terminal.
5. Select the access, then select **Approve**.
6. Go back to the terminal.
The token belongs to you. It has the scopes of your role, or the scopes that you ask for with `--scope`. It never has more than your role. It has no expiry.
| Flag | Meaning |
|---|---|
| `--scope 'read:*'` | Ask for this scope. Repeat the flag for more scopes. |
| `--no-browser` | Do not open the browser. Open the URL yourself. |
| `--device` | Start the device flow without a read of stdin. Use it in an agent. |
| `--name ` | The name of the context. The default is the server host. |
On a pipe, `ce` writes the URL and the code as one JSON line on stderr. An agent can show them to a person.
### Paste a token
You can also log in with an existing API token:
```sh
echo "$TOKEN" | ce login https://stats.example.com
```
Or use `--token-stdin` for a hidden prompt on a terminal:
```sh
ce login https://stats.example.com --token-stdin
```
## Contexts
Each `ce login` makes a *context*: a server URL and a token. Use contexts for many servers.
```sh
ce context list # each context, its URL and its token store
ce context use work # select the current context
ce logout work # remove the context and its token
```
`ce` has no default server. Without a context or `CHECKEND_URL`, it exits with code 2 and tells you to run `ce login `.
## Where ce keeps the token
`ce` stores the token in the OS keychain: the macOS Keychain, the Secret Service on Linux, or the Windows Credential Manager.
When no keychain works, for example on a server without a desktop session or in CI, `ce` uses the credentials file `~/.config/checkend/credentials`. With `XDG_CONFIG_HOME` set, the file is under that folder. The file has mode 0600. `ce` tells you which store it used.
To use only the credentials file, set `CHECKEND_KEYRING=file`.
> **Caution**
> Keep the credentials file at mode 0600. It holds the tokens in clear. `ce` refuses to read the file when its mode is wider.
## CI and agents without login
Set two environment variables. `ce` then needs no context and no keychain:
```sh
CHECKEND_URL=https://stats.example.com CHECKEND_TOKEN=ce_... ce brief example.com
```
On a pipe, `ce` prints one JSON value on stdout. An error is one JSON object on stderr, with the fields `error`, `message` and `next`. See [Troubleshooting](/docs/guides/troubleshooting.md) for the exit codes.
## What the agent can do
The MCP server gives tools for each part of the product, for example `get_brief`, `query`, `get_findings`, `update_finding` and `annotate`. It also gives three prompts: `weekly_seo_review`, `investigate_traffic_drop` and `launch_check`. See the [MCP tools reference](/docs/reference/mcp-tools.md).
A good first call is `get_brief`. It gives the top changes, the open findings, the recent changes and the outcomes of earlier changes. See [The closed loop](/docs/concepts/closed-loop.md).
## Prompt for a coding agent
```text
Connect this coding agent to my Checkend server over MCP.
1. Ask me for the URL of the Checkend server, for example https://stats.example.com.
2. Add the MCP server `/mcp` with the HTTP transport to your MCP configuration. In Claude Code: `claude mcp add --transport http checkend /mcp`.
3. Tell me to approve the access in the browser. Ask for read-only access, unless I say otherwise.
4. Call the `list_sites` tool and show me the sites.
If your client supports only stdio, use the command `ce mcp` as the MCP server.
```
---
# The closed loop
> How Checkend turns a finding into a change, measures the outcome, and keeps it as memory for the next agent run.
Most analytics tools stop when they show a number. Checkend continues. It tells the agent what to fix, records the change, measures the result, and keeps the result as memory.
```
Finding -> Change (annotation) -> Measured outcome -> Memory (the brief)
^ |
+--------------------------------------------------------------+
```
Agents do not remember between runs. Checkend keeps what was tried on each site and what worked.
## The four parts
| Part | What it is |
|---|---|
| **Finding** | A fact that a rule found, with evidence. For example: "The page views of /pricing fell 31 %." |
| **Change** | An annotation: a record of what the agent or a person changed, and when. |
| **Outcome** | The measured result of the change: the numbers 14 days before and 14 days after, with a statistical test. |
| **Memory** | The brief. It gives the outcomes of earlier changes to the next agent run, newest first. |
## A typical run
1. The agent calls `get_brief` (or `ce brief example.com`). It reads the open findings.
2. The agent claims a finding, so that other agents do not work on it.
3. The agent makes the change, for example a new title on `/pricing`.
4. The agent marks the finding done, with the text of the change. Checkend records an annotation.
5. Checkend starts to measure on the next UTC day.
6. After the 14 days after the change day, Checkend sets the outcome.
7. The next brief shows the outcome. A change that made things worse comes first, with a warning.
With `ce`:
```sh
ce findings list --status open
ce findings claim f_8k2
ce findings done f_8k2 --text "Title changed on /pricing" --link https://github.com/acme/web/pull/7
ce findings show f_8k2
```
With MCP, the agent uses the tools `get_findings`, `update_finding` and `annotate`.
## Finding statuses
| Status | Meaning |
|---|---|
| `open` | A rule found the fact. Nobody works on it. |
| `claimed` | A token claimed the finding. Another token gets an error with the name of the claimer. |
| `done` | The change is made. The finding has an annotation of the change. |
| `measuring` | Checkend measures. This starts at 00:00 UTC on the day after the change day. |
| `worked` | The primary metric moved in the wanted direction, significant at 95 %. |
| `got_worse` | The primary metric moved in the wrong direction, significant at 95 %. The change made it worse. |
| `no_clear_change` | The test had enough data, and found no change. |
| `too_few_to_compare` | The test cannot reach 95 % with the data that it has. |
| `dismissed` | A person or an agent decided not to act on the finding. |
The allowed changes:
| From | Possible actions |
|---|---|
| `open` | claim, done, dismiss |
| `claimed` | done, dismiss, reopen |
| `done` | reopen. Checkend moves it to `measuring` on the next UTC day. |
| `measuring` | reopen, remeasure (admin). Checkend sets the outcome when the after window is complete. |
| An outcome status | reopen, remeasure (admin) |
| `dismissed` | reopen |
**Done needs a change.** Give an existing annotation of the same site, or a text for a new annotation. Each status change goes to the history of the finding and to the audit log. Use `--dry-run` (or `dry_run` in MCP) to check a change without making it.
A rules run does not reopen a finding that is not open. A person or an agent can reopen it.
## How Checkend measures an outcome
**The windows.** The change day is the UTC day of the annotation. The before window is the 14 days before the change day. The after window is the 14 days after it. The change day is in neither window. Each window has each weekday two times.
**The primary metric decides.** It is the metric that the change wants to move. Checkend uses one of two tests:
| Kind of metric | Test | Examples |
|---|---|---|
| A rate | Two-proportion z-test | Conversion rate, search CTR, the share of good page loads |
| A count | Poisson rate test | Page views, 404 events, search clicks, search impressions |
Checkend also shows secondary metrics with the same tests: page views, visitors, conversions and the conversion rate. They do not decide.
**The decision**, in this order:
1. A window with fewer than 12 of 14 days with data: `too_few_to_compare`.
2. The primary metric changed, significant at 95 %: `worked` in the wanted direction, else `got_worse`.
3. Not significant, and the test has less than 80 % power to find a change of the site's minimum detectable effect: `too_few_to_compare`.
4. Else: `no_clear_change`.
For a search finding, Checkend waits until the search data covers the after window. Search Console data is 2 to 3 days late.
### The outcome sensitivity
The minimum detectable effect (MDE) is the smallest change that the test must find with 80 % power. It is a setting of each site:
- The default is 10 %.
- The range is 5 % to 50 %.
- A small MDE finds small changes, but needs more data.
- A large MDE needs less data, but misses small changes.
As an example, at an MDE of 10 %, a count needs about 1,648 events in the 14 days before for 80 % power to find a rise of 10 %. At 20 %, it needs about 431 events.
Only an admin can change the MDE:
```sh
ce sites set example.com --outcome-mde 20
```
There is no MCP tool for this setting. An agent must not change how its own work is judged. Each outcome stores the MDE that it used. A new setting does not change old outcomes.
### Remeasure
An admin can compute an outcome again, for example after a change of the MDE:
```sh
ce findings remeasure f_8k2
```
Remeasure uses the MDE that the site has now. It adds a new outcome and keeps the old outcomes in the history. It can change the status.
### The outcome record
Each outcome stores the windows, the values before and after with n, the test, the z statistic, the p-value, the power, the MDE, the result, the reason, the change, the data date and who measured. `ce findings show ` shows the newest outcome and the history.
## The memory: the brief
The brief is the main entry point for an agent: `get_brief` in MCP, `ce brief` in the CLI, `GET /v1/brief` in the REST API. It has:
| Part | Content |
|---|---|
| Warnings | One sentence for each `got_worse` outcome: the change, the finding and the numbers, then "Do not repeat this change". Read them first. |
| Top changes | Page views, visitors and conversions of each site, and its top pages, against the period before, with a significance test. |
| Open findings | The open and claimed findings, high priority first. |
| Annotations | The changes and releases of the period. |
| Outcomes | The findings with an outcome, newest first: what was tried and what happened. |
| Data dates | The time of the newest event of each site. |
The default period is 7 days.
## Annotations
An annotation records a change on a date. It has a text (1 to 500 characters), a kind and a time. It can link to a finding and to a URL.
| Kind | Use |
|---|---|
| `agent_change` | A change by an agent. This is the default. |
| `release` | A release. Record it from CI. |
| `note` | Any other note. |
```sh
ce annotate example.com "Title changed on /pricing" --finding f_8k2
ce annotate example.com "v1.4.0" --kind release
```
The site chart shows each annotation as a line.
## The rules
Rules make findings. Each finding has a type, an action type, evidence, an estimated impact, a priority and a confidence level. The rules job runs each hour. An admin can run it now with `ce findings run`.
The thresholds below are choices, not measured needs.
### Traffic and conversion rules
These rules compare the last 7 complete UTC days with the 7 days before. A site with a shorter history gets no drop, spike or conversion findings.
| Type | Condition | Action type |
|---|---|---|
| `traffic_drop` | The page views of a page fell at least 25 %, with at least 20 page views in one of the two weeks, and p < 0.05. | `investigate` |
| `traffic_spike` | The same, but a rise. The page must have page views in the week before. | `investigate` |
| `conversion_drop` | The conversion rate of a goal fell at least 20 %, with at least 10 conversions in the week before, and p < 0.05. | `investigate` |
| `traffic_without_conversions` | A page with at least 50 page views in the week, and no visitor of the page converted. Not for sites in strict mode. | `improve_content` |
| `not_found_with_traffic` | At least 3 `404` events on a page in the week. | `fix_redirect` |
For these rules, the priority is high when the impact is at least 10 % of the site's number, medium from 2 %, else low. The confidence is high when p < 0.01, medium when p < 0.05.
### Search and speed rules
These rules use the 28 days that end at the newest Search Console date. They need a Search Console connection. See [Search Console and Bing](/docs/guides/search-console-and-bing.md).
| Type | Condition | Action type |
|---|---|---|
| `striking_distance_query` | A query with an average position of 8 to 20 and at least 200 impressions. | `improve_content` |
| `low_ctr_for_position` | A page with a CTR below 60 % of the expected CTR for its position. The expected CTR comes from the other pages of the site. | `rewrite_title` |
| `content_decay` | The clicks of a page fell at least 30 % against the same 28 days one year before. | `improve_content` |
| `cannibalization` | Two or more pages each get a large part of the impressions of the same query. | `merge_pages` |
| `impressions_without_visits` | A page with search clicks, but no Checkend page view. | `fix_tracking` or `fix_redirect` |
| `index_problem` | The URL Inspection of the page shows a failure or a blocked indexing state. | `fix_index` |
| `slow_page_losing_position` | A page lost at least 1.0 position, and a core web vital (LCP, INP or CLS) is not good. | `improve_speed` |
## Get told about changes
Webhooks send `finding.created` and `finding.updated` when a rule finds something or an outcome is set. An agent can then start without a poll. See [Webhooks](/docs/guides/webhooks.md).
## Prompt for a coding agent
```text
Run one cycle of the Checkend loop for example.com. Replace example.com with my site.
1. Call `get_brief` for the site, or run `ce brief example.com`. Read the open findings, the recent changes and their outcomes. Do not repeat a change that got worse or that had no clear change.
2. Select one open finding with clear evidence. Claim it with `update_finding`, or `ce findings claim `.
3. Make the smallest change in this repository that answers the finding. Open a pull request and show me the diff.
4. After I merge and deploy it, mark the finding done with the text of the change and the pull request link: `ce findings done --text "" --link `. Checkend records the annotation and measures the outcome.
Do not change goals, and do not touch other findings.
```
---
# Modules
> Checkend is one product with four modules. Analytics is available now; errors, logs and uptime are plans.
Checkend is one product with four modules. All modules run in one binary on one server. They share one timeline, one query core, one MCP server and one CLI.
| Module | Status | What it does |
|---|---|---|
| Analytics | Available | Private web analytics, joined with Google Search Console and Bing Webmaster Tools data. |
| Errors | Coming | Error tracking through the Checkend SDK. |
| Logs | Coming | Log ingest, archive and search. |
| Uptime | Coming | Endpoint checks from many regions. |
> **Note**
> Only analytics is available in this version. Errors, logs and uptime are not started. The sections about them below are plans, not features. The plans can change.
## Why one product
- **One story.** "What broke, why, and did the fix work" needs one timeline.
- **One agent connection.** One MCP server with one set of concepts gives better agent results than four.
- **One operation.** One install, one backup, one sign-in, one upgrade.
## What the modules share
| Shared part | What it does |
|---|---|
| Projects | A *project* holds sites. Later modules add services and endpoints to the same project. |
| Timeline | Releases and annotations show in one place. |
| Query core | The web UI, the REST API, MCP and the CLI read through one query layer. They give the same numbers. |
| Findings | Rules make findings with evidence. |
| Outcomes | An agent makes a change. Checkend measures if it worked. |
| No AI inside | Checkend calculates facts. Your agent does the thinking. |
## Analytics
Analytics is available now. It has:
- a tracking script under 2 KB gzip, with page views, custom events, outbound links, file downloads, 404 pages and web vitals,
- three privacy modes, without cookies,
- channels, goals, conversions and revenue for each currency,
- Search Console and Bing data, a page crawler, URL inspections and IndexNow,
- AI crawler hits from your access logs,
- findings, annotations, measured outcomes and the brief,
- scheduled reports and signed webhooks.
Start with the [Quickstart](/docs/start/quickstart.md).
## Errors (coming)
> **Note**
> Coming. This module is a plan. Nothing in this section is built.
The plan: error tracking through the Checkend SDK (see below). The plan includes issue grouping, secret scrubbing before storage, source maps, and an issue brief for a coding agent. It also includes a fix loop: an agent links a fix, CI reports the deploy, and Checkend checks that the error does not come back.
## The Checkend SDK (coming)
> **Note**
> Coming. The SDK is a plan. Nothing in this section is built. Today, only the analytics script tag exists.
The plan: one Checkend SDK for each language. One setup and one Checkend key send analytics, errors, app logs and job check-ins. A plain HTML site uses the same SDK as one script tag. For a script tag, the Checkend settings select the parts, and the HTML does not change.
- The SDK comes with the errors module. The first SDK is for JavaScript, in the browser and in Node.js. Python, Ruby, Go and PHP come next.
- The current analytics script becomes the analytics part of the JavaScript SDK. Its tag keeps working, and it stays under 2 KB gzip.
- The Checkend key holds the server address and a public key. The key can only send data to one project. It cannot read data, so it is safe in a browser.
- To move an app from another tool, you make one change, for example one line in `package.json`. The SDK recognizes the old calls, for example the calls of the Sentry SDK. It sends their data to Checkend and shows the Checkend call to use instead. The server keeps the Sentry and OpenTelemetry endpoints during a migration.
Outside uptime checks, server logs and source maps do not use the SDK.
## Logs (coming)
> **Note**
> Coming. This module is a plan. Nothing in this section is built.
The plan: log ingest with OpenTelemetry (OTLP) over HTTP, syslog and journald directly, and JSON lines over HTTP. It includes a live tail, search by time, service, severity, fields and text, and a link from logs to errors by trace ID. Logs is planned as the last module.
## Uptime (coming)
> **Note**
> Coming. This module is a plan. Nothing in this section is built.
The plan: HTTP and TCP checks from many regions, with the checks on Cloudflare Workers in your own Cloudflare account. The main server keeps the configuration, the results and the reports. The plan includes an alert from the Worker when the main server does not answer.
## Each module alone
The plan is that each module also works alone, with its own docs and its own install. Each module gets its own disk budget, so that one module cannot fill the disk for another.
---
# Privacy
> The three privacy modes of a site, what Checkend keeps in each mode, and what it never keeps.
Checkend keeps as little data as possible. It never stores an IP address, a user agent or a raw visitor ID. The tracking script sets no cookies.
## The three privacy modes
Each site has one privacy mode. The mode is a named preset, not a set of separate switches.
| Mode | Visitor identity | Location kept |
|---|---|---|
| `strict` | No visitor ID. Counts only. | Country |
| `standard` (default) | A hash of the IP address, the user agent and a daily salt. | Country, region |
| `extended` | A persistent ID that you give. | City |
### Strict
Checkend keeps no visitor ID. It counts page views and events. It cannot count visitors or return visits. Conversions count each event, and the conversion rate divides by page views.
### Standard
Checkend makes a visitor ID from a hash of the IP address, the user agent and a daily salt. It then discards the IP address. Checkend deletes the salt each day, so the same person gets a new ID on the next UTC day. Checkend cannot connect the visits of two days.
This is the default mode.
### Extended
Checkend uses a persistent ID, so it can count return visits over many days and follow user funnels. The ID comes from one of two places:
- your own user ID, in the `data-uid` attribute of the script tag,
- a random first-party ID that the script keeps in `localStorage`, with the `data-persist` attribute.
Checkend stores only a hash of the ID, never the raw ID. Without a persistent ID, the event uses the standard visitor ID.
> **Caution**
> Check the law of your country before you use extended mode. A consent banner is probably necessary.
### Select a mode
```sh
ce sites add example.com --privacy strict
ce sites set example.com --privacy extended
```
## Rules for all modes
- Checkend never stores the IP address. It discards the IP address after the visitor hash and the location lookup.
- Checkend never stores the user agent or a raw visitor ID.
- Click IDs (`gclid`, `msclkid`, `fbclid`) mark a visit as paid. Checkend never stores their values.
- A web vitals report keeps the page and the numbers only: no visitor ID and no location.
- An access log push keeps only the hits of known crawlers: the day, the path without the query, the bot name and the status.
## Do Not Track and Global Privacy Control
When a visitor sends Do Not Track (DNT) or Global Privacy Control (GPC), Checkend still counts the page view, but it uses strict mode for that event. The event has no visitor hash and keeps the country only. This applies to every site, in every mode.
## Server events
A server can send a custom event with `POST /v1/events` or `ce event send`, for example a purchase from a payment webhook. The request comes from your server, not from the visitor. So the event keeps no location and no IP-based visitor hash. In extended mode, a `vid` gives a visitor ID.
## The data-center filter
Checkend drops events from hosting and cloud networks, because they are bots. It does not drop VPN networks: people who use a VPN count as people. The filter also drops known bot user agents and headless browsers.
To keep events from hosting networks, start the server with `--no-datacenter-filter`.
## Location data
Checkend finds the location with the DB-IP City Lite file. The file needs no license key. The server downloads it at the first start.
The data is licensed under [CC BY 4.0](https://creativecommons.org/licenses/by/4.0/). Wherever you show location data from Checkend, show this attribution with a link:
> IP Geolocation by [DB-IP](https://db-ip.com)
## Custom event data
> **Caution**
> Do not put personal data (email addresses, names, user IDs) in event props. Checkend stores props as they are, for the full retention time.
## Data and AI
No analytics data leaves your server unless your own agent asks for it. Checkend has no AI inside, so it never sends data to an AI provider. See [No AI inside](/docs/concepts/no-ai-inside.md).
## Cookie banners
> **Caution**
> Do not use Checkend as a legal answer about cookie banners. Laws are different in each country. Read what data each mode keeps, and decide for your site.
---
# No AI inside
> Checkend has no language model, no model keys and no model calls. You bring your own agent.
Checkend has no AI inside. It has no language model, no model keys and no model calls. It calculates facts. Your agent does the thinking.
## What this means
- Checkend never sends your data to an AI provider.
- Checkend never asks you for a model key, and it has no token bills.
- Checkend never writes text with a model. Each sentence in a report or a brief comes from a fixed template and computed numbers.
- The MCP server does not use MCP sampling. It never asks the model of your client for text.
## Bring your own agent
You connect the agent that you already use. Checkend gives it data and records its work through open interfaces:
| Interface | Example clients |
|---|---|
| MCP | Claude Code, Cursor, any MCP client |
| `ce` CLI | Any coding agent with a terminal, CI jobs, scripts |
| REST API | Your own code, n8n flows, scheduled jobs |
| Webhooks | Any receiver that can start an agent |
See [Connect your agent](/docs/start/connect-your-agent.md).
## Why
| Reason | What it gives you |
|---|---|
| **Privacy** | Data goes to an AI provider only when your own agent asks for it. |
| **No cost and no keys** | You pay no model bills through Checkend, and you manage no model keys in it. |
| **No lock-in** | Use Claude, ChatGPT, a local model, or any client of MCP, the CLI or HTTP. |
| **Trust** | Checkend cannot invent numbers. Each number comes from your data, with its sample size and a test where one applies. |
## Who does what
| Work | Who does it |
|---|---|
| Collect events, sync search data, crawl pages | Checkend |
| Run the rules and make findings | Checkend |
| Measure each change with a statistical test | Checkend |
| Read the brief and decide what to change | Your agent |
| Write the new title, the fix or the pull request | Your agent |
| Record the change | Your agent (an annotation) |
| Approve risky actions | A person |
Checkend keeps the data schedule: syncs, rules, outcomes and webhooks. Your agent keeps the thinking schedule, for example a weekly review. Connect them in two ways:
- **Push:** a webhook starts your agent. See [Webhooks](/docs/guides/webhooks.md).
- **Pull:** your agent runs on its own schedule and calls MCP or the CLI.
## MCP prompts
The MCP server gives three prompts: `weekly_seo_review`, `investigate_traffic_drop` and `launch_check`. A prompt gives your agent the task, the rules of the closed loop and the data. Your model does the work.
---
# Roles and tokens
> The three roles of people, the scopes of API tokens, install tokens, and who owns a token.
People have roles. Agents and scripts have API tokens. Roles and tokens use the same scopes, so Checkend checks people and agents the same way.
## Roles
Each person has one role for the whole organization. A role is not per site.
| Role | Scope | What the person can do |
|---|---|---|
| Admin | `admin` | Everything: tokens, webhooks, people, retention, limits and settings. |
| Member | `write:*` | Change sites, goals, annotations, findings and search connections. Not tokens of other people, webhooks, people or settings. |
| Viewer | `read:*` | Read only. |
- Only Admins and Members see the people list. A Viewer sees no email addresses of other people.
- The last admin stays. Checkend refuses to remove the last admin or to change the role of the last admin. Make another person admin first.
- Roles, viewer accounts and passkeys are free.
To invite people and change roles, see [People and invites](/docs/guides/people-and-invites.md).
## Token scopes
| Scope | Access |
|---|---|
| `read:*` | Read all projects. |
| `write:*` | Read and write all projects. |
| `read:` | Read one project. |
| `write:` | Read and write one project. |
| `admin` | Everything. |
Write includes read. Give agents read-only tokens, unless they must write.
Make a token:
```sh
ce tokens create --name "weekly agent" --scope 'read:*'
```
Checkend shows the secret one time. It stores only a hash of the token. List and revoke tokens:
```sh
ce tokens list
ce tokens revoke
```
You can also manage tokens in **Settings → API tokens**.
## A token belongs to a person
Each token belongs to the person who made it. This is also true for an agent token that an admin makes, and for the token of an MCP app that a person approved.
- A token gets only scopes inside the role of its person.
- An admin sees and revokes all tokens.
- A Member or a Viewer sees, makes and revokes only their own tokens.
- When a person leaves, Checkend revokes all their tokens.
### A role change revokes tokens
When an admin gives a person a lower role, Checkend revokes each token of the person that is outside the new role. Checkend does not narrow the tokens. The agent gets a clear "not authenticated" error, not a mix of allowed and refused calls.
The role change takes effect on the next request of the person.
## Install tokens
An install token is a special token for one site. It starts with `ce_inst_` and has the scope `install:`. It expires after 24 hours.
It can do only two things:
- check the install of its site,
- send one test event to its site.
It cannot read analytics, list sites, write data or sign in. The token API cannot make an install token. Only the install prompt makes one:
```sh
ce sites prompt example.com
```
See [Add a site](/docs/start/add-a-site.md).
## OAuth tokens for MCP apps
An MCP app that you approve on the consent page gets an access token for one hour and a refresh token.
- The access token belongs to you. It shows in your token list with the app name and "(MCP)".
- It works only at `/mcp`. The REST API refuses it.
- Each refresh gives a new access token and a new refresh token. The old ones stop.
- To end the access, revoke the token in **Settings** or with `ce tokens revoke`.
See [Connect your agent](/docs/start/connect-your-agent.md).
## Errors for missing access
| Case | HTTP status | `ce` exit code |
|---|---|---|
| No token, or a token that is not valid | 401 | 2 |
| A valid token without the scope or role for the action | 403 | 5 |
## The audit log
Each write by a token goes to the audit log: the token, its scopes, the interface (`rest`, `mcp`, `cli`, `ui` or `job`), the action, the target and the time. An admin reads it with `ce audit`, or in **Settings**.
```sh
ce audit --limit 50
```
---
# Goals and revenue
> Send custom events, define goals, record revenue and read conversions.
A goal tells Checkend what success looks like on a site, for example a signup or a purchase. Checkend then counts conversions and revenue, and the rules can find conversion drops.
## Send custom events
The tracking script defines `checkend.track(name, {props, revenue})`:
```html
```
To call `track` before the script loads, add this queue snippet before the script tag. The script sends the queued calls after the first page view:
```html
```
You can also send an event with an HTML attribute. A click on the element sends the event:
```html
```
### Automatic events
The script sends these events without code:
| Event name | When | Props |
|---|---|---|
| `Outbound Link` | A click (also a middle click) on a link to another host. | `url`: the link without query and fragment. |
| `File Download` | A click on a link on the same host to a file, for example `.pdf`, `.zip`, `.dmg` or `.csv`. | `url`, as above. |
| `404` | A page with `` loads. Add the tag to your 404 page. | None |
### Limits
| Field | Rule |
|---|---|
| `name` | 1 to 120 characters: letters of any language, digits, spaces and `_ - . : /`. No space at the start or the end. `pageview` is reserved. |
| `props` | At most 20. A key has 1 to 64 letters, digits, `_`, `-` or `.`. A value has 1 to 256 characters and no control characters. |
| `revenue.amount` | More than 0 and less than 1,000,000,000, with at most 6 decimals. Checkend keeps the exact value. |
| `revenue.currency` | An ISO 4217 code, for example `USD`. Lower case is accepted. `XXX` and `XTS` are refused. |
Props and revenue need an event name.
> **Caution**
> Do not put personal data (email addresses, names, user IDs) in props. Checkend stores props as they are, for the full retention time.
## Send events from a server
A server can send a custom event, for example a purchase from a payment webhook. The token needs write access to the project of the site.
```sh
ce event send example.com --name Purchase --revenue 49.90 --currency USD --prop plan=pro
```
The REST route is `POST /v1/events`. The body is `{site, name, url?, ts?, vid?, props?, revenue?}`.
- The URL defaults to `https:///`. It must be on the site domain or a subdomain.
- The event keeps no location and no IP-based visitor hash, because the request comes from your server.
- In extended privacy mode, `--vid` (or `vid`) gives a visitor ID.
- The bot filter does not apply. The rate limits and the daily event limit apply.
## Add goals
A goal belongs to one site. It has a name and one of two kinds:
| Kind | Flag | Matches |
|---|---|---|
| Event goal | `--event Signup` | The custom events with this name. |
| Page goal | `--page '/pricing*'` | The page views with a path that matches the pattern. `*` matches any characters. |
```sh
ce goals add example.com signup --event Signup
ce goals add example.com pricing --page '/pricing*'
ce goals list example.com
ce goals delete example.com --yes
```
You can also use the **Add goal** form on the site page, or the MCP tool `manage_goals`. Add and delete need write access. A site has at most 50 goals. The goal name must be unique on the site.
Use the same event names in your code and in your goals.
## Read conversions and revenue
```sh
ce query example.com --period 7d
```
`ce query` shows the conversions, the conversion rate, the revenue for each currency, and a table of the goals. The MCP tools `query` and `timeseries` give the same numbers.
| Number | Meaning |
|---|---|
| Conversions | The visitors who completed at least one goal in the period. An event without a visitor ID counts as one conversion. |
| Conversion rate | Conversions divided by visitors. Each event without a visitor ID also counts as one more person, so the rate is at most 100 %. |
| Revenue | The revenue of all custom events in the period, one entry for each currency, with the event count. |
| Goals | The same numbers for each goal, and the numbers of the period before. Only event goals have revenue. |
An event has no visitor ID in strict mode, with Do Not Track or Global Privacy Control, and from a server without `vid`.
> **Note**
> Checkend never adds amounts of different currencies. Each currency has its own total.
### Strict mode
A site in strict mode has no visitor IDs. Each conversion event counts, and the conversion rate divides by page views. The answer says this rule in words.
## Goals and the closed loop
Goals feed the rules. Checkend makes a `conversion_drop` finding when the conversion rate of a goal falls, and a `traffic_without_conversions` finding for a page with traffic and no conversions. See [The closed loop](/docs/concepts/closed-loop.md).
A finding whose goal you delete gets the outcome `too_few_to_compare`.
## Prompt for a coding agent
```text
Add conversion tracking for example.com to this project. Replace example.com with my site.
1. Find the signup and purchase flows in the code.
2. After a successful signup, send a custom event: `window.checkend && checkend.track('Signup')`. Or put `data-checkend-event="Signup"` on the button.
3. After a successful purchase, send the revenue with the real amount and currency: `window.checkend && checkend.track('Purchase', { revenue: { amount: 49, currency: 'USD' } })`.
4. Add a goal for each event, for example `ce goals add example.com signup --event Signup`.
5. Use the same event names in the code and in the goals. Show me the events and goals that you added.
```
---
# Search Console and Bing
> Connect Google Search Console with your own OAuth client, connect Bing Webmaster Tools, and read search data with visits and conversions.
Checkend joins Google Search Console and Bing Webmaster Tools data with your page views and conversions. The search rules use this data to make findings, for example a query on position 8 to 20.
## Connect Google Search Console
Checkend uses the OAuth client of your own Google Cloud project. Checkend never ships a shared OAuth client.
### Make the OAuth client
1. In the Google Cloud console, make a project.
2. Turn on the "Google Search Console API".
3. Set up the OAuth consent screen. Add the scope `https://www.googleapis.com/auth/webmasters.readonly`.
4. Make an OAuth client of the type "Web application".
5. Add the redirect URI `/v1/search/oauth/callback`, for example `https://stats.example.com/v1/search/oauth/callback`.
`ce search status ` also shows the redirect URI.
### Give the client to the server
Start the server with the client ID as a flag and the client secret as an environment variable:
```sh
CHECKEND_GOOGLE_CLIENT_SECRET= \
checkend serve --google-client-id .apps.googleusercontent.com
```
With the installer, put both in `/etc/checkend/checkend.env`, then restart the service.
> **Caution**
> Keep the client secret out of the command line. Use the environment variable, so that the secret is not in the process list.
### Connect a site
1. Run the connect command:
```sh
ce search connect example.com --google
```
Or select **Connect Search Console** on the Search tab, or use the MCP tool `connect_search_console`.
2. Open the consent page that Checkend prints. Use a Google account with access to the property.
3. Approve the access within 15 minutes.
The browser goes back to the Search tab.
The property is a domain property by default (`sc-domain:example.com`). For a URL-prefix property, use `--property`:
```sh
ce search connect example.com --google --property https://example.com/
```
The property must cover the site.
## Connect Bing Webmaster Tools
1. In Bing Webmaster Tools, open **Settings → API Access**, and copy the API key.
2. Give the key to Checkend on stdin:
```sh
echo "$BING_KEY" | ce search connect example.com --bing --api-key-stdin
```
Checkend checks the key. The site must be a verified site of the key's user.
> **Caution**
> Keep the Bing API key safe. It gives access to all sites of its user.
## How Checkend keeps the tokens
Checkend seals the Google refresh token and the Bing API key with AES-256-GCM. The key is the file `/secret.key`. A copy of the database without the key file does not give the tokens.
> **Danger**
> Back up `secret.key` with the data folder. Without it, you must connect each source again.
## The sync
A job syncs the search data each hour. Change the interval with `--search-every`.
- **Google:** the newest day is today minus 2 days (UTC). Each run gets the newest 4 days again, because Google changes recent data. The first run gets the newest 7 days. Then the backfill goes back 31 days in each run, until it reaches 16 months.
- **Bing:** Bing gives query rows and page rows, without a date range. Bing updates its data each week.
Checkend keeps the search rows permanently. The raw-event retention does not delete them. So you keep the history after the 16 months that Google gives.
After an error, the sync waits and tries again. The connection shows the error and the next step.
Run a sync now, or check the status:
```sh
ce search sync example.com
ce search status example.com
```
### Disconnect
```sh
ce search disconnect example.com google --yes
```
Disconnect deletes the stored token and revokes it at Google. The synced rows stay.
## Read the search data
```sh
ce search example.com --period 28d --by query
ce search example.com --by page
ce search example.com --by query --position 8-20
```
The answer has clicks, impressions, CTR and the average position, by query or by page. Page rows also have the Checkend page views, visitors, page views from Organic Search, the visitors who completed a goal, and the current title.
| Flag | Values |
|---|---|
| `--period` | `7d`, `28d` (default), `90d`, `365d`. The period ends at the newest search date. |
| `--by` | `query` or `page` |
| `--position` | A range, for example `8-20` for striking-distance rows |
| `--source` | `google`, `bing` or `all` |
The MCP tool is `search_performance`. The web UI has a Search tab.
### Limits of the search data
- Search Console data is 2 to 3 days late.
- Search Console gives at most 50,000 rows a day for each search type, and it leaves out rare queries. So the query totals are not equal to the page totals.
- Exit code 4 means that the site has no search data yet.
## The page crawler
A small crawler stores the title, meta description, H1, canonical URL and indexability of each page with traffic or search impressions. An agent needs this to propose a better title.
```sh
ce pages example.com --path /pricing
```
The MCP tool is `get_page`.
- The crawler runs each hour (`--crawl-every`). It crawls at most 100 pages of a site in each run, and a page again after 7 days.
- It obeys `robots.txt`. It reads the `CheckendBot` group, else `*`. When `robots.txt` gives a server error, it does not crawl.
- It waits 2 seconds between two requests to a site. It reads at most 2 MB of a page.
- Its user agent is `Mozilla/5.0 (compatible; CheckendBot/1.0; +)`.
- It does not follow redirects. It stores a redirect as "redirects to ".
- It refuses loopback, private and special addresses. `--crawler-allow-private` turns this protection off.
A page is indexable when it has status 200, is an HTML page, has no `noindex`, and has a canonical URL that is the page itself.
## URL inspection
With a Search Console connection, Checkend also inspects the top pages with the URL Inspection API. It uses at most 50 inspections a day for each site. A failed inspection makes an `index_problem` finding. `ce pages` and `get_page` show the newest inspection.
## Search findings
The search data feeds these rules: `striking_distance_query`, `low_ctr_for_position`, `content_decay`, `cannibalization`, `impressions_without_visits`, `index_problem` and `slow_page_losing_position`. See [The closed loop](/docs/concepts/closed-loop.md#the-rules).
## Prompt for a coding agent
```text
Find the pages of example.com that are close to the first search page. Replace example.com with my site.
1. Run `ce search status example.com`. If Google is not connected, tell me to run `ce search connect example.com --google`, and stop.
2. Run `ce search example.com --period 28d --by query --position 8-20`.
3. List the 10 queries with the most impressions, with their page, position and clicks.
4. For each page, suggest a better title and meta description from the code of this repository.
Do not change files yet. Wait for my choice.
```
---
# Web vitals, crawlers and IndexNow
> Read the web vitals of real visitors, see AI crawler hits from your access logs, and send changed pages to IndexNow.
This page covers three parts of analytics: the web vitals from real visitors, the AI and search crawler hits from your access logs, and the IndexNow submit of changed pages.
## Web vitals
The tracking script measures the Core Web Vitals of real visitors. It needs no change to your tag and no third-party library.
| Metric | What it measures | Good up to | Poor above |
|---|---|---|---|
| LCP | Largest Contentful Paint | 2,500 ms | 4,000 ms |
| INP | Interaction to Next Paint | 200 ms | 500 ms |
| CLS | Cumulative Layout Shift | 0.1 | 0.25 |
| TTFB | Time to First Byte | 800 ms | 1,800 ms |
The thresholds come from [web.dev](https://web.dev/articles/vitals).
### Read the web vitals
```sh
ce vitals example.com --period 7d
ce vitals example.com --path /pricing
```
The answer has the 75th percentile (p75) of each metric for the site and its top pages. Each value has n, the share of good values and the rating: `good`, `needs_improvement` or `poor`.
The MCP tool is `web_vitals`. The site page shows a **Web vitals** panel.
| Confidence | Reports |
|---|---|
| High | 100 or more |
| Medium | 20 or more |
| Low | Fewer than 20 |
> **Note**
> The web vitals are not tested for significance. Exit code 4 means that the period has no reports yet.
### What the script sends
The script sends one report for each page load, when the page is hidden first. A report has the page URL and the metrics that the browser measured. No report comes from a browser without the APIs, from automation, or from a `file:` page.
A report keeps no visitor ID and no location. A report is not a page view, so the page view numbers do not change.
A page with bad vitals that loses search position makes a `slow_page_losing_position` finding.
## AI crawlers from access logs
A JavaScript tag cannot see crawlers. To see GPTBot, ClaudeBot and the other crawlers, push the access log of your web server to Checkend.
```sh
ce logs push example.com /var/log/nginx/access.log
```
The token needs write access to the project of the site.
### Push the log each day
Run the push from cron on the web server, for example with the log of yesterday:
```sh
CHECKEND_URL=https://stats.example.com CHECKEND_TOKEN=ce_... \
ce logs push example.com /var/log/nginx/access.log.1
```
For a compressed log, read it from stdin:
```sh
zcat access.log.2.gz | ce logs push example.com -
```
### Formats and limits
- Formats: the combined format of Nginx and Apache, and the JSON access log of Caddy.
- At most 10 MB in one push. Split a larger file.
- A line longer than 16 KiB refuses the push.
- Lines older than 31 days, or in the future, are not kept.
- A Caddy line of another host is not kept.
- Crawler hits do not count against the daily event limit.
A second push of the same lines adds nothing to the data. The answer of each push counts the hits that it found and sent, so a second push shows the same count again.
### What Checkend keeps
Checkend keeps only the lines of known crawlers: GPTBot, OAI-SearchBot, ChatGPT-User, ClaudeBot, Claude-SearchBot, Claude-User, PerplexityBot, Perplexity-User, Googlebot, Bingbot, Applebot, CCBot, Bytespider, Amazonbot, Meta-ExternalAgent and DuckAssistBot.
For each hit, it keeps the UTC day, the path without the query, the bot name and the status. It keeps no IP address and no user agent. It drops the lines of people and other clients.
> **Note**
> Checkend does not check the user agent against the IP ranges of the crawler operators. A client can claim to be GPTBot. Google-Extended and Applebot-Extended are `robots.txt` tokens only: their fetches show as Googlebot and Applebot.
### Read the crawler hits
```sh
ce crawlers example.com --period 7d
ce crawlers example.com --bot GPTBot
```
The answer has the hits, the pages, the errors (status 400 or more) and the last day of each crawler, and the top pages. The MCP tool is `ai_crawlers`. The site page shows an **AI crawlers** panel.
## IndexNow
IndexNow tells Bing and other IndexNow search engines that pages changed. Checkend sends:
- the pages that the crawler finds changed (title, meta description, H1 or canonical URL),
- the page of each agent change on a finding with a page,
- the URLs that you submit.
### Turn IndexNow on
Only an admin can turn IndexNow on. There is no MCP action for it, because an agent must not send data to an outside service without the permission of an admin.
1. Turn IndexNow on for the site:
```sh
ce indexnow set example.com --on
```
Checkend makes a key of 32 hex characters.
2. Show the status and the key file snippet:
```sh
ce indexnow status example.com
```
3. Make the key file available at `https://example.com/.txt`. Use one of the two modes below.
### The key file
The search engines read the key file at the root of your site. A key file in a folder covers only the URLs of that folder.
**Mode `proxy` (default).** Checkend serves the key at `/indexnow/.txt`. Map `/.txt` on your site to it. Nginx example:
```nginx
location = /.txt {
proxy_pass https://stats.example.com/indexnow/.txt;
proxy_set_header Host stats.example.com;
proxy_ssl_server_name on;
}
```
`ce indexnow status` also gives snippets for Caddy and Next.js.
**Mode `manual`.** Put a file `.txt` with the key in the web root of your site:
```sh
ce indexnow set example.com --on --mode manual
```
To make a new key, add `--new-key`.
### Submit URLs
```sh
ce indexnow submit example.com https://example.com/pricing --reason "new prices"
```
The MCP tool `indexnow` can also submit URLs (write access). One submit takes at most 100 URLs, and only URLs on the host of the site. A URL that was submitted in the last 24 hours does not go to the queue again.
### The submit job
- The job runs every 10 minutes (`--indexnow-every`).
- It checks the key file first, and again after 7 days. When the key file is missing, it waits one hour and shows the next step.
- It sends at most 1,000 URLs of a site in 24 hours (`--indexnow-max-per-day`).
- The default endpoint is `https://api.indexnow.org/indexnow`. Select another one with `--indexnow-endpoint`, for example `https://www.bing.com/indexnow`.
`ce indexnow status` shows the key check, the last answer, the error with the next step, and the queue. Each submit writes an audit entry.
## Prompt for a coding agent
```text
Find the slowest pages of example.com and their causes. Replace example.com with my site.
1. Run `ce vitals example.com --period 7d`.
2. List the pages with a p75 LCP over 2.5 s, a p75 INP over 200 ms or a p75 CLS over 0.1.
3. For each page, find the probable cause in the code of this repository: large images, render-blocking scripts, layout shifts or slow server responses.
4. Propose one fix for each page. Do not change files yet.
```
---
# Webhooks
> Get a signed POST request when Checkend makes a finding, sets an outcome, records an annotation or makes a report.
A webhook lets Checkend start your agent. Your agent does not need to poll. Checkend sends a signed POST request to your URL for each event.
## Add a webhook
Only an admin can manage webhooks.
```sh
ce webhooks add https://agents.example.com/checkend
```
Checkend shows the signing secret one time. Keep it in your secret store.
To get only some events, repeat `--event`:
```sh
ce webhooks add https://agents.example.com/checkend --event finding.created --event report.ready
```
Other commands:
```sh
ce webhooks list
ce webhooks test # sends a ping event
ce webhooks deliveries # the delivery log
ce webhooks delete --yes
```
## Events
| Event | When | Content |
|---|---|---|
| `finding.created` | A rule makes a new finding. | The finding and the 7-day brief of the site. |
| `finding.updated` | A finding changes, for example a new outcome or a remeasure. | The finding with its `outcome`, and the brief. |
| `annotation.created` | Somebody records an annotation. | The annotation and the brief. |
| `disk.pressure` | The disk guard deleted old raw data. | `disk`: the disk use, the limit, the module, what was deleted, a note. No site and no brief. |
| `report.ready` | A scheduled report is ready. | The full report. No brief. |
| `ping` | You run `ce webhooks test`. | A test event. |
By default, a webhook gets all events.
## The request
The body is JSON: `{id, type, created_at, site, finding | annotation, brief}`. The brief is the same JSON as `GET /v1/brief`, so your agent gets the same data from push and from pull.
The request has three headers:
| Header | Content |
|---|---|
| `Checkend-Event` | The event type. |
| `Checkend-Delivery` | The event ID. Use it to drop duplicates. |
| `Checkend-Signature` | `t=,v1=` |
## Check the signature
The signature is HMAC-SHA256 with the secret over `.`.
1. Read `t` and `v1` from the `Checkend-Signature` header.
2. Refuse the request when `t` is more than 5 minutes old.
3. Compute HMAC-SHA256 with your secret over the string `.`. Use the raw body.
4. Compare the hex result with `v1` in constant time.
5. Refuse the request when they are not equal.
Example in Node.js:
```js
function verify(secret, header, rawBody) {
const parts = Object.fromEntries(header.split(",").map((p) => p.split("=")));
const age = Math.floor(Date.now() / 1000) - Number(parts.t);
if (!(age >= 0 && age <= 300)) return false;
const mac = crypto.createHmac("sha256", secret).update(`${parts.t}.${rawBody}`).digest("hex");
return mac.length === parts.v1.length &&
crypto.timingSafeEqual(Buffer.from(mac), Buffer.from(parts.v1));
}
```
## Retries
A delivery needs a 2xx answer. After a failure, Checkend waits 30 seconds, 2 minutes, 10 minutes, 1 hour and 6 hours. After 6 attempts, the delivery fails. Delivery is at least once, so drop duplicates with `Checkend-Delivery`.
`ce webhooks deliveries ` shows each attempt.
## Private targets are refused
Checkend refuses webhook URLs on loopback, private, link-local, shared (100.64.0.0/10) and other special addresses. It checks at create time and again at connect time, after DNS. It does not follow redirects.
For a self-hosted receiver on a private network, start the server with this flag:
```sh
checkend serve --webhook-allow-private
```
> **Caution**
> Use `--webhook-allow-private` only when you need a receiver on a private network. It turns off the protection against requests to your internal services.
## The secret at rest
Checkend must keep the secret to sign each request. It seals the secret with the key file `/secret.key`. The database never holds the secret in clear. A backup has `secret.key`, so the webhooks work after a restore.
> **Caution**
> Keep the webhook secret and `secret.key` safe. The secret signs the payloads, and the key file opens the stored secrets.
To change the secret, delete the webhook and add it again.
A server with another `secret.key` cannot open the secrets. Each delivery then fails with the reason, and `ce webhooks test` tells you to add the webhook again.
## Prompt for a coding agent
```text
Add a Checkend webhook receiver to this project.
1. Add an HTTPS endpoint that accepts the POST request of Checkend.
2. Check the signature of each request, as the Webhooks page below says. Refuse a request with a wrong signature or an old timestamp.
3. On `finding.created`, start the agent job of this project. Return a 2xx answer quickly.
4. Keep the signing secret in the secret store of the project, never in the code.
5. After the deploy, tell me to run `ce webhooks add --event finding.created`, then `ce webhooks test `.
```
---
# Scheduled reports
> Make a daily, weekly or monthly report of a site or a project, and get it as a webhook, through MCP or with ce report.
A report is the numbers of one window, against the window before, with significance tests. A schedule makes a report each day, week or month, and sends the webhook `report.ready`.
> **Note**
> Checkend sends no email. A report goes out as a webhook, and you read it with `ce report`, the MCP tool `get_report`, the REST API or the Reports view of the web UI.
## What a report contains
Each report covers one window in whole days of a time zone. It compares the window with the window before.
| Part | Content |
|---|---|
| Main numbers | Page views, visitors, conversions and the conversion rate, each with the change, the test, the p-value and the result. |
| Top lists | The top pages, sources and channels. |
| Goals | The numbers of each goal. |
| Revenue | The revenue for each currency, and the revenue of the window before. Checkend never adds currencies. |
| Web vitals | The p75 of each metric. |
| Search | The search totals of the window, when a search source is connected. |
| Findings | The findings opened and closed in the window. |
| Outcomes | The outcomes measured in the window. |
| Warnings | One sentence for each `got_worse` outcome of the window. |
| Annotations | The changes and releases of the window. |
| Summary | One fixed sentence for each site, and one for the loop. |
A report contains computed data and fixed templates only. No model writes its text.
### The windows
| Frequency | Window |
|---|---|
| Daily | The day before the run day. |
| Weekly | The 7 days before the run day. |
| Monthly | The calendar month before. |
The window before has the same number of days. For a monthly report, it is the month before.
### The tests
A count uses the Poisson rate test, with the hours of each window as the exposure. The conversion rate uses the two-proportion z-test. A metric has no test when the window before has no data. The report then says why.
## Add a schedule
Admins and Members can manage schedules.
```sh
ce report schedules add example.com --frequency weekly --weekday monday --hour 7 --time-zone Europe/Berlin
```
| Flag | Meaning | Default |
|---|---|---|
| `--frequency` | `daily`, `weekly` or `monthly`. Monthly runs on day 1. | Required |
| `--weekday` | The weekday of a weekly report. | Monday |
| `--hour` | The local hour, 0 to 23. | 6 |
| `--time-zone` | An IANA time zone, for example `Europe/Berlin`. | UTC |
| `--project` | A report of all sites of a project, in place of one site. | None |
You can also add schedules in the Reports view of the web UI. It proposes the time zone of your browser.
Other commands:
```sh
ce report schedules list
ce report schedules set --hour 8
ce report schedules delete --yes
```
An organization has at most 50 schedules. There are no MCP tools for schedules: an agent reads reports, and a person decides what Checkend sends.
## How a schedule runs
A job checks the schedules every 5 minutes. Change the interval with `--reports-every`. For each schedule, the job finds each run time of the last 30 days that has no report yet. It makes these reports, oldest first, one time for each window. It stores each report and sends `report.ready`.
Thus after downtime, the missed reports of the last 30 days come too. A run time before the schedule existed makes no report.
Checkend keeps reports for 13 months.
## The report.ready webhook
Add a webhook that gets the event `report.ready`:
```sh
ce webhooks add https://agents.example.com/checkend --event report.ready
```
The webhook body has the full report in `report`, and no brief. See [Webhooks](/docs/guides/webhooks.md) for the signature check.
## Read reports
```sh
ce report show example.com # the newest report of the site
ce report show --project --frequency monthly
ce report show example.com --date 2026-10-20 # the report whose window has this day
ce report list # the stored reports, newest first
```
To get the report as Markdown, add `--format md`. On a pipe, the JSON has the text in `markdown`:
```sh
ce report show example.com --format md | jq -r .markdown
```
The MCP tool `get_report` gives a report by `id`, or the newest report of a `site` or a `project`. With `format` set to `md`, it adds the Markdown text.
## Make a report now
`ce report run` makes a report of the newest complete window now. It needs write access. It sends no webhook.
```sh
ce report run example.com --frequency weekly --time-zone Europe/Berlin
```
Add `--dry-run` to check the request. Checkend then makes no report.
## Prompt for a coding agent
```text
Set up a weekly Checkend report for example.com. Replace example.com with my site.
1. Ask me for my time zone, for example Europe/Berlin.
2. Run `ce report schedules add example.com --frequency weekly --weekday monday --hour 7 --time-zone `.
3. Run `ce report run example.com --frequency weekly --time-zone ` to make the first report now.
4. Run `ce report show example.com --format md` and summarize the report for me in five lines.
```
---
# People and invites
> Invite people with a one-time link, change their roles and remove them.
An admin invites people with a one-time link. Each person signs in with a passkey and has one role. For the roles, see [Roles and tokens](/docs/concepts/roles-and-tokens.md).
## Invite a person
1. Make the invite. Only an admin can do this:
```sh
ce people invite ana@example.com --role member
```
The role is `admin`, `member` or `viewer`. You can also invite people on the People screen of the web UI.
2. Copy the link that Checkend prints. It looks like `https://stats.example.com/invite#ceinv_...`.
3. Give the link to the person in a safe channel.
> **Note**
> Checkend sends no email. You give the link to the person yourself.
The link works one time, for 7 days. One email address has at most one pending invite.
> **Caution**
> Keep the invite link private. Anybody with the link can join with the role of the invite.
## Accept an invite
The person opens the link in a browser:
1. The page shows the email address, the role and the end date of the invite.
2. Select **Create a passkey**.
3. The web UI opens with a session.
The page removes the secret from the address bar. A used, revoked or expired link shows the reason. Then ask an admin for a new invite link.
### Without a passkey
Passkeys need HTTPS on a domain name, or `localhost`. On an IP address or plain HTTP, the page offers **Create an API token** in place of a passkey. It shows the token one time.
The person can also accept the invite with `ce`:
```sh
ce invites accept 'https://stats.example.com/invite#ceinv_...'
```
`ce` makes an API token with the scopes of the role, and stores it as `ce login` does.
## Manage invites
```sh
ce invites list # pending, accepted, expired and revoked invites
ce invites revoke # stop a pending invite
```
Only an admin can list and revoke invites.
## List people
```sh
ce people list
```
The list shows the email address, the role, the last sign-in and the passkeys of each person. Admins and Members can see the list. A Viewer gets exit code 5.
## Change a role
```sh
ce people role ana@example.com viewer
```
The change takes effect on the next request of the person. Checkend revokes each token of the person that is outside the new role.
Nobody can raise their own role. The last admin stays: make another person admin first.
## Remove a person
```sh
ce people remove ana@example.com --yes
```
Checkend ends the sessions of the person at once, revokes all their API tokens and MCP app tokens, and deletes their passkeys.
## Passkeys
Each person can have more than one passkey. Add a passkey in the web UI, in **Settings**. List and delete your own passkeys with `ce`:
```sh
ce passkeys list
ce passkeys delete --yes
```
## Agents are not people
There are no MCP tools for people and invites. An agent must not invite people, change roles or remove people. Use `ce` or the web UI.
---
# Backups and upgrades
> Back up and restore the data folder, keep backups and the lake in an S3-compatible bucket, set the retention and upgrade the server.
Checkend keeps all data in one data folder, for example `/var/lib/checkend`. This page shows how to back it up, restore it, limit its size and upgrade the server.
> **Danger**
> Do not lose `/secret.key`. It opens the stored Search Console and Bing tokens and the webhook secrets. Without it, you must connect each search source and add each webhook again.
## What a backup contains
- `app.sqlite` (the metadata) and `lake.sqlite` (the lake catalog), as consistent copies,
- the Parquet files of the lake (with the lake on a bucket, the backup reads them from the bucket),
- `secret.key`,
- `manifest.json`, with the size and the SHA-256 of each file.
The event log is not in a backup. The events that were not yet in the lake at the time of the backup are lost after a restore.
> **Caution**
> Keep a backup as safe as the data folder. It contains `secret.key`.
## Make a backup by hand
A backup works while the server runs:
```sh
checkend backup create --data /var/lib/checkend --to /backups/checkend
```
With the lake on a bucket, add the same `--lake-bucket` as the server:
```sh
checkend backup create --data /var/lib/checkend --to /backups/checkend \
--lake-bucket s3:///checkend
```
The scheduled backup uses the `--lake-bucket` flag of the server.
List the backups in a folder:
```sh
checkend backup list --from /backups/checkend
```
## Scheduled backups
Add these flags to `CHECKEND_FLAGS` in `/etc/checkend/checkend.env`:
```sh
--backup-dir /backups/checkend --backup-every 24h --backup-keep 7
```
| Flag | Meaning | Default |
|---|---|---|
| `--backup-dir` | A folder, or `s3:///`. Without it, there is no scheduled backup. | None |
| `--backup-every` | The interval. | 24 hours |
| `--backup-keep` | The count of backups to keep. | 7 |
With the installer, the systemd service can write only to `/var/lib/checkend`. Add the backup folder to `ReadWritePaths`:
1. Open an override file:
```sh
sudo systemctl edit checkend
```
2. Add these lines:
```ini
[Service]
ReadWritePaths=/backups/checkend
```
3. Restart the service:
```sh
sudo systemctl restart checkend
```
## The bucket
An S3-compatible bucket can hold the backups and the Parquet files of the lake. Examples are AWS S3, MinIO, Cloudflare R2 and Hetzner Object Storage. The bucket must exist. Make it in the console of your provider.
Set the bucket access in the environment of the server. For the service, put the variables in `/etc/checkend/checkend.env`:
| Variable | Meaning | Default |
|---|---|---|
| `CHECKEND_S3_ENDPOINT` | The endpoint URL. | `https://s3..amazonaws.com` |
| `CHECKEND_S3_REGION` | The region. | `us-east-1` |
| `CHECKEND_S3_ACCESS_KEY_ID` | The access key ID. | None |
| `CHECKEND_S3_SECRET_ACCESS_KEY` | The secret access key. | None |
| `CHECKEND_S3_URL_STYLE` | `path`, or `vhost` for the bucket in the host name. | `path` |
Checkend uses Signature Version 4. An example for Hetzner Object Storage in `fsn1`:
```sh
CHECKEND_S3_ENDPOINT=https://fsn1.your-objectstorage.com
CHECKEND_S3_REGION=fsn1
CHECKEND_S3_ACCESS_KEY_ID=...
CHECKEND_S3_SECRET_ACCESS_KEY=...
CHECKEND_S3_URL_STYLE=vhost
```
[Hetzner documents](https://docs.hetzner.com/storage/object-storage/getting-started/using-s3-api-tools/) the endpoint `https://.your-objectstorage.com` and URLs with the bucket in the host name.
> **Note**
> Checkend was tested only with MinIO and path-style URLs. It was not tested with Hetzner or with other providers.
### Backups in the bucket
Use `s3:///` in place of a folder, in `--backup-dir`, `--to` and `--from`:
```sh
checkend backup create --data /var/lib/checkend --to s3:///backups
```
### The lake on the bucket
The Parquet files of the lake can go to the bucket. The lake catalog (`lake.sqlite`) and the metadata (`app.sqlite`) stay in the data folder.
1. Add `--lake-bucket s3:///checkend` to `CHECKEND_FLAGS` in `/etc/checkend/checkend.env`.
2. For a server that already has data, stop the server:
```sh
sudo systemctl stop checkend
```
3. Copy the Parquet files to the bucket. A second run copies only the new files.
```sh
sudo -u checkend env $(grep CHECKEND_S3 /etc/checkend/checkend.env | xargs) \
checkend lake push --data /var/lib/checkend --to s3:///checkend
```
4. Start the server:
```sh
sudo systemctl start checkend
```
5. Run `checkend doctor`. The check `lake_files` must be OK.
```sh
checkend doctor --data /var/lib/checkend --lake-bucket s3:///checkend
```
6. When the server works on the bucket, delete `/var/lib/checkend/objects`.
With `--lake-bucket`, the first start also downloads the DuckDB extension `httpfs`. DuckDB keeps the bucket key in memory only. The writer sends new data to the bucket about every 5 seconds. The compaction job merges the small files each hour. The retention and the compaction also work on the bucket.
The server does not start when the catalog names data files and the lake store has none of them. The cause is a wrong `--lake-bucket`, or a missing `checkend lake push`. The message tells you what to do.
> **Caution**
> Do not give the same `--lake-bucket` prefix to two servers. Each server has its own catalog, and the cleanup of one server deletes the files of the other.
### Litestream
[Litestream](https://litestream.io) copies `app.sqlite` and `lake.sqlite` to the bucket each second. It is optional, and Checkend does not start it. Checkend was tested with Litestream v0.5.17.
The Checkend source has an example configuration, `deploy/litestream.yml`. It takes its values from the environment. Copy it to the server, and give its path to `-config`:
```sh
CHECKEND_DATA=/var/lib/checkend CHECKEND_LITESTREAM_BUCKET= \
CHECKEND_LITESTREAM_PREFIX=checkend/litestream \
CHECKEND_S3_ENDPOINT=... CHECKEND_S3_REGION=... CHECKEND_S3_ACCESS_KEY_ID=... CHECKEND_S3_SECRET_ACCESS_KEY=... \
litestream replicate -config litestream.yml
```
Run Litestream as the user `checkend`, as its own systemd service. `checkend doctor` shows a `litestream` check. With the lake on a bucket, it warns when Litestream does not copy `lake.sqlite`.
> **Caution**
> Keep `secret.key` and a scheduled backup too. Litestream does not copy `secret.key`. Without it, the sealed tokens and webhook secrets do not open.
### Restore from the bucket
Use these steps on a new server with the same bucket.
1. Install Checkend with `--no-start`.
2. Copy `secret.key` from a backup into the new data folder. Give it mode 0600 and the owner `checkend`.
3. Restore the two SQLite files into the data folder:
- With Litestream:
```sh
litestream restore -config litestream.yml -o /var/lib/checkend/app.sqlite /var/lib/checkend/app.sqlite
litestream restore -config litestream.yml -o /var/lib/checkend/lake.sqlite /var/lib/checkend/lake.sqlite
```
- Without Litestream, restore the newest backup. The restore writes a Parquet file to the bucket only when the bucket does not have it.
```sh
checkend backup restore --from s3:///backups --data /var/lib/checkend \
--lake-bucket s3:///checkend
```
4. Add the same `--lake-bucket` to `CHECKEND_FLAGS`.
5. Start the server.
6. Run `checkend doctor`. The checks `lake_files` and `data_folder` must be OK.
The events of the last seconds before the loss are lost. They were in the event log, and not yet in the lake.
## Restore a backup
1. Stop the server:
```sh
sudo systemctl stop checkend
```
2. Restore the newest backup:
```sh
checkend backup restore --from /backups/checkend --data /var/lib/checkend
```
To restore an older backup, add `--id `. Get the ID from `checkend backup list`. With the lake on a bucket, add `--lake-bucket`. The restore then writes only the Parquet files that the bucket does not have.
3. Start the server:
```sh
sudo systemctl start checkend
```
The restore checks every file before it writes. A data folder that already has data needs `--force`. The old data then moves to a folder next to it.
> **Caution**
> Make sure that the user `checkend` owns the restored files before you start the server. A restore as root can leave files that the server cannot write:
>
> ```sh
> sudo chown -R checkend:checkend /var/lib/checkend
> ```
## Retention
Checkend keeps the raw events for 24 months by default. A nightly job deletes the older months as whole months.
- The daily rollups stay permanently, so the numbers of old days stay.
- The search rows stay permanently.
- After a month is deleted, a visitor counts one time on each day of that month.
Show and change the retention (admin):
```sh
ce retention
ce retention set analytics --months 18 --dry-run
ce retention set analytics --months 18
```
## The disk guard
The disk guard protects the server from a full disk. It runs every 10 minutes.
When the disk use goes above the limit, it deletes the oldest raw month first. It never deletes the current or the previous month. The rollups stay.
| Flag | Meaning | Default |
|---|---|---|
| `--disk-limit` | The disk use in percent above which the guard deletes. `0` turns it off. | 90 |
| `--disk-budget` | The raw-data budget of a module, for example `analytics=50GiB`. | None |
| `--disk-guard-every` | The interval. | 10 minutes |
The disk guard tells you in three ways:
- an audit entry `data.delete_raw` for each deleted month,
- the webhook event `disk.pressure`,
- a failed `disk` check in `ce health` and `checkend doctor`.
## Upgrades
### With the installer
1. Make a backup.
2. Run the installer again:
```sh
curl -fsSL [INSTALL_URL] | sudo sh
```
It downloads the new version only when the version is different. It replaces the binaries and restarts the server.
3. Check the server:
```sh
checkend doctor --data /var/lib/checkend --public-url https://stats.example.com
```
### With Docker
1. Make a backup.
2. Pull the new image.
3. Start a new container on the same volume.
### Migrations
The new binary runs the pending migrations at start. You never run a migration by hand. Migrations only go forward.
Before it migrates `app.sqlite`, Checkend copies the file to `app.sqlite.before-migration-` (mode 0600). This is the rollback copy.
> **Caution**
> Delete `app.sqlite.before-migration-` when the new version works. The copy can hold old secrets in clear, for example the webhook secrets of an older version.
## Check the health
After a backup, a restore or an upgrade, run the checks in [Troubleshooting](/docs/guides/troubleshooting.md).
---
# Troubleshooting
> Check the health of the server with checkend doctor and ce health, read the ce exit codes, and fix common errors.
Checkend has three health checks. Start with the check that fits your access.
| Check | Where it runs | What it checks |
|---|---|---|
| `checkend doctor` | On the server | The files: disk, data folder, `secret.key`, data and cache disks, lake files, Litestream, GeoIP, DuckDB extensions, backups, jobs, certificates. |
| `ce health` | Anywhere, admin token | The running server: also the event log and the writer lag. |
| `checkend healthcheck` | On the server, or in a container | Only that `/health` answers. |
## checkend doctor
Run it on the server:
```sh
checkend doctor --data /var/lib/checkend
```
Give it the same `--cache-dir` and `--lake-bucket` as the server:
```sh
checkend doctor --data /var/lib/checkend --cache-dir /var/cache/checkend \
--lake-bucket s3:///checkend
```
It reads the files of the data folder directly. See [What checkend doctor checks for disks](/docs/start/install.md#what-checkend-doctor-checks-for-disks) for the disk warnings. It checks:
- the disk use,
- the data folder and `secret.key` (mode 0600),
- the data and cache disks: the file system, the own mount, the free space and a short write test (`--no-speed-test` turns the test off),
- the live lake files in the lake store (`lake_files`),
- Litestream: with the lake on a bucket, a warning when Litestream does not copy `lake.sqlite`,
- the GeoIP file,
- the DuckDB extensions,
- the backups (add `--backup-dir ` to check a backup folder),
- the newest run and error of each job,
- the certificates of `--domain`: a warning under 14 days, a failure when a certificate has expired.
With `--public-url`, it also gets `/health` as a browser does, and shows the end date of the certificate:
```sh
checkend doctor --data /var/lib/checkend --public-url https://stats.example.com
```
## ce health
```sh
ce health
```
`ce health` needs an admin token. It shows the disk use and the disk guard, the data and cache disks without the write test, the DuckDB settings, the data folder and `secret.key`, the GeoIP file, the DuckDB extensions, the event log and the writer lag, the backups, and the newest run and error of each job. The web UI shows the same report in **Settings → Health**.
It exits with 0 when the status is `ok` or `warn`, and with 1 when the status is `fail`. A monitor can run it.
## checkend healthcheck
```sh
checkend healthcheck --url http://127.0.0.1:8080/health
```
It exits with 0 when the server answers. The Docker image uses it. `/health` is a small public check that answers `{"status": "ok"}`.
## Exit codes of ce
| Code | Meaning | What to do |
|---|---|---|
| 0 | OK. | Read stdout. |
| 1 | Other error: a bad flag or value, a conflict, a server error. Also `ce health` with the status `fail`. | Read the error message. |
| 2 | Not authenticated: no token, a token that is not valid, or no server. | Run `ce login `. |
| 3 | Not found. | Run `ce sites list`, or the `next` command of the error. |
| 4 | No data yet. stdout has the empty answer. | Run `ce sites verify `. |
| 5 | Forbidden: the token is valid, but it has no scope or role for the action. Also a wrong or used setup token. | Ask an admin for a token with the scope. |
On a pipe, or with `--json`, each error is one JSON object on stderr:
```json
{"error": "not_authenticated", "message": "...", "next": "ce login https://stats.example.com"}
```
| `error` | Exit code | When |
|---|---|---|
| `usage` | 1 | A bad flag, argument or value, an unknown command, or no `--yes` without a terminal. |
| `invalid_input` | 1 | The server refused a value (HTTP 400). |
| `conflict` | 1 | The action is not allowed now (HTTP 409). |
| `rate_limited` | 1 | HTTP 429. |
| `server_error` | 1 | HTTP 5xx. |
| `connection_failed` | 1 | No answer from the server. |
| `health_fail` | 1 | `ce health`: the status is `fail`. |
| `credentials` | 1 | The config file, the keychain or the credentials file failed. |
| `error` | 1 | All other errors, for example a local file that is not there. |
| `not_authenticated` | 2 | HTTP 401. |
| `no_server` | 2 | No context and no `CHECKEND_URL`. |
| `not_found` | 3 | HTTP 404. |
| `no_data` | 4 | The answer is empty. |
| `forbidden` | 5 | HTTP 403. |
## Common problems
### ce says "no_server"
`ce` has no default server. Log in, or set the URL:
```sh
ce login https://stats.example.com
```
In CI, set `CHECKEND_URL` and `CHECKEND_TOKEN`.
### ce refuses the credentials file
`ce` refuses to read the credentials file when other users can read it. Set the mode:
```sh
chmod 600 ~/.config/checkend/credentials
```
### The passkey does not work
Passkeys need HTTPS on a domain name, or `localhost`.
- On `127.0.0.1`, open the URL with `localhost`.
- On a remote server without a domain, use an SSH tunnel: `ssh -L 8080:localhost:8080 you@server`.
- On an IP address only, use the API token way of the setup page or the invite page.
### The setup URL does not work
A restart of the server makes a new setup URL. Read the new URL in `/setup-url`, in the server log, or with `docker logs checkend 2>&1 | grep setup`. After the first admin exists, the setup URL does not work again.
### No events arrive
1. Check the install:
```sh
ce sites verify example.com --send-test
```
2. If the test event arrives, but page views do not, check the script tag on the site. The `data-site` value must be the site domain.
3. Check that the page URL is on the site domain or a subdomain. Checkend refuses events for other hosts.
4. Check that the visitor is not on a hosting network. The data-center filter drops these events.
5. Check the Content-Security-Policy of the site. Add your Checkend URL to `script-src` and `connect-src`.
### All visitors have one address, or the countries are wrong
The server is behind a proxy, but `--trusted-proxies` is not set. Add the proxy address to `--trusted-proxies`. With the first-party proxy route, also add the address of the site's proxy. See [Install Checkend](/docs/start/install.md#behind-a-reverse-proxy).
### An MCP client refuses the server metadata
Set `--public-url` to the URL that the MCP client uses. The issuer and the metadata URLs come from it.
### Webhook deliveries fail
1. Read the delivery log:
```sh
ce webhooks deliveries
```
2. Check that the receiver answers with a 2xx status.
3. Check that the URL is not on a private address. Checkend refuses private targets unless the server runs with `--webhook-allow-private`.
4. After a restore with another `secret.key`, delete the webhook and add it again.
### The Search Console sync shows an error
Run `ce search status example.com`. It shows the error and the next step. Check that the redirect URI of your OAuth client is `/v1/search/oauth/callback`.
### The disk is full
The disk guard deletes the oldest raw months above the disk limit (90 % by default). Check `ce health`, then lower the retention or add disk space. See [Backups and upgrades](/docs/guides/backups-and-upgrades.md#the-disk-guard).
### The server does not start: the data folder is not on its own mount
The server runs with `--require-mount`, and the data folder is not on its own mounted file system. The installer sets this flag with `--data-dir`. Usually the volume is not mounted.
1. Run `df -h`, and look for the mount folder of the volume.
2. Mount the volume, or attach it to the server again.
3. Start the server: `sudo systemctl start checkend`.
4. Run `checkend doctor`. The check `data_folder` must be OK.
See [Disks, folders and memory](/docs/start/install.md#disks-folders-and-memory).
### The server does not start: the lake store has no data files
The lake catalog names data files, but the lake store has none of them. The cause is a wrong `--lake-bucket`, or a missing `checkend lake push`. The message tells you what to do. See [The lake on the bucket](/docs/guides/backups-and-upgrades.md#the-lake-on-the-bucket).
### The first start is slow
The first start downloads the DuckDB extensions (about 85 MB) and the DB-IP file (about 60 MB compressed). For a server without network access, see [Offline install](/docs/start/install.md#offline-install).
## Prompt for a coding agent
```text
Check the health of my Checkend server.
1. Run `ce health`. It needs an admin token. Exit code 1 means that a check failed.
2. If you can reach the server shell, run `checkend doctor --data /var/lib/checkend`.
3. For each failed check, find the fix on the Troubleshooting page below.
4. Report each problem with its fix. Do not change the server without my approval.
```
---
# CLI reference
> Each command of ce, the Checkend CLI: usage, flags, examples and exit codes, for people, scripts and coding agents.
> **Note: Generated page**
> Generated from the Checkend repository at `ee1e2be` (`ee1e2be`) by `npm run sync`. Do not edit this page by hand.
The `ce` client runs on a laptop, in CI or in an agent sandbox. The `checkend` server binary also contains all `ce` commands.
```text
Usage: ce [flags]
Commands:
annotate Record a change, a release or a note on a site
annotations List the annotations of a site
audit Show the audit log of writes by tokens (admin)
brief Show the brief: top changes, findings, annotations, outcomes
context List the contexts, or select one
crawlers Show the AI and search crawler hits of a site (from pushed access logs)
event Send one custom event from a server or a script
findings List, show, claim, finish, dismiss, reopen and remeasure findings
goals List, add and delete the goals of a site
health Show the health of the server: disk, event log, backups, jobs (admin)
indexnow Show, set and use the IndexNow submit of changed URLs of a site
invites List and revoke invites (admin); accept an invite link with a token
login Log in to a server: in the browser (device flow), or with an API token
logout Remove a context and its token
logs Push a web server access log, for the AI crawler data
mcp Run a stdio MCP server that forwards each tool to the server's /mcp endpoint
pages Show what the crawler found on the pages of a site (title, H1, indexability)
passkeys List and delete the passkeys of your user (add one in the web UI, Settings)
people List people, invite a person, change a role, remove a person (admin)
projects List projects, set the daily event limit (admin)
query Show page views, visitors, top pages and conversions of a site
report Show, list and make reports, and manage the report schedules
retention Show or set the raw-data retention of each module (admin)
search Show Search Console and Bing rows; connect, check, sync and disconnect the sources
setup Make the first admin with the setup token, and log in
sites List, add, change, delete and verify sites
tokens List, create and revoke API tokens (admin; each person: own tokens)
vitals Show the web vitals (p75 of LCP, INP, CLS, TTFB) of a site and its pages
webhooks Add, list, delete and test webhooks (admin)
version Show the version
Run "ce --help" for the flags and examples of a command.
For CI and agents, set CHECKEND_URL and CHECKEND_TOKEN.
On a pipe, stdout has one JSON value and an error is a JSON object on stderr.
Exit codes: 0 OK, 1 error, 2 not authenticated or no server, 3 not found,
4 no data yet, 5 forbidden (the token has no scope for the action).
```
| Command | What it does |
|---|---|
| [`ce annotate`](#ce-annotate) | Record a change, a release or a note on a site |
| [`ce annotations`](#ce-annotations) | List the annotations of a site |
| [`ce audit`](#ce-audit) | Show the audit log of writes by tokens (admin) |
| [`ce brief`](#ce-brief) | Show the brief: top changes, findings, annotations, outcomes |
| [`ce context`](#ce-context) | List the contexts, or select one |
| [`ce crawlers`](#ce-crawlers) | Show the AI and search crawler hits of a site (from pushed access logs) |
| [`ce event`](#ce-event) | Send one custom event from a server or a script |
| [`ce findings`](#ce-findings) | List, show, claim, finish, dismiss, reopen and remeasure findings |
| [`ce goals`](#ce-goals) | List, add and delete the goals of a site |
| [`ce health`](#ce-health) | Show the health of the server: disk, event log, backups, jobs (admin) |
| [`ce indexnow`](#ce-indexnow) | Show, set and use the IndexNow submit of changed URLs of a site |
| [`ce invites`](#ce-invites) | List and revoke invites (admin); accept an invite link with a token |
| [`ce login`](#ce-login) | Log in to a server: in the browser (device flow), or with an API token |
| [`ce logout`](#ce-logout) | Remove a context and its token |
| [`ce logs`](#ce-logs) | Push a web server access log, for the AI crawler data |
| [`ce mcp`](#ce-mcp) | Run a stdio MCP server that forwards each tool to the server's /mcp endpoint |
| [`ce pages`](#ce-pages) | Show what the crawler found on the pages of a site (title, H1, indexability) |
| [`ce passkeys`](#ce-passkeys) | List and delete the passkeys of your user (add one in the web UI, Settings) |
| [`ce people`](#ce-people) | List people, invite a person, change a role, remove a person (admin) |
| [`ce projects`](#ce-projects) | List projects, set the daily event limit (admin) |
| [`ce query`](#ce-query) | Show page views, visitors, top pages and conversions of a site |
| [`ce report`](#ce-report) | Show, list and make reports, and manage the report schedules |
| [`ce retention`](#ce-retention) | Show or set the raw-data retention of each module (admin) |
| [`ce search`](#ce-search) | Show Search Console and Bing rows; connect, check, sync and disconnect the sources |
| [`ce setup`](#ce-setup) | Make the first admin with the setup token, and log in |
| [`ce sites`](#ce-sites) | List, add, change, delete and verify sites |
| [`ce tokens`](#ce-tokens) | List, create and revoke API tokens (admin; each person: own tokens) |
| [`ce vitals`](#ce-vitals) | Show the web vitals (p75 of LCP, INP, CLS, TTFB) of a site and its pages |
| [`ce webhooks`](#ce-webhooks) | Add, list, delete and test webhooks (admin) |
| [`ce version`](#ce-version) | Show the version |
## ce annotate
Record a change, a release or a note on a site.
```text
Usage: ce annotate "" [--kind agent_change|release|note] [--date ]
[--finding ] [--link ] [--dry-run]
Records a change on a site at a time, so that Checkend can measure it. Agents
record each change that they make (kind agent_change, the default). CI records
releases (kind release). --date is RFC 3339 or a UTC date; default now.
Needs write access to the site's project.
Examples:
ce annotate example.com "Title changed on /pricing" --finding f_8k2 --link https://github.com/acme/web/pull/7
ce annotate example.com "v1.4.0" --kind release
Flags:
-context string
the context to use (see "ce context list")
-date string
the time of the change: RFC 3339 or a UTC date (default now)
-dry-run
show the change, do not make it
-finding string
the ID of the finding that the change is for
-json
print JSON, also for errors (default when stdout is not a terminal)
-kind string
agent_change (default), release or note
-link string
a URL about the change, for example the pull request
-plain
Quiet output: plain columns (also env CHECKEND_PLAIN=1)
-url string
the server URL (default: the current context, or env CHECKEND_URL)
```
## ce annotations
List the annotations of a site.
```text
Usage: ce annotations list [--period 24h|7d|30d|365d]
Lists the annotations of a site (agent changes, releases, notes), newest first.
Example:
ce annotations list example.com --period 7d
Flags:
-context string
the context to use (see "ce context list")
-json
print JSON, also for errors (default when stdout is not a terminal)
-period string
the period: 24h, 7d, 30d (default) or 365d
-plain
Quiet output: plain columns (also env CHECKEND_PLAIN=1)
-url string
the server URL (default: the current context, or env CHECKEND_URL)
```
## ce audit
Show the audit log of writes by tokens (admin).
```text
Usage: ce audit [--limit 100] [--token ] [--action ] [--before ]
Prints the audit log, newest first: each write by a token (sites, goals,
tokens, annotations, findings, webhooks), with the token, its scopes and the
interface (rest, mcp, cli, ui or job). Admin only.
Example:
ce audit --token tok_0192... --limit 20
ce audit --action finding
Flags:
-action string
only these writes: an action (finding.claim) or its first part (finding)
-before string
only entries older than this entry ID (the next page)
-context string
the context to use (see "ce context list")
-json
print JSON, also for errors (default when stdout is not a terminal)
-limit int
the most entries (default 100, at most 500)
-plain
Quiet output: plain columns (also env CHECKEND_PLAIN=1)
-token string
only the writes of this token ID
-url string
the server URL (default: the current context, or env CHECKEND_URL)
```
## ce brief
Show the brief: top changes, findings, annotations, outcomes.
```text
Usage: ce brief [] [--project ] [--period 24h|7d|30d|365d]
Prints the brief: the top changes against the period before (with a
significance test), the open findings, the annotations and releases, the
outcomes of earlier changes and the data date of each site. Without a site or
a project: all sites that the token can read. Agents: start here.
Examples:
ce brief example.com
ce brief --project prj_default --period 30d --json
Flags:
-context string
the context to use (see "ce context list")
-json
print JSON, also for errors (default when stdout is not a terminal)
-period string
the period: 24h, 7d (default), 30d or 365d
-plain
Quiet output: plain columns (also env CHECKEND_PLAIN=1)
-project string
one project
-url string
the server URL (default: the current context, or env CHECKEND_URL)
```
## ce context
List the contexts, or select one.
```text
Usage: ce context list
ce context use
"list" shows each context with its server URL and the store of its token:
keychain (the OS keychain) or file (the credentials file, when no keychain
works).
Flags:
-json
print JSON, also for errors (default when stdout is not a terminal)
-plain
Quiet output: plain columns (also env CHECKEND_PLAIN=1)
```
## ce crawlers
Show the AI and search crawler hits of a site (from pushed access logs).
```text
Usage: ce crawlers [--period 24h|7d|30d|365d] [--bot ] [--limit 20]
Shows the hits of AI and search crawlers (GPTBot, ClaudeBot, PerplexityBot,
Googlebot, Bingbot and others) on the pages of a site, from the access logs that
you push with "ce logs push". A JavaScript tag cannot see crawlers. Exit code 4
means that the period has no crawler hits yet.
Examples:
ce crawlers example.com --period 7d
ce crawlers example.com --bot GPTBot --json
Flags:
-bot string
one crawler, for example GPTBot
-context string
the context to use (see "ce context list")
-json
print JSON, also for errors (default when stdout is not a terminal)
-limit int
the most pages (default 20)
-period string
24h, 7d, 30d (default) or 365d
-plain
Quiet output: plain columns (also env CHECKEND_PLAIN=1)
-url string
the server URL (default: the current context, or env CHECKEND_URL)
```
## ce event
Send one custom event from a server or a script.
```text
Usage: ce event send --name [--revenue --currency ]
[--prop key=value]... [--page-url ] [--vid ] [--dry-run]
Sends one custom event from a server or a script, for example a purchase from a
payment webhook. Needs a token with write access to the site's project. The
server keeps no IP address, user agent or location for it. --vid gives a visitor
ID only for a site in extended privacy mode.
Examples:
ce event send example.com --name Signup
ce event send example.com --name Purchase --revenue 49.90 --currency USD --prop plan=pro
Flags:
-context string
the context to use (see "ce context list")
-currency string
the ISO 4217 currency code of the revenue, for example USD
-dry-run
show the event, do not send it
-json
print JSON, also for errors (default when stdout is not a terminal)
-name string
the custom event name (required)
-page-url string
the page URL on the site (default: https:///)
-plain
Quiet output: plain columns (also env CHECKEND_PLAIN=1)
-prop value
a custom property key=value (repeat the flag for more)
-revenue string
the revenue amount, for example 49.90 (needs --currency)
-url string
the server URL (default: the current context, or env CHECKEND_URL)
-vid string
a persistent visitor ID (extended privacy mode only)
```
## ce findings
List, show, claim, finish, dismiss, reopen and remeasure findings.
```text
Usage: ce findings list [--site ] [--project ] [--status open,claimed] [--limit 50]
[--token ] [--action ] [--period 24h|7d|30d|365d]
ce findings show
ce findings claim [--note ] [--dry-run]
ce findings done (--annotation | --text "" [--link ] [--date ]) [--note ] [--dry-run]
ce findings dismiss [--note ] [--dry-run]
ce findings reopen [--note ] [--dry-run]
ce findings remeasure [--note ] [--dry-run]
ce findings run
A finding is a fact that a rule found, with evidence. Claim a finding before
you work on it. "done" needs the annotation of the change; Checkend starts to
measure on the next UTC day, and sets the outcome when the 14 days after the
change day are complete: worked, got_worse, no_clear_change or
too_few_to_compare. got_worse means that the change made the metric worse.
"remeasure" computes the outcome again and keeps the history (admin).
--dry-run asks the server to check the change without making it. "run" runs
the rules and the outcomes now (admin; the job runs each hour).
Examples:
ce findings list --status open
ce findings claim f_8k2
ce findings done f_8k2 --text "Title changed on /pricing" --link https://github.com/acme/web/pull/7
Flags:
-action string
list: action types, comma-separated, for example fix_redirect
-annotation string
done: the ID of the annotation of the change
-context string
the context to use (see "ce context list")
-date string
done: the time of the change for the new annotation (default now)
-dry-run
check the change on the server, do not make it
-json
print JSON, also for errors (default when stdout is not a terminal)
-limit int
list: the most findings (default 50)
-link string
done: a URL about the change for the new annotation
-note string
a note for the history
-period string
list: only findings with a change in this period: 24h, 7d, 30d or 365d
-plain
Quiet output: plain columns (also env CHECKEND_PLAIN=1)
-project string
list: one project
-site string
list: one site
-status string
list: statuses, comma-separated (default: all)
-text string
done: the text of a new annotation of the change
-token string
list: only the findings that this token (name) claimed
-url string
the server URL (default: the current context, or env CHECKEND_URL)
```
## ce goals
List, add and delete the goals of a site.
```text
Usage: ce goals list
ce goals add (--event | --page ) [--dry-run]
ce goals delete [--yes] [--dry-run]
A goal is a custom event name (--event, for example Signup) or a page path
pattern (--page, for example /pricing or /blog/*; "*" matches any characters).
"ce query " shows the conversions, the conversion rate and the revenue
of each goal. "add" and "delete" need write access to the site's project.
Examples:
ce goals add example.com signup --event Signup
ce goals add example.com pricing --page '/pricing*'
ce goals list example.com --plain
ce goals delete example.com goal_0192... --yes
Flags:
-context string
the context to use (see "ce context list")
-dry-run
show the change, do not make it
-event string
add: the custom event name of the goal
-json
print JSON, also for errors (default when stdout is not a terminal)
-page string
add: the page path pattern of the goal
-plain
Quiet output: plain columns (also env CHECKEND_PLAIN=1)
-url string
the server URL (default: the current context, or env CHECKEND_URL)
-yes
delete without a question
```
## ce health
Show the health of the server: disk, event log, backups, jobs (admin).
```text
Usage: ce health
Shows the health report of the server (GET /v1/health/detail): the disk use and
the disk guard, the data folder and secret.key, the GeoIP file, the DuckDB
extensions, the event log and the writer lag, the backups, and the newest run
and error of each job (rules, outcomes, search sync, crawl, rollups,
retention, compaction, backup, disk guard, webhooks). Admin only. On the
server, "checkend doctor" checks the files directly.
Exit codes: 0 when the status is ok or warn, 1 when it is fail (D62), so that a
monitor can run "ce health".
Flags:
-context string
the context to use (see "ce context list")
-json
print JSON, also for errors (default when stdout is not a terminal)
-plain
Quiet output: plain columns (also env CHECKEND_PLAIN=1)
-url string
the server URL (default: the current context, or env CHECKEND_URL)
```
## ce indexnow
Show, set and use the IndexNow submit of changed URLs of a site.
```text
Usage: ce indexnow status
ce indexnow set (--on | --off) [--mode proxy|manual] [--new-key]
ce indexnow submit ... [--reason ]
IndexNow tells Bing and the other IndexNow search engines that pages changed.
Checkend sends the pages that the crawler finds changed, the pages of agent
changes (annotations on a finding with a page) and the URLs of "submit". A job
sends the queue every 10 minutes, at most 1,000 URLs a day for each site.
The search engines check a key file at https:///.txt. With --mode
proxy (default) Checkend serves it at /indexnow/.txt: add the proxy snippet
of "status" to the web server of the site. With --mode manual, put the file in
the web root yourself. "set" needs an admin token.
Examples:
ce indexnow set example.com --on
ce indexnow status example.com
ce indexnow submit example.com https://example.com/pricing --reason "new prices"
Flags:
-context string
the context to use (see "ce context list")
-json
print JSON, also for errors (default when stdout is not a terminal)
-mode string
set: proxy or manual
-new-key
set: make a new key
-off
set: turn IndexNow off
-on
set: turn IndexNow on
-plain
Quiet output: plain columns (also env CHECKEND_PLAIN=1)
-reason string
submit: why the pages changed
-url string
the server URL (default: the current context, or env CHECKEND_URL)
```
## ce invites
List and revoke invites (admin); accept an invite link with a token.
```text
Usage: ce invites list
ce invites revoke [--dry-run]
ce invites accept [--name ]
"list" and "revoke" are for an admin. Make an invite with "ce people invite".
"accept" is for the invited person, where a passkey cannot work (an IP
address, or plain HTTP): it accepts the link and makes an API token with the
scopes of the role, then stores the token as "ce login" does. In a browser on
HTTPS, open the link and register a passkey instead.
Example:
ce invites accept 'https://stats.example.com/invite#ceinv_...'
Flags:
-context string
the context to use (see "ce context list")
-dry-run
show the change, do not make it
-json
print JSON, also for errors (default when stdout is not a terminal)
-name string
accept: the context name (default: the server host)
-plain
Quiet output: plain columns (also env CHECKEND_PLAIN=1)
-url string
the server URL (default: the current context, or env CHECKEND_URL)
```
## ce login
Log in to a server: in the browser (device flow), or with an API token.
```text
Usage: ce login [--name ] [--scope ]... [--no-browser]
ce login --device
echo "$TOKEN" | ce login [--name ]
ce login --token-stdin
Without a pasted token, ce login runs the device flow: it prints a URL and a
code, and opens the browser on a terminal. Sign in there (a passkey), check
the code and approve. Then ce gets a token of your person, with the scopes of
your role, or the --scope values (never more than the role).
On a pipe, the URL and the code are a JSON line on stderr ("notice":
"device_code"), and stdout has the JSON result. --device starts the device
flow without a read of stdin.
With a token on stdin (a pipe), or --token-stdin (a hidden prompt on a
terminal), ce checks the token. Make a token with "ce tokens create".
ce stores the token in the OS keychain. When no keychain works (a server
without a desktop session, CI), or with CHECKEND_KEYRING=file, the token goes
to the credentials file ~/.config/checkend/credentials (or under
$XDG_CONFIG_HOME), mode 0600. ce says which store it used.
For CI and agents, set CHECKEND_URL and CHECKEND_TOKEN instead.
Examples:
ce login https://checkend.example.com
ce login https://checkend.example.com --scope 'read:*'
echo "$TOKEN" | ce login https://checkend.example.com --name work
Flags:
-device
use the device flow (the browser); do not read stdin
-name string
the context name (default: the server host)
-no-browser
device flow: do not open the browser
-scope value
device flow: a scope to ask for, for example read:* (repeat it; default: the scopes of your role)
-token-stdin
read a token from stdin (a hidden prompt on a terminal)
```
## ce logout
Remove a context and its token.
```text
Usage: ce logout []
Removes a context and deletes its token from the store that holds it: the OS
keychain or the credentials file. The token stays valid on the server: revoke
it with "ce tokens revoke ".
Flags:
```
## ce logs
Push a web server access log, for the AI crawler data.
```text
Usage: ce logs push
Sends a web server access log to Checkend (at most 10 MB in one push; split a
larger file). Formats: the combined format of Nginx and Apache, and Caddy JSON.
Checkend keeps only the hits of the AI and search crawlers of its list: the day,
the page, the bot and the status. It drops the other lines, and it keeps no IP
address and no user agent. Lines older than 31 days are not kept. The answer
counts the crawler hits that the push found and sent ("hits"). A second push of
the same lines adds nothing to the data, but its answer counts the hits again.
Needs write access to the site's project.
Examples:
ce logs push example.com /var/log/nginx/access.log
zcat access.log.2.gz | ce logs push example.com -
# each day, from cron:
ce logs push example.com /var/log/nginx/access.log.1
Flags:
-context string
the context to use (see "ce context list")
-json
print JSON, also for errors (default when stdout is not a terminal)
-plain
Quiet output: plain columns (also env CHECKEND_PLAIN=1)
-url string
the server URL (default: the current context, or env CHECKEND_URL)
```
## ce mcp
Run a stdio MCP server that forwards each tool to the server's /mcp endpoint.
```text
Usage: ce mcp [--url ] [--context ]
Runs a stdio MCP server (JSON-RPC on stdin and stdout) for MCP clients that support
only stdio. It connects to /mcp with the token of the current context (or
CHECKEND_URL and CHECKEND_TOKEN), and it offers the same tools as the server. Each
call goes to the server. Logs and errors go to stderr. ce mcp stops at the end of
stdin.
Example client configuration:
{"mcpServers": {"checkend": {"command": "ce", "args": ["mcp"],
"env": {"CHECKEND_URL": "https://checkend.example.com", "CHECKEND_TOKEN": "ce_..."}}}}
Flags:
-context string
the context to use (see "ce context list")
-url string
the server URL (default: the current context, or env CHECKEND_URL)
```
## ce pages
Show what the crawler found on the pages of a site (title, H1, indexability).
```text
Usage: ce pages [--path ]
Shows what the Checkend crawler found on the pages of a site: the status, the
title, the meta description, the H1, the canonical URL, the indexability (and
why not), and the newest Search Console URL Inspection. The crawler visits the
pages with traffic or search impressions, at most once a week, and it obeys
robots.txt (User-agent: CheckendBot). Read the current title before you propose
a new one. Exit code 4 means that the crawler has no page of the site yet.
Examples:
ce pages example.com
ce pages example.com --path /pricing --json
Flags:
-context string
the context to use (see "ce context list")
-json
print JSON, also for errors (default when stdout is not a terminal)
-path string
one page path, for example /pricing
-plain
Quiet output: plain columns (also env CHECKEND_PLAIN=1)
-url string
the server URL (default: the current context, or env CHECKEND_URL)
```
## ce passkeys
List and delete the passkeys of your user (add one in the web UI, Settings).
```text
Usage: ce passkeys list
ce passkeys delete [--yes]
Lists the passkeys of the person of the token (an admin that ce setup made, or a
web sign-in). A passkey needs a browser: add one in the web UI, Settings,
Passkeys. An agent token has no person and no passkeys.
Flags:
-context string
the context to use (see "ce context list")
-json
print JSON, also for errors (default when stdout is not a terminal)
-plain
Quiet output: plain columns (also env CHECKEND_PLAIN=1)
-url string
the server URL (default: the current context, or env CHECKEND_URL)
-yes
delete without a question
```
## ce people
List people, invite a person, change a role, remove a person (admin).
```text
Usage: ce people list
ce people invite --role [--dry-run]
ce people role [--dry-run]
ce people remove [--yes] [--dry-run]
People have one role for the whole organization (decision D56):
admin everything: tokens, webhooks, people, retention, limits, settings
member sites, goals, annotations, findings and search connections
viewer read only
"invite" prints a one-time link (7 days). Checkend sends no email: give the
link to the person. The person opens it and registers a passkey. A role change
takes effect on the next request of the person; "remove" ends the person's
sessions at once and revokes the person's API tokens. The last admin stays.
An admin can do all of this. Admins and Members can list the people; a Viewer gets exit 5
(decision D69).
Examples:
ce people invite ana@example.com --role member
ce people role ana@example.com viewer
ce people remove ana@example.com --yes
Flags:
-context string
the context to use (see "ce context list")
-dry-run
show the change, do not make it
-json
print JSON, also for errors (default when stdout is not a terminal)
-plain
Quiet output: plain columns (also env CHECKEND_PLAIN=1)
-role string
the role of the invite: admin, member or viewer
-url string
the server URL (default: the current context, or env CHECKEND_URL)
-yes
remove without a question
```
## ce projects
List projects, set the daily event limit (admin).
```text
Usage: ce projects list
ce projects limit --daily-events [--dry-run]
"limit" sets the most events that the project accepts in one UTC day. The
server refuses events over the limit before it writes them. 0 means no limit.
Example:
ce projects limit prj_default --daily-events 1000000
Flags:
-context string
the context to use (see "ce context list")
-daily-events int
the daily event limit (0: no limit) (default -1)
-dry-run
show the change, do not make it
-json
print JSON, also for errors (default when stdout is not a terminal)
-plain
Quiet output: plain columns (also env CHECKEND_PLAIN=1)
-url string
the server URL (default: the current context, or env CHECKEND_URL)
```
## ce query
Show page views, visitors, top pages and conversions of a site.
```text
Usage: ce query [--period 24h|7d|30d|365d] [--json]
ce query --series [--period 24h|7d|30d|365d] [--goal ] [--json]
Prints the page views, the visitors, the top pages and the top countries of one
site. --series prints the time series instead: one row for each hour (24h) or
each UTC day, with the conversions (of one goal with --goal). Exit code 4 means
that the period has no data yet.
Examples:
ce query example.com --period 7d
ce query example.com --series --period 30d
Flags:
-context string
the context to use (see "ce context list")
-goal string
--series: the conversions of this goal ID only
-json
print JSON, also for errors (default when stdout is not a terminal)
-period string
the period: 24h, 7d, 30d or 365d (default "30d")
-plain
Quiet output: plain columns (also env CHECKEND_PLAIN=1)
-series
print the time series (GET /v1/timeseries)
-site string
the site domain (or give it as the first argument)
-url string
the server URL (default: the current context, or env CHECKEND_URL)
```
## ce report
Show, list and make reports, and manage the report schedules.
```text
Usage: ce report [show] [] [--project ] [--frequency daily|weekly|monthly] [--date YYYY-MM-DD] [--id ] [--format md]
ce report list [] [--project ] [--frequency ] [--limit ] [--before ]
ce report run [] [--project ] [--frequency ] [--time-zone ] [--schedule ] [--dry-run]
ce report schedules list
ce report schedules add [] [--project ] --frequency [--weekday ] [--hour <0-23>] [--time-zone ] [--dry-run]
ce report schedules set [--frequency ] [--weekday ] [--hour <0-23>] [--time-zone ] [--dry-run]
ce report schedules delete [--yes] [--dry-run]
A report is the numbers of one window in whole days of a time zone, against
the window before (with the significance tests), the top pages, sources and
channels, the goals, the revenue for each currency, the web vitals, the search
totals, the findings opened and closed, the outcomes and the annotations.
"show" prints the newest report (of a site, a project, a frequency, a date),
or one --id. --format md prints it as Markdown to paste; on a pipe the JSON
has the text in "markdown". "run" makes a report now of the newest complete
window: daily is yesterday, weekly the 7 days before today, monthly the
month before. A schedule makes a report daily, weekly on a weekday or monthly
on the first day, at a local hour, and sends the webhook report.ready.
Admins and Members manage schedules. Checkend sends no email.
Examples:
ce report show example.com
ce report show --project prj_default --frequency monthly --format md | jq -r .markdown
ce report run example.com --frequency weekly --time-zone Europe/Berlin
ce report schedules add example.com --frequency weekly --weekday monday --hour 7 --time-zone Europe/Berlin
Flags:
-before string
list: only reports older than this report ID (the next page)
-context string
the context to use (see "ce context list")
-date string
show: a day in the window, YYYY-MM-DD
-dry-run
show the change, do not make it
-format string
show: md prints the report as Markdown
-frequency string
daily, weekly or monthly
-hour int
schedules: the local hour, 0 to 23 (default 6)
-id string
show: a report ID
-json
print JSON, also for errors (default when stdout is not a terminal)
-limit int
list: the most reports, 1 to 100 (default 20)
-plain
Quiet output: plain columns (also env CHECKEND_PLAIN=1)
-project string
a project ID: the project reports (all sites)
-schedule string
run: use the site, frequency and time zone of a schedule
-time-zone string
run, schedules: an IANA time zone, for example Europe/Berlin (default UTC)
-url string
the server URL (default: the current context, or env CHECKEND_URL)
-weekday string
schedules: the day of a weekly report, monday to sunday
-yes
delete without a question
```
## ce retention
Show or set the raw-data retention of each module (admin).
```text
Usage: ce retention
ce retention set --months [--dry-run]
Checkend keeps the raw events of each module for a number of months (default
24, decision D26). The nightly retention job deletes the older months as file
deletes. The daily rollups and the search rows stay permanently, so the
numbers of old days stay; a visitor then counts one time on each day of a
deleted month. Admin only. The modules: analytics.
Examples:
ce retention
ce retention set analytics --months 18 --dry-run
Flags:
-context string
the context to use (see "ce context list")
-dry-run
show the change, do not make it
-json
print JSON, also for errors (default when stdout is not a terminal)
-months int
set: the months of raw events to keep (1 to 120)
-plain
Quiet output: plain columns (also env CHECKEND_PLAIN=1)
-url string
the server URL (default: the current context, or env CHECKEND_URL)
```
## ce search
Show Search Console and Bing rows; connect, check, sync and disconnect the sources.
```text
Usage: ce search [--period 7d|28d|90d|365d] [--by query|page] [--position 8-20]
[--source google|bing|all] [--query ] [--page ] [--limit 50]
ce search connect (--google | --bing --api-key-stdin) [--property ]
ce search status
ce search disconnect google|bing [--yes] [--dry-run]
ce search sync
Shows the Search Console and Bing rows of a site: clicks, impressions, CTR and the
average position, by query or by page. Page rows also have the Checkend page views,
visitors, page views from Organic Search and conversions of the same days, and the
current title. Search data is 2 to 3 days late: the period ends at the newest
search date. --position 8-20 shows the striking-distance rows. Exit code 4 means
that the site has no search data yet.
connect --google prints a consent page URL: open it in a browser as a person with
access to the property. The server needs a Google OAuth client of your own Google
Cloud project (checkend serve --google-client-id). connect --bing reads the API key
from stdin (Bing Webmaster Tools, Settings, API Access). The server stores the
token or the key encrypted. disconnect deletes them; the synced rows stay.
Examples:
ce search example.com --period 28d --by query --position 8-20
ce search example.com --by page
ce search connect example.com --google --property sc-domain:example.com
echo "$BING_KEY" | ce search connect example.com --bing --api-key-stdin
Flags:
-api-key-stdin
connect --bing: read the API key from stdin
-bing
connect: Bing Webmaster Tools (API key)
-by string
query (default) or page
-context string
the context to use (see "ce context list")
-dry-run
disconnect: show the change, do not make it
-google
connect: Search Console (OAuth consent page)
-json
print JSON, also for errors (default when stdout is not a terminal)
-limit int
the most rows (default 50, at most 500)
-page string
keep rows whose page URL contains this text
-period string
7d, 28d (default), 90d or 365d
-plain
Quiet output: plain columns (also env CHECKEND_PLAIN=1)
-position string
keep rows with an average position in this range, for example 8-20
-property string
connect: sc-domain:example.com or https://example.com/ (default: sc-domain:, or https:/// for bing)
-query string
keep rows whose query contains this text
-source string
google, bing or all (default)
-url string
the server URL (default: the current context, or env CHECKEND_URL)
-yes
disconnect: do not ask
```
## ce setup
Make the first admin with the setup token, and log in.
```text
Usage: ce setup --email --setup-token [--name ]
Makes the first admin of a new server with the one-time setup token that the
server printed at its first start. Then it stores the new admin token as "ce
login" does: in the OS keychain, or in the credentials file when no keychain
works.
Example:
ce setup https://checkend.example.com --email admin@example.com --setup-token cesetup_...
Flags:
-email string
the email address of the first admin (required)
-name string
the context name (default: the server host)
-setup-token string
the setup token from the server output (required)
```
## ce sites
List, add, change, delete and verify sites.
```text
Usage: ce sites list [--project ]
ce sites add [--privacy strict|standard|extended] [--project ] [--dry-run]
ce sites set [--privacy strict|standard|extended] [--outcome-mde ] [--dry-run]
ce sites delete [--yes] [--dry-run]
ce sites prompt [--dry-run]
ce sites verify [--wait 2m] [--send-test]
"add" prints the script tag and the install prompt for a coding agent.
"set --outcome-mde" sets the outcome sensitivity of the site (admin): the
smallest change, in percent (5 to 50, default 10), that an outcome test must
find with 80 % power. A larger value needs less data, but misses small
changes. It applies to new outcomes; "ce findings remeasure" uses it too.
"prompt" makes a new install prompt with a new install token (24 hours).
The install token can only run "verify" for its site.
"verify" waits for the first page view of the site, and exits with 0 when
it arrives, or 4 when it does not. --send-test first sends one test event
("Checkend install test"), to check that the server takes events.
Examples:
ce sites add example.com
ce sites prompt example.com --plain # only the prompt text
ce sites prompt example.com | jq -r .prompt | pbcopy
CHECKEND_TOKEN=ce_inst_... ce sites verify example.com --wait 5m
Flags:
-context string
the context to use (see "ce context list")
-dry-run
show the change, do not make it
-every duration
verify: the time between two checks (default 2s)
-json
print JSON, also for errors (default when stdout is not a terminal)
-outcome-mde string
set: the outcome sensitivity in percent, 5 to 50 (admin)
-plain
Quiet output: plain columns (also env CHECKEND_PLAIN=1)
-privacy string
the privacy mode: strict, standard (default) or extended
-project string
the project ID (add: default project; list: all projects)
-send-test
verify: send one test event first
-url string
the server URL (default: the current context, or env CHECKEND_URL)
-wait duration
verify: how long to wait for the first event (default 2m0s)
-yes
delete without a question
```
## ce tokens
List, create and revoke API tokens (admin; each person: own tokens).
```text
Usage: ce tokens list
ce tokens create --name --scope [--scope ...] [--dry-run]
ce tokens revoke [--dry-run]
Scopes: admin, read:*, write:*, read:, write:. Write includes
read. Give agents read-only tokens. "create" prints the secret one time.
An admin sees and revokes all tokens. A member or a viewer (D56) sees, makes and
revokes only the own tokens, with scopes inside the role (member: write:*,
viewer: read:*). A role change revokes the own tokens outside the new role.
Example:
ce tokens create --name "weekly agent" --scope read:*
Flags:
-context string
the context to use (see "ce context list")
-dry-run
show the change, do not make it
-json
print JSON, also for errors (default when stdout is not a terminal)
-name string
the token name
-plain
Quiet output: plain columns (also env CHECKEND_PLAIN=1)
-scope value
a scope (repeat the flag for more)
-url string
the server URL (default: the current context, or env CHECKEND_URL)
```
## ce vitals
Show the web vitals (p75 of LCP, INP, CLS, TTFB) of a site and its pages.
```text
Usage: ce vitals [--period 24h|7d|30d|365d] [--path ] [--limit 20]
Shows the web vitals from real visitors: the 75th percentile (p75) of LCP, INP,
CLS and TTFB for the site and its top pages, with n and the web.dev rating
(good, needs_improvement, poor). The tracking script sends one report for each
page load. The numbers are not tested for significance. Exit code 4 means that
the period has no reports yet.
Examples:
ce vitals example.com --period 7d
ce vitals example.com --path /pricing --json
Flags:
-context string
the context to use (see "ce context list")
-json
print JSON, also for errors (default when stdout is not a terminal)
-limit int
the most pages (default 20)
-path string
one page path, for example /pricing
-period string
24h, 7d, 30d (default) or 365d
-plain
Quiet output: plain columns (also env CHECKEND_PLAIN=1)
-url string
the server URL (default: the current context, or env CHECKEND_URL)
```
## ce webhooks
Add, list, delete and test webhooks (admin).
```text
Usage: ce webhooks add [--event ]... [--dry-run]
ce webhooks list
ce webhooks delete [--yes] [--dry-run]
ce webhooks test
ce webhooks deliveries
A webhook gets a signed POST request for each event: finding.created, finding.updated, annotation.created, disk.pressure, report.ready
(default: all). The body has the brief of the site; report.ready has the report. Check the Checkend-Signature header: HMAC-SHA256 with the secret over
".". "add" shows the secret one time. The server refuses loopback,
private and link-local URLs unless it runs with --webhook-allow-private.
Admin only.
Examples:
ce webhooks add https://agents.example.com/checkend --event finding.created
ce webhooks test wh_0192...
Flags:
-context string
the context to use (see "ce context list")
-dry-run
show the change, do not make it
-event value
add: an event (repeat the flag for more)
-json
print JSON, also for errors (default when stdout is not a terminal)
-plain
Quiet output: plain columns (also env CHECKEND_PLAIN=1)
-url string
the server URL (default: the current context, or env CHECKEND_URL)
-yes
delete without a question
```
## ce version
Show the version.
```text
{
"version": "dev"
}
```
---
# MCP tools
> The tools and prompts of the Checkend MCP server, with each input parameter and output field, for Claude Code, Cursor and other agents.
> **Note: Generated page**
> Generated from the Checkend repository at `ee1e2be` (`ee1e2be`) by `npm run sync`. Do not edit this page by hand.
The MCP server is at `/mcp` (Streamable HTTP). To connect a client, read [Connect your agent](/docs/start/connect-your-agent.md). Each tool checks the scopes of the token. The REST API and `ce` give the same numbers, because all three use one query core.
| Tool | What it does |
|---|---|
| [`query`](#query) | Page views, visitors, the top 10 pages and the top 10 countries of one site, for a period that ends now. |
| [`timeseries`](#timeseries) | Page views, visitors, conversions, conversion rate and revenue of one site for each hour (period 24h) or each UTC day (7d, 30d, 365d). |
| [`list_sites`](#list_sites) | List the sites that this token can read, with the project and the privacy mode of each site.. |
| [`manage_goals`](#manage_goals) | List, add or delete the goals of a site. |
| [`get_brief`](#get_brief) | The main entry point for an agent: one compact package for a site, a project or all readable sites, for a period that ends now (default 7d). |
| [`get_findings`](#get_findings) | Findings that the rules found, with evidence numbers, the estimated impact, priority, confidence, status and data_until, and the outcome (worked, got_worse, no_clear_change or too_few_to_compare, with the test, the p-value and the power) once measured. |
| [`update_finding`](#update_finding) | Change the status of a finding: claim (you work on it), done (the change is made: give annotation, the ID of an annotation, or text for a new one, with link to the pull request), dismiss (not useful; give a note) or reopen. |
| [`annotate`](#annotate) | Record a change on a site at a time, for example "Title changed on /pricing" with the pull request link. |
| [`search_performance`](#search_performance) | Search Console and Bing rows of one site, by query (default) or by page, for a period that ends at the newest search date (7d, 28d default, 90d, 365d): clicks, impressions, CTR and the average position. |
| [`connect_search_console`](#connect_search_console) | The Search Console (google) and Bing Webmaster Tools (bing) connection of a site. |
| [`get_page`](#get_page) | What the Checkend crawler found on a page now: title, meta description, H1, canonical URL, indexability (and why not), robots.txt, and the newest Search Console URL Inspection. |
| [`web_vitals`](#web_vitals) | Web vitals from real visitors of one site, for a period that ends now (24h, 7d, 30d default, 365d): the 75th percentile of LCP (ms), INP (ms, the longest interaction), CLS (score) and TTFB (ms) for the site and for its top pages (path gives one page), each with n, the web.dev rating (good, needs_improvement, poor) and the share of good loads. |
| [`ai_crawlers`](#ai_crawlers) | AI and search crawlers on one site, from the web server access logs that the admin pushes (ce logs push): the hits, pages, errors and last day of each crawler (GPTBot, OAI-SearchBot, ChatGPT-User, ClaudeBot, Claude-User, PerplexityBot, Googlebot, Bingbot and others), with its operator and purpose (training, ai_search, user_request, search), and the top pages with the hits of each crawler. |
| [`indexnow`](#indexnow) | IndexNow of one site: tell Bing and the other IndexNow engines that pages changed. |
| [`get_report`](#get_report) | A stored report (report_v1) of a site or a project for one window in whole days of a time zone: the main numbers against the window before (Poisson rate test for counts, two-proportion z-test for the conversion rate, 95 %), the top pages, sources and channels, the goals, the revenue for each currency (never added), the web vitals p75, the search totals when a source is connected, the findings opened and closed, the outcomes measured (worked, got_worse, no_clear_change, too_few_to_compare), the warnings and the annotations of the window. |
| [`create_site`](#create_site) | Create a site and return the script snippet to add to the site's root layout, and the install prompt (install.prompt): the steps for Next.js, Astro, Rails, WordPress and static HTML, the goal events, the optional first-party proxy route and an install token that can only check the install of this site for 24 hours. |
| [`verify_install`](#verify_install) | Check that the events of a site arrive: status waiting (no event in 7 days), test_only (only the test event; the script tag sends nothing yet) or receiving (page views arrive), with the first and the newest event time and the newest page. |
## query
Page views, visitors, the top 10 pages and the top 10 countries of one site, for a period that ends now. Periods: 24h, 7d, 30d (default), 365d. The answer gives data_until (the newest event), sample_size (the raw event count), the top 10 referrer hosts ("" is Direct) and the totals of the period before (previous; not tested for significance). It also gives conversions (visitors who completed a goal), conversion_rate, revenue for each currency (never added across currencies) and the numbers of each goal. In strict privacy mode conversions count events, not visitors (conversions_unit and conversions_note say so). Needs read access to the site's project.
**Input**
| Name | Type | Required | Description |
|---|---|---|---|
| `site` | string | yes | the site domain, for example example.com |
| `period` | string | no | the period: 24h, 7d, 30d or 365d (default 30d) |
**Output fields:** `site` (string), `period` (string), `from` (string), `to` (string), `pageviews` (integer), `visitors` (integer), `top_pages` (object[]), `top_countries` (object[]), `top_sources` (object[]), `top_channels` (object[]), `data_until` (string), `sample_size` (integer), `events` (integer), `conversions` (integer), `conversion_rate` (number), `conversions_unit` (string), `visitors_note` (string), `conversions_note` (string), `revenue` (object[]), `goals` (object[]), `previous` (object).
## timeseries
Page views, visitors, conversions, conversion rate and revenue of one site for each hour (period 24h) or each UTC day (7d, 30d, 365d). Give a goal ID for the conversions and the revenue of one goal. Buckets without events have zeros; the first and the last bucket are partial. Needs read access to the site's project.
**Input**
| Name | Type | Required | Description |
|---|---|---|---|
| `site` | string | yes | the site domain, for example example.com |
| `period` | string | no | the period: 24h, 7d, 30d or 365d (default 30d) |
| `goal` | string | no | a goal ID: the conversions and the revenue of this goal only; all goals when empty |
**Output fields:** `site` (string), `period` (string), `interval` (string), `from` (string), `to` (string), `points` (object[]), `goal` (string), `conversions_unit` (string), `data_until` (string), `sample_size` (integer).
## list_sites
List the sites that this token can read, with the project and the privacy mode of each site.
**Input**
| Name | Type | Required | Description |
|---|---|---|---|
| `project` | string | no | a project ID; all readable projects when empty |
**Output fields:** `sites` (object[]).
## manage_goals
List, add or delete the goals of a site. A goal is a page path pattern (page, for example /pricing or /blog/*) or a custom event name (event, for example Signup). action: list (default; needs read access), add (give name and exactly one of page and event) or delete (give id); add and delete need write access to the site's project. The answer is the goal list after the action.
**Input**
| Name | Type | Required | Description |
|---|---|---|---|
| `site` | string | yes | the site domain, for example example.com |
| `action` | string | no | list (default), add or delete |
| `name` | string | no | add: the goal name; default: the event name or the page pattern |
| `page` | string | no | add: a page path pattern, for example /pricing or /blog/* |
| `event` | string | no | add: a custom event name, for example Signup |
| `id` | string | no | delete: the goal ID |
**Output fields:** `site` (string), `goals` (object[]).
## get_brief
The main entry point for an agent: one compact package for a site, a project or all readable sites, for a period that ends now (default 7d). It has the top changes against the period before (each with a Poisson test p-value; significant means 95 % confidence), the open and claimed findings, the annotations and releases of the period, the outcomes of earlier changes, newest first (site memory: what was tried, the result, the test and the power), warnings for each change that made its metric worse (got_worse: do not repeat it) and the data date of each source. Webhooks send the same JSON. Needs read access.
**Input**
| Name | Type | Required | Description |
|---|---|---|---|
| `site` | string | no | a site domain; give a site or a project, or nothing for all readable sites |
| `project` | string | no | a project ID |
| `period` | string | no | the period that ends now: 24h, 7d (default), 30d or 365d |
**Output fields:** `version` (string), `site` (string), `project` (string), `sites` (string[]), `period` (string), `from` (string), `to` (string), `generated_at` (string), `top_changes` (object[]), `open_findings` (object[]), `annotations` (object[]), `outcomes` (object[]), `warnings` (string[]), `data_dates` (object[]), `sample_size` (integer).
## get_findings
Findings that the rules found, with evidence numbers, the estimated impact, priority, confidence, status and data_until, and the outcome (worked, got_worse, no_clear_change or too_few_to_compare, with the test, the p-value and the power) once measured. By default only open and claimed findings (the work to do); give status for others. Give id for one finding with its history and its outcome history. Before you start on a finding, claim it with update_finding, so that other agents do not repeat the work. Needs read access.
**Input**
| Name | Type | Required | Description |
|---|---|---|---|
| `id` | string | no | one finding ID: the answer has this finding only, with its history |
| `site` | string | no | a site domain; all readable sites when empty |
| `project` | string | no | a project ID; all readable projects when empty |
| `status` | string | no | statuses, comma-separated; default: open,claimed |
| `limit` | integer | no | the most findings to return, 1 to 200 (default 50) |
| `token` | string | no | the name of the token that claimed the finding |
| `action` | string | no | action types, comma-separated, for example fix_redirect,investigate |
| `period` | string | no | only findings with a change in the period that ends now: 24h, 7d, 30d or 365d; default: all |
**Output fields:** `findings` (object[]).
## update_finding
Change the status of a finding: claim (you work on it), done (the change is made: give annotation, the ID of an annotation, or text for a new one, with link to the pull request), dismiss (not useful; give a note) or reopen. After done, Checkend starts measuring on the next UTC day, and sets the outcome when the 14 days after the change day are complete (D48 tests at 95 %). remeasure (admin token only) computes the outcome again and keeps the history. dry_run true checks the change only. Needs write access to the site's project.
**Input**
| Name | Type | Required | Description |
|---|---|---|---|
| `id` | string | yes | the finding ID |
| `action` | string | yes | claim, done, dismiss, reopen or remeasure (admin: compute the outcome again) |
| `note` | string | no | why, at most 500 characters; it goes to the history |
| `annotation` | string | no | done: the ID of the annotation of the change |
| `text` | string | no | done: the text of a new annotation of the change (kind agent_change) |
| `link` | string | no | done: a URL about the change for the new annotation, for example the pull request |
| `at` | string | no | done: the time of the change for the new annotation; default now |
| `dry_run` | boolean | no | true: check the change and return the result, but do not make it |
**Output fields:** `id` (string), `site` (string), `project` (string), `type` (string), `action` (string), `page` (string), `query` (string), `summary` (string), `page_now` (object), `evidence` (object), `impact` (object), `priority` (string), `confidence` (string), `status` (string), `claimed_by` (object), `annotation` (string), `change_at` (string), `data_until` (string), `first_seen` (string), `last_seen` (string), `updated_at` (string), `history` (object[]), `outcome` (object), `outcome_history` (object[]).
## annotate
Record a change on a site at a time, for example "Title changed on /pricing" with the pull request link. kind: agent_change (default), release or note. Give finding to link the change to a finding (update_finding done also makes the annotation). at is RFC 3339 or a UTC date; default now. Needs write access to the site's project.
**Input**
| Name | Type | Required | Description |
|---|---|---|---|
| `site` | string | yes | the site domain, for example example.com |
| `text` | string | yes | what changed, 1 to 500 characters, for example: Title changed on /pricing |
| `kind` | string | no | agent_change (default), release or note |
| `at` | string | no | the time of the change: RFC 3339 or a UTC date (2026-10-22); default now |
| `finding` | string | no | the ID of the finding that the change is for |
| `link` | string | no | an http or https URL about the change, for example the pull request |
**Output fields:** `id` (string), `site` (string), `at` (string), `kind` (string), `text` (string), `finding` (string), `link` (string), `actor` (object), `created_at` (string).
## search_performance
Search Console and Bing rows of one site, by query (default) or by page, for a period that ends at the newest search date (7d, 28d default, 90d, 365d): clicks, impressions, CTR and the average position. Page rows also have the Checkend page views, visitors, the page views from Organic Search, the conversions of the page's visitors and the current title. position 8-20 gives the striking-distance rows. Search data is 2 to 3 days late, and Search Console leaves out rare queries (notes says so). Without a connection, the answer says the next step. Needs read access.
**Input**
| Name | Type | Required | Description |
|---|---|---|---|
| `site` | string | yes | the site domain, for example example.com |
| `period` | string | no | 7d, 28d (default), 90d or 365d; the period ends at the newest search date |
| `by` | string | no | query (default) or page |
| `position` | string | no | keep rows with an average position in this range, for example 8-20 (striking distance) |
| `source` | string | no | google, bing or all (default) |
| `query` | string | no | keep rows whose query contains this text |
| `page` | string | no | keep rows whose page URL contains this text |
| `limit` | integer | no | the most rows (default 50, at most 500) |
**Output fields:** `site` (string), `period` (string), `by` (string), `source` (string), `from` (string), `to` (string), `totals` (object), `previous` (object), `rows` (object[]), `data_until` (string), `sample_size` (integer), `connections` (object[]), `conversions_unit` (string), `notes` (string[]), `next` (string).
## connect_search_console
The Search Console (google) and Bing Webmaster Tools (bing) connection of a site. action status (default; read access) shows each source, its synced dates and the last error. action connect (write access): google returns authorize_url in next, a consent page that a person with access to the property must open in a browser (Checkend stores the refresh token encrypted); bing needs api_key. property: sc-domain:example.com or https://example.com/ (default sc-domain:). action disconnect deletes the stored token of source. action sync syncs now (the job also runs each hour).
**Input**
| Name | Type | Required | Description |
|---|---|---|---|
| `site` | string | yes | the site domain, for example example.com |
| `action` | string | no | status (default), connect, disconnect or sync |
| `source` | string | yes | google (Search Console) or bing (Bing Webmaster Tools) |
| `property` | string | no | google: sc-domain:example.com (domain property) or https://example.com/ (URL-prefix property); bing: the site URL, for example https://example.com/; default: sc-domain: (google) or https://