{"openapi":"3.1.0","info":{"title":"Virent API","version":"1.0.0","description":"The documented public HTTP interfaces of Virent: browser analytics ingest, server-side AI crawler ingest, and read endpoints for human analytics. Other routes, including every `/api/*` route and any `/v1/*` route not listed here, are internal and not a supported contract.\n\nRead endpoints take a private API key (`vnt_sk_`) and need a plan that includes the read API; without it they answer 402 Payment Required. A key reads only the workspace it was created in. Read responses are JSON aggregates computed in Postgres and may be up to 60 seconds stale. Browser CORS is only open for `/v1/ingest/batch`; call the read API from a server.\n\nVersioning: the major version is in the path (`/v1`). Backward-compatible changes (new operations, optional parameters, response fields) ship within `/v1`, so clients should ignore unknown fields; breaking changes ship only under a new major version. A deprecated operation is marked `deprecated: true` here, and its responses carry `Deprecation` (RFC 9745) and `Sunset` (RFC 8594) headers at least 6 months before it is removed. Policy: https://virent.app/docs/api#versioning-and-deprecation\n\nErrors are JSON objects with an `error` message; ingest errors add `reasonCode` and `status`. Full guide: https://virent.app/docs/api"},"externalDocs":{"description":"API and SDK reference","url":"https://virent.app/docs/api"},"servers":[{"url":"https://virent.app","description":"Production origin. `@virent.app/sdk` sends browser ingest (`/v1/ingest/batch`) and crawler ingest (`/v1/ingest/bot`) here by default."},{"url":"https://app.virent.app","description":"Host of the hosted browser script (`/js/script.js`), which posts to `/v1/ingest/batch` on this host unless `data-api` or `data-api-url` is set; first-party relays forward `/v1/*` here."}],"tags":[{"name":"Ingest","description":"Write endpoints for browser pageviews and custom events (publishable key) and server-captured AI crawler requests (server ingest secret)."},{"name":"Analytics","description":"Read endpoints for human analytics totals, time series, traffic, pages, referrers and visitors."}],"paths":{"/v1/ingest/batch":{"post":{"operationId":"ingestBrowserEvents","tags":["Ingest"],"summary":"Send a batch of browser pageviews and custom events","description":"Records up to 100 human pageview or custom events for the website that owns the publishable key; this is what the hosted script and `@virent.app/sdk/browser` call. Use it for visitor activity in a browser, never for crawler traffic. Each event's `event_id` is its idempotency key for 90 days per website: resending the same payload reports `duplicate`, a different payload with a used `event_id` reports `conflict`.\n\nThe request must carry an `Origin` header allowed for the key (the key's allowed origins, or else its website's domain and subdomains); requests without one are refused with 403. Every event's `url`, `hostname` and `canonicalUrl` must be on an allowed host. Events dated more than 72 hours ago or more than 5 minutes ahead are rejected one by one. Requests from automated user agents are answered 200 with every event `ignored`.\n\nRate limits: 120 requests per minute per key and visitor network (hashed IP), and per key `max(1000, ceil(monthly plan events × 20 / 43,200))` events per minute across all visitors. Volume above the plan's monthly events is still accepted. `?dryRun=1` runs every check and stores nothing.","security":[{"publishableKey":[]},{"publishableKeyBearer":[]},{"publishableKeyQuery":[]}],"parameters":[{"name":"dryRun","in":"query","required":false,"description":"`1` validates the batch (key, origin, plan, rate limits, schema, hosts, timestamps) and returns per-event `validated` or `rejected` results without storing or metering anything. Other values are ignored.","schema":{"type":"string","enum":["1"]}},{"name":"writeKey","in":"query","required":false,"description":"Alias of the `key` query parameter for the publishable key; used only when neither `x-api-key` nor `Authorization: Bearer` is sent.","schema":{"type":"string","pattern":"^vha_pk_"}},{"name":"Origin","in":"header","required":true,"description":"Page origin, sent automatically by browsers. Must be an http(s) origin allowed for the key.","schema":{"type":"string","format":"uri"}},{"$ref":"#/components/parameters/XRequestId"},{"name":"x-virent-proxy-key","in":"header","required":false,"description":"First-party relays only: the same website's server ingest secret (`vha_sk_`). With it, `x-virent-forwarded-for` and `x-virent-forwarded-country` are trusted as the visitor's network; a secret for another website is refused with 403 `ingest_proxy_key_denied`. Never send it from browser code.","schema":{"type":"string","pattern":"^vha_sk_"}},{"name":"x-virent-forwarded-for","in":"header","required":false,"description":"First-party relays only: the visitor's IP address (first value of a comma list). Ignored without a valid `x-virent-proxy-key`.","schema":{"type":"string","maxLength":64}},{"name":"x-virent-forwarded-country","in":"header","required":false,"description":"First-party relays only: the visitor's ISO 3166-1 alpha-2 country code. Ignored without a valid `x-virent-proxy-key`.","schema":{"type":"string","pattern":"^[A-Za-z]{2}$"}},{"$ref":"#/components/parameters/XVirentSdk"}],"requestBody":{"required":true,"description":"JSON body of at most 1 MB (1,048,576 bytes, checked against `Content-Length` and the received body).","content":{"application/json":{"schema":{"$ref":"#/components/schemas/IngestBatchRequest"}}}},"responses":{"200":{"description":"Processed. Check `events` for each event's outcome; referrer-spam events are reported `accepted` but not stored.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/IngestBatchResponse"}}},"headers":{"RateLimit-Policy":{"$ref":"#/components/headers/RateLimit-Policy"},"RateLimit":{"$ref":"#/components/headers/RateLimit"},"X-RateLimit-Limit":{"$ref":"#/components/headers/X-RateLimit-Limit"},"X-RateLimit-Remaining":{"$ref":"#/components/headers/X-RateLimit-Remaining"},"X-RateLimit-Reset":{"$ref":"#/components/headers/X-RateLimit-Reset"}}},"400":{"description":"Invalid JSON or schema (`reasonCode: invalid_payload`), or an event `timestamp` that is not a valid date (`invalid_timestamp`). Nothing is stored.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/IngestError"}}}},"401":{"description":"No key (`missing_api_key`) or an unknown, revoked or non-publishable key (`invalid_api_key`).","content":{"application/json":{"schema":{"$ref":"#/components/schemas/IngestError"}}}},"402":{"description":"The workspace's plan does not allow browser ingest (`billing_required`, `subscription_not_active` or `feature_not_in_plan`). A plan that ended within the last 30 days still ingests.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/IngestError"}}}},"403":{"description":"Missing or disallowed `Origin` (`ingest_origin_denied`), an event on a host the key does not allow (`ingest_host_denied`), or an `x-virent-proxy-key` for another website (`ingest_proxy_key_denied`). Nothing is stored.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/IngestError"}}}},"409":{"description":"Every event in the batch conflicted with an earlier event that used the same `event_id` with a different payload; the body has the `IngestBatchResponse` shape with `accepted: 0`.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/IngestBatchResponse"}}}},"413":{"description":"The body exceeds 1 MB (`reasonCode: invalid_payload`, `status: invalid`).","content":{"application/json":{"schema":{"$ref":"#/components/schemas/IngestError"}}}},"429":{"description":"Per-client request limit (`ingest_rate_limited`, error `Rate limit exceeded.`) or per-website event limit (`ingest_rate_limited`, error `Website event rate limit exceeded.`). Nothing is stored; retry after `Retry-After` seconds.","headers":{"RateLimit-Policy":{"$ref":"#/components/headers/RateLimit-Policy"},"RateLimit":{"$ref":"#/components/headers/RateLimit"},"X-RateLimit-Limit":{"$ref":"#/components/headers/X-RateLimit-Limit"},"X-RateLimit-Remaining":{"$ref":"#/components/headers/X-RateLimit-Remaining"},"X-RateLimit-Reset":{"$ref":"#/components/headers/X-RateLimit-Reset"},"Retry-After":{"$ref":"#/components/headers/Retry-After"}},"content":{"application/json":{"schema":{"$ref":"#/components/schemas/IngestError"}}}}}}},"/v1/ingest/bot":{"post":{"operationId":"ingestCrawlerRequest","tags":["Ingest"],"summary":"Record one server-captured crawler request","description":"Records one HTTP request your server received, so Virent can store it when the user agent is an AI crawler; this is what `@virent.app/sdk/server` and `@virent.app/sdk/next` call. Call it from server code only, with the website's server ingest secret; `siteId` must be that website's id. Only GET and HEAD page requests are considered (not `/api`, `/_next`, `/v1/ingest` or static assets), and Virent classifies the user agent itself: a client-sent `classification` is ignored.\n\n`requestId` deduplicates per website. The query string, fragment and credentials are removed from `url`; `path` is taken from `url`; `city`, `region`, `referer` and `query` are discarded; `headers` keeps only `accept`, `accept-language`, `user-agent` and `x-vercel-ip-country`. Visits dated more than 7 days ago or more than 1 hour ahead are not stored. Rate limit: 240 requests per minute per key and client (hashed IP, or the sent `ipHash`). No request body size limit is enforced by the handler.","security":[{"ingestSecret":[]},{"ingestSecretBearer":[]}],"parameters":[{"$ref":"#/components/parameters/XRequestId"},{"$ref":"#/components/parameters/XVirentSdk"}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/BotIngestRequest"}}}},"responses":{"200":{"description":"Processed. `accepted` is true only when a new AI crawler visit was stored.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/BotIngestResponse"}}},"headers":{"RateLimit-Policy":{"$ref":"#/components/headers/RateLimit-Policy"},"RateLimit":{"$ref":"#/components/headers/RateLimit"},"X-RateLimit-Limit":{"$ref":"#/components/headers/X-RateLimit-Limit"},"X-RateLimit-Remaining":{"$ref":"#/components/headers/X-RateLimit-Remaining"},"X-RateLimit-Reset":{"$ref":"#/components/headers/X-RateLimit-Reset"}}},"400":{"description":"Invalid JSON or schema (`reasonCode: invalid_payload`).","content":{"application/json":{"schema":{"$ref":"#/components/schemas/IngestError"}}}},"401":{"description":"No key (`missing_write_key`) or an unknown, revoked or non-secret key (`invalid_write_key`).","content":{"application/json":{"schema":{"$ref":"#/components/schemas/IngestError"}}}},"402":{"description":"The workspace's plan does not allow crawler ingest (`billing_required`, `subscription_not_active` or `feature_not_in_plan`). A plan that ended within the last 30 days still ingests.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/IngestError"}}}},"403":{"description":"`siteId` is not the website the secret belongs to (`site_key_mismatch`).","content":{"application/json":{"schema":{"$ref":"#/components/schemas/IngestError"}}}},"429":{"description":"Per-key, per-client request limit (`ingest_rate_limited`). Retry after `Retry-After` seconds.","headers":{"RateLimit-Policy":{"$ref":"#/components/headers/RateLimit-Policy"},"RateLimit":{"$ref":"#/components/headers/RateLimit"},"X-RateLimit-Limit":{"$ref":"#/components/headers/X-RateLimit-Limit"},"X-RateLimit-Remaining":{"$ref":"#/components/headers/X-RateLimit-Remaining"},"X-RateLimit-Reset":{"$ref":"#/components/headers/X-RateLimit-Reset"},"Retry-After":{"$ref":"#/components/headers/Retry-After"}},"content":{"application/json":{"schema":{"$ref":"#/components/schemas/IngestError"}}}}}}},"/v1/analytics/overview":{"get":{"operationId":"getAnalyticsOverview","tags":["Analytics"],"summary":"Human analytics totals for a date range","description":"Returns pageview, custom event, session, bounce and visitor-day totals of human (non-bot) traffic for one website in `[startAt, endAt)`, plus versioned metric metadata. Use it for headline numbers; use `/v1/analytics/traffic` to get totals, a time series and goal conversions in one call. Rate limit: 300 requests per minute per client IP, counted before authentication; every response reports it in `RateLimit-Policy` and `RateLimit`.","security":[{"privateApiKey":[]},{"privateApiKeyBearer":[]}],"parameters":[{"$ref":"#/components/parameters/WebsiteId"},{"$ref":"#/components/parameters/StartAt"},{"$ref":"#/components/parameters/EndAt"},{"$ref":"#/components/parameters/Timezone"}],"responses":{"200":{"description":"Totals for the range.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/AnalyticsOverviewResponse"}}},"headers":{"RateLimit-Policy":{"$ref":"#/components/headers/RateLimit-Policy"},"RateLimit":{"$ref":"#/components/headers/RateLimit"}}},"400":{"$ref":"#/components/responses/ReadBadRequest"},"401":{"$ref":"#/components/responses/ReadUnauthorized"},"402":{"$ref":"#/components/responses/ReadPaymentRequired"},"403":{"$ref":"#/components/responses/ReadForbidden"},"429":{"$ref":"#/components/responses/ReadTooManyRequests"}}}},"/v1/analytics/timeseries":{"get":{"operationId":"getAnalyticsTimeseries","tags":["Analytics"],"summary":"Pageviews and sessions per hour or day","description":"Returns one row per hour or day bucket (in `timezone`) covering the whole range, including empty buckets, with human pageviews and sessions started in that bucket. Hourly ranges are limited to 31 days. Rate limit: 300 requests per minute per client IP, counted before authentication; every response reports it in `RateLimit-Policy` and `RateLimit`.","security":[{"privateApiKey":[]},{"privateApiKeyBearer":[]}],"parameters":[{"$ref":"#/components/parameters/WebsiteId"},{"$ref":"#/components/parameters/StartAt"},{"$ref":"#/components/parameters/EndAt"},{"$ref":"#/components/parameters/Timezone"},{"$ref":"#/components/parameters/Interval"}],"responses":{"200":{"description":"Buckets in ascending order.","content":{"application/json":{"schema":{"type":"array","items":{"$ref":"#/components/schemas/AnalyticsTimeseriesRow"}}}},"headers":{"RateLimit-Policy":{"$ref":"#/components/headers/RateLimit-Policy"},"RateLimit":{"$ref":"#/components/headers/RateLimit"}}},"400":{"$ref":"#/components/responses/ReadBadRequest"},"401":{"$ref":"#/components/responses/ReadUnauthorized"},"402":{"$ref":"#/components/responses/ReadPaymentRequired"},"403":{"$ref":"#/components/responses/ReadForbidden"},"429":{"$ref":"#/components/responses/ReadTooManyRequests"}}}},"/v1/analytics/traffic":{"get":{"operationId":"getAnalyticsTraffic","tags":["Analytics"],"summary":"Totals, time series and goal conversions in one call","description":"Combines the overview totals, the hourly or daily time series and the total goal conversions for one website and range. Prefer it over separate overview and timeseries calls when you need both. Hourly ranges are limited to 31 days. Rate limit: 300 requests per minute per client IP, counted before authentication; every response reports it in `RateLimit-Policy` and `RateLimit`.","security":[{"privateApiKey":[]},{"privateApiKeyBearer":[]}],"parameters":[{"$ref":"#/components/parameters/WebsiteId"},{"$ref":"#/components/parameters/StartAt"},{"$ref":"#/components/parameters/EndAt"},{"$ref":"#/components/parameters/Timezone"},{"$ref":"#/components/parameters/Interval"}],"responses":{"200":{"description":"Traffic summary.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/AnalyticsTrafficResponse"}}},"headers":{"RateLimit-Policy":{"$ref":"#/components/headers/RateLimit-Policy"},"RateLimit":{"$ref":"#/components/headers/RateLimit"}}},"400":{"$ref":"#/components/responses/ReadBadRequest"},"401":{"$ref":"#/components/responses/ReadUnauthorized"},"402":{"$ref":"#/components/responses/ReadPaymentRequired"},"403":{"$ref":"#/components/responses/ReadForbidden"},"429":{"$ref":"#/components/responses/ReadTooManyRequests"}}}},"/v1/analytics/pages":{"get":{"operationId":"getAnalyticsPages","tags":["Analytics"],"summary":"Pageviews per page path","description":"Lists every page path with human pageviews in the range (pageviews with a visitor id only), ordered by pageviews then path. The list is not paginated or truncated. Rate limit: 300 requests per minute per client IP, counted before authentication; every response reports it in `RateLimit-Policy` and `RateLimit`.","security":[{"privateApiKey":[]},{"privateApiKeyBearer":[]}],"parameters":[{"$ref":"#/components/parameters/WebsiteId"},{"$ref":"#/components/parameters/StartAt"},{"$ref":"#/components/parameters/EndAt"},{"$ref":"#/components/parameters/Timezone"}],"responses":{"200":{"description":"Page rows.","content":{"application/json":{"schema":{"type":"array","items":{"$ref":"#/components/schemas/AnalyticsPageRow"}}}},"headers":{"RateLimit-Policy":{"$ref":"#/components/headers/RateLimit-Policy"},"RateLimit":{"$ref":"#/components/headers/RateLimit"}}},"400":{"$ref":"#/components/responses/ReadBadRequest"},"401":{"$ref":"#/components/responses/ReadUnauthorized"},"402":{"$ref":"#/components/responses/ReadPaymentRequired"},"403":{"$ref":"#/components/responses/ReadForbidden"},"429":{"$ref":"#/components/responses/ReadTooManyRequests"}}}},"/v1/analytics/referrers":{"get":{"operationId":"getAnalyticsReferrers","tags":["Analytics"],"summary":"Sessions per referrer and traffic source","description":"Groups human sessions started in the range by their entry referrer, traffic source, medium, origin and AI referrer (for example ChatGPT or Perplexity). Use it to see where visits, including AI-referred visits, come from. The list is not paginated. Rate limit: 300 requests per minute per client IP, counted before authentication; every response reports it in `RateLimit-Policy` and `RateLimit`.","security":[{"privateApiKey":[]},{"privateApiKeyBearer":[]}],"parameters":[{"$ref":"#/components/parameters/WebsiteId"},{"$ref":"#/components/parameters/StartAt"},{"$ref":"#/components/parameters/EndAt"},{"$ref":"#/components/parameters/Timezone"}],"responses":{"200":{"description":"Referrer rows ordered by visitor-days, then sessions.","content":{"application/json":{"schema":{"type":"array","items":{"$ref":"#/components/schemas/AnalyticsReferrerRow"}}}},"headers":{"RateLimit-Policy":{"$ref":"#/components/headers/RateLimit-Policy"},"RateLimit":{"$ref":"#/components/headers/RateLimit"}}},"400":{"$ref":"#/components/responses/ReadBadRequest"},"401":{"$ref":"#/components/responses/ReadUnauthorized"},"402":{"$ref":"#/components/responses/ReadPaymentRequired"},"403":{"$ref":"#/components/responses/ReadForbidden"},"429":{"$ref":"#/components/responses/ReadTooManyRequests"}}}},"/v1/analytics/visitors":{"get":{"operationId":"getAnalyticsVisitors","tags":["Analytics"],"summary":"Visitors by browser, device, operating system and country","description":"Breaks down visitor-days in the range by browser, device, operating system and country, using each visitor's latest pageview that day. Rate limit: 300 requests per minute per client IP, counted before authentication; every response reports it in `RateLimit-Policy` and `RateLimit`.","security":[{"privateApiKey":[]},{"privateApiKeyBearer":[]}],"parameters":[{"$ref":"#/components/parameters/WebsiteId"},{"$ref":"#/components/parameters/StartAt"},{"$ref":"#/components/parameters/EndAt"},{"$ref":"#/components/parameters/Timezone"}],"responses":{"200":{"description":"Visitor breakdowns.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/AnalyticsVisitors"}}},"headers":{"RateLimit-Policy":{"$ref":"#/components/headers/RateLimit-Policy"},"RateLimit":{"$ref":"#/components/headers/RateLimit"}}},"400":{"$ref":"#/components/responses/ReadBadRequest"},"401":{"$ref":"#/components/responses/ReadUnauthorized"},"402":{"$ref":"#/components/responses/ReadPaymentRequired"},"403":{"$ref":"#/components/responses/ReadForbidden"},"429":{"$ref":"#/components/responses/ReadTooManyRequests"}}}}},"components":{"securitySchemes":{"privateApiKey":{"type":"apiKey","in":"header","name":"x-api-key","description":"Private API key (`vnt_sk_…`) for the read API, created in Settings → Connection. It reads only the workspace it was created in, needs the `analytics_read` permission (granted by default), expires one year after creation, and stops working when its creator leaves the workspace or loses the right to create keys. Invalid keys and unknown website ids both answer 401."},"privateApiKeyBearer":{"type":"http","scheme":"bearer","bearerFormat":"vnt_sk_…","description":"The private API key as `Authorization: Bearer vnt_sk_…`; used when `x-api-key` is absent."},"publishableKey":{"type":"apiKey","in":"header","name":"x-api-key","description":"The website's publishable write key (`vha_pk_…`). Safe in browser code; bound to the website's allowed origins and hosts. Header wins over `Authorization` and the query parameters."},"publishableKeyBearer":{"type":"http","scheme":"bearer","bearerFormat":"vha_pk_…","description":"The publishable write key as `Authorization: Bearer vha_pk_…`; used when `x-api-key` is absent."},"publishableKeyQuery":{"type":"apiKey","in":"query","name":"key","description":"The publishable write key in the `key` (or `writeKey`) query parameter, as the hosted script sends it with `sendBeacon`; used only when no header carries a key."},"ingestSecret":{"type":"apiKey","in":"header","name":"x-api-key","description":"The website's server ingest secret (`vha_sk_…`). Server-side only: never ship it to browsers."},"ingestSecretBearer":{"type":"http","scheme":"bearer","bearerFormat":"vha_sk_…","description":"The server ingest secret as `Authorization: Bearer vha_sk_…` (what `@virent.app/sdk/server` sends); used when `x-api-key` is absent."}},"parameters":{"WebsiteId":{"name":"websiteId","in":"query","required":true,"description":"Id of the website to read.","schema":{"type":"string","minLength":1}},"StartAt":{"name":"startAt","in":"query","required":true,"description":"Inclusive range start. Any date string JavaScript `Date` parses is accepted; send ISO 8601 with an offset. Must be earlier than `endAt`; the range may not exceed 366 days.","schema":{"type":"string","format":"date-time"}},"EndAt":{"name":"endAt","in":"query","required":true,"description":"Exclusive range end, at most 5 minutes in the future. Same format as `startAt`.","schema":{"type":"string","format":"date-time"}},"Timezone":{"name":"timezone","in":"query","required":false,"description":"IANA time zone for day and hour buckets and for visitor-days (a visitor counts once per local date).","schema":{"type":"string","maxLength":255,"default":"UTC"}},"Interval":{"name":"interval","in":"query","required":true,"description":"Bucket size. `hour` allows ranges up to 31 days.","schema":{"type":"string","enum":["hour","day"]}},"XRequestId":{"name":"x-request-id","in":"header","required":false,"description":"Correlation id recorded in Virent's ingest logs; a UUID is generated when absent.","schema":{"type":"string"}},"XVirentSdk":{"name":"x-virent-sdk","in":"header","required":false,"description":"Optional adapter report, `<adapter>/<version>[; runtime=<runtime>]` (for example `server/0.3.1; runtime=node`). Malformed values are ignored and never affect acceptance.","schema":{"type":"string"}}},"headers":{"RateLimit-Policy":{"description":"The quota policy of the request bucket, as an IETF `RateLimit-Policy` field (draft-ietf-httpapi-ratelimit-headers-11): `\"browser\";q=120;w=60` and `\"crawler\";q=240;w=60` on ingest, `\"read\";q=300;w=60` on the read API. Not sent for the per-website ingest event bucket.","schema":{"type":"string"}},"RateLimit":{"description":"Remaining quota `r` and seconds until the window resets `t`, as an IETF `RateLimit` field, for example `\"read\";r=299;t=55`. Sent on accepted and throttled responses.","schema":{"type":"string"}},"X-RateLimit-Limit":{"description":"Requests (or, for the per-website bucket, events) allowed per 60-second window.","schema":{"type":"integer"}},"X-RateLimit-Remaining":{"description":"Remaining allowance in the current window.","schema":{"type":"integer"}},"X-RateLimit-Reset":{"description":"When the current window resets.","schema":{"type":"string","format":"date-time"}},"Retry-After":{"description":"Seconds to wait before retrying; omitted when unknown.","schema":{"type":"integer"}}},"responses":{"ReadBadRequest":{"description":"Invalid query: a validation message such as `` `startAt` and `endAt` must be valid ISO timestamps. ``, `` `timezone` must be a valid IANA time zone. ``, `` `startAt` must be earlier than `endAt`. ``, `Analytics ranges cannot exceed 366 days.`, `Hourly analytics ranges cannot exceed 31 days.`, `` `endAt` cannot be more than 5 minutes in the future. ``. Validated before authentication.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}},"headers":{"RateLimit-Policy":{"$ref":"#/components/headers/RateLimit-Policy"},"RateLimit":{"$ref":"#/components/headers/RateLimit"}}},"ReadUnauthorized":{"description":"No key, an invalid, expired or revoked key, a key from another workspace, or an unknown `websiteId`. Body: `{\"error\": \"Unauthorized\"}`.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}},"headers":{"RateLimit-Policy":{"$ref":"#/components/headers/RateLimit-Policy"},"RateLimit":{"$ref":"#/components/headers/RateLimit"}}},"ReadPaymentRequired":{"description":"The workspace has no active plan that includes the read API, or reads are restricted after two billing periods over the plan's analytics volume. Body: `{\"error\": \"A paid read API entitlement is required.\"}`.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}},"headers":{"RateLimit-Policy":{"$ref":"#/components/headers/RateLimit-Policy"},"RateLimit":{"$ref":"#/components/headers/RateLimit"}}},"ReadForbidden":{"description":"The key lacks the `analytics_read` permission. Body: `{\"error\": \"Forbidden.\"}`.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}},"headers":{"RateLimit-Policy":{"$ref":"#/components/headers/RateLimit-Policy"},"RateLimit":{"$ref":"#/components/headers/RateLimit"}}},"ReadTooManyRequests":{"description":"More than 300 read requests in a minute from this client IP. Wait `Retry-After` seconds.","headers":{"RateLimit-Policy":{"$ref":"#/components/headers/RateLimit-Policy"},"RateLimit":{"$ref":"#/components/headers/RateLimit"},"Retry-After":{"$ref":"#/components/headers/Retry-After"}},"content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}},"schemas":{"Error":{"type":"object","required":["error"],"properties":{"error":{"type":"string","description":"Human-readable message."}}},"IngestError":{"type":"object","required":["error","reasonCode","status"],"properties":{"error":{"type":"string","description":"Human-readable message."},"reasonCode":{"type":"string","description":"Machine-readable reason: `missing_api_key`, `invalid_api_key`, `missing_write_key`, `invalid_write_key`, `ingest_origin_denied`, `ingest_host_denied`, `ingest_proxy_key_denied`, `site_key_mismatch`, `billing_required`, `subscription_not_active`, `feature_not_in_plan`, `invalid_payload`, `invalid_timestamp` or `ingest_rate_limited`."},"status":{"type":"string","enum":["rejected","invalid","rate_limited"],"description":"Outcome class."},"retryAfter":{"type":["integer","null"],"description":"Seconds until the rate-limit window resets (429 only)."}}},"IngestBatchRequest":{"type":"object","required":["events"],"properties":{"events":{"type":"array","minItems":1,"maxItems":100,"items":{"type":"object","oneOf":[{"$ref":"#/components/schemas/PageviewEvent"},{"$ref":"#/components/schemas/CustomEvent"}],"discriminator":{"propertyName":"type","mapping":{"pageview":"#/components/schemas/PageviewEvent","event":"#/components/schemas/CustomEvent"}}}},"sdk":{"$ref":"#/components/schemas/SdkReport"}}},"SdkReport":{"type":"object","description":"Optional adapter report sent by the browser SDK. Any value is accepted; a malformed report is ignored and never rejects the batch.","required":[],"properties":{"adapter":{"type":"string","pattern":"^[a-z][a-z0-9-]{0,31}$","description":"Adapter name."},"version":{"type":"string","pattern":"^\\d{1,4}\\.\\d{1,4}\\.\\d{1,4}(-[0-9A-Za-z.]{1,32})?$","description":"Adapter version (semver)."},"runtime":{"type":"string","pattern":"^[a-z][a-z0-9-]{0,15}$","description":"Runtime environment."}}},"PageviewEvent":{"type":"object","required":["type","event_id","timestamp","url","path"],"properties":{"type":{"type":"string","const":"pageview","description":"Event type."},"event_id":{"type":"string","minLength":1,"maxLength":255,"description":"Unique id of this event; the idempotency key per website for 90 days."},"timestamp":{"type":"string","format":"date-time","minLength":1,"description":"When the event happened (ISO 8601). Must fall within the last 72 hours and at most 5 minutes ahead, or the event is rejected."},"url":{"type":"string","format":"uri","maxLength":2048,"description":"Absolute page URL; its host must be allowed for the key."},"path":{"type":"string","minLength":1,"maxLength":1024,"description":"Page path."},"hostname":{"type":"string","minLength":1,"maxLength":255,"description":"Page hostname; defaults to the host of `url`."},"canonicalUrl":{"type":"string","format":"uri","maxLength":2048,"description":"The page's canonical URL."},"referrer":{"type":"string","minLength":1,"maxLength":2048,"description":"Document referrer."},"visitorId":{"type":"string","minLength":1,"maxLength":255,"description":"Anonymous visitor id chosen by the client."},"sessionId":{"type":"string","minLength":1,"maxLength":255,"description":"Client session id."},"contentId":{"type":"string","minLength":1,"maxLength":255,"description":"Optional content identifier stored with the event."},"idempotencyKey":{"type":"string","minLength":1,"maxLength":255,"description":"Accepted for compatibility but not used; `event_id` is the idempotency key."},"properties":{"type":"object","additionalProperties":true,"description":"Arbitrary JSON properties, stored as sent."}}},"CustomEvent":{"type":"object","required":["type","name","event_id","timestamp"],"properties":{"type":{"type":"string","const":"event","description":"Event type."},"name":{"type":"string","minLength":1,"maxLength":255,"description":"Event name."},"event_id":{"type":"string","minLength":1,"maxLength":255,"description":"Unique id of this event; the idempotency key per website for 90 days."},"timestamp":{"type":"string","format":"date-time","minLength":1,"description":"When the event happened (ISO 8601). Must fall within the last 72 hours and at most 5 minutes ahead, or the event is rejected."},"referrer":{"type":"string","minLength":1,"maxLength":2048,"description":"Document referrer."},"visitorId":{"type":"string","minLength":1,"maxLength":255,"description":"Anonymous visitor id chosen by the client."},"sessionId":{"type":"string","minLength":1,"maxLength":255,"description":"Client session id."},"contentId":{"type":"string","minLength":1,"maxLength":255,"description":"Optional content identifier stored with the event."},"idempotencyKey":{"type":"string","minLength":1,"maxLength":255,"description":"Accepted for compatibility but not used; `event_id` is the idempotency key."},"properties":{"type":"object","additionalProperties":true,"description":"Arbitrary JSON properties, stored as sent."}}},"IngestEventResult":{"type":"object","required":["event_id","status"],"properties":{"event_id":{"type":"string","description":"The event's `event_id`."},"status":{"type":"string","enum":["accepted","duplicate","conflict","rejected","ignored","validated"],"description":"`accepted` stored (or filtered as referrer spam), `duplicate` already stored with the same payload, `conflict` already used with a different payload, `rejected` timestamp outside the accepted window, `ignored` automated user agent, `validated` dry run passed."},"reason":{"type":"string","enum":["duplicate_event_id","duplicate_event_id_in_batch","conflicting_event_id","conflicting_event_id_in_batch","timestamp_out_of_range","automated_user_agent"],"description":"Why the event was not newly stored."}}},"IngestBatchResponse":{"type":"object","required":["accepted","duplicates","conflicts","events","websiteId","workspaceId"],"properties":{"accepted":{"type":"integer","description":"Events accepted."},"duplicates":{"type":"integer","description":"Events already stored."},"conflicts":{"type":"integer","description":"Events whose `event_id` was used with a different payload."},"rejected":{"type":"integer","description":"Events rejected for a timestamp outside the window. Absent when the user agent is automated."},"automated":{"type":"integer","description":"Present only when the request's user agent is automated: the number of ignored events."},"dryRun":{"type":"boolean","const":true,"description":"Present only for `?dryRun=1`."},"validated":{"type":"integer","description":"Present only for `?dryRun=1`: events that passed validation."},"events":{"type":"array","items":{"$ref":"#/components/schemas/IngestEventResult"},"description":"One result per event, in request order."},"websiteId":{"type":"string","description":"Website the key belongs to."},"workspaceId":{"type":"string","description":"Workspace the website belongs to."}}},"BotIngestRequest":{"type":"object","required":["siteId","requestId","method","url","path"],"properties":{"siteId":{"type":"string","minLength":1,"description":"Id of the website the secret belongs to."},"requestId":{"type":"string","minLength":1,"maxLength":255,"description":"Unique id of the captured request; deduplicates per website."},"method":{"type":"string","minLength":1,"maxLength":16,"description":"HTTP method of the captured request. Only GET and HEAD are recorded."},"url":{"type":"string","format":"uri","maxLength":2048,"description":"Absolute URL requested. Query, fragment and credentials are stripped before storage."},"path":{"type":"string","minLength":1,"maxLength":1024,"description":"Request path, used to skip internal and static-asset requests. The stored path comes from `url`."},"timestamp":{"type":"string","format":"date-time","minLength":1,"description":"When the request happened (ISO 8601); defaults to receipt time. Must be within the last 7 days and at most 1 hour ahead to be stored."},"userAgent":{"type":["string","null"],"maxLength":1024,"description":"User-Agent of the captured request; Virent classifies it."},"statusCode":{"type":["integer","null"],"minimum":100,"maximum":599,"description":"HTTP status your server answered."},"host":{"type":["string","null"],"minLength":1,"maxLength":255,"description":"Requested host."},"ipHash":{"type":["string","null"],"maxLength":64,"description":"Your own hash of the client IP. When absent, Virent hashes the connecting address."},"country":{"type":["string","null"],"maxLength":64,"description":"Client country."},"accept":{"type":["string","null"],"maxLength":512,"description":"Accept header."},"acceptLanguage":{"type":["string","null"],"maxLength":512,"description":"Accept-Language header."},"crawlRule":{"type":["string","null"],"maxLength":64,"description":"Name of the crawl rule your server applied."},"robotsAllowed":{"type":["boolean","null"],"description":"Whether robots.txt allowed the request."},"headers":{"type":"object","additionalProperties":{"type":"string"},"description":"Request headers as a string map; only `accept`, `accept-language`, `user-agent` and `x-vercel-ip-country` are kept."},"city":{"type":["string","null"],"maxLength":128,"description":"Accepted but discarded."},"region":{"type":["string","null"],"maxLength":128,"description":"Accepted but discarded."},"referer":{"type":["string","null"],"maxLength":2048,"description":"Accepted but discarded."},"query":{"type":"object","additionalProperties":{"type":["string","array"],"items":{"type":"string"}},"description":"Accepted but discarded."},"rulesMatched":{"type":"array","items":{"type":"string"},"description":"Accepted but replaced by Virent's match reasons."},"classification":{"$ref":"#/components/schemas/BotClassificationInput"}}},"BotClassificationInput":{"type":"object","description":"Validated when sent, then ignored: Virent always classifies the user agent itself.","required":["confidence","family","isAiCrawler","isBot","name"],"properties":{"classificationState":{"type":"string","enum":["known","generic","unknown"],"default":"known","description":"Recognition state."},"confidence":{"type":"string","enum":["low","medium","high"],"description":"Confidence."},"crawlerType":{"type":"string","enum":["answer_engine","ai_training","ai_search","generic_bot","link_preview","monitoring","scraper","search_indexer","seo_tool","unknown"],"default":"unknown","description":"Crawler purpose."},"family":{"$ref":"#/components/schemas/BotFamily"},"isAiCrawler":{"type":"boolean"},"isBot":{"type":"boolean"},"matchReason":{"type":"array","items":{"type":"string"}},"name":{"type":["string","null"]},"provider":{"$ref":"#/components/schemas/BotFamily"},"sourceIds":{"type":"array","items":{"type":"string","minLength":1,"maxLength":128}}}},"BotFamily":{"type":"string","enum":["openai","anthropic","perplexity","google","deepseek","common-crawler","apple","bytedance","meta","amazon","duckduckgo","huawei","you","unknown"],"description":"Crawler family or provider."},"BotClassification":{"type":"object","required":["classificationState","confidence","crawlerId","crawlerType","family","identityVersion","isAiCrawler","isBot","matchReason","name","provider","providerId","sourceIds"],"properties":{"classificationState":{"type":"string","enum":["known","generic","unknown"],"description":"How the user agent was recognized."},"confidence":{"type":"string","enum":["low","medium","high"],"description":"Classification confidence."},"crawlerId":{"type":["string","null"],"description":"Catalog crawler id, when resolved."},"crawlerType":{"type":"string","enum":["answer_engine","ai_training","ai_search","generic_bot","link_preview","monitoring","scraper","search_indexer","seo_tool","unknown"],"description":"Crawler purpose: `ai_training`, `ai_search` and `answer_engine` are AI purposes."},"family":{"$ref":"#/components/schemas/BotFamily"},"identityVersion":{"type":"string","description":"Version of the crawler identity catalog used."},"isAiCrawler":{"type":"boolean","description":"Whether the crawler is an AI crawler."},"isBot":{"type":"boolean","description":"Whether the user agent is a bot."},"matchReason":{"type":"array","items":{"type":"string"},"description":"Rules that matched."},"name":{"type":["string","null"],"description":"Crawler display name."},"provider":{"$ref":"#/components/schemas/BotFamily"},"providerId":{"type":["string","null"],"description":"Catalog provider id, when resolved."},"sourceIds":{"type":"array","items":{"type":"string"},"description":"Upstream registry ids that matched."}}},"BotIngestResponse":{"type":"object","description":"One of three shapes, told apart by `reason`.","oneOf":[{"$ref":"#/components/schemas/BotIngestIneligible"},{"$ref":"#/components/schemas/BotIngestNotAiTraffic"},{"$ref":"#/components/schemas/BotIngestResult"}]},"BotIngestIneligible":{"type":"object","required":["accepted","reason"],"properties":{"accepted":{"type":"boolean","const":false},"reason":{"type":"string","const":"ineligible-request","description":"Not a GET/HEAD page request."}}},"BotIngestNotAiTraffic":{"type":"object","required":["accepted","classification","reason"],"properties":{"accepted":{"type":"boolean","const":false},"classification":{"$ref":"#/components/schemas/BotClassification"},"reason":{"type":"string","const":"not-ai-traffic","description":"The user agent is not an AI crawler; nothing is stored."}}},"BotIngestResult":{"type":"object","required":["accepted","classification","duplicate","reason","websiteId","workspaceId"],"properties":{"accepted":{"type":"boolean","description":"True when a new visit was stored."},"classification":{"$ref":"#/components/schemas/BotClassification"},"duplicate":{"type":"boolean","description":"True when `requestId` was already stored for this website."},"reason":{"type":["string","null"],"enum":[null,"duplicate-request","invalid-timestamp","timestamp-out-of-range"],"description":"Null when stored; otherwise why not."},"websiteId":{"type":"string","description":"Website the secret belongs to."},"workspaceId":{"type":"string","description":"Workspace the website belongs to."}}},"AnalyticsOverviewTotals":{"type":"object","description":"Human (non-bot) totals for the range.","required":["bouncedSessions","pageviewSessions","totalEvents","totalPageviews","totalSessions","uniqueVisitors"],"properties":{"bouncedSessions":{"type":"integer","description":"Sessions with exactly one pageview."},"pageviewSessions":{"type":"integer","description":"Sessions with at least one pageview; the bounce-rate denominator."},"totalEvents":{"type":"integer","description":"Custom events."},"totalPageviews":{"type":"integer","description":"Pageviews."},"totalSessions":{"type":"integer","description":"Sessions started in the range (a session ends after 30 minutes without activity)."},"uniqueVisitors":{"type":"integer","description":"Visitor-days: distinct visitors per local date in `timezone`, summed over the range."}}},"MetricResult":{"type":"object","required":["value","unit","state","evidenceClass","grain","metricVersion","source","startAt","endAt","timezone"],"properties":{"value":{"type":["number","null"],"description":"The metric value; null when it cannot be calculated."},"unit":{"type":"string","description":"Unit, for example `pageviews`, `sessions`, `visitor-days`, `conversions` or `ratio`."},"state":{"type":"string","enum":["ready","empty","unavailable"],"description":"`ready` has a value, `empty` is a zero count, `unavailable` has no value (see `unavailableReason`)."},"evidenceClass":{"type":"string","enum":["observed","unavailable"],"description":"`observed` for measured values."},"grain":{"type":"string","description":"What one unit counts."},"metricVersion":{"type":"string","description":"Versioned definition, for example `human.sessions.v1`."},"source":{"type":"string","description":"Data source, `human_ingest`."},"startAt":{"type":"string","format":"date-time","description":"Window start."},"endAt":{"type":"string","format":"date-time","description":"Window end (exclusive)."},"timezone":{"type":"string","description":"Time zone of the window."},"numerator":{"type":"number","description":"Bounce rate only: bounced sessions."},"denominator":{"type":"number","description":"Bounce rate only: sessions with a pageview."},"unavailableReason":{"type":"string","description":"Present when `value` is null."}}},"HumanOverviewMetrics":{"type":"object","description":"Versioned metadata for the totals; `conversions` is present only in the traffic response.","required":["bounceRate","events","pageviews","sessions","visitorDays"],"properties":{"bounceRate":{"$ref":"#/components/schemas/MetricResult"},"conversions":{"$ref":"#/components/schemas/MetricResult"},"events":{"$ref":"#/components/schemas/MetricResult"},"pageviews":{"$ref":"#/components/schemas/MetricResult"},"sessions":{"$ref":"#/components/schemas/MetricResult"},"visitorDays":{"$ref":"#/components/schemas/MetricResult"}}},"AnalyticsOverviewResponse":{"type":"object","description":"The totals plus `metrics`.","required":["bouncedSessions","pageviewSessions","totalEvents","totalPageviews","totalSessions","uniqueVisitors","metrics"],"properties":{"bouncedSessions":{"type":"integer","description":"Sessions with exactly one pageview."},"pageviewSessions":{"type":"integer","description":"Sessions with at least one pageview; the bounce-rate denominator."},"totalEvents":{"type":"integer","description":"Custom events."},"totalPageviews":{"type":"integer","description":"Pageviews."},"totalSessions":{"type":"integer","description":"Sessions started in the range (a session ends after 30 minutes without activity)."},"uniqueVisitors":{"type":"integer","description":"Visitor-days: distinct visitors per local date in `timezone`, summed over the range."},"metrics":{"$ref":"#/components/schemas/HumanOverviewMetrics"}}},"AnalyticsTimeseriesRow":{"type":"object","required":["bucketStart","pageviews","sessions"],"properties":{"bucketStart":{"type":"string","format":"date-time","description":"Bucket start (UTC instant of the local hour or day)."},"pageviews":{"type":"integer","description":"Pageviews in the bucket."},"sessions":{"type":"integer","description":"Sessions that started in the bucket."}}},"AnalyticsTrafficResponse":{"type":"object","required":["metrics","overview","timeseries","totalConversions"],"properties":{"metrics":{"$ref":"#/components/schemas/HumanOverviewMetrics"},"overview":{"$ref":"#/components/schemas/AnalyticsOverviewTotals"},"timeseries":{"type":"array","items":{"$ref":"#/components/schemas/AnalyticsTimeseriesRow"},"description":"Buckets in ascending order."},"totalConversions":{"type":"integer","description":"Goal conversions in the range."}}},"AnalyticsPageRow":{"type":"object","required":["path","pageviews","uniqueVisitors"],"properties":{"path":{"type":"string","description":"Page path."},"pageviews":{"type":"integer","description":"Pageviews."},"uniqueVisitors":{"type":"integer","description":"Visitor-days on the page."}}},"AnalyticsReferrerRow":{"type":"object","required":["trafficSource","trafficMedium","trafficOrigin","referrer","referrerDomain","aiReferrer","aiReferrerKind","sessions","uniqueVisitors"],"properties":{"trafficSource":{"type":"string","enum":["direct","referral","search","social","ai","unknown"],"description":"Traffic source class."},"trafficMedium":{"type":["string","null"],"description":"Traffic medium."},"trafficOrigin":{"type":["string","null"],"description":"Traffic origin."},"referrer":{"type":["string","null"],"description":"Entry referrer."},"referrerDomain":{"type":["string","null"],"description":"Entry referrer domain."},"aiReferrer":{"type":["string","null"],"description":"Recognized AI assistant that referred the session."},"aiReferrerKind":{"type":["string","null"],"description":"Kind of AI referral."},"sessions":{"type":"integer","description":"Sessions started in the range."},"uniqueVisitors":{"type":"integer","description":"Visitor-days."}}},"VisitorBreakdownRow":{"type":"object","required":["key","label","share","visitors"],"properties":{"key":{"type":"string","description":"Slug of the label."},"label":{"type":"string","description":"Display label, `Unknown` when not recognized."},"share":{"type":"number","description":"Percentage of `totalVisitors` (0–100, one decimal)."},"visitors":{"type":"integer","description":"Visitor-days."}}},"CountryBreakdownRow":{"type":"object","required":["countryCode","key","label","share","visitors"],"properties":{"countryCode":{"type":["string","null"],"pattern":"^[A-Z]{2}$","description":"ISO 3166-1 alpha-2 code; null when unknown."},"key":{"type":"string","description":"Country code, or `unknown`."},"label":{"type":"string","description":"English country name, or `Unknown`."},"share":{"type":"number","description":"Percentage of `totalVisitors` (0–100, one decimal)."},"visitors":{"type":"integer","description":"Visitor-days."}}},"AnalyticsVisitors":{"type":"object","description":"Each list is ordered by visitors, then label.","required":["browsers","countries","devices","operatingSystems","totalVisitors"],"properties":{"browsers":{"type":"array","items":{"$ref":"#/components/schemas/VisitorBreakdownRow"}},"countries":{"type":"array","items":{"$ref":"#/components/schemas/CountryBreakdownRow"}},"devices":{"type":"array","items":{"$ref":"#/components/schemas/VisitorBreakdownRow"}},"operatingSystems":{"type":"array","items":{"$ref":"#/components/schemas/VisitorBreakdownRow"}},"totalVisitors":{"type":"integer","description":"Visitor-days in the range."}}}}}}