Skip to main content
15 min read

External API

The External API allows you to embed Zenovay analytics data on your customer-facing websites. Use your API key to fetch live visitor counts, analytics summaries, page statistics, and more.

Base URL

All External API requests should be made to:

https://api.zenovay.com/api/external/v1

Authentication

External API endpoints require an API key passed in the request header:

API Key HeaderBash
curl -X GET 'https://api.zenovay.com/api/external/v1/websites' \
-H 'X-API-Key: YOUR_API_KEY' \
-H 'Content-Type: application/json'

Generate your API key at Settings → Security → API keys. Each key can have specific permissions (read, write, admin).

The External API requires a paid plan. Free-tier accounts receive a 403 API_PAID_PLAN_REQUIRED response on every endpoint. Upgrade to Pro, Scale, or Enterprise to use API keys programmatically.

Account Endpoints

Get Account Usage

Retrieve your account usage statistics and limits:

GET/api/external/v1/usage

Get account usage and limits

RequestBash
curl -X GET 'https://api.zenovay.com/api/external/v1/usage' \
-H 'X-API-Key: YOUR_API_KEY'
Response (200 OK)JSON
{
"usage": {
  "websites": 5,
  "pageviews_this_month": 125000,
  "events_this_month": 45000
},
"limits": {
  "websites": 10,
  "pageviews_per_month": 500000,
  "events_per_month": 100000
},
"plan": "pro",
"period": {
  "start": "2025-01-01T00:00:00Z",
  "end": "2025-01-31T23:59:59Z"
}
}

Website Endpoints

List Websites

Get all websites linked to your account:

GET/api/external/v1/websites

List all tracked websites

RequestBash
curl -X GET 'https://api.zenovay.com/api/external/v1/websites' \
-H 'X-API-Key: YOUR_API_KEY'
Response (200 OK)JSON
{
"websites": [
  {
    "id": "ws_abc123",
    "domain": "example.com",
    "name": "Example Website",
    "tracking_code": "ZV_XXXXXXXXXXX",
    "created_at": "2025-01-01T00:00:00Z",
    "live_visitors": 42
  },
  {
    "id": "ws_def456",
    "domain": "shop.example.com",
    "name": "Example Shop",
    "tracking_code": "ZV_YYYYYYYYYYY",
    "created_at": "2025-01-10T00:00:00Z",
    "live_visitors": 18
  }
],
"total": 2
}

Get Website Details

Retrieve details for a specific website:

GET/api/external/v1/websites/:websiteId

Get website details

RequestBash
curl -X GET 'https://api.zenovay.com/api/external/v1/websites/ws_abc123' \
-H 'X-API-Key: YOUR_API_KEY'
Response (200 OK)JSON
{
"website": {
  "id": "ws_abc123",
  "domain": "example.com",
  "name": "Example Website",
  "tracking_code": "ZV_XXXXXXXXXXX",
  "created_at": "2025-01-01T00:00:00Z",
  "settings": {
    "track_outbound_links": true,
    "track_downloads": true,
    "session_replay": true
  }
},
"stats": {
  "live_visitors": 42,
  "today_pageviews": 1234,
  "today_visitors": 567
}
}

Analytics Endpoints

Get Analytics Summary

Retrieve comprehensive analytics data for a website:

GET/api/external/v1/analytics/:websiteId

Get analytics overview

Query Parameters:

ParameterTypeRequiredDescription
rangestringNoTime range: 24h, 7d, 30d, 90d, 1y (default: 7d)
RequestBash
curl -X GET 'https://api.zenovay.com/api/external/v1/analytics/ws_abc123?range=30d' \
-H 'X-API-Key: YOUR_API_KEY'
Response (200 OK)JSON
{
"summary": {
  "visitors": 12543,
  "pageviews": 45231,
  "sessions": 15420,
  "bounce_rate": 0.42,
  "avg_session_duration": 185,
  "pages_per_session": 2.9
},
"period": {
  "start": "2024-12-21T00:00:00Z",
  "end": "2025-01-20T23:59:59Z"
},
"live_visitors": 42
}

Get Visitors Data

Retrieve visitor information with optional filtering:

GET/api/external/v1/analytics/:websiteId/visitors

Get visitor data

Query Parameters:

ParameterTypeRequiredDescription
rangestringNoTime range: 24h, 7d, 30d, 90d, 1y
limitintegerNoMax results (default: 50, max: 100)
offsetintegerNoPagination offset
RequestBash
curl -X GET 'https://api.zenovay.com/api/external/v1/analytics/ws_abc123/visitors?range=7d&limit=10' \
-H 'X-API-Key: YOUR_API_KEY'
Response (200 OK)JSON
{
"visitors": [
  {
    "visitor_id": "vis_abc123",
    "first_seen": "2025-01-15T10:00:00Z",
    "last_seen": "2025-01-20T14:30:00Z",
    "total_sessions": 8,
    "total_pageviews": 45,
    "value_score": 87,
    "country": "US",
    "device": "desktop"
  }
],
"total": 1234,
"limit": 10,
"offset": 0
}

Get Page Analytics

Retrieve page-level statistics:

GET/api/external/v1/analytics/:websiteId/pages

Get top pages

Query Parameters:

ParameterTypeRequiredDescription
rangestringNoTime range: 24h, 7d, 30d, 90d, 1y
limitintegerNoMax results (default: 20)
RequestBash
curl -X GET 'https://api.zenovay.com/api/external/v1/analytics/ws_abc123/pages?range=7d&limit=10' \
-H 'X-API-Key: YOUR_API_KEY'
Response (200 OK)JSON
{
"pages": [
  { "url": "/", "views": 5423, "avg_value_score": 45.32 },
  { "url": "/pricing", "views": 2134, "avg_value_score": 72.18 },
  { "url": "blog.example.com/launch", "views": 812, "avg_value_score": 0 }
]
}

views are page views (one per page view, single-page app route changes included). url is the path for pages on the site's own domain and host + path for pages on other hosts; query strings and trailing slashes are dropped. avg_value_score is the mean value score of the visits that entered on the page (0 when none did). An optional note gives the date page views are counted from when the range starts before page view tracking began for the site.

Get Geographic Data

Retrieve visitor data by country:

GET/api/external/v1/analytics/:websiteId/countries

Get geographic breakdown

Query Parameters:

ParameterTypeRequiredDescription
rangestringNoTime range: 24h, 7d, 30d, 90d, 1y
RequestBash
curl -X GET 'https://api.zenovay.com/api/external/v1/analytics/ws_abc123/countries?range=30d' \
-H 'X-API-Key: YOUR_API_KEY'
Response (200 OK)JSON
{
"countries": [
  {
    "code": "US",
    "name": "United States",
    "visitors": 4521,
    "percentage": 36.1
  },
  {
    "code": "GB",
    "name": "United Kingdom",
    "visitors": 1823,
    "percentage": 14.5
  },
  {
    "code": "DE",
    "name": "Germany",
    "visitors": 1456,
    "percentage": 11.6
  }
]
}

Get Technology Breakdown

Retrieve device, browser, and OS statistics:

GET/api/external/v1/analytics/:websiteId/technology

Get technology breakdown

Query Parameters:

ParameterTypeRequiredDescription
rangestringNoTime range: 24h, 7d, 30d, 90d, 1y
RequestBash
curl -X GET 'https://api.zenovay.com/api/external/v1/analytics/ws_abc123/technology?range=7d' \
-H 'X-API-Key: YOUR_API_KEY'
Response (200 OK)JSON
{
"devices": [
  { "type": "desktop", "visitors": 7234, "percentage": 57.7 },
  { "type": "mobile", "visitors": 4521, "percentage": 36.1 },
  { "type": "tablet", "visitors": 788, "percentage": 6.3 }
],
"browsers": [
  { "name": "Chrome", "visitors": 6543, "percentage": 52.2 },
  { "name": "Safari", "visitors": 3421, "percentage": 27.3 },
  { "name": "Firefox", "visitors": 1234, "percentage": 9.8 }
],
"operating_systems": [
  { "name": "Windows", "visitors": 4521, "percentage": 36.1 },
  { "name": "macOS", "visitors": 3234, "percentage": 25.8 },
  { "name": "iOS", "visitors": 2876, "percentage": 22.9 }
]
}

Advanced Endpoints

Get Heatmap Pages

List pages with heatmap data:

GET/api/external/v1/heatmaps/:websiteId/pages

List heatmap pages

RequestBash
curl -X GET 'https://api.zenovay.com/api/external/v1/heatmaps/ws_abc123/pages' \
-H 'X-API-Key: YOUR_API_KEY'
Response (200 OK)JSON
{
"pages": [
  {
    "url": "/",
    "title": "Home",
    "total_clicks": 12543,
    "total_sessions": 4521,
    "last_updated": "2025-01-20T14:30:00Z"
  },
  {
    "url": "/pricing",
    "title": "Pricing",
    "total_clicks": 8765,
    "total_sessions": 2134,
    "last_updated": "2025-01-20T14:25:00Z"
  }
]
}

Get Session Replays

List recorded sessions:

GET/api/external/v1/replays/:websiteId/sessions

List session replays

RequestBash
curl -X GET 'https://api.zenovay.com/api/external/v1/replays/ws_abc123/sessions' \
-H 'X-API-Key: YOUR_API_KEY'
Response (200 OK)JSON
{
"sessions": [
  {
    "session_id": "sess_abc123",
    "visitor_id": "vis_xyz789",
    "started_at": "2025-01-20T14:00:00Z",
    "duration": 245,
    "pages_visited": 5,
    "country": "US",
    "device": "desktop"
  }
],
"total": 156
}

Get Error Groups

List JavaScript error groups:

GET/api/external/v1/errors/:websiteId/groups

List error groups

RequestBash
curl -X GET 'https://api.zenovay.com/api/external/v1/errors/ws_abc123/groups' \
-H 'X-API-Key: YOUR_API_KEY'
Response (200 OK)JSON
{
"groups": [
  {
    "fingerprint": "err_abc123",
    "message": "Cannot read property 'length' of undefined",
    "type": "TypeError",
    "occurrences": 45,
    "affected_users": 23,
    "first_seen": "2025-01-15T10:00:00Z",
    "last_seen": "2025-01-20T14:30:00Z"
  }
],
"total": 12
}

Stats API (Plausible parity, Pro+)

The Stats API exposes three Plausible-compatible endpoints for programmatic analytics queries. Use them to build custom dashboards, embed live metrics in BI tools, or migrate from Plausible with minimal code changes.

Plan required: Pro, Scale, or Enterprise. As with the rest of the External API, free-tier accounts receive a 403 API_PAID_PLAN_REQUIRED response.

Endpoints

EndpointPurpose
GET /stats/aggregateSingle-number metrics over a period (totals, rates, durations).
GET /stats/timeseriesTime-bucketed series (one row per day, hour, or month).
GET /stats/breakdownGroup-by metrics (top pages, top countries, top browsers, …).

OpenAPI spec: /api/external/v1/openapi.json (live, public, no auth required for the spec itself).

Common parameters

ParamRequiredDescription
site_idyesWebsite UUID. Find it in your dashboard URL or via GET /websites.
periodyesday, 7d, 30d, month, 6mo, 12mo, or custom:YYYY-MM-DD,YYYY-MM-DD (max 366 days).
datenoISO 8601 anchor for the period (default = today).
metricsyesComma-separated. Allowed: visitors, pageviews, visit_duration, bounce_rate, events.
filtersnoPlausible-style: country==US;browser==Chrome;page!=/admin. See Filters below.

Metric definitions. pageviews counts page views (one per page view, single-page app route changes included). bounce_rate is the share of visits with exactly one page view, in percent (0 to 100); a period's bounce rate is weighted by visits, never a plain average of days. Completed days come from the stored daily figures; today is added live (meta.blended_today) unless a stored row for today exists. events equals pageviews (V1).

GET /stats/aggregate

Returns single-number values for each requested metric over a period.

GET/api/external/v1/stats/aggregate

Aggregate metrics over a period

Request — visitors and pageviews over last 7 daysBash
curl -G 'https://api.zenovay.com/api/external/v1/stats/aggregate' \
-H 'X-API-Key: YOUR_API_KEY' \
--data-urlencode 'site_id=00000000-0000-0000-0000-000000000001' \
--data-urlencode 'period=7d' \
--data-urlencode 'metrics=visitors,pageviews,bounce_rate'
Response (200 OK)JSON
{
"success": true,
"data": {
  "results": {
    "visitors":    { "value": 12453 },
    "pageviews":   { "value": 38219 },
    "bounce_rate": { "value": 42.1 }
  },
  "meta": {
    "period": "7d",
    "period_start": "2026-05-07T00:00:00.000Z",
    "period_end":   "2026-05-13T23:59:59.999Z",
    "filters_applied": []
  }
},
"timestamp": "2026-05-14T08:30:00.000Z"
}

GET /stats/timeseries

Returns one row per bucket over the period. Default interval=day; interval=hour requires period=day; interval=month requires period of 6mo, 12mo, month, or custom.

GET/api/external/v1/stats/timeseries

Time-bucketed metric series

Request — daily visitors over last 30 daysBash
curl -G 'https://api.zenovay.com/api/external/v1/stats/timeseries' \
-H 'X-API-Key: YOUR_API_KEY' \
--data-urlencode 'site_id=00000000-0000-0000-0000-000000000001' \
--data-urlencode 'period=30d' \
--data-urlencode 'metrics=visitors,pageviews' \
--data-urlencode 'interval=day'
Response (200 OK)JSON
{
"success": true,
"data": {
  "results": [
    { "date": "2026-04-14", "visitors": 1820, "pageviews": 5640 },
    { "date": "2026-04-15", "visitors": 1933, "pageviews": 5921 }
  ],
  "meta": {
    "period": "30d",
    "interval": "day",
    "period_start": "2026-04-14T00:00:00.000Z",
    "period_end":   "2026-05-13T23:59:59.999Z",
    "filters_applied": []
  }
},
"timestamp": "2026-05-14T08:30:00.000Z"
}

The response is dense — days with zero traffic still appear with visitors: 0. This keeps chart libraries happy without client-side gap-filling.

With interval=hour, pageviews counts the page views made in each hour. In hourly and in filtered series, hours and days before page view tracking started for the site return pageviews: null, not 0.

GET /stats/breakdown

Returns metrics grouped by a dimension, sorted by visitors descending.

GET/api/external/v1/stats/breakdown

Group-by metrics (top pages, top countries, …)

Request — top 10 pages by visitors over last 7 daysBash
curl -G 'https://api.zenovay.com/api/external/v1/stats/breakdown' \
-H 'X-API-Key: YOUR_API_KEY' \
--data-urlencode 'site_id=00000000-0000-0000-0000-000000000001' \
--data-urlencode 'period=7d' \
--data-urlencode 'property=event:page' \
--data-urlencode 'metrics=visitors,pageviews' \
--data-urlencode 'limit=10'
Response (200 OK)JSON
{
"success": true,
"data": {
  "results": [
    { "page": "/pricing",      "visitors": 4421, "pageviews": 6930 },
    { "page": "/blog/launch",  "visitors": 2018, "pageviews": 2511 }
  ],
  "meta": {
    "period": "7d",
    "property": "event:page",
    "pagination": { "page": 1, "limit": 10, "total": 47, "has_more": true }
  }
},
"timestamp": "2026-05-14T08:30:00.000Z"
}

Allowed property values: event:page, visit:country, visit:browser, visit:device, visit:os, visit:source.

Pagination: limit is 1–1000 (default 100); page is 1-indexed (default 1).

event:page lists pages by page views. page is the path (/pricing) for pages on the site's own domain and host + path (blog.example.com/) for other hosts; query strings and trailing slashes are dropped. visitors is the number of visits that viewed the page. At most the top 500 pages are available. When the period starts before page view tracking began for the site, meta.note gives the date page views are counted from. Without page view data the endpoint lists landing pages under the same labels, with pageviews approximated by landing visits and a meta.note.

Other properties: visitors is the number of visits in the group. pageviews, events and bounce_rate come from the page views of the group's visits. Beyond 5,000 visits in the period, visit:device and visit:browser take them from the Analytics KPI for each group shown; for other properties pageviews and events are approximated by the visit count and meta.note says so. With filters they are null when they cannot be counted.

Filters

Plausible-style filter clauses, joined with ;:

  • country==US: equals
  • country==US,CA,GB: equals one of
  • browser!=Safari: not equals

Allowed keys: country, browser, device, os, source, utm_source, utm_medium, utm_campaign, page.

Filters and page views: With filters, visitors counts the matching visits, and pageviews, events and bounce_rate come from the page views of those visits. A single device== or browser== filter (one value each) works for any period size; other filters work up to 5,000 matching visits in a period after page view tracking started for the site. Beyond that these metrics return null (never the visit count) and meta.note explains why. visit_duration is null with filters.

JavaScript example

Fetch aggregate metrics with fetch()JavaScript
async function getAggregate(siteId, period = '7d', metrics = ['visitors', 'pageviews']) {
const url = new URL('https://api.zenovay.com/api/external/v1/stats/aggregate');
url.searchParams.set('site_id', siteId);
url.searchParams.set('period', period);
url.searchParams.set('metrics', metrics.join(','));

const res = await fetch(url, {
  headers: { 'X-API-Key': process.env.ZENOVAY_API_KEY }
});
if (!res.ok) {
  const err = await res.json();
  throw new Error(`${err.error.code}: ${err.error.message}`);
}
return res.json();
}

const data = await getAggregate('00000000-0000-0000-0000-000000000001', '7d');
console.log(`Visitors: ${data.data.results.visitors.value}`);

Error codes

The Stats API returns codes you can switch on for graceful UX:

CodeHTTPWhen
MISSING_SITE_ID400site_id query param is required.
MISSING_METRICS400metrics query param is required.
MISSING_PERIOD400period query param is required.
MISSING_PROPERTY400/stats/breakdown requires property.
INVALID_METRIC400One of the comma-separated metrics is not in the allowlist.
INVALID_PROPERTY400The breakdown property isn't allowed.
INVALID_PERIOD400The period isn't day/7d/30d/month/6mo/12mo/custom:....
INVALID_CUSTOM_PERIOD400Malformed custom: syntax or invalid date(s).
PERIOD_TOO_LONG400Custom period > 366 days.
INTERVAL_PERIOD_MISMATCH400e.g. interval=hour with period=7d.
UNKNOWN_FILTER_KEY400Filter key isn't in the allowlist.
EMPTY_FILTER_VALUE400Filter clause has no value.
FORBIDDEN403Free-tier account (External API requires a paid plan), or site_id out of API-key scope.
NOT_FOUND404Site doesn't exist or is hidden from this key.
(rate limit)429Per-tier rate limit exceeded. Retry-After header indicates wait time.

Migrating from Plausible

The parameter shape is intentionally Plausible-compatible:

PlausibleZenovayNotes
site_idsite_idPlausible uses a domain string; Zenovay uses a UUID. Map domain → site_id once via GET /websites.
period, dateperiod, dateSame allowlist + custom:YYYY-MM-DD,YYYY-MM-DD.
metricsmetricsPlausible's visitors, pageviews, bounce_rate, visit_duration all map 1:1. events is V1-approximated as pageviews.
propertypropertySame shape: event:page, visit:country, etc.
filters (v1 string)filtersSame key==value;key!=value shape. JSON v2 array filters land in Zenovay V2.

JavaScript Example

Embed analytics data on your website using JavaScript:

Fetch and Display AnalyticsJavaScript
// Fetch analytics data from your backend
async function fetchAnalytics() {
const response = await fetch('/api/analytics', {
  headers: {
    'X-API-Key': 'YOUR_API_KEY' // Use server-side proxy to protect your key
  }
});

const data = await response.json();

// Update your UI
document.getElementById('live-visitors').textContent = data.live_visitors;
document.getElementById('total-pageviews').textContent = data.summary.pageviews.toLocaleString();
document.getElementById('bounce-rate').textContent = (data.summary.bounce_rate * 100).toFixed(1) + '%';
}

// Refresh every 30 seconds
fetchAnalytics();
setInterval(fetchAnalytics, 30000);

Security Note: Never expose your API key in client-side code. Create a server-side proxy endpoint that calls the Zenovay API with your key, then have your frontend call your proxy.

Rate Limits

External API rate limits by plan:

PlanRequests/Minute (target)Monthly Limit (hard cap)
FreeN/A (API not available)N/A
Pro3010,000
Scale60100,000
Enterprise1201,000,000

How rate-limiting is enforced. The per-minute number is a target, not a hard ceiling. We use Cloudflare's edge rate-limit, which is per data center with a small burst allowance — a sustained client may transiently see 1.5–3× the headline number before being throttled, especially when requests fan out across regions. The hard cap is the monthly quota, enforced atomically against your account regardless of which edge POP serves the request. Plan capacity-sensitive workloads against the monthly quota; treat the per-minute target as a smoothing signal.

Error Responses

Status CodeDescription
200Success
400Bad Request - Invalid parameters
401Unauthorized - Invalid or missing API key
403Forbidden - Insufficient permissions
404Not Found - Website not found
429Too Many Requests - Rate limit exceeded
500Internal Server Error
Error Response ExampleJSON
{
"error": {
  "code": "unauthorized",
  "message": "Invalid API key provided"
}
}

Next Steps

Was this page helpful?