# Salience (LogLens) Public API reference

> REST API at https://api.loglens.ai/public/v1. Bearer API key (llapi_…). Generated from https://loglens.ai/api-docs.html.

# Public API Reference

Access your LogLens analytics data programmatically. Build custom dashboards, integrate with your tools, or automate reporting.

Base URL
 `https://api.loglens.ai`

##  1 Authentication

All API requests require authentication using an API key. API keys can be created in your [Organization Settings](https://app.loglens.ai/organization/settings).

Pass your API key in the `Authorization` header:
Request Header

```
Authorization: Bearer llapi_your_api_key_here
```

**Note:** API keys start with `llapi_` (ingest keys for your CDN or log drain start with `ll_` and cannot call this API). The rest of the key is shown only once, when it is created.

##  2 Rate Limits

Rate limits are applied per organization, per hour. The limit depends on your plan:

| Plan | Requests/Hour |
|---|---|
| Free | No API access |
| Starter | 100 |
| Growth | 500 |
| Scale | 2,000 |
| Unlimited | 10,000 |
| Professional (legacy) | 500 |
| Enterprise (legacy) | 2,000 |

Write endpoints (see [Actions & key scopes](https://loglens.ai/api-docs.html#write-side)) have a separate bucket of 120 writes per hour per organisation.

Rate limit headers are included in every response:

```
X-RateLimit-Limit: 500
X-RateLimit-Remaining: 499
X-RateLimit-Reset: 2024-01-21T18:00:00Z
```

##  3 Error Handling

The API uses standard HTTP status codes and returns JSON error responses:

| Status | Description |
|---|---|
| `200` | Success |
| `401` | Invalid or missing API key |
| `403` | API access not available on your plan |
| `404` | Website not found |
| `429` | Rate limit exceeded |
| `500` | Internal server error |

Error Response Example

```
{
"detail": "Rate limit exceeded",
"limit": 100,
"reset_at": "2024-01-21T18:00:00Z"
}
```

##  4 Date Ranges

Many log analytics endpoints accept a time range. Use `hours` for a rolling window, or `start` and `end` for a specific date range (ISO 8601 format).
Rolling window (last 7 days)

```
GET /websites/{id}/bots?hours=168
```

Specific date range

```
GET /websites/{id}/bots?start=2026-03-05&end=2026-03-27
```

If neither is provided, the default is `hours=24`. When `start`/`end` are present, `hours` is ignored. Defaults differ by endpoint (24, 168 or 720 hours), and snapshot endpoints take no time range: Site Checks (`checks/history` takes `days`), URL search, Page Importance, health and alert configuration. Index coverage's `hours` is a crawl-recency threshold measured back from now, not a historical selection. Each endpoint below lists its parameters and [openapi.json](https://loglens.ai/openapi.json) carries the reviewed set; do not assume a parameter applies where it is not listed.

### Common Query Parameters

Two more parameters: `max_rows` works on every JSON `GET /public/v1/websites/{id}/…` read; `segment` only on reads whose log queries apply it:

| Parameter | Description |
|---|---|
| `segment=<segment_id>` | Narrow the read to one saved segment (page group). Ids come from `GET segments`. It is currently applied by summary, traffic, bots, bots/access, paths, status-codes, referrers, devices, live, llms, ai-funnel, ai-landing, crawl-report, crawl-audit, crawl-links and the `logs` section of url. Other reads — including geography, ips, the `seo/…` family, requests, recommendations, Search Console, Site Checks, index coverage and URL search — ignore it and return unfiltered data. Pre-aggregated rollups are bypassed while a segment is active, so long windows are a little slower. An id that doesn't belong to the site is ignored and the response is unfiltered. |
| `max_rows=N` | Caps *every* list in the response to its first `N` entries and adds a `_truncated` map — `{"max_rows", "original_counts": {"<field path>": <original length>}, "note"}` — so you can tell what was cut. It exists to keep payloads small for MCP / agent tooling: the big detail endpoints (`seo`, `llms`, `ips`) return multi-megabyte bodies over long windows. Omit it for full data, or use Create Export for large datasets. |

Segment + row cap

```
GET /websites/{id}/paths?hours=720&segment=seg_ab12cd34ef&max_rows=50

{
"paths": [ … first 50 … ],
"_truncated": { "max_rows": 50, "original_counts": { "paths": 4812 }, "note": "Lists capped at max_rows entries. Omit max_rows for full data, or use create_export for large datasets." }
}
```

### Googlebot Split Variants

The bots endpoint accepts `split_variants=true` to show Googlebot Desktop and Googlebot Smartphone as separate entries instead of merged:
Split Googlebot variants

```
GET /websites/{id}/bots?hours=168&split_variants=true
```

##     Command-Line Interface (CLI) NEW

Access your LogLens analytics from the terminal. Query traffic, bots, SEO data, and more — with table, JSON, and CSV output.

### Installation
Terminal

```
npm install -g @salience/lens-cli
```

### Setup
Terminal

```
# Save your API key
salience config set-key YOUR_API_KEY

# Set a default website (optional)
salience config set-website YOUR_WEBSITE_ID
```

### Examples
Terminal

```
# List websites
salience websites

# Bot breakdown (last 7 days, JSON output)
salience bots -w <id> -h 168 --json

# SEO crawl budget by directory
loglens seo budget-urls -w <id> --dir /blog/

# Export paths as CSV
salience paths -w <id> --csv > paths.csv

# Pipe to jq
salience bots -w <id> --json | jq '.[].name'

# Query a specific date range
salience bots -w <id> --start 2026-03-05 --end 2026-03-27

# Show Googlebot Desktop and Smartphone separately
salience bots -w <id> --split-variants
```

Run `salience --help` (or `salience <command> --help`) for commands and their options. Coding agents can use the bundled skill: `salience skills install <new-directory>` copies it without touching existing files, and a copied skill is not updated when the CLI is upgraded. See the [help docs](https://loglens.ai/help#cli) for detailed usage.

##     MCP Server Integration NEW

Connect LogLens to AI assistants like **Claude**, **Cursor**, and other tools that support the [Model Context Protocol (MCP)](https://modelcontextprotocol.io). The MCP server exposes public API reads and read_write actions as tools that AI assistants can call. Parameters are tool-specific: many log tools take `hours` or `start`/`end`, while snapshot tools such as Site Checks, index coverage and URL search take none.

MCP Endpoint
 `https://mcp-logs.salience.com/mcp?apiKey=YOUR_API_KEY`

### Connect with OAuth (Claude)

No API key needed. Add `https://mcp-logs.salience.com/mcp` as a custom connector in Claude.ai, or run `claude mcp add --transport http salience https://mcp-logs.salience.com/mcp` in Claude Code. Claude opens a Salience page in your browser: sign in with Google or an emailed code (a new email creates the account and offers Free or a 14-day trial), pick the organisation and approve the scope. The connection is listed under Organization → API Access as a key named after the client; revoke it there to disconnect. The key-based setup below still works for every client.

### Setup

Install the `mcp-remote` bridge (requires Node.js 18+):
Terminal

```
npm install -g mcp-remote
```

### Claude Desktop

Add to your `claude_desktop_config.json`:
claude_desktop_config.json

```
{
"mcpServers": {
"salience": {
"command": "npx",
"args": [
"mcp-remote",
"https://mcp-logs.salience.com/mcp?apiKey=YOUR_API_KEY"
]
}
}
}
```

If `npx` isn't found, use the full path to node and mcp-remote. Run `which node` and `which mcp-remote` to find them.

### Cursor

Go to **Settings → MCP Servers** and add the endpoint URL:
MCP Server URL

```
https://mcp-logs.salience.com/mcp?apiKey=YOUR_API_KEY
```

### Other MCP Clients

Any MCP-compatible client can connect using the endpoint URL. Clients that don't support remote servers directly can use the `mcp-remote` bridge as shown above.

### Available Tools

Read tools cover traffic, bots, SEO, crawl budget, index coverage, URL evidence, Site Checks, segments, crawls, recommendations and more, alongside a smaller set of write tools. Clients discover the current catalogue when they connect; the MCP setup guide lists every tool and its parameters. Write tools need a `read_write` key (see [Actions & key scopes](https://loglens.ai/api-docs.html#write-side)).

| Tool | Description |
|---|---|
| `list_websites` | List all websites in your account |
| `get_summary` | Traffic summary for a website |
| `get_traffic` | Traffic time-series data |
| `get_bots` | Bot and crawler breakdown (supports `split_variants` for Googlebot Desktop/Smartphone) |
| `get_seo` | SEO crawler analytics |
| `get_budget_urls` | Per-URL crawl budget breakdown |
| `get_url_patterns` | Auto-detected URL patterns |
| `get_index_coverage` | Google index coverage summary |

Plus more read tools (paths, geography, status codes, IPs, referrers, devices, LLM crawlers, AI funnel and landing pages, Search Console, Google Analytics, URL detail and search, segments, crawl join / audit / links, recommendations, insights, health, Site Checks, page importance, robots.txt, sitemap history, site events, exports, alerts) and write tools (acknowledge alerts, add site events, suppress / unsuppress bots, resolve / re-open recommendations, create / delete segments, start / cancel crawls). `max_rows` and `segment` are offered only on the tools that list them.

---
GET

## List Websites

Returns all websites accessible to your API key.
Endpoint

```
GET /public/v1/websites
```

### Response

```
{
"websites": [
{
"id": "ws_abc123",
"domain": "example.com",
"name": "Main Website",
"created_at": "2024-01-01T00:00:00Z"
}
],
"count": 1
}
```

POST

## Create Website
NEW

Register a website in the key's organisation, mint its ingest key and detect the hosting platform. This is what the MCP `create_website` tool calls. Requires a key with the `read_write` scope (a read-only key gets a 403).

```
POST /public/v1/websites
```

### Request Body

| Name | Type | Description |
|---|---|---|
| `domain` | string | Required. The site's hostname; scheme and path are stripped. |
| `name` | string | Display name. Defaults to the domain. |
| `source_type` | string | Force a platform: `cloudflare`, `cloudfront`, `vercel`, `vector`, `kinsta`, `shopify`, `netlify`. Otherwise detected from the domain, falling back to `cloudflare`. |

### Response

```
{
"website_id": "…", "domain": "example.com", "name": "example.com", "source_type": "cloudflare",
"ingest": { "url": "https://ingest.salience.com/", "header": "X-API-Key", "api_key": "…shown once…" },
"detected": { "cdn": "cloudflare", "display_name": "Cloudflare", "compatible": true },
"guide_url": "https://salience.com/help#cloudflare"
}
```

GET

## Setup Status
NEW

Platform, ingest endpoint, whether logs have arrived and what to do next. Backs the MCP `get_setup_instructions` and `get_setup_status` tools. The ingest key is only returned by Create Website; this endpoint never repeats it.

```
GET /public/v1/websites/{website_id}/setup
```

### Response

```
{
"website_id": "…", "domain": "example.com", "source_type": "cloudflare",
"ingest": { "url": "https://ingest.salience.com/", "header": "X-API-Key" },
"logs": { "state": "received", "ever_received": true, "last_received_date": "2026-09-18", "records_last_30d": 120483 },
"receiving": true, "next_steps": ["Logs are arriving. …"], "guide_url": "…", "checked_at": "…"
}
```

GET

## Billing Status
NEW

The organisation's plan, trial state, whether a card is on file, a Stripe customer-portal link when a customer exists, and the paid plans a trial or checkout can target.

```
GET /public/v1/billing
```

### Response

```
{
"org_id": "…", "plan": "free", "plan_name": "Free", "subscription_status": null,
"trial": null, "card_on_file": false, "portal_url": null,
"available_plans": [{ "id": "basic", "name": "Solo", "price_monthly_usd": 19, "trial_period_days": 14 }, …]
}
```

POST

## Start Trial
NEW

Start the one 14-day no-card trial of a paid plan for a Free organisation. Same rules as the dashboard: one trial per user account and per organisation; a 409 with a `code` explains a refusal. Requires a key with the `read_write` scope (a read-only key gets a 403).

```
POST /public/v1/billing/trial
```

### Request Body

| Name | Type | Description |
|---|---|---|
| `plan_id` | string | Required. A public paid plan id, e.g. `basic`, `starter`, `growth`, `scale`. |

### Response

```
{ "plan": "growth", "subscription_status": "trial_nocard", "trial_ends_at": "2026-10-02T…" }
```

POST

## Start Checkout
NEW

Create a Stripe Checkout session for a plan and return its URL. The customer opens the link in a browser and enters card details on Stripe's page; nothing about the card passes through this API. During a no-card trial the remaining trial days and the first-month discount apply; an organisation with a live subscription is switched in place instead and gets no URL. Requires a key with the `read_write` scope (a read-only key gets a 403).

```
POST /public/v1/billing/checkout
```

### Request Body

| Name | Type | Description |
|---|---|---|
| `plan_id` | string | Required. A purchasable plan id. |
| `interval` | string | `monthly` (default) or `yearly`. |

### Response

```
{ "checkout_url": "https://checkout.stripe.com/c/pay/…" }
```

GET

## Get Summary

Returns summary statistics for a website.
Endpoint

```
GET /public/v1/websites/{website_id}/summary
```

### Parameters

| Name | Type | Description |
|---|---|---|
| `hours` | integer | Time period in hours (default: 24, max: 8760) |

GET

## Get Traffic

Returns traffic data with hourly breakdown.

```
GET /public/v1/websites/{website_id}/traffic?hours=24
```

GET

## Get Bots

Returns bot analytics including identification and verification status.

```
GET /public/v1/websites/{website_id}/bots?hours=24
```

GET

## Get Bot Access

Served vs rejected, per bot. For each bot: `served` (2xx/3xx) and `rejected` (401/403/429/451) requests, each split into verified and unverified identity; `rejected_by_status` (which codes the site used); `rejected_paths` (top 5 paths where rejections happened, with status); `access` = `mixed` (served on some requests, rejected on others — listed first), `rejected`, `served` or `none` (only 404s/errors); and `rejected_who` = `impersonators_only`, `verified_only`, `both` or `unverifiable`. Blocking only impersonators is the healthy pattern; `verified_only` means a rule is turning away the real bot. `hours` defaults to 168; `limit` up to 500.

```
GET /public/v1/websites/{website_id}/bots/access?hours=168
```

GET

## Get Paths

Returns path/URL analytics sorted by request count.

```
GET /public/v1/websites/{website_id}/paths?hours=24&limit=100
```

### Parameters

| Name | Type | Description |
|---|---|---|
| `hours` | integer | Time period (default: 24) |
| `limit` | integer | Max results (default: 100, max: 1000) |

GET

## Get Geography

Returns geographic distribution of traffic by country and city.

```
GET /public/v1/websites/{website_id}/geography?hours=24
```

GET

## Get Status Codes

Returns HTTP status code distribution with hourly breakdown.

```
GET /public/v1/websites/{website_id}/status-codes?hours=24
```

GET

## Get IPs

Returns IP address and IP range analytics.

```
GET /public/v1/websites/{website_id}/ips?hours=24
```

GET

## Get Referrers

Returns referrer domain analytics - see which sites are sending traffic to you.

```
GET /public/v1/websites/{website_id}/referrers?hours=24&limit=100
```

### Query Parameters

- `hours` - Time period (1-8760, default: 24)
- `limit` - Maximum referrers to return (1-500, default: 100)

GET

## Get Devices

Returns device, browser, and operating system analytics.

```
GET /public/v1/websites/{website_id}/devices?hours=24
```

### Response includes

- `browsers` - Browser breakdown (Chrome, Safari, Firefox, etc.)
- `operating_systems` - OS breakdown (Windows, macOS, iOS, Android, etc.)
- `device_types` - Device type breakdown (Desktop, Mobile, Tablet)

GET

## Get SEO Stats

Returns search engine crawler statistics - Googlebot, Bingbot, etc.

```
GET /public/v1/websites/{website_id}/seo?hours=24&bot=googlebot
```

### Query Parameters

- `hours` - Time period (1-8760, default: 24)
- `bot` - Filter by specific bot (optional, e.g. "googlebot", "bingbot")

### Response includes

- `crawler_requests` - Total crawler requests
- `verified_requests` - Verified (legitimate) crawler requests
- `unverified_suspicious` - Potentially spoofed crawler requests
- `avg_response_time_ms` - Average response time to crawlers
- `top_crawlers` - Breakdown by crawler

## Actions & key scopes

API keys have a **scope**: `read` (the default — every endpoint above) or `read_write`, which can also perform the actions below. Create a read & write key under *Organization → API Access*. A read-only key gets `403` with `"This API key is read-only"`. Writes are limited to 120 per hour per organisation, and **every write is recorded as a site event** on the timeline naming the key. Nothing here deletes data or touches your hosting.

Two conveniences for clients: `PATCH` is accepted alongside `PUT` on `segments/{segment_id}` (both replace the segment with the body you send), and `GET recommendations/snippet` accepts `days=N` as well as `hours` (`days` is converted to hours, clamped to 1–365, and only used when `hours` is absent).
Endpoints (all under /public/v1/websites/{website_id})

```
POST   alerts/{alert_id}/acknowledge
POST   site-events                       {"title", "date": "YYYY-MM-DD", "time"?, "category"?, "description"?}
DELETE site-events/{event_id}
POST   bots/{bot_name}/suppress          {"days"?: 1-365, "reason"?}     — stops SEO-error alerts for that bot; expires after `days`
DELETE bots/{bot_name}/suppress
POST   recommendations/resolve           {"key"}                          — key from GET recommendations
DELETE recommendations/resolve/{key}
POST   segments                          {"name", "rules": [{"type","value"}], "exclude_rules"?, "parent_id"?, "colour"?}
PUT    segments/{segment_id}             same body as POST — PATCH is accepted as an alias of PUT
DELETE segments/{segment_id}   ·   POST segments/library   (see Segments)
POST   crawls/run                        {"max_pages"?: 100-50000}
POST   crawls/{crawl_id}/cancel   ·   GET crawls            (see Crawls)
```

Example

```
curl -X POST https://api.loglens.ai/public/v1/websites/{website_id}/bots/ByteSpider/suppress \
  -H "Authorization: Bearer llapi_…" -H "Content-Type: application/json" \
  -d '{"days": 30, "reason": "known scraper, reviewed"}'

{"crawler": "ByteSpider", "suppressed": true, "until": "2026-10-04T10:12:00+00:00"}
```

## Deploy snippets

Turns a recommendation into the exact rule for your platform. `type`: `block_ips` (abusive / unverified-bot IPs), `block_bots` (fake bot user agents — prefer a challenge to a hard block) or `gone_404s` (paths to return 410 Gone). `target`: `cloudflare` (WAF expression), `cloudfront` (CloudFront Function), `nginx`, `apache`, `netlify`, `vercel` or `robots`; defaults to the site's platform. Lists are capped at 500 entries and every response carries `notes` with the caveats. Nothing is applied automatically — review, then paste.

Time window: `hours`, or `start`/`end`, or `days=N` (1–365; converted to hours when `hours` is not given). Pass one of them explicitly: without a window the underlying recommendation queries use the API's default of 24 hours for the IP and bot lists, not 30 days.
Request / response

```
GET /public/v1/websites/{website_id}/recommendations/snippet?type=block_ips&target=cloudflare

{
"type": "block_ips", "target": "cloudflare", "filename": "cloudflare-block-ips.txt", "count": 37,
"snippet": "(ip.src in {203.0.113.7 198.51.100.22 …})",
"notes": ["Security → WAF → Custom rules → Create rule → Edit expression; action Block (or Managed Challenge to be safe)."]
}
```

## Segments

Saved page groups defined in the dashboard (SEO → Segments): a name and rules — `prefix`, `contains`, `exact`, `regex` on the path, or `query` on the query string — with optional exclusions and nesting. They are evaluated on the logs at query time, so they apply to all history. Add `?segment=<segment_id>` to a segment-aware read — `/summary`, `/traffic`, `/bots`, `/paths`, `/llms`, `/ai-funnel`, `/ai-landing`, `/crawl-report`, `/crawl-audit`, `/url` and the others listed under [Common Query Parameters](https://loglens.ai/api-docs.html#date-ranges) — to filter it to one group; other reads ignore it. `/segments-breakdown` returns every segment (plus `_all`) with requests, human visits, verified search-bot hits and share, Googlebot, AI bots, unique paths, crawl coverage and errors; `compare=1` adds the previous period of the same length with percentage deltas, and `segments=<id>,<id>` restricts the breakdown to that subset of segments.
Requests

```
GET /public/v1/websites/{website_id}/segments
GET /public/v1/websites/{website_id}/segments-breakdown?hours=720&compare=1
GET /public/v1/websites/{website_id}/segments-breakdown?hours=720&segments=seg_ab12cd34ef,seg_9f8e7d6c5b
GET /public/v1/websites/{website_id}/crawl-audit?hours=720&segment=seg_ab12cd34ef
```

List response (abridged)

```
{
"segments": [
{ "segment_id": "seg_ab12cd34ef", "name": "Products", "colour": "#0ea5e9", "parent_id": null,
"rules": [{ "type": "prefix", "value": "/products" }], "exclude_rules": [{ "type": "query", "value": "sort=" }] }
]
}
```

### Creating and editing segments (read_write key)

`POST segments` creates a segment; `PUT segments/{segment_id}` (or `PATCH`, an alias) replaces it with the same body shape. `name` (≤60 chars) and at least one rule are required; up to 30 rules and 30 exclusions; regexes are validated; `colour`, `parent_id` (nest under another segment), `notes` and `order` are optional. Max 100 segments per site. Both return `{"segment": {…}}`. `DELETE segments/{segment_id}` returns `{"deleted": "<id>"}`.
Create / update body

```
POST /public/v1/websites/{website_id}/segments          — or —  PUT|PATCH /public/v1/websites/{website_id}/segments/{segment_id}

{
"name": "Products",
"rules": [{ "type": "prefix", "value": "/products" }, { "type": "regex", "value": "^/p/[0-9]+" }],
"exclude_rules": [{ "type": "query", "value": "sort=" }],
"colour": "#0ea5e9", "parent_id": null, "notes": "PDPs only", "order": 10
}
```

### Segment library (read_write key)

`POST segments/library` adds the eight common page groups in one call — **Blog** (/blog, /news, /articles, /posts), **Products** (/product, /products, /p/, /shop), **Categories** (/category, /categories, /c/, /collections), **Guides & help** (/guides, /help, /support, /docs, /knowledge-base), **Pagination** (`/page/N`, `page=`, `p=`), **Parameter URLs** (`utm_`, `sort=`, `filter=`, `sessionid=`), **Search results** (/search, `q=`, `s=`) and **Account & checkout** (/account, /login, /cart, /checkout, /my-account). The body is empty (send `{}`). Segments whose name already exists on the site are skipped, so it is safe to call again; the response lists what was actually created.
Library request / response

```
POST /public/v1/websites/{website_id}/segments/library   body: {}

{ "added": ["Blog", "Pagination", "Parameter URLs", "Search results"] }
```

Breakdown response (abridged)

```
{
"compare": true,
"segments": [
{ "segment_id": "_all", "name": "All pages", "search_hits": 18420, "delta": { "search_hits": -4.2 } },
{ "segment_id": "seg_ab12cd34ef", "name": "Products", "search_hits": 9110, "search_share": 49.5, "crawl_coverage": 71.3, "delta": { "search_hits": 12.8, "errors_5xx": 0 } }
]
}
```

## Crawl Audit

Technical findings from the latest crawl, each row crossed with the logs for the period (`search_hits` from verified search bots, `human_hits`, `last_search_hit`). `findings` is ordered by severity and bot hits: broken_links, links_to_redirects, redirect_chains, canonical_issues, noindex_pages, robots_blocked, duplicate_titles, duplicate_descriptions, missing_titles, missing_descriptions, long_titles, missing_h1, multiple_h1, thin_pages, hreflang_issues, slow_pages, large_pages, sitemap_issues. `totals.bot_hits_on_findings` is the crawl budget spent on things to fix. Defaults to the latest `ready` crawl; pass `crawl_id=` (from [GET crawls](https://loglens.ai/api-docs.html#get-crawls)) to audit a specific one. `limit` caps rows per finding (default 100).
Request

```
GET /public/v1/websites/{website_id}/crawl-audit?hours=720&limit=100
GET /public/v1/websites/{website_id}/crawl-audit?hours=720&crawl_id=20260901T020000_1a2b3c4d
```

Response (abridged)

```
{
"totals": { "high": 12, "medium": 58, "low": 140, "bot_hits_on_findings": 431 },
"findings": [
{ "key": "broken_links", "severity": "high", "count": 9, "bot_hits": 212,
"rows": [{ "path": "/old-offers", "status": 404, "linked_from": { "count": 37 }, "search_hits": 118 }] }
]
}
```

## Crawl Join

The latest site crawl (an uploaded export from your site crawler, a URL list, or Salience's own crawl — managed in the dashboard under SEO → Crawl Join) crossed with the logs for the period. `summary` counts pages, **active** (fetched by a verified search bot), **ignored** (indexable and linked but never fetched) and **orphans** (fetched but not linked from the site). `ignored` is sorted by Search Console impressions, inlinks and depth; `by_depth` / `by_inlinks` give crawl attention per bucket. Returns `available: false, reason: "no_crawl"` until a crawl exists. Defaults to the latest `ready` crawl; pass `crawl_id=` (from [GET crawls](https://loglens.ai/api-docs.html#get-crawls)) to join a specific one. `limit` caps the `ignored` / `orphans` lists (default 200).
Request

```
GET /public/v1/websites/{website_id}/crawl-report?hours=720&limit=200
GET /public/v1/websites/{website_id}/crawl-report?hours=720&crawl_id=20260901T020000_1a2b3c4d
```

Response (abridged)

```
{
"available": true,
"crawl": { "source": "salience_crawler", "url_count": 1842, "completed_at": "2026-09-04T11:02:10Z" },
"summary": { "pages_in_crawl": 1842, "indexable_pages": 1610, "active": 1204, "ignored": 406, "orphans": 57, "active_pct": 74.8 },
"ignored": [{ "path": "/guides/spa-days", "depth": 4, "inlinks": 2, "impressions_28d": 1180, "search_hits": 0 }],
"orphans": [{ "path": "/old-offers", "search_hits": 312, "last_status": 200 }],
"by_depth": [{ "bucket": "3", "pages": 640, "crawled_pct": 61.2, "hits_per_page": 1.9 }]
}
```

## Link Map (crawl-links)

The internal link graph of the latest Salience crawl, crossed with your logs. Every page is a node with an internal `pagerank` (0–1, the most-linked-into page = 1), `inlinks`, `outlinks`, click `depth`, HTTP `status`, and the verified search-bot / AI / human hits in the period; `edges` are `[source_id, target_id]` pairs between the returned nodes. Nodes are the top `limit` by PageRank (public API default 200, clamped to 50–3,000); the homepage is always included. The `prefix` and `max_depth` filters are not currently applied by the public API, so filter the returned nodes yourself. Two lists come with it: `linked_but_ignored` (indexable pages with the most link equity that no search bot fetched) and `fetched_but_barely_linked` (pages search bots fetched that have two or fewer inlinks). Needs a Salience crawl — uploaded exports carry no link edges (`reason: "no_link_graph"`). Pass `crawl_id=` for a specific crawl. Standard time parameters apply to the hit counts.
Request / response (abridged)

```
GET /public/v1/websites/{website_id}/crawl-links?hours=720&limit=400

{
"totals": { "pages": 1842, "edges": 61230, "matched": 612, "shown": 400, "shown_edges": 14880 },
"nodes": [ { "id": 0, "path": "/", "depth": 0, "inlinks": 1790, "outlinks": 212, "status": 200, "pagerank": 1.0, "search_hits": 318, "google_hits": 301, "ai_hits": 12, "human_hits": 4410 }, … ],
"edges": [ [0, 17], [0, 18], … ],
"linked_but_ignored": [ { "path": "/blog/old-guide", "pagerank": 0.41, "inlinks": 96, "search_hits": 0 } ],
"fetched_but_barely_linked": [ { "path": "/tools/checker", "search_hits": 140, "inlinks": 1 } ]
}
```

## Crawls

The 20 most recent crawls of the site, newest first, plus `latest` — the newest crawl with `status: "ready"`, which is what [crawl-report](https://loglens.ai/api-docs.html#get-crawl-report) and [crawl-audit](https://loglens.ai/api-docs.html#get-crawl-audit) use unless you pass `crawl_id=`. Each crawl has `source` (`upload` — a crawl export or URL list — or `salience_crawler`, our own crawler), `status` (`queued`, `processing`, `ready`, `failed`, `blocked`, `cancelled`; uploads also pass through `pending_upload`), page counts (`url_count`, `indexable_count`, `status_200`, `unlinked_count`, `max_depth`), an `audit_summary` of findings by severity, timestamps (`created_at`, `started_at`, `completed_at`) and, for our crawler, `trigger`, `seeds`, `render_js`, `progress` and a `delta` against the previous crawl. The site-level crawl settings (`auto_crawl`, `auto_crawl_max_pages`, `crawl_render_js`, `crawl_query_keys`) and the fixed `crawler_egress_ip` to allow-list are returned alongside. Start or stop crawls with `POST crawls/run` / `POST crawls/{crawl_id}/cancel` (see [Actions](https://loglens.ai/api-docs.html#write-side)). No time-range parameters.
Request

```
GET /public/v1/websites/{website_id}/crawls
```

Response (abridged)

```
{
"crawls": [
{ "crawl_id": "20260904T020000_1a2b3c4d", "source": "salience_crawler", "status": "ready", "trigger": "scheduled",
"url_count": 1842, "indexable_count": 1610, "status_200": 1790, "unlinked_count": 57, "max_depth": 6,
"audit_summary": { "high": 12, "medium": 58, "low": 140 },
"created_at": "2026-09-04T02:00:00+00:00", "started_at": "2026-09-04T02:00:04+00:00", "completed_at": "2026-09-04T02:41:10+00:00" },
{ "crawl_id": "20260828T101512_9f8e7d6c", "source": "upload", "status": "ready", "filename": "internal_all.csv", "url_count": 1799, "completed_at": "2026-08-28T10:16:02+00:00" },
{ "crawl_id": "20260821T020000_5e4d3c2b", "source": "salience_crawler", "status": "blocked", "error": "403 from origin — allow-list the crawler IP" }
],
"latest": { "crawl_id": "20260904T020000_1a2b3c4d", "status": "ready", … },
"auto_crawl": true, "auto_crawl_max_pages": 10000, "crawl_render_js": false, "crawl_query_keys": ["page"],
"crawler_egress_ip": "18.132.26.88"
}
```

## AI Funnel

Two reports on what AI gives back. **operators**: per AI company, the search/answer fetches it made (`fetches`), its training crawls (`training_requests`), the human visitors who arrived from its answers (`referrals`, detected from the referrer host or a `utm_source=chatgpt.com`-style tag) and `fetches_per_visit` — the scrape-to-referral ratio. **discovery**: Search Console pages with at least `min_impressions` impressions (28 days) that no AI search or answer bot fetched in the period — invisible to AI search. Requires a connected Search Console property; otherwise `discovery.gsc_connected` is `false`.
Request

```
GET /public/v1/websites/{website_id}/ai-funnel?hours=720&min_impressions=50
```

Response (abridged)

```
{
"operators": [
{ "operator": "OpenAI", "bots": ["ChatGPT User", "GPTBot", "OpenAI SearchBot"],
"fetches": 1240, "training_requests": 9860, "referrals": 729, "fetches_per_visit": 1.7,
"top_landing_pages": [{ "path": "/gifts/for-him", "visits": 112 }] }
],
"totals": { "fetches": 2210, "training_requests": 14400, "referrals": 1357, "fetches_per_visit": 1.6 },
"discovery": { "gsc_connected": true, "min_impressions": 50, "checked": 312, "never_fetched": 41,
"pages": [{ "path": "/experiences/spa-days", "impressions_28d": 4810, "clicks_28d": 96, "avg_position": 6.2, "ai_fetches": 0 }] }
}
```

## AI Landing Pages

Which pages AI assistants (ChatGPT, Perplexity, Claude, Gemini, Copilot …) send people to, and what those visitors do next. A visit is AI-referred when a **human** request’s referrer host is an AI assistant (`chatgpt.com`, `perplexity.ai`, `claude.ai`, `gemini.google.com`, `copilot.microsoft.com` …) or its URL carries a `utm_source` naming one (`utm_source=chatgpt.com`). **totals**: `ai_referred_visits`, `unique_paths`, `operators`, `humans_total`, `share_of_human_traffic_pct` and `previous` (the equal period before, with `delta_pct`). **by_operator**: visits, unique paths and the most-cited page per assistant. **pages** (top 200 by visits, `limit` up to 500): `operators` {name: visits}, `humans_total` (all human visits to the page), `ai_share_pct`, `bounce_proxy_pct` (share of AI-referred visits whose IP made no other page request in the period — a heuristic), `next_paths` (top 3 pages the same IPs requested within 30 minutes, counted once per IP; assets and API routes excluded), `first_seen`, `last_seen`. **series**: visits per assistant per bucket at the period’s chart granularity. Accepts `hours` (default 720) or `start`/`end`, `countries`, `segment` and `operator` (one assistant name, e.g. `OpenAI`). `data_buffering: true` means the Athena queries are still running — repeat the call shortly.
Request

```
GET /public/v1/websites/{website_id}/ai-landing?hours=720&operator=OpenAI
```

Response (abridged)

```
{
"totals": { "ai_referred_visits": 412, "unique_paths": 57, "operators": 4, "humans_total": 188420, "share_of_human_traffic_pct": 0.22,
"previous": { "ai_referred_visits": 301, "delta_pct": 36.9 } },
"by_operator": [ { "operator": "OpenAI", "visits": 260, "unique_paths": 41, "top_path": "/gifts/for-him" } ],
"pages": [
{ "path": "/gifts/for-him", "visits": 88, "operators": { "OpenAI": 71, "Perplexity": 17 },
"humans_total": 2140, "ai_share_pct": 4.1, "bounce_proxy_pct": 38.6, "bounced": 34,
"next_paths": [ { "path": "/gifts/for-him/driving", "visits": 21 } ],
"first_seen": "2026-08-09T10:12:44Z", "last_seen": "2026-09-05T18:40:02Z" }
],
"series": [ { "hour": "2026-09-05-00-00", "OpenAI": 9, "Perplexity": 2 } ],
"granularity": "1 day",
"detection": { "referrer_hosts": ["chatgpt.com", "perplexity.ai", "claude.ai", …], "utm_sources": ["chatgpt.com", …], "follow_on_minutes": 30 }
}
```

GET

## LLM / AI Crawler Analytics
NEW

Dedicated analytics for LLM and AI crawlers (GPTBot, ClaudeBot, PerplexityBot, Google-Extended, etc.). Every crawler carries an `intent`: `training` (model-training corpus crawl), `search` (AI-search index crawl) or `fetcher` (a person asked the AI about a page; fetchers ignore robots.txt by design). `intent_totals` gives the request split. `pages_by_intent` provides ranked URL paths for each intent. Returns per-bot time series, crawler verification status, and the pages those bots are reading.
Endpoint

```
GET /public/v1/websites/{website_id}/llms?hours=168
```

### Query Parameters

| Name | Type | Description |
|---|---|---|
| `hours` | integer | Rolling window in hours (default: 24, max: 8760) |
| `start` | string | ISO 8601 start timestamp (use with `end` instead of `hours`) |
| `end` | string | ISO 8601 end timestamp |
| `countries` | string | Comma-separated ISO country codes to filter by (e.g. `US,GB,DE`) |

### Response

```
{
"total_requests": 184201,
"total_ai_requests": 21084,
"ai_percentage": 11.4,
"unique_crawlers": 7,
"unique_pages_crawled": 3419,
"intent_totals": { "training": 9860, "search": 2210, "fetcher": 430 },
"period_hours": 168,
"granularity": "hourly",
"crawlers": [
{
"bot_name": "GPTBot",
"requests": 12847,
"unique_pages": 2105,
"intent": "training",
"verified_count": 12840,
"unverified_count": 7,
"verification_status": "verified"
}
],
"bot_hourly": [
{ "time_bucket": "2026-05-09-14-00", "GPTBot": 432, "ClaudeBot": 118 }
],
"bot_names": ["GPTBot", "ClaudeBot", "PerplexityBot"],
"pages": [
{
"path": "/blog/launch",
"requests": 412,
"unique_bots": 5,
"status_2xx": 410,
"status_4xx": 2
}
]
}
```

Available on Starter and above.

GET

## Crawl Budget URLs

Per-URL crawl budget breakdown within a directory. Shows which paths search engine crawlers are spending budget on, so you can spot waste.

```
GET /public/v1/websites/{website_id}/seo/budget-urls?hours=168&directory=/blog/
```

### Query Parameters

| Name | Type | Description |
|---|---|---|
| `hours` | integer | Time window (default: 24, max: 8760) |
| `directory` | string | Directory prefix to drill into (default: `/`) |
| `bot` | string | Filter to a single crawler (e.g. `googlebot`) |
| `page` | integer | Page number (default: 1) |
| `page_size` | integer | Results per page (default: 50, max: 500) |
| `params_filter` | string | Filter by URL params (e.g. `with_params`, `no_params`) |
| `file_type` | string | Filter by file extension (e.g. `html`, `pdf`) |

### Response

```
{
"urls": [
{
"path": "/blog/seo-guide",
"requests": 842,
"percentage": 12.4,
"daily_avg": 120.3
}
],
"total": 317,
"page": 1,
"page_size": 50,
"directory": "/blog/"
}
```

Available on Starter and above.

GET

## URL Patterns

Auto-detected URL pattern templates with crawl frequency. Variable segments (numbers, UUIDs, dates) are replaced with placeholders so you can see how crawlers treat each route family.

```
GET /public/v1/websites/{website_id}/seo/url-patterns?hours=168&min_urls=3
```

### Query Parameters

| Name | Type | Description |
|---|---|---|
| `hours` | integer | Time window (default: 168, max: 8760) |
| `bot` | string | Filter to a single crawler |
| `min_urls` | integer | Minimum unique URLs per pattern to be returned (default: 3) |
| `params_filter` | string | Filter by URL params |
| `file_type` | string | Filter by file extension |

### Response

```
{
"patterns": [
{
"pattern": "/products/{id}",
"total_requests": 14820,
"unique_urls": 1042,
"daily_avg": 2117.1,
"pct_of_crawl": 22.5,
"status_2xx": 14501,
"status_3xx": 12,
"status_4xx": 301,
"status_5xx": 6,
"pattern_type": "numeric_id",
"flags": ["high_cardinality"],
"example_url": "/products/8421"
}
],
"total_crawl_requests": 65820,
"days_in_period": 7.0,
"min_urls": 3
}
```

Available on Starter and above.

GET

## Path Explorer

Hierarchical view of crawled paths with per-bot and per-status breakdowns. Use to build a directory tree of crawler activity.

```
GET /public/v1/websites/{website_id}/seo/path-explorer?hours=24
```

### Query Parameters

| Name | Type | Description |
|---|---|---|
| `hours` | integer | Time window (default: 24, max: 8760) |
| `bot` | string | Filter to a single crawler |
| `params_filter` | string | Filter by URL params |
| `file_type` | string | Filter by file extension |

### Response

```
{
"paths": [
{
"path": "/blog/seo-guide",
"requests": 842,
"bots": { "Googlebot": 621, "Bingbot": 221 },
"status_2xx": 840,
"status_3xx": 0,
"status_4xx": 2,
"status_5xx": 0
}
],
"top_bots": ["Googlebot", "Bingbot", "GPTBot"],
"total_paths": 3219
}
```

Available on Starter and above.

GET

## Status Consistency

URLs whose HTTP status changed across crawls — e.g. mostly `200` but occasionally `404` or `5xx`. Useful for surfacing flapping pages and intermittent errors that hit crawlers.

```
GET /public/v1/websites/{website_id}/seo/status-consistency?hours=168&min_requests=3
```

### Query Parameters

| Name | Type | Description |
|---|---|---|
| `hours` | integer | Time window (default: 24, max: 8760) |
| `bot` | string | Filter to a single crawler |
| `min_requests` | integer | Only include pages with at least this many crawls (default: 3) |
| `params_filter` | string | Filter by URL params |
| `file_type` | string | Filter by file extension |

### Response

```
{
"pages": [
{
"path": "/checkout",
"total_requests": 214,
"status_2xx": 198,
"status_3xx": 0,
"status_4xx": 12,
"status_5xx": 4,
"statuses": { "200": 198, "404": 12, "503": 4 },
"dominant_class": "2xx",
"consistency_pct": 92.5,
"non_2xx_pct": 7.5,
"impact_score": 16,
"last_non_2xx": "2026-05-09T18:42:00Z",
"status_classes": 3
}
],
"summary": {
"total_pages_checked": 2418,
"total_inconsistent": 147,
"critical": 12,
"warning": 38,
"healthy": 97
}
}
```

Available on Starter and above.

GET

## Raw Request Log

The request-level log behind the dashboard's Log Explorer — every stored request (bots *and* humans), newest first, with the full enriched row: geography, ASN, bot verification verdict, protocol, device and timing. All filters are optional and combine freely: `bot_name`, `is_bot`, `bot_verified`, `client_ip`, `path` (substring), `status`, `status_category`, `method`, `host`, `country`, `user_agent` (substring). Time range via `hours` or `start`/`end` (ISO 8601). Up to 500 rows per call (`limit`); continue with the returned `next_cursor` or `page`. A response with `data_buffering: true` means the archive query is still running — retry the same call in a few seconds. For bulk pulls, use [create export](https://loglens.ai/api-docs.html#create-export) with `export_type=requests`.

```
GET /public/v1/websites/{website_id}/requests?hours=24&bot_name=GPTBot&status_category=4xx&limit=100
```

Available on all plans.

GET

## Crawler Request Log

Raw crawler request log with filtering. Each row is one crawler hit — timestamp, path, status, bot identity and verification, and response time.

```
GET /public/v1/websites/{website_id}/seo/requests?hours=24&filter_type=4xx&page=1
```

### Query Parameters

| Name | Type | Description |
|---|---|---|
| `hours` | integer | Time window (default: 24, max: 8760) |
| `bot` | string | Filter to a single crawler |
| `filter_type` | string | One of: `all`, `verified`, `unverified`, `2xx`, `3xx`, `4xx`, `5xx`, `slowest` |
| `page` | integer | Page number (default: 1) |
| `page_size` | integer | Results per page (default: 50, max: 500) |
| `params_filter` | string | Filter by URL params |
| `file_type` | string | Filter by file extension |

### Response

```
{
"items": [
{
"timestamp": "2026-05-09T18:42:11Z",
"path": "/blog/old-post",
"status": 404,
"method": "GET",
"client_ip": "66.249.66.1",
"user_agent": "Mozilla/5.0 (compatible; Googlebot/2.1; ...)",
"bot_name": "Googlebot",
"bot_verified": true,
"bot_verification_status": "verified",
"time_taken_ms": 182.4
}
],
"total_count": 312,
"page": 1,
"page_size": 50,
"total_pages": 7,
"has_next": true,
"has_prev": false,
"filter": "4xx"
}
```

Available on Starter and above.

GET

## robots.txt Audit

Fetches your live `robots.txt`, parses the rule groups, and cross-references them against actual crawler activity to surface violations (bots hitting paths they were told to skip).

```
GET /public/v1/websites/{website_id}/seo/robots?hours=168
```

### Query Parameters

| Name | Type | Description |
|---|---|---|
| `hours` | integer | Time window for violation lookup (default: 24, max: 8760) |
| `bot` | string | Limit violation check to a single crawler |

### Response

```
{
"domain": "example.com",
"status": 200,
"fetch_blocked": false,
"using_saved": false,
"raw_content": "User-agent: *\nDisallow: /admin/\n...",
"rule_groups": [
{
"user_agent": "*",
"rules": [
{ "type": "disallow", "path": "/admin/" }
]
}
],
"sitemaps": ["https://example.com/sitemap.xml"],
"violations": [
{
"bot_name": "SemrushBot",
"path": "/admin/login",
"requests": 42,
"rule": "Disallow: /admin/"
}
],
"violations_total": 42,
"violation_summary": {
"SemrushBot": { "total_violations": 1, "total_requests": 42, "unique_paths": 1 }
},
"history": [],
"is_new_version": false,
"current_hash": "a1b2c3..."
}
```

Available on Starter and above.

GET

## Get Sitemap Coverage

Returns sitemap coverage data - tracks which URLs from your sitemap have been crawled by search engines, their crawl frequency, and current status.
Endpoint

```
GET /public/v1/websites/{website_id}/seo/sitemap?page=1&page_size=50&status=never_crawled
```

### Query Parameters

- `page` - Page number (default: 1)
- `page_size` - Results per page (1-500, default: 50)
- `status` - Filter: all, never_crawled, recently_crawled, stale, not_in_sitemap
- `sort` - Sort by: path, times_crawled, last_crawled, first_seen
- `sort_dir` - Sort direction: asc, desc

### Response fields (per URL)

- `url` - Full URL from sitemap
- `status` - Crawl status: crawled or not_crawled
- `last_crawl_date` - Timestamp of most recent crawl
- `crawl_count` - Total number of times this URL has been crawled
- `content_type` - Content type of the URL (e.g. text/html)
- `response_code` - HTTP response code from last crawl

GET

## URL Crawl History

Per-URL crawl history — every individual crawl event for a single sitemap URL, including which bot, status code, response time, and country.

```
GET /public/v1/websites/{website_id}/seo/sitemap/url-history?url_path=/blog/post-1&hours=720
```

### Query Parameters

| Name | Type | Description |
|---|---|---|
| `url_path` | string | Required. Path of the URL to look up (e.g. `/blog/post-1`) |
| `hours` | integer | Time window (default: 168, max: 8760) |
| `bot` | string | Filter to a single crawler |
| `page` | integer | Page number (default: 1) |
| `page_size` | integer | Results per page (default: 50, max: 500) |

### Response

```
{
"url_path": "/blog/post-1",
"total_crawls": 42,
"hours": 720,
"events": [
{
"timestamp": "2026-05-09 14:22:00",
"bot_name": "Googlebot",
"status": 200,
"method": "GET",
"user_agent": "Mozilla/5.0 (compatible; Googlebot/2.1; ...)",
"response_time_ms": 182,
"country": "US"
}
],
"page": 1,
"page_size": 50,
"total_pages": 1,
"bot_summary": {
"Googlebot": { "count": 38, "last_crawled": "2026-05-09 14:22:00" }
}
}
```

Available on Starter and above.

GET

## Index Coverage

Google index coverage for your current sitemap URLs plus any URL whose last inspection says indexed, crossed with recorded Googlebot crawls. `crawled` means a Googlebot crawl was recorded within `hours` of now (default 720) — a recency threshold, not a historical selection. `indexed` / `not_indexed` come from the last stored Search Console URL Inspection result, not a live check. `pending_inspection` counts URLs without a stored indexed or not_indexed result, including URLs never inspected; it is not evidence either way. `directories` (paged with `dir_page` and `dir_page_size`) break the buckets down by first path segment, and `data_freshness` describes each source. Returns `gsc_connected: false` with a `message` when Search Console is not connected, and `503` when the stored records cannot be read.
Endpoint

```
GET /public/v1/websites/{website_id}/seo/index-coverage
```

### Response

```
{
"gsc_connected": true, "property_url": "sc-domain:example.com", "last_sync_at": "2026-09-14T02:10:00+00:00",
"buckets": {
"crawled_indexed": 1842,
"crawled_not_indexed": 356,
"not_crawled_indexed": 23,
"not_crawled_not_indexed": 491,
"pending_inspection": 118
},
"total_urls": 2830,
"directories": [{ "directory": "/blog", "crawled_indexed": 640, "crawled_not_indexed": 88, "not_crawled_indexed": 4, "not_crawled_not_indexed": 51, "pending_inspection": 12 }],
"total_directories": 14, "dir_page": 1, "dir_page_size": 20,
"data_freshness": { "crawl_data": "Recorded Googlebot crawl recency (last 30 days)", "index_data": "Last recorded Google inspection (normally refreshed after 14 days)" }
}
```

GET

## Index Coverage URLs

Paginated URLs from index coverage, sorted by 28-day impressions, with the inspection provenance for each row. `index_status` is the last stored inspection coverage (`pending` when never inspected); `inspected_at` dates it; `inspected_url` is the exact URL Google inspected (protocol, www/apex, trailing slash) and is `null` for older inspections that did not record it; `google_canonical` is Google's choice and not proof of indexing. `crawl_count` and `last_crawled_at` come from Googlebot crawls recorded within `hours` of now. Returns `400` when Search Console is not connected; `sitemap_status` explains an empty list when no sitemap URLs have synced.
Endpoint

```
GET /public/v1/websites/{website_id}/seo/index-coverage/urls?directory=/blog&page=1&page_size=50
```

### Parameters

| Name | Type | Description |
|---|---|---|
| `bucket` | string | One of: crawled_indexed, crawled_not_indexed, not_crawled_indexed, not_crawled_not_indexed, pending_inspection. Omit for every bucket; an unrecognised name returns no rows. |
| `directory` | string | Path prefix filter, e.g. /blog |
| `page` | integer | Page number (default: 1) |
| `page_size` | integer | Results per page (default: 50). 0 returns every matching row; prefer bounded pages. |
| `hours` | integer | Googlebot crawl-recency window measured back from now (default: 720) |

### Response

```
{
"urls": [
{
"url_path": "/blog/post-1", "bucket": "crawled_indexed",
"crawl_count": 38, "last_crawled_at": "2026-09-13T14:22:00", "last_crawled_by": "Googlebot Smartphone",
"index_status": "indexed", "index_status_reason": "Submitted and indexed", "inspection_verdict": "PASS",
"inspected_at": "2026-09-02T10:00:00+00:00", "inspected_url": "https://www.example.com/blog/post-1/", "google_canonical": "https://www.example.com/blog/post-1/",
"impressions_28d": 5120, "clicks_28d": 212, "avg_position": 8.4
},
{
"url_path": "/blog/new-post", "bucket": "pending_inspection",
"crawl_count": 2, "index_status": "pending",
"inspected_at": null, "inspected_url": null, "google_canonical": null
}
],
"page": 1, "page_size": 50,
"total": 783, "total_pages": 16,
"bucket_filter": null, "directory_filter": "/blog"
}
```

GET

## Search Performance

Search Console impressions, clicks, CTR and average position as a daily series for the period, totals against the previous equal period, site-wide top queries and top pages. Requires a Search Console connection; returns `gsc_connected: false` otherwise. Data lags Google by 2–3 days and is synced daily.
Endpoint

```
GET /public/v1/websites/{website_id}/search/performance?hours=720
```

### Parameters

| Name | Type | Description |
|---|---|---|
| `hours` | integer | Period length in hours; 24 when omitted (MCP and CLI send 720). Rounded up to whole days; the window ends on the latest day Search Console has data for. `start`/`end` ISO timestamps are also accepted. |

### Response

```
{
"gsc_connected": true,
"window": { "start": "2026-08-05", "end": "2026-09-03", "days": 30, "data_through": "2026-09-03" },
"series": [
{ "day": "2026-08-05", "impressions": 61230, "clicks": 1842, "ctr": 0.0301, "position": 12.4 }
],
"totals": { "impressions": 1712340, "clicks": 52110, "ctr": 0.0304, "position": 11.9, "days_with_data": 28 },
"previous": { "impressions": 1650012, "clicks": 49870, "ctr": 0.0302, "position": 12.3, "days_with_data": 28 },
"top_queries": [ { "query": "signal checker", "impressions": 14020, "clicks": 1255, "ctr": 0.0895, "position": 2.1 } ],
"top_pages": [ { "path": "/network-status", "impressions": 437347, "clicks": 18211, "ctr": 0.0416, "position": 6.8 } ],
"top_pages_window": "28d (all pages)",
"last_sync_at": "2026-09-06T14:00:43Z",
"last_sync_counts": { "search_analytics_requests": 206, "pages_with_queries": 200 }
}
```

Before the first sync the response carries `available: false, reason: "not_synced_yet"`. A data-store failure returns `503` with `retryable: true` — never an empty series.

GET

## Search Queries (per page)

The Search Console queries that showed one page over the trailing 28 days (up to 10, synced for the top 200 pages by impressions) plus the page's daily impressions/clicks series (top 500 pages). The same fields are embedded in the [URL detail](https://loglens.ai/api-docs.html#get-url-detail) response under `index_status.top_queries` and `index_status.daily`.
Endpoint

```
GET /public/v1/websites/{website_id}/search/queries?path=/network-status
```

### Response

```
{
"gsc_connected": true,
"path": "/network-status",
"top_queries": [
{ "query": "is sky mobile down", "impressions": 21044, "clicks": 1399, "ctr": 0.0665, "position": 3.2 }
],
"daily": [ { "day": "2026-08-07", "impressions": 15230, "clicks": 612, "ctr": 0.0402, "position": 6.9 } ],
"queries_synced_at": "2026-09-06T14:00:31"
}
```

GET

## Index vs Fetch Reconciliation

Search Console index status per URL crossed with verified Googlebot fetches from your logs over the period. Four buckets: `indexed_fetched`, `indexed_not_fetched` (Google is not revisiting), `not_indexed_fetched` (fetched but refused) and `fetched_not_inspected` (status unknown). Counts cover every path in the union; examples are capped at 50 per bucket. Static assets are excluded from the fetch side. Cached for one hour per period.
Endpoint

```
GET /public/v1/websites/{website_id}/search/reconcile?hours=168
```

### Response

```
{
"gsc_connected": true,
"period_hours": 168,
"counts": { "indexed_fetched": 3074, "indexed_not_fetched": 27957, "not_indexed_fetched": 137, "fetched_not_inspected": 528, "not_indexed_not_fetched": 1479 },
"inspected_paths": 32647,
"fetched_paths": 3739,
"buckets": {
"not_indexed_fetched": [
{ "path": "/oxford-oxfordshire", "fetches": 5, "last_fetch": "2026-09-06T02:50:11Z", "error_fetches": 0, "index_status": "not_indexed", "index_status_reason": "Crawled - currently not indexed", "impressions_28d": 1012, "clicks_28d": 49 }
]
},
"examples_cap": 50,
"data_buffering": false
}
```

While the log scan is still running the response has `data_buffering: true` and `counts: null` — poll the same call again.

GET

## Google Analytics Overview

Google Analytics 4 is a **connected source**, like Search Console — not an ingest path. It adds the human outcomes the logs cannot see (sessions, active users, engagement, key events, revenue) next to the request stream. This endpoint returns the period totals against the previous equal period with deltas, a daily series that carries `log_humans` alongside the GA numbers, the top 25 traffic sources, `ai_sources` (sessions and outcomes from AI assistants; `operator` names match [AI Landing Pages](https://loglens.ai/api-docs.html#get-ai-landing)) and `measurement_gap`: human page requests in your logs against GA pageviews, with `unmeasured_pct` — the share of real human page requests GA never recorded. Synced nightly from the GA4 Data API; lags about a day; aggregates only, no visitor identifiers and no bots. Requires a connected GA4 property (Settings → Integrations → Google Analytics); returns `ga_connected: false` otherwise.
Endpoint

```
GET /public/v1/websites/{website_id}/ga/overview?hours=720
```

### Parameters

| Name | Type | Description |
|---|---|---|
| `hours` | integer | Period length in hours; 24 (one GA day) when omitted (MCP and CLI send 720). Whole GA days; the window ends on the latest day GA4 has finalised, which is normally yesterday (`window.lag_days`). `start`/`end` ISO timestamps are also accepted. |

### Response

```
{
"ga_connected": true,
"property_id": "123456789",
"property_name": "Signal Checker - GA4",
"last_sync_at": "2026-09-06T03:12:40Z",
"available": true,
"currency": "GBP",
"window": { "from": "2026-08-06", "to": "2026-09-04", "days": 30, "lag_days": 1 },
"totals": { "sessions": 48210, "active_users": 39880, "engaged_sessions": 29553, "engagement_rate": 0.613, "key_events": 812, "revenue": 3140.5, "avg_engagement_secs": 74.2, "pageviews": 71004 },
"previous": { "window": { "from": "2026-07-07", "to": "2026-08-05" }, "sessions": 45395, "active_users": 37944, "engaged_sessions": 27191, "engagement_rate": 0.599, "key_events": 731, "revenue": 2884.1, "pageviews": 67817 },
"deltas": { "sessions_pct": 6.2, "active_users_pct": 5.1, "key_events_pct": 11.1, "revenue_pct": 8.9, "pageviews_pct": 4.7, "engagement_rate_pp": 1.4 },
"series": [
{ "day": "2026-08-06", "sessions": 1588, "active_users": 1320, "pageviews": 2361, "key_events": 27, "log_humans": 3104 }
],
"sources": [
{ "source": "google", "medium": "organic", "sessions": 31205, "engaged_sessions": 20283, "engagement_rate": 0.65, "key_events": 540, "revenue": 2105.0 }
],
"ai_sources": [
{ "operator": "OpenAI", "source": "chatgpt.com", "sessions": 412, "engaged_sessions": 301, "engagement_rate": 0.731, "key_events": 19, "revenue": 96.0 },
{ "operator": "Perplexity", "source": "perplexity.ai", "sessions": 138, "engaged_sessions": 97, "engagement_rate": 0.703, "key_events": 4, "revenue": 0 }
],
"measurement_gap": { "available": true, "ga_pageviews": 71004, "log_human_page_requests": 93410, "unmeasured_pct": 24.0, "days_compared": 30, "note": "Share of human page requests in the logs that GA4 did not record (blockers, declined consent, tag failures)." }
}
```

Not connected: `ga_connected: false, available: false, reason: "ga_not_connected"`. Connected but the first sync has not finished: `reason: "not_synced_yet"`; nothing in the window: `reason: "no_data_in_window"` (`totals`, `previous`, `deltas` and `window` are `null` in all three). `deltas` values are `null` when the previous period was zero. While the log side of the gap is still computing, `measurement_gap` is `{ available: false, data_buffering: true }` and the top-level `data_buffering: true` is set — poll the same call again; with no log data it is `{ available: false, reason: "no_log_data" }`. `currency` is the property's reporting currency or `null`. Modelled data from consent mode is included in GA's figures as GA reports it.

GET

## Google Analytics Pages

GA4 landing pages for the period, sorted by sessions. Each row carries sessions, engaged sessions, engagement rate, key events (GA4 conversions), revenue, pageviews, active users and average engagement time, plus `ai_sessions` (sessions that landed here from an AI assistant) and `search_sessions` (from organic search). `path` is the GA landing-page path without the query string, so it lines up with [URL detail](https://loglens.ai/api-docs.html#get-url-detail), whose `ga` section holds the same figures for one page together with its own measurement gap.
Endpoint

```
GET /public/v1/websites/{website_id}/ga/pages?hours=720&limit=100&q=/blog/
```

### Parameters

| Name | Type | Description |
|---|---|---|
| `hours` | integer | Period length in hours; 24 (one GA day) when omitted (MCP and CLI send 720). Whole GA days; the window ends on the latest day GA4 has finalised, which is normally yesterday (`window.lag_days`). `start`/`end` ISO timestamps are also accepted. |
| `limit` | integer | Maximum pages returned (default 100). `total` is the count before the limit. |
| `q` | string | Case-insensitive substring filter on `path`. |

### Response

```
{
"ga_connected": true,
"available": true,
"currency": "GBP",
"window": { "from": "2026-08-06", "to": "2026-09-04", "days": 30, "lag_days": 1 },
"pages": [
{ "path": "/network-status", "sessions": 9120, "engaged_sessions": 6201, "engagement_rate": 0.68, "key_events": 144, "revenue": 512.0, "pageviews": 12877, "active_users": 8410, "avg_engagement_secs": 81.5, "ai_sessions": 96, "search_sessions": 7710 }
],
"total": 1842
}
```

Same `ga_connected` / `available` / `reason` contract as the overview; `pages` is empty and `window` is `null` when unavailable. MCP: `get_ga_overview` / `get_ga_pages`. CLI: `salience ga overview` / `salience ga pages [-l N] [-q filter]`.

GET

## Site Events

Annotated, site-wide events that overlay onto charts in the dashboard — deploys, algorithm updates, marketing campaigns, anything you want to correlate against traffic shifts.

```
GET /public/v1/websites/{website_id}/site-events?start_date=2026-04-01&end_date=2026-05-10
```

### Query Parameters

| Name | Type | Description |
|---|---|---|
| `start_date` | string | Optional. Earliest event date (YYYY-MM-DD) |
| `end_date` | string | Optional. Latest event date (YYYY-MM-DD). Use with `start_date`. |

### Response

```
{
"events": [
{
"website_id": "ws_abc123",
"event_id": "evt_71d2...",
"date": "2026-05-08",
"time": "14:30",
"title": "Deployed v2.4",
"category": "deploy",
"description": "New caching layer rolled out to all regions",
"created_by": "user_abc",
"created_at": "2026-05-08T14:31:09Z",
"updated_at": "2026-05-08T14:31:09Z"
}
]
}
```

Available on Starter and above.

---

## Operations

Background exports, alert history, and alert configuration for orchestrating LogLens from your own systems.
GET

## List Exports
NEW

List your background export jobs for a website with status, row count, file size, and a presigned download URL for completed jobs (URL valid for 7 days).

```
GET /public/v1/websites/{website_id}/exports
```

### Response

```
{
"exports": [
{
"job_id": "9c1f...e31",
"export_type": "seo-requests",
"status": "completed",
"row_count": 12842,
"file_size_bytes": 4129083,
"created_at": "2026-05-09T10:30:00Z",
"completed_at": "2026-05-09T10:31:48Z",
"error": null,
"filters": "{\"hours\":\"168\",\"filter\":\"4xx\"}",
"description": "4xx filter | last 7d",
"download_url": "https://...s3.amazonaws.com/exports/9c1f...e31.csv?X-Amz-..."
}
]
}
```

Available on Starter and above. Only your own exports are returned.

Bot scope by export type

`requests` covers **all bot traffic** at request level (narrow with `bot_scope`); `seo-requests` covers **search-engine bots only** (Googlebot, Bing, DuckDuckBot, etc. — no AI or scraper bots); `bot-history` covers all bots. Request-level exports include a `host` column for multi-hostname feeds.

Filters an export type does not support are never silently applied-looking: they are ignored and reported back in the creation response's `ignored_filters` field with a warning.

Completed exports also have a short, stable download link: `GET /public/v1/websites/{website_id}/exports/{job_id}/download` (302-redirects to the file; add `?redirect=false` for JSON). All read endpoints accept `?max_rows=N` to cap every list in the response (a `_truncated` map reports what was cut) — useful for very large windows in agent tooling.

POST

## Create Export
NEW

Queue a new background export job. The export runs asynchronously; poll [List Exports](https://loglens.ai/api-docs.html#list-exports) for status, or wait for the email notification when it's ready. Files are CSV and links expire after 7 days.

```
POST /public/v1/websites/{website_id}/exports
```

### Request Body

| Name | Type | Description |
|---|---|---|
| `export_type` | string | Required. One of: `traffic`, `paths`, `status-codes`, `bots`, `bot-history`, `referrers`, `geography`, `devices`, `ips`, `sitemap-coverage`, `crawl-budget-urls`, `requests`, `seo-requests`, `url-pattern-urls`, `recommendations-paths-404`, `recommendations-slow-paths`, `recommendations-unverified-bots` |
| `hours` | integer | Rolling window in hours. Use this *or* `start`/`end`. |
| `start` / `end` | string | ISO 8601 timestamps for an explicit range. |
| `bot` / `bot_name` | string | Filter to a single crawler. Naming a bot on a request-level export overrides the type's default bot scope. |
| `bot_scope` | string | For the `requests` export: `search`, `ai`, or `all` (default) bot traffic. |
| `countries` | string | Comma-separated ISO country codes. |
| `status` / `status_code` / `status_category` | string | Filter by HTTP status (e.g. `404`) or category (`4xx`). |
| `filter` | string | Sub-filter passed through to the underlying endpoint (e.g. `verified`, `slowest` for `seo-requests`). |
| `directory` | string | Directory prefix (used by `crawl-budget-urls`). |
| `pattern` | string | URL pattern template (used by `url-pattern-urls`). |
| `params_filter` / `file_type` / `path_filter` | string | Optional URL/path filters, mirror the underlying SEO endpoints. |
| `notify` | boolean | Default `true` — the key's owner is emailed when the file is ready. Send `false` for scripted / agent use and poll List Exports instead. |

### Response

```
HTTP/1.1 201 Created
{
"job_id": "9c1f...e31",
"status": "pending",
"message": "Export started. You'll receive an email when it's ready."
}
```

Available on Starter and above.

GET

## Alert History
NEW

Fired alerts for a website (anomaly detector, error spikes, new-bot detection, etc.). Sorted most-recent first; includes a count of unacknowledged alerts in the last 30 days.

```
GET /public/v1/websites/{website_id}/alerts?limit=50&type=traffic_spike
GET /public/v1/websites/{website_id}/alerts?view=incidents
```

### Query Parameters

| Name | Type | Description |
|---|---|---|
| `limit` | integer | Max alerts to return (default: 50, max: 500) |
| `type` | string | Filter to a specific `alert_type` (e.g. `traffic_spike`, `error_rate`, `new_bot`) |
| `acknowledged` | boolean | Filter by acknowledgement state (`true`/`false`) |
| `view` | string | Set to `incidents` to additionally return `incidents`: the returned page of alerts grouped by fingerprint (`alert_type|severity|subject`, e.g. offending IP or crawler name) within a rolling 6-hour join window. Each incident carries `fingerprint, alert_type, severity, subject, title, first_seen, last_seen, duration_seconds, count, alerts[]` (alert ids), `latest_alert`, `status` (`active` / `cleared` — no repeat for longer than the type’s clear window, 2h by default and 24h for nightly checks / `snoozed`) and `acknowledged_all`. In this view `unacknowledged_count` counts incidents; the per-alert figure is `unacknowledged_alert_count`. Every alert also carries its `fingerprint`, plus `last_seen` / `occurrences` when repeats were coalesced into it. |

### Response

```
{
"alerts": [
{
"website_id": "ws_abc123",
"alert_id": "2026-05-09T14:22:00Z#a1b2",
"alert_type": "traffic_spike",
"severity": "warning",
"message": "Traffic 340% above baseline for the last hour",
"details": {
"current_value": 12842,
"baseline_value": 2914,
"deviation_pct": 340.6
},
"acknowledged": false,
"created_at": "2026-05-09T14:22:00Z"
}
],
"count": 1,
"unacknowledged_count": 3
}
```

Available on Starter and above.

GET

## Alert Configuration
NEW

Returns the website's current alert configuration — which alert types are enabled, how often each runs, suppression lists, email/webhook settings, and the anomaly-detector baseline status.

```
GET /public/v1/websites/{website_id}/alerts/config
```

### Response

```
{
"config": {
"alerts_enabled": true,
"traffic_alerts_enabled": true,
"traffic_frequency_minutes": 60,
"error_alerts_enabled": true,
"error_frequency_minutes": 5,
"bot_alerts_enabled": true,
"bot_frequency_minutes": 1440,
"seo_alerts_enabled": true,
"seo_frequency_minutes": 60,
"suppressed_crawlers": [],
"suppressed_bots": [],
"email_notifications_enabled": true,
"email_disabled_types": [],
"min_email_severity": 5,
"digest_enabled": true,
"min_digest_severity": 3,
"digest_send_hour_utc": 8,
"webhook_notifications_enabled": false,
"webhook_urls": []
},
"baseline_status": {
"status": "ready",
"message": "Baseline established. Anomaly detection is active.",
"data_points": 1248,
"ready": true
}
}
```

Available on Starter and above.

GET

## Search IP Requests

Returns all requests made by a specific IP address. Useful for investigating suspicious activity.

```
GET /public/v1/websites/{website_id}/ips/{ip_address}/requests?hours=24&page=1
```

### Query Parameters

- `hours` - Time period (1-168, default: 24, max 7 days)
- `page` - Page number (default: 1)
- `page_size` - Results per page (1-500, default: 50)

### Response includes (for each request)

- `timestamp` - Request timestamp
- `path` - URL path requested
- `method` - HTTP method
- `status` - HTTP status code
- `user_agent` - User agent string
- `is_bot` - Whether request was from a bot

## Example Usage

###  $ cURL

```
curl -X GET \
"https://api.loglens.ai/public/v1/websites" \
  -H "Authorization: Bearer llapi_your_key_here"
```

###  JS JavaScript / Node.js

```
const response = await fetch(
'https://api.loglens.ai/public/v1/websites',
{
headers: {
'Authorization': `Bearer ${apiKey}`
}
}
);
const data = await response.json();
console.log(data.websites);
```

###  PY Python

```
import requests

response = requests.get(
"https://api.loglens.ai/public/v1/websites",
headers={"Authorization": f"Bearer {api_key}"}
)
data = response.json()
print(data["websites"])
```

## Recommendations, Insights & Health NEW

Programmatic access to the same actionable recommendations, AI-generated insights, real-time feed, and site/ingestion health you see in the dashboard. All read-only (`GET`), authenticated with your API key like every other endpoint.

| Endpoint | Description |
|---|---|
| `GET /public/v1/websites/{website_id}/recommendations` | All current recommendations — 404 hotspots, slow paths, unverified bots, IPs to block. |
| `GET /public/v1/websites/{website_id}/recommendations/paths-404` | Top 404 (not-found) paths worth fixing or redirecting. |
| `GET /public/v1/websites/{website_id}/recommendations/slow-paths` | Slowest paths (highest response time) — performance hotspots. |
| `GET /public/v1/websites/{website_id}/recommendations/unverified-bots` | Bots claiming a verified identity whose IP failed verification (possible impersonation). |
| `GET /public/v1/websites/{website_id}/insights` | AI-generated insights — traffic / SEO / anomaly narratives. |
| `GET /public/v1/websites/{website_id}/health` | Current site + ingestion health — whether data is flowing, error rates, feed misconfiguration. |
| `GET /public/v1/websites/{website_id}/health/history` | Historical health-status trend over time. |
| `GET /public/v1/websites/{website_id}/live` | Real-time request feed (most recent requests). Optional `?type=all|bots|human|errors`. |
| `GET /public/v1/websites/{website_id}/checks` | Site Safety Check verdicts — nightly checks on crawler access, security and serving quality, each pass/warn/fail with evidence. |
| `GET /public/v1/websites/{website_id}/checks/history` | Daily check-verdict history and transitions. Optional `?days=1-90` (default 30). |
| `GET /public/v1/websites/{website_id}/page-importance` | Page Importance — per-page verified-crawler attention scores (0–10, log scale, 90-day window). Optional `?bot=google|bing&limit=N`. |
| `GET /public/v1/websites/{website_id}/url` | URL detail — every signal for one path (`?path=` required, URL-encoded): logs, search-bot fetches, crawl, sitemap, Search Console index status, Importance. Accepts `hours` or `start`/`end`, `countries`, `segment`. |
| `POST /public/v1/websites/{website_id}/reports/seo` | Generate a Technical SEO Report (async; appears in exports). Body: `{"period_days": 7-90, "prepared_for": "..."}`. Soft limit 10/month. |

Each of these is documented below. The time-scoped ones take `hours` *or* `start`/`end`, and all of them accept `segment` and `max_rows` (see [Common Query Parameters](https://loglens.ai/api-docs.html#date-ranges)).

## Recommendations

Actionable items distilled from the site's alerts over the window: `ips_to_block`, `paths_404`, `unverified_bots` and `slow_paths`, each scored and sorted (`score`, `reason_badges`). Window: `hours` or `start`/`end`. When neither is sent the API uses **24 hours**; pass `hours=720` for the last 30 days. **Every item carries a `key`** (`ip:<ip>`, `404:<path>`, `unverified:<bot>:<ip>`, `slow:<path>`) — that is the value `POST recommendations/resolve {"key"}` takes to mark it done and `DELETE recommendations/resolve/{key}` (URL-encode it) takes to un-resolve. Resolved items drop out of the list until they reappear in newer alerts; `resolved_count` says how many are hidden.

The three detail endpoints — `recommendations/paths-404`, `recommendations/slow-paths`, `recommendations/unverified-bots` — go back to the logs for the same window (same `hours` / `start` / `end`) and return `{"items": [...], "window_days": N}` with fuller per-row detail (hit counts, verified-bot share, sample user agents, last seen). On an Athena timeout they return an empty `items` with `"error": "timeout"` — retry rather than treating it as "nothing found". Turn a list into a firewall rule with [Deploy snippets](https://loglens.ai/api-docs.html#get-snippet).
Requests

```
GET /public/v1/websites/{website_id}/recommendations?hours=720
GET /public/v1/websites/{website_id}/recommendations/paths-404?start=2026-08-01&end=2026-08-31
GET /public/v1/websites/{website_id}/recommendations/slow-paths?hours=168
GET /public/v1/websites/{website_id}/recommendations/unverified-bots?hours=168
```

Response (abridged)

```
{
"window_days": 30, "resolved_count": 4,
"ips_to_block": [
{ "key": "ip:203.0.113.7", "ip": "203.0.113.7", "country": "SG", "request_count": 18420, "alert_types": ["hacking_probe", "scraper"],
"sample_user_agent": "python-requests/2.31", "sample_paths": "/wp-login.php, /.env", "last_seen": "2026-09-03T22:14:00Z", "score": 0.91, "reason_badges": ["probing", "high volume"] }
],
"paths_404": [{ "key": "404:/old-offers", "path": "/old-offers", "request_count": 612, "score": 0.74, "reason_badges": ["googlebot hitting"] }],
"unverified_bots": [{ "key": "unverified:Googlebot:198.51.100.22", "claimed_bot": "Googlebot", "ip": "198.51.100.22", "request_count": 940 }],
"slow_paths": [{ "key": "slow:/search", "path": "/search", "avg_latency_ms": 2860 }]
}
```

## Insights

The AI-written narratives already generated for the site in the dashboard (traffic, SEO and anomaly write-ups). This endpoint reads the cache — it does not generate new ones. Insights are keyed by the dashboard `page` they were written for (default `/`; e.g. `/seo`, `/bots`) and the JSON `filters` that were active (default `{}`), so pass both exactly as the dashboard did to find a match. Returns up to 10, newest first, each with `id`, `page`, `period`, `filters`, `content` (Markdown), `tools_used`, token counts and `created_at`. An empty `insights` array means nothing has been generated for that page/filters combination.
Request

```
GET /public/v1/websites/{website_id}/insights?page=/seo&filters=%7B%22bot%22%3A%22googlebot%22%7D
```

Response (abridged)

```
{
"insights": [
{ "id": 1482, "page": "/seo", "period": "30d", "filters": { "bot": "googlebot" },
"content": "Googlebot spent 38% of its crawl on paginated category pages…", "tools_used": ["get_seo", "get_url_patterns"],
"input_tokens": 18210, "output_tokens": 640, "created_at": "2026-09-02T08:11:45Z" }
]
}
```

## Health

`GET health` is the current picture with no parameters: per-metric `current` value against a rolling `baseline_mean` / `baseline_std` with a `z_score` and `status` (`normal`, `warning`, `critical` or `insufficient_data`), whether data is flowing, the baseline's maturity (`building`, `warming_up`, `mature`) and recent alerts. `GET health/history` returns one hourly series for one metric: `metric=traffic|errors|bots|crawlers|not_found|latency` (default `traffic`) over `period=24h|7d|30d` (default `24h`), each point with the value and the baseline band so you can plot it. `latency` is an average in ms; the rest are request counts.
Requests

```
GET /public/v1/websites/{website_id}/health
GET /public/v1/websites/{website_id}/health/history?metric=errors&period=7d
```

Health response (abridged)

```
{
"metrics": {
"traffic": { "current": 4210, "baseline_mean": 3980.5, "baseline_std": 410.2, "z_score": 0.56, "status": "normal" },
"errors": { "current": 96, "current_rate": 0.0228, "z_score": 3.4, "status": "critical" },
"latency": { "current": 412.7, "unit": "ms", "status": "normal" }
},
"baseline_maturity": { "status": "mature", "observations": 612 },
"evaluation_window_minutes": 60,
"active_alerts": [{ "alert_type": "error_spike_5xx", "severity": "critical", "created_at": "2026-09-04T09:00:00Z" }]
}
```

## Site Safety Checks

Standing checks on crawler access, security and serving quality (robots.txt and sitemap delivery, crawler blocking and rate limiting, exposed files, redirects and so on), evaluated nightly. `GET checks` takes no parameters and returns the current verdict for every check: `id`, `title`, `category`, `verdict`, `weight`, an `evidence` object, `updated_at` (when that verdict was evaluated) and, after a change, `previous_verdict` and `flipped_at`. Verdicts are `pass`, `warn`, `fail`, `unknown` (could not be evaluated this run — for example our probe was blocked or skipped; never a pass) and `not_applicable` (does not apply to this site, for example too little traffic). Sorted fail, warn, unknown, pass, not_applicable, then by weight. `summary` counts only the verdicts that occur (a missing key means zero); `evaluated: false` means no evaluation has been stored yet, which is not a pass.

`evidence` keys vary by check. Delivery probes report `requested_url`, `final_url` (after redirects), `redirects`, `status` and an `outcome`: `ok`, `partial`, `missing`, `blocked`, `server_error`, `http_error`, `invalid`, `unreachable` or `unconfirmed`. Retrieval and content outcomes decide `warn` / `fail`; a `warn` with `outcome: "ok"` and `advisories` (cookies, caching, content type) is delivery advice, not a retrieval failure, and `notes` are informational. A 403 or 429 answered to our checker is reported as `blocked` for our checker; it does not prove search crawlers are blocked. `GET checks/history` takes only `days` (1–90, default 30; no `hours`/`start`/`end`) and returns the stored daily verdict per check — `date` as `YYYYMMDD` in UTC, with days that have no stored verdict absent — plus derived `changes`, newest first, including changes into or out of `unknown`.
Requests

```
GET /public/v1/websites/{website_id}/checks
GET /public/v1/websites/{website_id}/checks/history?days=60
```

Checks response (abridged)

```
{
"evaluated": true, "summary": { "pass": 15, "warn": 2, "fail": 1, "unknown": 1, "not_applicable": 3 },
"checks": [
{ "id": "robots_txt_served_statically", "title": "robots.txt delivery", "category": "crawler_access", "verdict": "fail", "weight": 5,
"evidence": { "requested_url": "https://example.com/robots.txt", "final_url": "https://www.example.com/robots.txt", "status": 503, "outcome": "server_error",
"note": "robots.txt returned HTTP 503. Google may pause crawling your whole site while robots.txt returns server errors." },
"previous_verdict": "pass", "flipped_at": "2026-09-04T01:40:12Z", "updated_at": "2026-09-04T01:40:12Z" },
{ "id": "sitemap_served_properly", "verdict": "warn",
"evidence": { "requested_url": "https://www.example.com/sitemap.xml", "final_url": "https://www.example.com/sitemap.xml", "status": 200, "outcome": "ok",
"advisories": ["sets cookies (session), which can stop a CDN caching it; crawlers generally ignore them"] }, "updated_at": "2026-09-04T01:40:12Z" },
{ "id": "ab_test_crawler_exposure", "verdict": "unknown",
"evidence": { "note": "Our probe was blocked or failed, so this check could not be evaluated. See the 'Site reachable by our checker' check; allow the SalienceBot user agent to enable it." }, "updated_at": "2026-09-04T01:40:12Z" },
{ "id": "ai_crawlers_access", "verdict": "not_applicable", "evidence": { "note": "no AI crawler traffic" }, "updated_at": "2026-09-04T01:40:12Z" }
]
}
```

History response (abridged)

```
{
"days": 60,
"history": {
"robots_txt_served_statically": [{ "date": "20260903", "verdict": "pass" }, { "date": "20260904", "verdict": "fail" }],
"ab_test_crawler_exposure": [{ "date": "20260903", "verdict": "pass" }, { "date": "20260904", "verdict": "unknown" }]
},
"changes": [
{ "check_id": "robots_txt_served_statically", "date": "20260904", "from": "pass", "to": "fail" },
{ "check_id": "ab_test_crawler_exposure", "date": "20260904", "from": "pass", "to": "unknown" }
]
}
```

## Live Feed

The most recent requests as they arrive — the same feed as the dashboard's Live view. No time range; `type=all|bots|human|errors` (default `all`) filters what comes back. Each row carries `timestamp`, `client_ip`, `method`, `path`, `status` / `status_category`, `bytes_sent`, `time_taken` (ms), `user_agent` and the classification (`is_bot`, `bot_name`, `bot_category`, `bot_verified`, `bot_verification_status`). Poll it rather than streaming; use `max_rows` to keep the payload small.
Request / response (abridged)

```
GET /public/v1/websites/{website_id}/live?type=errors&max_rows=20

{
"feed_type": "errors", "count": 20,
"requests": [
{ "timestamp": "2026-09-04T10:41:52Z", "client_ip": "203.0.113.7", "method": "GET", "path": "/wp-login.php", "status": 404, "status_category": "4xx",
"is_bot": true, "bot_name": "python-requests", "bot_category": "scripting", "bot_verified": null, "bot_verification_status": null,
"bytes_sent": 1204, "time_taken": 38.0, "user_agent": "python-requests/2.31" }
]
}
```

## Page Importance

Per-page crawl-attention scores: how much of a verified crawler's attention each URL gets, on a 0–10 log scale, computed nightly over a rolling window (`window_days` in the response, currently 90). `bot=google|bing|…` picks the crawler family (default `google`; the families available for the site are listed in `bots` — an unknown one is a 400). Pages are sorted by crawl count; `limit` (1–50,000, default 200) caps them, or `full=1` returns every page regardless of `limit`. `distribution` is the histogram of scores. Until the first nightly build has run you get `available: false, reason: "not_built_yet"`. No time-range parameters — the window is fixed by the build.
Requests

```
GET /public/v1/websites/{website_id}/page-importance?bot=google&limit=500
GET /public/v1/websites/{website_id}/page-importance?bot=bing&full=1
```

Response (abridged)

```
{
"available": true, "bot": "google", "bots": ["bing", "google"], "generated_at": "2026-09-04T00:52:10Z", "window_days": 90,
"page_count": 4812, "total_crawls": 61240,
"distribution": { "0": 2210, "1": 1180, …, "10": 3 },
"pages": [{ "path": "/", "crawls": 2140, "score": 10 }, { "path": "/products", "crawls": 890, "score": 8.7 }]
}
```

## URL detail

Everything known about one path on the site, in one call. `path` is required (URL-encoded, matched exactly and case-sensitively; a query string or fragment is dropped). Time range as elsewhere: `hours` (default 24) or `start`/`end`, plus optional `countries` and `segment`. Each top-level section is independently nullable so a missing source never blanks the rest: `logs` (totals, status mix, avg/p95 response, first/last seen, a bucketed `series`, `top_bots`, `top_referrers`, `top_countries`, and a `variant` block when the trailing-slash twin also received traffic), `search_bots` (verified Googlebot/Bingbot fetches and last fetch), `crawl` (from the latest ready crawl, with its `crawl_id` and `completed_at`: status, depth, inlinks, outlinks, PageRank, indexable, the page's declared canonical, redirect target — or `in_crawl: false`, or `available: false` with `reason` `no_crawl` or `missing_results` when saved crawl files are gone), `sitemap`, `index_status`, `ga` and `importance` (per-family score, crawls, interval, last crawl, `generated_at`). If the log scan is still running the response carries `data_buffering: true` with `logs` null — poll again in a few seconds; it is not zero traffic.

Each source keeps its own date: the selected window applies to `logs`, `search_bots` and the Search Console period, while the crawl, sitemap snapshot, inspection and importance build are as of their own timestamps. `sitemap.membership_status` is `present` (confirmed in the sitemap), `removed` (confirmed, then removed; see `removed_at`), `not_found` (no confirmed membership and the last sitemap fetch was complete) or `unknown` (no confirmed membership and completeness not established — not proof of absence); read it with `fetch_complete`, `last_successful_fetch_at` and `last_checked_at`. `in_sitemap` is true only for confirmed, unremoved membership, and `reason: "not_in_sitemap"` only means no inventory record. `index_status` is the last stored URL Inspection result: `index_status`, `index_status_reason`, `inspection_verdict`, `inspected_at` and `inspected_url` — the exact URL Google inspected, which may be the www/apex or trailing-slash variant, and `null` for older inspections that did not record it. `google_canonical` is Google's choice, not proof of indexing. `reason: "not_inspected"` means no stored inspection, not "not indexed"; `available: false` with `gsc_not_connected` or `lookup_failed` means the source could not be used.
Request

```
GET /public/v1/websites/{website_id}/url?path=%2Fpricing&hours=168
```

Response (abridged)

```
{
"path": "/pricing", "period_hours": 168,
"logs": { "requests": 6204, "humans": 5065, "bots": 1139, "verified_search": 628,
"status_mix": { "2xx": 5712, "3xx": 365, "4xx": 127, "5xx": 0 },
"avg_time_ms": 17.4, "p95_time_ms": 73, "granularity": "1 hour",
"series": [{ "time": "2026-09-06T10:00:00Z", "requests": 41, "bots": 9, "verified_search": 4 }, …],
"top_bots": [{ "name": "Bingbot", "requests": 609, "verified": 609, "last_seen": "2026-09-06T10:20:06Z" }],
"top_referrers": [{ "domain": "www.google.com", "requests": 2887 }], "top_countries": [{ "country": "GB", "requests": 4739 }], "variant": null },
"search_bots": { "google": { "fetches": 16, "last_fetch": "2026-09-06T04:33:17Z" }, "bing": { "fetches": 609, "last_fetch": "2026-09-06T10:20:06Z" } },
"crawl": { "available": true, "in_crawl": true, "status": 200, "depth": 1, "inlinks": 597, "outlinks_internal": 12, "pagerank": 0.95, "indexable": true, "canonical": "https://example.com/pricing", "redirect_to": null, "completed_at": "2026-09-05T03:10:00Z" },
"sitemap": { "membership_status": "present", "membership_confirmed": true, "in_sitemap": true, "removed": false, "last_seen": "2026-09-06T03:00:00Z",
"fetch_complete": true, "last_successful_fetch_at": "2026-09-06T03:00:00Z", "times_crawled": 8909, "last_crawled_by": "Bingbot" },
"index_status": { "available": true, "index_status": "indexed", "inspection_verdict": "PASS", "inspected_at": "2026-09-02T10:00:00Z",
"inspected_url": "https://www.example.com/pricing", "google_canonical": "https://www.example.com/pricing", "impressions_28d": 437347, "clicks_28d": 14735, "avg_position": 37.4 },
"importance": { "available": true, "families": { "google": { "score": 6, "crawls": 93, "interval_days": 1.0 }, "bing": { "score": 10, "crawls": 2748 } } }
}
```

## URL search

Typeahead over the URLs Salience already knows for the site: the sitemap inventory, search-bot crawl records and your latest Site Crawler run. Use it to find the exact path for a page you half-remember, then call [URL detail](https://loglens.ai/api-docs.html#get-url-detail) with it. `q` is a path fragment, prefix or full URL (the origin and any query string are stripped server-side; an empty `q` returns the site's most-crawled URLs); `limit` defaults to 15 (max 50). Results are ranked prefix matches first, then substring matches, then by `crawls` descending. Each result has `path`, `source` (`sitemap` = confirmed sitemap membership, `crawl_discovered` = an inventory record without confirmed membership, `crawl` = seen only in the latest crawl), `membership_status` (`present`, `removed`, `not_found` — only after a complete sitemap fetch — or `unknown`, which is not proof of absence), `membership_confirmed`, `in_sitemap`, `removed` / `removed_at`, `crawls`, `last_crawled`, `last_bot`, and from the latest crawl `crawl_status_code` and `indexable` (both `null` without a crawl). `total_matches` counts every match, not just the page returned; `sources` splits it by origin; `sitemap_snapshot` gives `fetch_complete`, `last_successful_fetch_at` and `last_checked_at` for the inventory as a whole. Powers the search box on the URL detail page (URL Lookup). No time-range parameters. MCP: `search_urls`; CLI: `salience urls network`.
Request

```
GET /public/v1/websites/{website_id}/urls/search?q=network&limit=15
```

Response

```
{
"query": "network",
"results": [
{ "path": "/networks/three", "source": "sitemap", "membership_status": "present", "in_sitemap": true, "removed": false, "crawls": 412,
"last_crawled": "2026-09-06T04:33:17Z", "last_bot": "Googlebot", "crawl_status_code": 200, "indexable": true },
{ "path": "/network-status", "source": "crawl_discovered", "membership_status": "not_found", "in_sitemap": false, "removed": false, "crawls": 97,
"last_crawled": "2026-09-05T22:10:02Z", "last_bot": "Bingbot", "crawl_status_code": 200, "indexable": true }
],
"total_matches": 23,
"sources": { "sitemap_urls": 18, "crawl": 5 },
"sitemap_snapshot": { "fetch_complete": true, "last_successful_fetch_at": "2026-09-06T03:00:00Z", "last_checked_at": "2026-09-06T03:00:00Z" }
}
```

## Technical SEO Report

Generates the client-ready Technical SEO Report (AI narrative over the log evidence) as an asynchronous job. Body: `period_days` (7–90, default 30, clamped) and optional `prepared_for` (a client or site name, ≤120 chars, printed on the cover). Returns `201` with a `job_id`; the report lands in [List Exports](https://loglens.ai/api-docs.html#list-exports) as `export_type: "seo-report"` a couple of minutes later — poll for `status: "completed"` and fetch it via the export's download URL. Soft-capped at **10 reports per organisation per month**; the 11th returns `429`. Works with a read-only key (it produces a file, it doesn't change anything) but is not counted against the write bucket.
Request

```
POST /public/v1/websites/{website_id}/reports/seo

curl -X POST https://api.loglens.ai/public/v1/websites/{website_id}/reports/seo \
  -H "Authorization: Bearer llapi_…" -H "Content-Type: application/json" \
  -d '{"period_days": 30, "prepared_for": "Acme Ltd"}'
```

Response, then collect via exports

```
HTTP/1.1 201 Created
{ "job_id": "9c1f…e31", "status": "pending", "message": "Report generating — it will appear in Downloads in a couple of minutes." }

GET /public/v1/websites/{website_id}/exports
{ "exports": [{ "job_id": "9c1f…e31", "export_type": "seo-report", "status": "completed", "description": "Technical SEO Report | last 30d | for Acme Ltd", "download_url": "https://…" }] }
```
