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 from the Zenovay Dashboard under Settings > API Keys. Each key can have specific permissions (read, write, admin).
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 |
|---|---|---|---|
timeRange | string | No | Time range: today, 7d, 30d, 90d (default: 7d) |
curl -X GET 'https://api.zenovay.com/api/external/v1/analytics/ws_abc123?timeRange=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 |
|---|---|---|---|
timeRange | string | No | Time range: today, 7d, 30d, 90d |
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?timeRange=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 |
|---|---|---|---|
timeRange | string | No | Time range: today, 7d, 30d, 90d |
limit | integer | No | Max results (default: 20) |
curl -X GET 'https://api.zenovay.com/api/external/v1/analytics/ws_abc123/pages?timeRange=7d&limit=10' \
-H 'X-API-Key: YOUR_API_KEY'{
"pages": [
{
"path": "/",
"title": "Home",
"views": 5423,
"unique_visitors": 3241,
"avg_time_on_page": 125,
"bounce_rate": 0.35
},
{
"path": "/pricing",
"title": "Pricing",
"views": 2134,
"unique_visitors": 1876,
"avg_time_on_page": 210,
"bounce_rate": 0.28
}
]
}Get Geographic Data
Retrieve visitor data by country:
/api/external/v1/analytics/:websiteId/countriesGet geographic breakdown
Query Parameters:
| Parameter | Type | Required | Description |
|---|---|---|---|
timeRange | string | No | Time range: today, 7d, 30d, 90d |
curl -X GET 'https://api.zenovay.com/api/external/v1/analytics/ws_abc123/countries?timeRange=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 |
|---|---|---|---|
timeRange | string | No | Time range: today, 7d, 30d, 90d |
curl -X GET 'https://api.zenovay.com/api/external/v1/analytics/ws_abc123/technology?timeRange=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. Free-tier API keys can read these docs but receive a 403 PLAN_REQUIRED error when calling the endpoints.
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. |
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.
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": 4421 },
{ "page": "/blog/launch", "visitors": 2018, "pageviews": 2018 }
],
"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).
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.
V1 limitation: When filters is provided, only the visitors metric is computed; other metrics return null with a meta.note flag. Full filter support across all metrics ships in V2.
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 key calling a Pro+ endpoint, 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 | 10 | 1,000 |
| 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