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:
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:
/api/external/v1/usageGet account usage and limits
curl -X GET 'https://api.zenovay.com/api/external/v1/usage' \
-H 'X-API-Key: YOUR_API_KEY'{
"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:
/api/external/v1/websitesList all tracked websites
curl -X GET 'https://api.zenovay.com/api/external/v1/websites' \
-H 'X-API-Key: YOUR_API_KEY'{
"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:
/api/external/v1/websites/:websiteIdGet website details
curl -X GET 'https://api.zenovay.com/api/external/v1/websites/ws_abc123' \
-H 'X-API-Key: YOUR_API_KEY'{
"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:
/api/external/v1/analytics/:websiteIdGet analytics overview
Query Parameters:
| Parameter | Type | Required | Description |
|---|---|---|---|
range | string | No | Time range: 24h, 7d, 30d, 90d, 1y (default: 7d) |
curl -X GET 'https://api.zenovay.com/api/external/v1/analytics/ws_abc123?range=30d' \
-H 'X-API-Key: YOUR_API_KEY'{
"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:
/api/external/v1/analytics/:websiteId/visitorsGet visitor data
Query Parameters:
| Parameter | Type | Required | Description |
|---|---|---|---|
range | string | No | Time range: 24h, 7d, 30d, 90d, 1y |
limit | integer | No | Max results (default: 50, max: 100) |
offset | integer | No | Pagination offset |
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'{
"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:
/api/external/v1/analytics/:websiteId/pagesGet top pages
Query Parameters:
| Parameter | Type | Required | Description |
|---|---|---|---|
range | string | No | Time range: 24h, 7d, 30d, 90d, 1y |
limit | integer | No | Max results (default: 20) |
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'{
"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:
/api/external/v1/analytics/:websiteId/countriesGet geographic breakdown
Query Parameters:
| Parameter | Type | Required | Description |
|---|---|---|---|
range | string | No | Time range: 24h, 7d, 30d, 90d, 1y |
curl -X GET 'https://api.zenovay.com/api/external/v1/analytics/ws_abc123/countries?range=30d' \
-H 'X-API-Key: YOUR_API_KEY'{
"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:
/api/external/v1/analytics/:websiteId/technologyGet technology breakdown
Query Parameters:
| Parameter | Type | Required | Description |
|---|---|---|---|
range | string | No | Time range: 24h, 7d, 30d, 90d, 1y |
curl -X GET 'https://api.zenovay.com/api/external/v1/analytics/ws_abc123/technology?range=7d' \
-H 'X-API-Key: YOUR_API_KEY'{
"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:
/api/external/v1/heatmaps/:websiteId/pagesList heatmap pages
curl -X GET 'https://api.zenovay.com/api/external/v1/heatmaps/ws_abc123/pages' \
-H 'X-API-Key: YOUR_API_KEY'{
"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:
/api/external/v1/replays/:websiteId/sessionsList session replays
curl -X GET 'https://api.zenovay.com/api/external/v1/replays/ws_abc123/sessions' \
-H 'X-API-Key: YOUR_API_KEY'{
"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:
/api/external/v1/errors/:websiteId/groupsList error groups
curl -X GET 'https://api.zenovay.com/api/external/v1/errors/ws_abc123/groups' \
-H 'X-API-Key: YOUR_API_KEY'{
"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
| Endpoint | Purpose |
|---|---|
GET /stats/aggregate | Single-number metrics over a period (totals, rates, durations). |
GET /stats/timeseries | Time-bucketed series (one row per day, hour, or month). |
GET /stats/breakdown | Group-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
| Param | Required | Description |
|---|---|---|
site_id | yes | Website UUID. Find it in your dashboard URL or via GET /websites. |
period | yes | day, 7d, 30d, month, 6mo, 12mo, or custom:YYYY-MM-DD,YYYY-MM-DD (max 366 days). |
date | no | ISO 8601 anchor for the period (default = today). |
metrics | yes | Comma-separated. Allowed: visitors, pageviews, visit_duration, bounce_rate, events. |
filters | no | Plausible-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.
/api/external/v1/stats/aggregateAggregate metrics over a period
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'{
"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.
/api/external/v1/stats/timeseriesTime-bucketed metric series
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'{
"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.
/api/external/v1/stats/breakdownGroup-by metrics (top pages, top countries, …)
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'{
"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: equalscountry==US,CA,GB: equals one ofbrowser!=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
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:
| Code | HTTP | When |
|---|---|---|
MISSING_SITE_ID | 400 | site_id query param is required. |
MISSING_METRICS | 400 | metrics query param is required. |
MISSING_PERIOD | 400 | period query param is required. |
MISSING_PROPERTY | 400 | /stats/breakdown requires property. |
INVALID_METRIC | 400 | One of the comma-separated metrics is not in the allowlist. |
INVALID_PROPERTY | 400 | The breakdown property isn't allowed. |
INVALID_PERIOD | 400 | The period isn't day/7d/30d/month/6mo/12mo/custom:.... |
INVALID_CUSTOM_PERIOD | 400 | Malformed custom: syntax or invalid date(s). |
PERIOD_TOO_LONG | 400 | Custom period > 366 days. |
INTERVAL_PERIOD_MISMATCH | 400 | e.g. interval=hour with period=7d. |
UNKNOWN_FILTER_KEY | 400 | Filter key isn't in the allowlist. |
EMPTY_FILTER_VALUE | 400 | Filter clause has no value. |
FORBIDDEN | 403 | Free-tier account (External API requires a paid plan), or site_id out of API-key scope. |
NOT_FOUND | 404 | Site doesn't exist or is hidden from this key. |
| (rate limit) | 429 | Per-tier rate limit exceeded. Retry-After header indicates wait time. |
Migrating from Plausible
The parameter shape is intentionally Plausible-compatible:
| Plausible | Zenovay | Notes |
|---|---|---|
site_id | site_id | Plausible uses a domain string; Zenovay uses a UUID. Map domain → site_id once via GET /websites. |
period, date | period, date | Same allowlist + custom:YYYY-MM-DD,YYYY-MM-DD. |
metrics | metrics | Plausible's visitors, pageviews, bounce_rate, visit_duration all map 1:1. events is V1-approximated as pageviews. |
property | property | Same shape: event:page, visit:country, etc. |
filters (v1 string) | filters | Same key==value;key!=value shape. JSON v2 array filters land in Zenovay V2. |
JavaScript Example
Embed analytics data on your website using JavaScript:
// 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:
| Plan | Requests/Minute (target) | Monthly Limit (hard cap) |
|---|---|---|
| Free | N/A (API not available) | N/A |
| Pro | 30 | 10,000 |
| Scale | 60 | 100,000 |
| Enterprise | 120 | 1,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 Code | Description |
|---|---|
| 200 | Success |
| 400 | Bad Request - Invalid parameters |
| 401 | Unauthorized - Invalid or missing API key |
| 403 | Forbidden - Insufficient permissions |
| 404 | Not Found - Website not found |
| 429 | Too Many Requests - Rate limit exceeded |
| 500 | Internal Server Error |
{
"error": {
"code": "unauthorized",
"message": "Invalid API key provided"
}
}Next Steps
- Widgets - Embed ready-to-use widgets on your site
- Real-Time Data - Access live visitor data without authentication
- Rate Limits - Understand API rate limiting
On this page