Docs / Server-side tracking

Server-side tracking

Send events directly from your backend — ad-blocker resilient, source of truth for revenue.

When to use server-side tracking

  • Conversions where the truth lives in your DB (purchases, signups, plan upgrades).
  • Webhooks from payment processors, CRMs, or email providers.
  • Mobile backends until the native SDKs are released.
  • Any event you want to capture even when the user has an ad-blocker.

There is currently no official PHP/Node/Python SDK package — server-side tracking uses the REST API directly via your language's standard HTTP client. The examples below are intentionally minimal and have no extra dependencies.

Endpoint and auth

  • URL: POST https://api.openanalyticsapi.com/v1/projects/{PROJECT_ID}/events
  • Auth: Authorization: Bearer oa_live_xxxxxxxxxxxx
  • Content-Type: application/json
  • Response: 202 Accepted with {"queued":true} on success

Security reminder. The oa_live_* key is a secret. Read it from an environment variable; never commit it to git, never inline it in a public repository, and never expose it to the browser.

Event payload

Field Required Notes
event_typeyespageview, event, identify, conversion
event_nameconditionalRequired for event and conversion.
user_idrecommendedHashed / anonymized identifier. Never send raw emails.
urlrecommendedThe URL the event is associated with.
propertiesnoFree-form JSON object with up to 50 keys.
revenuenoDecimal, used for conversions.
currencynoISO 4217 code (e.g. USD, EUR).
timestampnoUnix ms. Defaults to receive time.

PHP example

Plain PHP 8 using the built-in cURL extension. No framework, no Composer dependency.

PHP
<?php

function oaa_track(string $eventType, string $eventName, array $extra = []): bool {
    $projectId = getenv('OAA_PROJECT_ID');
    $apiKey    = getenv('OAA_API_KEY'); // oa_live_xxx
    if ($projectId === false || $apiKey === false) {
        return false;
    }

    $payload = array_merge([
        'event_type' => $eventType,
        'event_name' => $eventName,
        'timestamp'  => (int) (microtime(true) * 1000),
    ], $extra);

    $url = "https://api.openanalyticsapi.com/v1/projects/{$projectId}/events";

    $ch = curl_init($url);
    curl_setopt_array($ch, [
        CURLOPT_POST           => true,
        CURLOPT_POSTFIELDS     => json_encode($payload, JSON_UNESCAPED_SLASHES),
        CURLOPT_HTTPHEADER     => [
            "Authorization: Bearer {$apiKey}",
            'Content-Type: application/json',
        ],
        CURLOPT_RETURNTRANSFER => true,
        CURLOPT_TIMEOUT        => 5,
    ]);
    $response = curl_exec($ch);
    $status   = curl_getinfo($ch, CURLINFO_HTTP_CODE);
    curl_close($ch);

    return $status === 202;
}

// Conversion from a webhook handler
oaa_track('conversion', 'purchase', [
    'user_id'    => hash('sha256', $userEmail),
    'url'        => 'https://example.com/thanks',
    'revenue'    => 49.99,
    'currency'   => 'USD',
    'properties' => ['plan' => 'pro'],
]);

For high-volume backends, batch with POST /v1/projects/{id}/events/batch (up to 1000 events per call).

Node.js example

Node 18+ with the built-in fetch. No npm dependency.

JavaScript (Node 18+)
const OAA_PROJECT_ID = process.env.OAA_PROJECT_ID;
const OAA_API_KEY    = process.env.OAA_API_KEY; // oa_live_xxx

async function oaaTrack(eventType, eventName, extra = {}) {
  const url = `https://api.openanalyticsapi.com/v1/projects/${OAA_PROJECT_ID}/events`;

  const payload = {
    event_type: eventType,
    event_name: eventName,
    timestamp:  Date.now(),
    ...extra,
  };

  const res = await fetch(url, {
    method: 'POST',
    headers: {
      'Authorization': `Bearer ${OAA_API_KEY}`,
      'Content-Type':  'application/json',
    },
    body: JSON.stringify(payload),
  });

  return res.status === 202;
}

// Conversion from a Stripe webhook
await oaaTrack('conversion', 'purchase', {
  user_id:    crypto.createHash('sha256').update(userEmail).digest('hex'),
  url:        'https://example.com/thanks',
  revenue:    49.99,
  currency:   'USD',
  properties: { plan: 'pro' },
});

Retries & duplicate handling

The current ingest pipeline does not deduplicate by an idempotency key. If your webhook handler retries the same event, you'll get the event twice in your analytics. Two practical patterns:

  • Use a stable, deterministic event_name + properties that include a unique business ID (e.g. order_id) — then dedupe in your downstream queries.
  • Track the last-sent event ID in your DB and skip the call if it was already sent.

Server-side Idempotency-Key header support is on the roadmap.

Part of the Open API ecosystem