Web Analyzer App
/ Docs

Tracking

Server-side Tracking

Send events from your backend using the Measurement Protocol. Track payment webhooks, CRM actions, IoT signals, and any server-side activity via a simple REST API.

1. Overview

Server-side tracking (also called the Measurement Protocol) lets you send events to Web Analyzer App from your backend instead of — or in addition to — the client-side JavaScript tracker. This is essential for tracking actions that happen outside the browser.

Common use cases include:

  • Payment webhooks — record purchases when Stripe, PayPal, or another payment provider sends a webhook to your server
  • Backend events — track user registrations, subscription changes, or account deletions that happen server-side
  • CRM integrations — log new leads, deal closures, or support ticket events from your CRM
  • IoT / hardware events — send data from devices, sensors, or kiosks that have no browser
  • Email campaign tracking — record email opens, link clicks, or unsubscribes from your email platform
  • Offline conversions — import phone orders, in-store purchases, or manual entries
Tip: Server-side tracking is a Pro plan feature. You need an API key to authenticate requests. Generate one from Settings → API Keys in your dashboard.

2. Authentication

All server-side tracking endpoints require authentication via an API key sent as a Bearer token in the Authorization header.

Base URL

https://webanalyzerapp.com/api/v1

Authorization Header

Authorization: Bearer YOUR_API_KEY

To create an API key:

  1. Go to your dashboard and click Settings → API Keys
  2. Click Create API Key and give it a descriptive name
  3. Copy the key immediately — it is only shown once
  4. Store it securely in your server's environment variables
Security: Never expose your API key in client-side code. It should only be used on your server. If you suspect a key has been compromised, revoke it immediately and create a new one.

3. Single Event Tracking

Send a single event to a specific website.

Endpoint

POST /api/v1/websites/{website_id}/track

Request Body

Field Type Required Description
name string Yes Event name (e.g. purchase, signup). Max 128 characters.
payload object No Key-value object with event metadata. Max 10 keys, values max 255 characters.
visitor_id string (UUID) No Link this event to an existing visitor. Use the ot_vid value from the client-side tracker.
url string No The URL associated with the event (e.g. the page where the action originated).
referrer string No The referrer URL for attribution purposes.

Example Request

cURL

curl -X POST https://webanalyzerapp.com/api/v1/websites/1/track \
  -H "Authorization: Bearer YOUR_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "name": "purchase",
    "payload": {
      "order_id": "ORD-1234",
      "amount": "49.99",
      "currency": "USD",
      "plan": "pro"
    },
    "visitor_id": "550e8400-e29b-41d4-a716-446655440000",
    "url": "https://example.com/checkout"
  }'

Response

200 OK

{
  "ok": true,
  "event_id": 4521
}

4. Batch Event Tracking

Send up to 25 events in a single request. Ideal for reducing API calls when processing queues, importing historical data, or handling webhook bursts.

Endpoint

POST /api/v1/websites/{website_id}/track/batch

Request Body

Field Type Required Description
events array Yes Array of event objects (max 25). Each object has the same fields as the single event endpoint (name, payload, visitor_id, url, referrer).

Example Request

cURL

curl -X POST https://webanalyzerapp.com/api/v1/websites/1/track/batch \
  -H "Authorization: Bearer YOUR_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "events": [
      {
        "name": "purchase",
        "payload": { "order_id": "ORD-1001", "amount": "29.99" },
        "visitor_id": "550e8400-e29b-41d4-a716-446655440000"
      },
      {
        "name": "purchase",
        "payload": { "order_id": "ORD-1002", "amount": "59.99" },
        "visitor_id": "7c9e6679-7425-40de-944b-e07fc1f90ae7"
      },
      {
        "name": "signup",
        "payload": { "source": "landing_page", "plan": "free" }
      }
    ]
  }'

Response

200 OK

{
  "ok": true,
  "processed": 3,
  "event_ids": [4522, 4523, 4524]
}
Tip: Batching is the most efficient way to send server-side events. A single batch request with 25 events counts as 1 API call towards your rate limit, compared to 25 individual calls.

5. Use Cases with Examples

Payment Webhook (Stripe)

Track purchases from Stripe webhook events. The visitor's browser session is long gone by the time the webhook fires, so server-side tracking is the only reliable way to capture this.

PHP (Laravel webhook handler)

// In your Stripe webhook controller
public function handleCheckoutCompleted(Request $request)
{
    $session = $request->input('data.object');

    Http::withToken(config('services.webanalyzer.api_key'))
        ->post('https://webanalyzerapp.com/api/v1/websites/1/track', [
            'name'       => 'purchase',
            'visitor_id' => $session['metadata']['visitor_id'] ?? null,
            'payload'    => [
                'order_id'   => $session['id'],
                'amount'     => number_format($session['amount_total'] / 100, 2),
                'currency'   => strtoupper($session['currency']),
                'customer'   => $session['customer_email'],
            ],
            'url' => $session['metadata']['page_url'] ?? null,
        ]);
}

Form Submission (PHP Backend)

Track form submissions from your server after processing the data.

PHP (Laravel controller)

public function submitContactForm(Request $request)
{
    // Process the form...
    ContactMessage::create($request->validated());

    // Track the event server-side
    Http::withToken(config('services.webanalyzer.api_key'))
        ->post('https://webanalyzerapp.com/api/v1/websites/1/track', [
            'name'       => 'form_submit',
            'visitor_id' => $request->input('visitor_id'),
            'payload'    => [
                'form'    => 'contact',
                'subject' => $request->input('subject'),
            ],
        ]);
}

CRM Integration (New Lead Created)

Fire an event when a new lead is created in your CRM system.

Node.js

// When a new lead is created in your CRM
async function onLeadCreated(lead) {
    await fetch('https://webanalyzerapp.com/api/v1/websites/1/track', {
        method: 'POST',
        headers: {
            'Authorization': `Bearer ${process.env.WEBANALYZER_API_KEY}`,
            'Content-Type': 'application/json',
        },
        body: JSON.stringify({
            name: 'lead_created',
            payload: {
                lead_id:  lead.id,
                source:   lead.source,
                company:  lead.company,
                value:    String(lead.estimatedValue),
            },
        }),
    });
}

Email Campaign Tracking

Track email opens and clicks from your email service provider's webhooks.

Node.js (Webhook handler)

// Mailgun / SendGrid webhook handler
app.post('/webhooks/email', async (req, res) => {
    const { event, recipient, campaign_id } = req.body;

    await fetch('https://webanalyzerapp.com/api/v1/websites/1/track', {
        method: 'POST',
        headers: {
            'Authorization': `Bearer ${process.env.WEBANALYZER_API_KEY}`,
            'Content-Type': 'application/json',
        },
        body: JSON.stringify({
            name: `email_${event}`,  // email_open, email_click, email_unsubscribe
            payload: {
                campaign_id: campaign_id,
                recipient:   recipient,
            },
        }),
    });

    res.sendStatus(200);
});

IoT / Hardware Events

Track events from physical devices, sensors, or kiosks that have no browser.

Python

import requests
import os

def track_sensor_reading(sensor_id, temperature, humidity):
    requests.post(
        'https://webanalyzerapp.com/api/v1/websites/1/track',
        headers={
            'Authorization': f'Bearer {os.environ["WEBANALYZER_API_KEY"]}',
            'Content-Type': 'application/json',
        },
        json={
            'name': 'sensor_reading',
            'payload': {
                'sensor_id':   sensor_id,
                'temperature': str(temperature),
                'humidity':    str(humidity),
            },
        },
    )

6. Linking Server Events to Visitors

To connect server-side events with a specific visitor's browser session, you need to pass the visitor ID from the frontend to your backend. The client-side tracker stores the visitor UUID in localStorage under the key ot_vid.

Step 1: Read the visitor ID on the frontend

JavaScript

// Get the visitor ID set by the tracker
const visitorId = localStorage.getItem('ot_vid');

Step 2: Include it in forms or API calls

Hidden form field

<form action="/checkout" method="POST">
    <input type="hidden" name="visitor_id" id="wa-visitor-id">
    <!-- other fields -->
    <button type="submit">Purchase</button>
</form>

<script>
    document.getElementById('wa-visitor-id').value =
        localStorage.getItem('ot_vid') || '';
</script>

Step 3: Pass it in your Stripe metadata (or similar)

PHP (Creating a Stripe Checkout session)

$session = \Stripe\Checkout\Session::create([
    'line_items'  => [/* ... */],
    'mode'        => 'payment',
    'success_url' => route('checkout.success'),
    'cancel_url'  => route('checkout.cancel'),
    'metadata'    => [
        'visitor_id' => $request->input('visitor_id'),
        'page_url'   => $request->header('Referer'),
    ],
]);

Step 4: Use the visitor_id in your server-side event

When the webhook fires, extract the visitor ID from the metadata and include it in your tracking call. This links the server event to the correct visitor profile in your dashboard.

Http::withToken(config('services.webanalyzer.api_key'))
    ->post('https://webanalyzerapp.com/api/v1/websites/1/track', [
        'name'       => 'purchase',
        'visitor_id' => $webhookPayload['metadata']['visitor_id'],
        'payload'    => ['order_id' => $webhookPayload['id']],
    ]);
Tip: If you do not include a visitor_id, the event will still be recorded and appear on the Events dashboard, but it will not be linked to a specific visitor's journey.

7. Language Examples

Here are complete examples for sending server-side events in popular languages.

PHP (cURL)

<?php
$apiKey    = getenv('WEBANALYZER_API_KEY');
$websiteId = 1;

$data = [
    'name'    => 'server_action',
    'payload' => [
        'action' => 'user_registered',
        'email'  => 'user@example.com',
    ],
];

$ch = curl_init();
curl_setopt_array($ch, [
    CURLOPT_URL            => "https://webanalyzerapp.com/api/v1/websites/{$websiteId}/track",
    CURLOPT_RETURNTRANSFER => true,
    CURLOPT_POST           => true,
    CURLOPT_HTTPHEADER     => [
        "Authorization: Bearer {$apiKey}",
        'Content-Type: application/json',
    ],
    CURLOPT_POSTFIELDS     => json_encode($data),
]);

$response = curl_exec($ch);
$httpCode = curl_getinfo($ch, CURLINFO_HTTP_CODE);
curl_close($ch);

if ($httpCode === 200) {
    echo "Event tracked successfully\n";
} else {
    echo "Error: HTTP {$httpCode} - {$response}\n";
}

Node.js (fetch)

const API_KEY    = process.env.WEBANALYZER_API_KEY;
const WEBSITE_ID = 1;

async function trackEvent(name, payload = {}, visitorId = null) {
    const body = { name, payload };
    if (visitorId) body.visitor_id = visitorId;

    const res = await fetch(
        `https://webanalyzerapp.com/api/v1/websites/${WEBSITE_ID}/track`,
        {
            method: 'POST',
            headers: {
                'Authorization': `Bearer ${API_KEY}`,
                'Content-Type':  'application/json',
            },
            body: JSON.stringify(body),
        }
    );

    if (!res.ok) {
        const error = await res.json();
        throw new Error(`Tracking failed: ${error.message}`);
    }

    return res.json();
}

// Usage
await trackEvent('signup', { plan: 'free', source: 'organic' });

Python (requests)

import requests
import os

API_KEY    = os.environ['WEBANALYZER_API_KEY']
WEBSITE_ID = 1
BASE_URL   = 'https://webanalyzerapp.com/api/v1'

def track_event(name, payload=None, visitor_id=None):
    data = {'name': name}
    if payload:
        data['payload'] = payload
    if visitor_id:
        data['visitor_id'] = visitor_id

    response = requests.post(
        f'{BASE_URL}/websites/{WEBSITE_ID}/track',
        headers={
            'Authorization': f'Bearer {API_KEY}',
            'Content-Type': 'application/json',
        },
        json=data,
        timeout=10,
    )
    response.raise_for_status()
    return response.json()

# Usage
track_event('invoice_paid', {
    'invoice_id': 'INV-2024-001',
    'amount':     '149.99',
    'currency':   'USD',
})

Ruby (net/http)

require 'net/http'
require 'json'
require 'uri'

API_KEY    = ENV['WEBANALYZER_API_KEY']
WEBSITE_ID = 1

def track_event(name, payload: {}, visitor_id: nil)
  uri = URI("https://webanalyzerapp.com/api/v1/websites/#{WEBSITE_ID}/track")

  body = { name: name, payload: payload }
  body[:visitor_id] = visitor_id if visitor_id

  http = Net::HTTP.new(uri.host, uri.port)
  http.use_ssl = true

  request = Net::HTTP::Post.new(uri.path)
  request['Authorization'] = "Bearer #{API_KEY}"
  request['Content-Type']  = 'application/json'
  request.body = body.to_json

  response = http.request(request)

  unless response.is_a?(Net::HTTPSuccess)
    raise "Tracking failed: #{response.code} #{response.body}"
  end

  JSON.parse(response.body)
end

# Usage
track_event('subscription_renewed', payload: {
  'plan'   => 'pro',
  'amount' => '14.99',
})

8. Rate Limits

Server-side tracking endpoints are rate limited to ensure fair usage and platform stability.

Limit Value Scope
Requests per minute 60 Per API key
Events per batch 25 Per request
Monthly event quota Unlimited (Pro) Per account

When you exceed the rate limit, the API returns a 429 Too Many Requests response with a Retry-After header indicating how many seconds to wait before retrying.

429 Response

HTTP/1.1 429 Too Many Requests
Retry-After: 30

{
  "ok": false,
  "error": "rate_limit_exceeded",
  "retry_after": 30
}
Tip: Use batch requests to stay well within rate limits. A single batch of 25 events counts as 1 request, giving you an effective throughput of 1,500 events per minute.

9. Error Handling

The API uses standard HTTP status codes. All error responses return a JSON body with an error field.

Status Error Code Description Action
200 - Success Event recorded
400 validation_error Invalid request body (missing name, too many events, etc.) Fix the request and retry
401 unauthorized Missing or invalid API key Check your API key
402 monthly_limit_reached Monthly event quota exceeded Upgrade to Pro or wait until next month
403 plan_required Pro plan required for server-side tracking Upgrade to Pro
404 website_not_found Website ID does not exist or does not belong to your account Verify the website ID
429 rate_limit_exceeded Too many requests Wait and retry (see Retry-After header)
500 server_error Internal server error Retry with exponential backoff

Retry Strategy

For transient errors (429, 500, 502, 503), implement exponential backoff with jitter:

Python — Retry with exponential backoff

import time
import random
import requests

def track_with_retry(name, payload, max_retries=3):
    for attempt in range(max_retries):
        response = requests.post(
            f'{BASE_URL}/websites/{WEBSITE_ID}/track',
            headers={
                'Authorization': f'Bearer {API_KEY}',
                'Content-Type': 'application/json',
            },
            json={'name': name, 'payload': payload},
            timeout=10,
        )

        if response.status_code == 200:
            return response.json()

        if response.status_code in (429, 500, 502, 503):
            wait = (2 ** attempt) + random.uniform(0, 1)
            retry_after = response.headers.get('Retry-After')
            if retry_after:
                wait = max(wait, int(retry_after))
            time.sleep(wait)
            continue

        # Non-retryable error
        response.raise_for_status()

    raise Exception(f'Failed after {max_retries} retries')
Important: Do not retry 400, 401, 403, or 404 errors. These indicate a problem with your request that will not be resolved by retrying.

10. Best Practices

Naming Conventions

Use consistent, descriptive event names with a clear naming scheme.

Pattern Example Notes
object_action order_completed Recommended. Groups well in the Events dashboard.
category_action email_opened Good for grouping by category.
snake_case user_signed_up Preferred over camelCase or kebab-case for consistency.

Payload Structure

  • Keep payloads flat — no nested objects
  • Use string values for all fields (numbers and booleans will be cast to strings)
  • Include an identifier when possible (e.g. order_id, invoice_id) for cross-referencing
  • Limit payloads to 10 keys maximum
  • Avoid sensitive data (passwords, full credit card numbers, personal health info)

Batching for Efficiency

  • Use the batch endpoint whenever you have multiple events to send
  • Queue events in your application and flush them periodically (e.g. every 5 seconds or every 25 events)
  • A batch of 25 events counts as 1 API call, giving you 25x more throughput
  • Process webhook bursts (e.g. many Stripe events at once) with the batch endpoint

Idempotency

Webhooks can be delivered more than once. To avoid duplicate events, implement idempotency in your webhook handler:

PHP — Idempotent webhook handler

public function handleWebhook(Request $request)
{
    $eventId = $request->input('id');

    // Check if we've already processed this webhook
    if (Cache::has("webhook_processed:{$eventId}")) {
        return response()->json(['ok' => true, 'skipped' => true]);
    }

    // Process the webhook and track the event
    Http::withToken(config('services.webanalyzer.api_key'))
        ->post('https://webanalyzerapp.com/api/v1/websites/1/track', [
            'name'    => 'purchase',
            'payload' => ['order_id' => $request->input('data.object.id')],
        ]);

    // Mark as processed (TTL: 48 hours)
    Cache::put("webhook_processed:{$eventId}", true, 172800);

    return response()->json(['ok' => true]);
}

General Tips

  • Store your API key in environment variables, never in code
  • Set a timeout on HTTP requests (10 seconds recommended) so tracking calls don't block your main flow
  • Send tracking calls asynchronously (fire-and-forget) when possible, so they don't slow down your user-facing responses
  • Log tracking failures for debugging, but do not let them crash your application
  • Use visitor_id wherever possible to connect server events to browser sessions
  • Prefix server-side event names with a namespace (e.g. server_) if you want to distinguish them from client-side events in your dashboard

Help & FAQ

Find answers instantly