Skip to main content
Free25 minutesAdvanced

Server-Side Tracking

Send events and pageviews to the Zenovay tracking endpoint from your server for ad-blocker immunity and full control over collection.

server-sideapitrackingbackendprivacy
Last updated:

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

BenefitDescription
Ad-blocker immunityEvents reach Zenovay even when the browser blocks the client script
Backend-only eventsTrack things that happen server-side — confirmed payments, cron jobs, webhooks
Data controlYou decide exactly what gets sent
Hybrid trackingCombine 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:

FieldNotes
session_idA string of at least 8 characters. Reuse the same value for events that belong to the same visit.
urlA full, valid URL (e.g. https://example.com/checkout/success).
device_typee.g. desktop, mobile, tablet, or server.
browserA browser/client name. Use something descriptive for server traffic (e.g. server).
osAn OS name (e.g. Linux).
user_agentA 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 to pageview.
  • Custom event — set event_type to custom, put the event name in event_name, and any metadata in properties.
# 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_id of 8+ characters, valid url, device_type, browser, os, user_agent). Missing fields return a 400 with a list of what's missing.
  • Your user_agent isn't being filtered as a bot (avoid curl, 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.

Next Steps

Was this article helpful?