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.

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) 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 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 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). 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).

Tool Description
list_websitesList all websites in your account
get_summaryTraffic summary for a website
get_trafficTraffic time-series data
get_botsBot and crawler breakdown (supports split_variants for Googlebot Desktop/Smartphone)
get_seoSEO crawler analytics
get_budget_urlsPer-URL crawl budget breakdown
get_url_patternsAuto-detected URL patterns
get_index_coverageGoogle 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 — 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) 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) 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 }]
}

Crawls

The 20 most recent crawls of the site, newest first, plus latest — the newest crawl with status: "ready", which is what crawl-report and 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). 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
hoursintegerTime window (default: 24, max: 8760)
directorystringDirectory prefix to drill into (default: /)
botstringFilter to a single crawler (e.g. googlebot)
pageintegerPage number (default: 1)
page_sizeintegerResults per page (default: 50, max: 500)
params_filterstringFilter by URL params (e.g. with_params, no_params)
file_typestringFilter 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
hoursintegerTime window (default: 168, max: 8760)
botstringFilter to a single crawler
min_urlsintegerMinimum unique URLs per pattern to be returned (default: 3)
params_filterstringFilter by URL params
file_typestringFilter 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
hoursintegerTime window (default: 24, max: 8760)
botstringFilter to a single crawler
params_filterstringFilter by URL params
file_typestringFilter 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
hoursintegerTime window (default: 24, max: 8760)
botstringFilter to a single crawler
min_requestsintegerOnly include pages with at least this many crawls (default: 3)
params_filterstringFilter by URL params
file_typestringFilter 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 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
hoursintegerTime window (default: 24, max: 8760)
botstringFilter to a single crawler
filter_typestringOne of: all, verified, unverified, 2xx, 3xx, 4xx, 5xx, slowest
pageintegerPage number (default: 1)
page_sizeintegerResults per page (default: 50, max: 500)
params_filterstringFilter by URL params
file_typestringFilter 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
hoursintegerTime window for violation lookup (default: 24, max: 8760)
botstringLimit 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_pathstringRequired. Path of the URL to look up (e.g. /blog/post-1)
hoursintegerTime window (default: 168, max: 8760)
botstringFilter to a single crawler
pageintegerPage number (default: 1)
page_sizeintegerResults 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 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) 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, 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_datestringOptional. Earliest event date (YYYY-MM-DD)
end_datestringOptional. 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 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
limitintegerMax alerts to return (default: 50, max: 500)
typestringFilter to a specific alert_type (e.g. traffic_spike, error_rate, new_bot)
acknowledgedbooleanFilter by acknowledgement state (true/false)
viewstringSet 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).

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.

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 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 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://…" }] }