Webhook & Real-Time Data
This guide covers strategies for getting real-time and near-real-time analytics data from Zenovay, including polling patterns, the public live endpoint, Server-Sent Events, and automated alerting.
Zenovay does not currently offer traditional webhook callbacks for analytics events. Instead, use the polling strategies and public live endpoints described below to achieve real-time data access.
Real-Time Live Visitor Count
The live visitor endpoint is public -- it requires no API key and can be called directly from the browser:
GET https://api.zenovay.com/e/live/YOUR_TRACKING_CODE
curl 'https://api.zenovay.com/e/live/ZV_XXXXXXXXXXX'{
"visitors": 42
}This endpoint returns the number of visitors currently active on your site (within the last 5 minutes). Poll it every 10 seconds for a smooth real-time counter.
Polling Strategies
Simple Interval Polling
The most straightforward approach -- poll at a fixed interval:
class ZenovayPoller {
private intervalId: ReturnType<typeof setInterval> | null = null;
constructor(
private apiKey: string,
private websiteId: string,
private onData: (data: any) => void,
private intervalMs: number = 30_000
) {}
start() {
this.fetchData();
this.intervalId = setInterval(() => this.fetchData(), this.intervalMs);
}
stop() {
if (this.intervalId) clearInterval(this.intervalId);
}
private async fetchData() {
try {
const res = await fetch(
`https://api.zenovay.com/api/external/v1/analytics/${this.websiteId}?range=24h`,
{ headers: { 'X-API-Key': this.apiKey } }
);
const json = await res.json();
if (json.success) this.onData(json.data);
} catch (error) {
console.error('Polling error:', error);
}
}
}
// Usage
const poller = new ZenovayPoller(
process.env.ZENOVAY_API_KEY!,
'ws_abc123',
(data) => console.log('Visitors:', data.summary.total_visitors),
30_000 // Poll every 30 seconds
);
poller.start();Adaptive Polling
Reduce polling frequency when data is not changing, increase it during active periods:
class AdaptivePoller {
private intervalId: ReturnType<typeof setInterval> | null = null;
private currentInterval: number;
private lastValue: number | null = null;
private readonly minInterval = 10_000; // 10 seconds minimum
private readonly maxInterval = 120_000; // 2 minutes maximum
constructor(
private fetcher: () => Promise<number>,
private onUpdate: (value: number) => void,
initialInterval = 30_000
) {
this.currentInterval = initialInterval;
}
start() {
this.tick();
}
stop() {
if (this.intervalId) clearTimeout(this.intervalId);
}
private async tick() {
try {
const value = await this.fetcher();
if (this.lastValue !== null && value !== this.lastValue) {
// Data changed -- poll faster
this.currentInterval = Math.max(this.minInterval, this.currentInterval / 2);
} else {
// Data stable -- slow down
this.currentInterval = Math.min(this.maxInterval, this.currentInterval * 1.5);
}
this.lastValue = value;
this.onUpdate(value);
} catch {
// On error, slow down
this.currentInterval = Math.min(this.maxInterval, this.currentInterval * 2);
}
this.intervalId = setTimeout(() => this.tick(), this.currentInterval);
}
}Recommended Polling Intervals
| Data Type | Minimum Interval | Recommended | Notes |
|---|---|---|---|
| Live visitors (public) | 5s | 10s | No auth, low cost |
| Analytics overview | 30s | 60s | Aggregated data |
| Visitor list | 30s | 60s | New records appear regularly |
| Countries/Technology | 60s | 300s | Changes slowly |
| Error groups | 60s | 300s | Unless actively debugging |
Building a Live Dashboard with Polling
Combine the public live endpoint with authenticated API calls for a complete real-time dashboard:
interface DashboardState {
liveVisitors: number;
todayVisitors: number;
todayPageViews: number;
topPages: Array<{ url: string; views: number }>;
recentErrors: Array<{ message: string; type: string; occurrence_count: number }>;
}
class LiveDashboard {
private state: DashboardState = {
liveVisitors: 0,
todayVisitors: 0,
todayPageViews: 0,
topPages: [],
recentErrors: [],
};
private listeners: Set<(state: DashboardState) => void> = new Set();
constructor(
private apiKey: string,
private websiteId: string,
private trackingCode: string
) {}
subscribe(listener: (state: DashboardState) => void) {
this.listeners.add(listener);
listener(this.state); // Send current state immediately
return () => this.listeners.delete(listener);
}
private notify() {
this.listeners.forEach(fn => fn({ ...this.state }));
}
start() {
// Live visitors: every 10 seconds (public, no auth)
this.pollLive();
setInterval(() => this.pollLive(), 10_000);
// Analytics overview: every 30 seconds
this.pollAnalytics();
setInterval(() => this.pollAnalytics(), 30_000);
// Pages and errors: every 60 seconds
this.pollPages();
setInterval(() => this.pollPages(), 60_000);
this.pollErrors();
setInterval(() => this.pollErrors(), 60_000);
}
private async pollLive() {
try {
const res = await fetch(`https://api.zenovay.com/e/live/${this.trackingCode}`);
const data = await res.json();
this.state.liveVisitors = data.visitors ?? 0;
this.notify();
} catch {}
}
private async pollAnalytics() {
try {
const res = await fetch(
`https://api.zenovay.com/api/external/v1/analytics/${this.websiteId}?range=24h`,
{ headers: { 'X-API-Key': this.apiKey } }
);
const json = await res.json();
if (json.success) {
this.state.todayVisitors = json.data.summary.total_visitors;
this.state.todayPageViews = json.data.summary.total_page_views;
this.notify();
}
} catch {}
}
private async pollPages() {
try {
const res = await fetch(
`https://api.zenovay.com/api/external/v1/analytics/${this.websiteId}/pages?range=24h&limit=10`,
{ headers: { 'X-API-Key': this.apiKey } }
);
const json = await res.json();
if (json.success) {
this.state.topPages = json.data.pages;
this.notify();
}
} catch {}
}
private async pollErrors() {
try {
const res = await fetch(
`https://api.zenovay.com/api/external/v1/errors/${this.websiteId}/groups?status=open`,
{ headers: { 'X-API-Key': this.apiKey } }
);
const json = await res.json();
if (json.success) {
this.state.recentErrors = json.data.error_groups.slice(0, 5);
this.notify();
}
} catch {}
}
}Server-Sent Events (SSE) Pattern
Use a backend SSE endpoint to push live data to your frontend without the browser polling Zenovay directly:
import express from 'express';
const app = express();
app.get('/api/sse/live', (req, res) => {
res.setHeader('Content-Type', 'text/event-stream');
res.setHeader('Cache-Control', 'no-cache');
res.setHeader('Connection', 'keep-alive');
res.flushHeaders();
const trackingCode = req.query.trackingCode as string;
const apiKey = process.env.ZENOVAY_API_KEY!;
const websiteId = req.query.websiteId as string;
// Poll live visitors every 10 seconds
const liveInterval = setInterval(async () => {
try {
const res2 = await fetch(`https://api.zenovay.com/e/live/${trackingCode}`);
const data = await res2.json();
res.write(`event: live\ndata: ${JSON.stringify({ visitors: data.visitors })}\n\n`);
} catch {}
}, 10_000);
// Poll analytics every 30 seconds
const analyticsInterval = setInterval(async () => {
try {
const res2 = await fetch(
`https://api.zenovay.com/api/external/v1/analytics/${websiteId}?range=24h`,
{ headers: { 'X-API-Key': apiKey } }
);
const json = await res2.json();
if (json.success) {
res.write(`event: analytics\ndata: ${JSON.stringify(json.data.summary)}\n\n`);
}
} catch {}
}, 30_000);
req.on('close', () => {
clearInterval(liveInterval);
clearInterval(analyticsInterval);
});
});
app.listen(3000);Connect from the browser:
const eventSource = new EventSource(
'/api/sse/live?trackingCode=ZV_XXXXXXXXXXX&websiteId=ws_abc123'
);
eventSource.addEventListener('live', (event) => {
const data = JSON.parse(event.data);
document.getElementById('live-count')!.textContent = data.visitors;
});
eventSource.addEventListener('analytics', (event) => {
const summary = JSON.parse(event.data);
document.getElementById('today-visitors')!.textContent =
summary.total_visitors.toLocaleString();
document.getElementById('today-pageviews')!.textContent =
summary.total_page_views.toLocaleString();
});
eventSource.onerror = () => {
console.log('SSE connection lost, reconnecting...');
// EventSource auto-reconnects
};High-Value Visitor Alerting
Monitor the visitor stream and alert when high-value visitors arrive. This is useful for sales teams who want to know when important prospects are browsing:
interface VisitorAlert {
visitorId: string;
country: string;
valueScore: number;
landingPage: string;
timestamp: string;
}
class HighValueVisitorMonitor {
private lastChecked: string | null = null;
constructor(
private apiKey: string,
private websiteId: string,
private valueThreshold: number = 70,
private onAlert: (alert: VisitorAlert) => void
) {}
async check() {
try {
const res = await fetch(
`https://api.zenovay.com/api/external/v1/analytics/${this.websiteId}/visitors?range=24h&limit=50`,
{ headers: { 'X-API-Key': this.apiKey } }
);
const json = await res.json();
if (!json.success) return;
const visitors = json.data.visitors;
for (const visitor of visitors) {
// Skip if we already processed this visitor
if (this.lastChecked && visitor.visited_at <= this.lastChecked) continue;
if (visitor.value_score >= this.valueThreshold) {
this.onAlert({
visitorId: visitor.id,
country: visitor.country_name || visitor.country_code || 'Unknown',
valueScore: visitor.value_score,
landingPage: visitor.landing_page || '/',
timestamp: visitor.visited_at,
});
}
}
if (visitors.length > 0) {
this.lastChecked = visitors[0].visited_at;
}
} catch (error) {
console.error('Monitor error:', error);
}
}
start(intervalMs = 60_000) {
this.check();
setInterval(() => this.check(), intervalMs);
}
}
// Usage: alert to Slack when a high-value visitor arrives
const monitor = new HighValueVisitorMonitor(
process.env.ZENOVAY_API_KEY!,
'ws_abc123',
70,
async (alert) => {
console.log(`High-value visitor! Score: ${alert.valueScore}, Country: ${alert.country}`);
// Send to Slack
await fetch(process.env.SLACK_WEBHOOK_URL!, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
text: `High-value visitor (score: ${alert.valueScore}) from ${alert.country} landed on ${alert.landingPage}`,
}),
});
}
);
monitor.start(60_000); // Check every minuteTraffic Spike Detection
Detect unusual traffic spikes by comparing current data to recent baselines:
async function detectTrafficSpike(
apiKey: string,
websiteId: string,
thresholdMultiplier = 2.0
): Promise<{ spikeDetected: boolean; currentVisitors: number; baseline: number }> {
// Get today's data
const todayRes = await fetch(
`https://api.zenovay.com/api/external/v1/analytics/${websiteId}?range=24h`,
{ headers: { 'X-API-Key': apiKey } }
);
const todayJson = await todayRes.json();
const currentVisitors = todayJson.data?.summary?.total_visitors || 0;
// Get last 7 days for baseline
const weekRes = await fetch(
`https://api.zenovay.com/api/external/v1/analytics/${websiteId}?range=7d`,
{ headers: { 'X-API-Key': apiKey } }
);
const weekJson = await weekRes.json();
const dailyStats = weekJson.data?.daily_stats || [];
// Calculate average daily visitors (excluding today)
const pastDays = dailyStats.slice(0, -1);
const avgVisitors = pastDays.length > 0
? pastDays.reduce((sum: number, d: any) => sum + d.total_visitors, 0) / pastDays.length
: 0;
return {
spikeDetected: avgVisitors > 0 && currentVisitors > avgVisitors * thresholdMultiplier,
currentVisitors,
baseline: Math.round(avgVisitors),
};
}Next Steps
- Getting Started with the API -- API key setup and first call
- Building a Custom Dashboard -- React frontend with charts
- Server-Side Analytics -- Node.js and Python wrapper classes
- External API Reference -- Full endpoint documentation