> ## Documentation Index
> Fetch the complete documentation index at: https://docs.traceten.com/llms.txt
> Use this file to discover all available pages before exploring further.

# Detection API

> What the bot filter removed, and how much revenue we could tie to a session.

## Base URL and authentication

```
https://api.traceten.com/v1/detection
```

Takes `Authorization: Bearer <YOUR_API_KEY>` ([details](/api/authentication)) and the `stats:read` permission.

These two endpoints exist so you can check our work. Every other number in this API depends on what the bot filter removed and on which conversions we could tie back to a browsing session. Without these, our figures are unfalsifiable.

## `GET /v1/detection/quality`

What automated traffic we removed before it reached your analytics, and what we let through and flagged.

```bash theme={null}
curl "https://api.traceten.com/v1/detection/quality?site_id=ttid_7Rb4TrC1dTbnD8w3s1TS12&from=2026-08-01&to=2026-08-07" \
  -H "Authorization: Bearer <YOUR_API_KEY>"
```

```json theme={null}
{
  "status": "success",
  "data": {
    "site_id": "ttid_7Rb4TrC1dTbnD8w3s1TS12",
    "from": "2026-08-01",
    "to": "2026-08-07",
    "dropped_bot_requests": 18400,
    "passthrough_bot_requests": 2100,
    "total_bot_requests": 20500,
    "by_class": [
      { "ua_class": "search_crawler", "request_count": 14200, "dropped": true },
      { "ua_class": "ai_training_crawler", "request_count": 2100, "dropped": false }
    ]
  }
}
```

### Parameters

| Parameter | Type   | Required | Notes                                              |
| --------- | ------ | -------- | -------------------------------------------------- |
| `site_id` | string | yes      | The snippet key.                                   |
| `from`    | string | no       | `YYYY-MM-DD`, inclusive. Must be sent with `to`.   |
| `to`      | string | no       | `YYYY-MM-DD`, inclusive. Must be sent with `from`. |

With neither date, the window is the last 7 days. The range may not span more than 366 days. `timezone` is not accepted: these are UTC calendar day totals.

### Response fields

| Field                      | Type    | Notes                                                             |
| -------------------------- | ------- | ----------------------------------------------------------------- |
| `dropped_bot_requests`     | integer | Requests filtered out at the edge. These never reached analytics. |
| `passthrough_bot_requests` | integer | Bot requests we let through and flagged. These did reach it.      |
| `total_bot_requests`       | integer | The sum of the two.                                               |
| `by_class`                 | array   | Per user agent class, largest first.                              |

Each `by_class` row carries `ua_class` (the coarse class we assigned, such as `search_crawler` or `ai_training_crawler`), `request_count`, and `dropped`.

### The two counts are separate on purpose

`dropped` and `passthrough` are not two halves of one number you should add up and call "filtered". Only dropped requests were filtered out. Passthrough requests, mostly AI training crawlers such as CCBot, reached the pipeline and are surfaced here for transparency.

The one rate you can defend is:

```
dropped_bot_requests / total_bot_requests
```

Requests against requests. We do not return that rate as a field, because naming one invites the wrong one.

Everything on this endpoint is a request count. Comparing any of these figures against a session or visitor count from another endpoint gives a percentage with a different denominator on each side. It will look plausible and mean nothing.

## `GET /v1/detection/coverage`

How much of your revenue we could tie back to a browsing session, and by what method.

```bash theme={null}
curl "https://api.traceten.com/v1/detection/coverage?site_id=ttid_7Rb4TrC1dTbnD8w3s1TS12&from=2026-08-01&to=2026-08-07" \
  -H "Authorization: Bearer <YOUR_API_KEY>"
```

```json theme={null}
{
  "status": "success",
  "data": {
    "site_id": "ttid_7Rb4TrC1dTbnD8w3s1TS12",
    "from": "2026-08-01",
    "to": "2026-08-07",
    "currency": "USD",
    "coverage": [
      {
        "match_method": "visitor_reference",
        "source_kind": "ai",
        "conversion_count": 61,
        "revenue_usd_cents": 419000
      },
      {
        "match_method": "",
        "source_kind": "unmatched",
        "conversion_count": 88,
        "revenue_usd_cents": 940000
      }
    ],
    "totals": {
      "matched_revenue_usd_cents": 900000,
      "unmatched_revenue_usd_cents": 940000,
      "matched_conversion_count": 122,
      "unmatched_conversion_count": 88,
      "revenue_usd_cents": 1840000,
      "conversion_count": 210
    }
  }
}
```

Same parameters and same window rules as `/quality`.

### `match_method` is the point of this endpoint

| Value                 | Meaning                                                              |
| --------------------- | -------------------------------------------------------------------- |
| `visitor_reference`   | The order carried the visitor reference our snippet set.             |
| `email_hash`          | The order's hashed email matched an identified visitor.              |
| `metadata_session_id` | Your checkout passed the session id through to the payment provider. |
| `cart_token`          | The cart token matched a session.                                    |
| `""`                  | Nothing matched. This is the gap.                                    |

The empty bucket is not an error and it is not filtered out of `coverage`. On most sites it is the largest single row, and making it visible is the reason this endpoint exists. `totals.unmatched_revenue_usd_cents` is the same figure summarised.

Raising coverage is a change to your checkout, not to our detection. Passing the session id through as payment metadata is the highest leverage single step.

### `source_kind` here includes a value the traffic endpoints do not have

`source_kind` on this endpoint can be `ai`, `non_ai`, `unmatched`, or `""` for older rows. `unmatched` exists only on revenue: it means no session was found.

[`GET /v1/breakdowns/source-kinds`](/api/breakdowns) returns `ai`, `non_ai` and `unknown`, and cannot return `unmatched`. The two endpoints describe overlapping populations along different axes, so reconciling them by addition gives a wrong number.

There is one more trap here worth stating plainly. A small number of older rows carry `source_kind: "unmatched"` while `match_method` is set, meaning a session was found and the revenue is genuinely attributed. Split matched from unmatched on `match_method`, never on `source_kind`. Our own totals do exactly that.

### Revenue here is US dollars

`revenue_usd_cents` is US cents and there is no `revenue_display` field, whatever your reporting currency.

Our rule is to convert at the rate of the day money was earned, and this endpoint reports one total for the whole window rather than a figure per day. There is no honest rate for that total, so we return the stored currency and label it rather than invent one.

## Errors

| Status | Cause                                                                                             |
| ------ | ------------------------------------------------------------------------------------------------- |
| `401`  | Missing, malformed, revoked or expired key.                                                       |
| `403`  | The key is valid but lacks `stats:read`. The body names the permission.                           |
| `404`  | No such site, or the key does not cover it.                                                       |
| `422`  | Bad `site_id`, `from` without `to`, a range over 366 days, a `timezone`, or an unknown parameter. |
| `429`  | Rate limited. Back off and retry.                                                                 |
| `500`  | The query failed. You will never get zeroes standing in for an error.                             |

## Next

* [Breakdowns](/api/breakdowns) for the traffic these figures qualify
* [AI crawlers](/api/ai-crawls) for crawler traffic, which is a separate population from visitors
