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

# Export Page Analytics

> Export page-level crawl counts — which pages bots hit, with verified/declared tiers — as JSON

```
POST /functions/v1/export-page-analytics
```

<Info>
  **Beta.** This endpoint is new and its response shape may still change. It is
  the page-level companion to
  [Export Crawl Analytics](/api-reference/endpoint/export-crawl-analytics):
  same authentication and time windows, but broken down by **page** instead of
  aggregated per domain.
</Info>

Returns the pages of **one domain** ranked by bot crawl volume. Each page carries its `verified` / `declared` / `unknown` tier split, a per-bot breakdown, and (by default) a bucketed time series. Because a domain can have thousands of crawled paths, this endpoint is **single-domain** and **cursor-paginated** — it does not have an account-wide mode.

<Warning>
  Always call this endpoint from a **server-side environment** (backend API, serverless function, build script, etc.). Never include your API key in client-side code — it will be visible to anyone inspecting your frontend.
</Warning>

<Info>
  Programmatic (API key) access requires the **Pro plan and above** — requests
  from Starter accounts return `403 plan_upgrade_required`.
</Info>

<Note>
  Page-level crawl data begins on **2026-07-09** (`legacyCutover`). Windows that
  start earlier are automatically clamped to that date (the response echoes your
  original start as `requestedStartDate`); windows that end before it return
  `400 invalid_request`.
</Note>

## Request

### Headers

| Header          | Required | Description                    |
| --------------- | -------- | ------------------------------ |
| `Authorization` | Yes      | `Bearer hado_sk_your_key_here` |
| `Content-Type`  | Yes      | `application/json`             |

### Body Parameters

| Parameter     | Type                | Required | Description                                                                                                                                                                                                                                             |
| ------------- | ------------------- | -------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `domainId`    | string              | **Yes**  | The domain to export. Get IDs from [List Domains](/api-reference/endpoint/list-domains).                                                                                                                                                                |
| `period`      | string              | No       | Rolling window ending today: `1d`, `7d`, `30d`, `90d`, or `365d`. Defaults to `30d`. **Cannot** be combined with `startDate`/`endDate`.                                                                                                                 |
| `startDate`   | string              | No       | Start of an explicit date range, `YYYY-MM-DD` (UTC, inclusive).                                                                                                                                                                                         |
| `endDate`     | string              | No       | End of an explicit date range, `YYYY-MM-DD` (UTC, inclusive). Optional — defaults to **today** when `startDate` is supplied. Cannot be sent without `startDate`.                                                                                        |
| `granularity` | string              | No       | Series bucket size: `auto` (default), `day`, or `week`. `auto` picks `day` for windows up to 90 days and `week` beyond. Requesting `day` for a longer window is a `400`.                                                                                |
| `sortBy`      | string              | No       | Page ranking: `total` (default) or `verified`.                                                                                                                                                                                                          |
| `pageSize`    | number              | No       | Pages per response, `1`–`500`. Defaults to `50`.                                                                                                                                                                                                        |
| `series`      | boolean             | No       | Include the per-page time series. Defaults to `true`; set `false` for a lighter, aggregate-only payload.                                                                                                                                                |
| `botId`       | string \| string\[] | No       | Only count crawls from these bots (e.g. `"gptbot"` or `["gptbot", "claudebot"]`). Also switches the series to a **per-bot** breakdown.                                                                                                                  |
| `tier`        | string \| string\[] | No       | Only count crawls in these tiers: `verified`, `declared`, `unknown`.                                                                                                                                                                                    |
| `pathPrefix`  | string              | No       | Only include paths starting with this prefix (e.g. `"/blog/"`). Matched literally — `%` and `_` have no special meaning.                                                                                                                                |
| `cursor`      | string              | No       | Continuation token from a previous response's `nextCursor`. When set, **all other parameters except `series` are taken from the cursor** — it pins the window, granularity, sort, filters, and page size so later pages stay consistent with the first. |

Window rules (same as the other export endpoints): `period` and `startDate`/`endDate` are mutually exclusive, dates must be real `YYYY-MM-DD` calendar dates, neither may be in the future, `startDate ≤ endDate`, and the span is capped at **365 days**.

### Example Request

First page:

```bash theme={null}
curl -X POST https://api.hadoseo.com/functions/v1/export-page-analytics \
  -H "Authorization: Bearer hado_sk_your_key_here" \
  -H "Content-Type: application/json" \
  -d '{
    "domainId": "11111111-1111-1111-1111-111111111111",
    "period": "30d"
  }'
```

Next page — just the cursor:

```bash theme={null}
curl -X POST https://api.hadoseo.com/functions/v1/export-page-analytics \
  -H "Authorization: Bearer hado_sk_your_key_here" \
  -H "Content-Type: application/json" \
  -d '{
    "cursor": "eyJ2IjoxLCJkIjoiMTExMTExMTEt..."
  }'
```

Only GPTBot crawls of the blog, ranked by verified volume:

```bash theme={null}
curl -X POST https://api.hadoseo.com/functions/v1/export-page-analytics \
  -H "Authorization: Bearer hado_sk_your_key_here" \
  -H "Content-Type: application/json" \
  -d '{
    "domainId": "11111111-1111-1111-1111-111111111111",
    "period": "90d",
    "botId": "gptbot",
    "pathPrefix": "/blog/",
    "sortBy": "verified"
  }'
```

## Response

### Success (200)

```json theme={null}
{
  "generatedAt": "2026-07-31T12:00:00.000Z",
  "period": "30d",
  "startDate": "2026-07-02",
  "endDate": "2026-07-31",
  "granularity": "day",
  "userId": "00000000-0000-0000-0000-000000000000",
  "legacyCutover": "2026-07-09",
  "domain": "example.com",
  "domainId": "11111111-1111-1111-1111-111111111111",
  "role": "owner",
  "pageCount": 4210,
  "pages": [
    {
      "path": "/pricing",
      "total": 940,
      "tiers": { "verified": 700, "declared": 200, "unknown": 40 },
      "bots": {
        "googlebot": { "verified": 500, "declared": 10, "unknown": 0, "total": 510 },
        "gptbot":    { "verified": 200, "declared": 190, "unknown": 0, "total": 390 }
      },
      "series": [
        {
          "bucketStart": "2026-07-30",
          "bucketEnd": "2026-07-30",
          "verified": 24, "declared": 6, "unknown": 1, "total": 31
        }
      ]
    }
  ],
  "nextCursor": "eyJ2IjoxLCJkIjoiMTExMTExMTEt..."
}
```

### Fields

| Field                          | Type           | Description                                                                                                                                                                                                                                                                                    |
| ------------------------------ | -------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `generatedAt`                  | string         | ISO 8601 timestamp of when the export was produced.                                                                                                                                                                                                                                            |
| `period`                       | string \| null | The rolling period applied, or `null` for an explicit range **or any cursor request**.                                                                                                                                                                                                         |
| `startDate` / `endDate`        | string         | Resolved window (`YYYY-MM-DD`, inclusive). `startDate` may be later than requested if the window was clamped to `legacyCutover`.                                                                                                                                                               |
| `requestedStartDate`           | string         | Present only when the window was clamped — your original start date.                                                                                                                                                                                                                           |
| `granularity`                  | string         | The bucket size actually used for `series`: `day` or `week`.                                                                                                                                                                                                                                   |
| `userId`                       | string         | The account that owns the API key.                                                                                                                                                                                                                                                             |
| `legacyCutover`                | string         | First date with page-level data (`2026-07-09`).                                                                                                                                                                                                                                                |
| `domain` / `domainId` / `role` | string         | The exported domain and your access level (`owner`, `manager`, `viewer`).                                                                                                                                                                                                                      |
| `pageCount`                    | number         | Total distinct paths matching the window and filters — across **all** pages of results, not just this one.                                                                                                                                                                                     |
| `pages[].path`                 | string         | The crawled path.                                                                                                                                                                                                                                                                              |
| `pages[].total`                | number         | Total crawls of this path in the window (after filters).                                                                                                                                                                                                                                       |
| `pages[].tiers`                | object         | The path's `verified` / `declared` / `unknown` split.                                                                                                                                                                                                                                          |
| `pages[].bots`                 | object         | Per-bot window aggregate for this path, each with its own tier split and `total`.                                                                                                                                                                                                              |
| `pages[].series`               | array          | Bucketed time series (omitted when `series: false`). Each entry has `bucketStart`/`bucketEnd` plus either the tier counts, or — when a `botId` filter is active — a tier object under each bot key.                                                                                            |
| `overflow`                     | object         | Crawls recorded against the synthetic `__overflow__` path (created when a domain-day exceeds the per-day path cap at ingest), with its own `verified` / `declared` / `unknown` / `total`. Reported separately so it never appears in the ranked list. **Omitted entirely** when there is none. |
| `nextCursor`                   | string \| null | Pass as `cursor` to fetch the next page. `null` on the last page.                                                                                                                                                                                                                              |

<Note>
  **Buckets are anchored to your window, not the calendar.** With `week`
  granularity, buckets advance in 7-day steps from `startDate`, so the first and
  last bucket may be partial — check `bucketStart`/`bucketEnd` rather than
  assuming full weeks. Aggregates (`total`, `tiers`, `bots`) are always exact
  window sums regardless of granularity.
</Note>

<Tip>
  The cursor pins the resolved window, so paging through results at midnight
  won't shift the data between pages — page 2 always matches the window page 1
  was computed from.
</Tip>

### Error Responses

| Status | Body                                                             | Description                                                                                                                                                                                                                                 |
| ------ | ---------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| 400    | `{ "error": "invalid_request", "message": "..." }`               | Missing `domainId`; an invalid window, `granularity`, `sortBy`, `pageSize`, `botId`, `tier`, or `pathPrefix`; `day` granularity on a window over 90 days; or a window that ends before `legacyCutover`. The `message` field explains which. |
| 400    | `{ "error": "invalid_cursor", "message": "..." }`                | The `cursor` token is malformed or expired. Restart from the first page.                                                                                                                                                                    |
| 401    | `{ "error": "invalid_api_key" }`                                 | API key is missing, invalid, or revoked.                                                                                                                                                                                                    |
| 403    | `{ "error": "plan_upgrade_required" }`                           | Programmatic export requires the Pro plan or above.                                                                                                                                                                                         |
| 404    | `{ "error": "domain_not_found" }`                                | The `domainId` doesn't exist or isn't accessible to your account.                                                                                                                                                                           |
| 429    | `{ "error": "rate_limit_exceeded", "usage": 300, "limit": 300 }` | Monthly or per-minute rate limit exceeded.                                                                                                                                                                                                  |

## Examples

### Paging through every crawled page

```javascript theme={null}
async function fetchAllPages(domainId) {
  const url = "https://api.hadoseo.com/functions/v1/export-page-analytics";
  const headers = {
    "Authorization": `Bearer ${process.env.HADOSEO_API_KEY}`,
    "Content-Type": "application/json",
  };

  const pages = [];
  let body = { domainId, period: "30d", series: false };
  while (true) {
    const res = await fetch(url, { method: "POST", headers, body: JSON.stringify(body) });
    if (!res.ok) throw new Error(`${res.status}: ${(await res.json()).message}`);
    const data = await res.json();
    pages.push(...data.pages);
    if (!data.nextCursor) return pages;
    body = { cursor: data.nextCursor, series: false };
  }
}
```

<Tip>
  Each API request counts against your monthly quota, so prefer a larger
  `pageSize` (up to 500) over many small pages when exporting a whole domain —
  and set `series: false` if you only need the aggregates.
</Tip>
