Send analytics events to Zenovay directly from your backend. Server-side tracking is immune to ad blockers, lets you record events that only happen on the server (confirmed purchases, webhook-driven subscriptions), and gives you full control over what's sent.
Why Server-Side Tracking?
Benefits
| Benefit | Description |
|---|---|
| Ad-blocker immunity | Events reach Zenovay even when the browser blocks the client script |
| Backend-only events | Track things that happen server-side — confirmed payments, cron jobs, webhooks |
| Data control | You decide exactly what gets sent |
| Hybrid tracking | Combine with the client script for full coverage |
When to Use
- E-commerce purchase confirmation (after the payment provider confirms)
- Backend events (subscription created, subscription cancelled)
- Webhook-driven events (Stripe, payment providers)
- Hybrid tracking (client script for browsing + server for confirmed conversions)
Tracking Endpoint
Server-side requests use the same ingest endpoint as the client script. Send a POST to the tracking endpoint with your website's tracking code in the path:
POST https://api.zenovay.com/e/{trackingCode}
This is a public, unauthenticated endpoint — it does not require an API key, and it is open on every plan (it's the same path the tracking script posts to). The tracking code is the same value you put in the data-tracking-code attribute of your install snippet.
Info
The ingest endpoint is shared with the browser tracker, so its payload is shaped like the data a browser would send. That means a few fields the browser fills in automatically — session_id, device_type, browser, os, and user_agent — have to be supplied by you in the JSON body when calling from a server. Requests missing them are rejected.
Required Fields
Every event you send must include these fields in the JSON body:
| Field | Notes |
|---|---|
session_id | A string of at least 8 characters. Reuse the same value for events that belong to the same visit. |
url | A full, valid URL (e.g. https://example.com/checkout/success). |
device_type | e.g. desktop, mobile, tablet, or server. |
browser | A browser/client name. Use something descriptive for server traffic (e.g. server). |
os | An OS name (e.g. Linux). |
user_agent | A user-agent string. Avoid generic bot-like agents (curl, python, wget, etc.) — they are filtered out as bots. |
visitor_id is optional but recommended (≥8 characters) so repeat events tie to the same visitor.
Pageview vs. Custom Event
The event kind is set with the event_type field:
- Pageview — omit
event_type(the default) or set it topageview. - Custom event — set
event_typetocustom, put the event name inevent_name, and any metadata inproperties.
# Custom event (e.g. a confirmed purchase)
curl -X POST "https://api.zenovay.com/e/YOUR_TRACKING_CODE" \
-H "Content-Type: application/json" \
-d '{
"event_type": "custom",
"event_name": "purchase",
"url": "https://example.com/checkout/success",
"referrer": "https://example.com/cart",
"session_id": "srv-9f2a7c41bd",
"device_type": "server",
"browser": "server",
"os": "Linux",
"user_agent": "MyApp-Server/1.0",
"properties": {
"order_id": "ORD-12345",
"value": 99.99,
"currency": "USD"
}
}'
# Pageview
curl -X POST "https://api.zenovay.com/e/YOUR_TRACKING_CODE" \
-H "Content-Type: application/json" \
-d '{
"event_type": "pageview",
"url": "https://example.com/products/widget",
"referrer": "https://google.com/search",
"session_id": "srv-9f2a7c41bd",
"device_type": "server",
"browser": "server",
"os": "Linux",
"user_agent": "MyApp-Server/1.0"
}'
A Note on Geolocation
For browser traffic, Zenovay derives location from the connecting visitor's IP. When you call the endpoint from your server, the connecting IP is your server's — so events will be geolocated to your infrastructure, not the end user.
If you need accurate per-visitor geolocation, do that tracking client-side (the standard install snippet) or use the first-party proxy, and reserve server-side tracking for backend events (purchases, subscriptions) where the user's exact location isn't the point. Adding an X-Forwarded-For header on a plain server call does not override the server's IP for the public endpoint.
Implementation Examples
Node.js / Express
// lib/analytics.js
const TRACKING_CODE = process.env.ZENOVAY_TRACKING_CODE;
const ENDPOINT = `https://api.zenovay.com/e/${TRACKING_CODE}`;
// Base fields every server-side event needs.
const SERVER_DEFAULTS = {
device_type: 'server',
browser: 'server',
os: 'Linux',
user_agent: 'MyApp-Server/1.0'
};
async function trackEvent(eventName, { url, sessionId, properties = {} }) {
const payload = {
event_type: 'custom',
event_name: eventName,
url,
session_id: sessionId,
properties,
...SERVER_DEFAULTS
};
try {
const response = await fetch(ENDPOINT, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(payload)
});
return response.ok;
} catch (error) {
console.error('Analytics error:', error);
return false;
}
}
async function trackPageview({ url, sessionId, referrer = '' }) {
const payload = {
event_type: 'pageview',
url,
referrer,
session_id: sessionId,
...SERVER_DEFAULTS
};
try {
const response = await fetch(ENDPOINT, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(payload)
});
return response.ok;
} catch (error) {
console.error('Pageview tracking error:', error);
return false;
}
}
module.exports = { trackEvent, trackPageview };
Python (Flask)
# analytics.py
import os
import requests
from flask import request
TRACKING_CODE = os.getenv('ZENOVAY_TRACKING_CODE')
TRACKING_URL = f'https://api.zenovay.com/e/{TRACKING_CODE}'
SERVER_DEFAULTS = {
'device_type': 'server',
'browser': 'server',
'os': 'Linux',
'user_agent': 'MyApp-Server/1.0',
}
def track_event(event_name, session_id, url, properties=None):
payload = {
'event_type': 'custom',
'event_name': event_name,
'url': url,
'session_id': session_id,
'properties': properties or {},
**SERVER_DEFAULTS,
}
try:
response = requests.post(TRACKING_URL, json=payload, timeout=5)
return response.ok
except Exception as e:
print(f'Analytics error: {e}')
return False
def track_pageview(session_id, url, referrer=''):
payload = {
'event_type': 'pageview',
'url': url,
'referrer': referrer,
'session_id': session_id,
**SERVER_DEFAULTS,
}
try:
response = requests.post(TRACKING_URL, json=payload, timeout=5)
return response.ok
except Exception as e:
print(f'Pageview tracking error: {e}')
return False
PHP
<?php
class ZenovayAnalytics {
private $trackingUrl;
private $defaults = [
'device_type' => 'server',
'browser' => 'server',
'os' => 'Linux',
'user_agent' => 'MyApp-Server/1.0',
];
public function __construct($trackingCode) {
$this->trackingUrl = "https://api.zenovay.com/e/{$trackingCode}";
}
public function trackEvent($eventName, $sessionId, $url, $properties = []) {
$payload = array_merge([
'event_type' => 'custom',
'event_name' => $eventName,
'url' => $url,
'session_id' => $sessionId,
'properties' => $properties,
], $this->defaults);
return $this->sendRequest($payload);
}
public function trackPageview($sessionId, $url, $referrer = '') {
$payload = array_merge([
'event_type' => 'pageview',
'url' => $url,
'referrer' => $referrer,
'session_id' => $sessionId,
], $this->defaults);
return $this->sendRequest($payload);
}
private function sendRequest($payload) {
$ch = curl_init($this->trackingUrl);
curl_setopt_array($ch, [
CURLOPT_POST => true,
CURLOPT_POSTFIELDS => json_encode($payload),
CURLOPT_HTTPHEADER => ['Content-Type: application/json'],
CURLOPT_RETURNTRANSFER => true,
CURLOPT_TIMEOUT => 5
]);
curl_exec($ch);
$httpCode = curl_getinfo($ch, CURLINFO_HTTP_CODE);
curl_close($ch);
return $httpCode >= 200 && $httpCode < 300;
}
}
// Usage
$analytics = new ZenovayAnalytics(getenv('ZENOVAY_TRACKING_CODE'));
$analytics->trackEvent('purchase', 'srv-9f2a7c41bd', 'https://example.com/checkout/success', [
'order_id' => 'ORD-12345',
'value' => 99.99
]);
Hybrid Tracking
The most common pattern: track browsing with the client script, and confirm conversions from the server once the payment provider has confirmed them.
// Client-side: track the intent using the Zenovay script
window.zenovay('track', 'checkout_started');
// Server-side: track the confirmed event from your webhook handler
app.post('/webhooks/stripe', async (req, res) => {
const event = req.body;
if (event.type === 'checkout.session.completed') {
await trackEvent('purchase', {
url: 'https://example.com/checkout/success',
sessionId: 'srv-' + event.data.object.id.slice(-12),
properties: {
order_id: event.data.object.id,
value: event.data.object.amount_total / 100
}
});
}
res.sendStatus(200);
});
Bot Filtering
Zenovay already filters obvious bot traffic on the ingest side, so events sent with generic agents like curl, wget, python, or anything matching common crawler patterns are rejected. That's why the examples above use a descriptive user_agent for your server.
If you also forward real client requests through your backend, filter crawlers before sending so you don't burn rate limit on traffic that would be dropped anyway:
function isBot(userAgent) {
const botPatterns = [
/bot/i, /crawler/i, /spider/i, /scraper/i,
/curl/i, /wget/i, /python/i, /java\//i,
/googlebot/i, /bingbot/i, /yandex/i
];
return botPatterns.some(pattern => pattern.test(userAgent || ''));
}
Rate Limits
The tracking endpoint is rate-limited per IP address:
- Burst limit: 60 requests per 10 seconds
- Sustained limit: 5,000 requests per hour
These limits apply to the tracking ingest endpoint specifically, not to the REST API (which is a separate, paid-plan feature with its own limits).
Troubleshooting
Events Not Appearing
Check:
- The tracking code in the URL path is correct.
- All required fields are present (
session_idof 8+ characters, validurl,device_type,browser,os,user_agent). Missing fields return a400with a list of what's missing. - Your
user_agentisn't being filtered as a bot (avoidcurl,python, etc.). - The IP isn't rate-limited.
Duplicate Events
Ensure:
- You're not tracking the same event both client-side and server-side.
- Webhook handlers aren't firing more than once for the same event.
Geolocation Reflects Your Server
This is expected for server-side calls — the connecting IP is your server, not the visitor. Use the client script (or first-party proxy) for visitor-accurate geolocation, and use server-side tracking for backend events where location isn't the goal.