Zum Hauptinhalt springen
8 Min. Lesedauer

Server-Side-Tracking

Über den Endpunkt POST /api/v1/events kann Ihr Backend Analyse-Events direkt an Zenovay senden. Verwenden Sie ihn für Käufe, die aus einem Zahlungs-Webhook stammen, für Identifizierungs-Events aus Ihrem Authentifizierungsdienst oder für alles, bei dem der Browser des Clients nicht die maßgebliche Datenquelle ist.

Events landen in denselben Postgres-Tabellen wie Events des Browser-Trackers, werden jedoch mit source: 'server' markiert, sodass sie in den Dashboards parallel zum Browser-Stream erscheinen. Abgleichsberichte in app.zenovay.com nutzen dieses Flag, um die beiden Streams zu unterscheiden.

Der Browser-Tracker und der serverseitige Endpunkt ergänzen sich — sie sind nicht austauschbar. Verwenden Sie beide. Der Browser-Tracker erfasst Seitenaufrufe und Engagement; der Server-Endpunkt erfasst Events des Systems der Wahrheit, denen man Browser nicht zumuten kann (Käufe, Identifizierung, Aktivität außerhalb der Website).

Endpunkt

POST/api/v1/events

Nimmt einen Batch von bis zu 1.000 serverseitigen Events entgegen

URL: https://api.zenovay.com/api/v1/events

Header:

  • Authorization: Bearer zv_... — erforderlich
  • Content-Type: application/json — erforderlich
  • Sec-GPC: 1 — optional; wenn vorhanden, wird der gesamte Batch mit dem Grund gpc_opted_out abgelehnt

Authentifizierung

Authentifizieren Sie sich mit einem zv_*-API-Schlüssel, der in app.zenovay.com → Einstellungen → API-Schlüssel ausgestellt wird. Der Schlüssel berechtigt den Aufrufer für ein einzelnes Team und entweder für eine einzelne Website (Scope site_access) oder für jede Website dieses Teams (Scope full_access).

Der Schlüssel wird am Edge mit SHA-256 gehasht und gegen website_api_keys validiert. Es gelten tarifabhängige monatliche und minütliche Limits (siehe Rate Limits).

Request-Body

{
  "trackingCode": "ZV_AbC123...",
  "events": [
    {
      "type": "pageview",
      "ts": 1735689600000,
      "visitorId": "11111111-1111-4111-9111-111111111111",
      "sessionId": "22222222-2222-4222-9222-222222222222",
      "props": { "url": "https://example.com/pricing", "referrer": "" },
      "idempotencyKey": "pv-2026-05-02-abc"
    }
  ],
  "serverContext": {
    "clientIp": "203.0.113.42",
    "userAgent": "Mozilla/5.0 ...",
    "acceptLanguage": "en-US"
  }
}
FeldTypErforderlichHinweise
trackingCodestringjaDer Tracking-Code der Website, zu finden unter app.zenovay.com → Website-Einstellungen.
eventsServerEvent[]ja1–1.000 Events.
events[i].typepageview | event | identify | goal | purchasejaEvent-Taxonomie — siehe unten.
events[i].tsnumberjaUnix-Millisekunden. Muss innerhalb von ±24 Stunden zur Serverzeit liegen, andernfalls Ablehnung mit invalid_ts.
events[i].sessionIdstringbedingtErforderlich für pageview und goal. UUID erforderlich für pageview (die Spalte ist vom Typ UUID).
events[i].visitorIdstringbedingtErforderlich für pageview, identify und goal. UUID erforderlich für pageview.
events[i].propsobjectjaTypabhängiger Payload — siehe unten.
events[i].idempotencyKeystringempfohlenEindeutig pro trackingCode. Verhindert doppelte Schreibvorgänge für 24 Stunden. Maximal 128 Zeichen.
events[i].consentopt-in | opt-out | unknownoptionalopt-out lehnt ausschließlich dieses eine Event mit dem Grund consent_opted_out ab.
serverContext.clientIpstringoptionalWird vor jeder Persistierung gehasht (täglich gesalzenes SHA-256). Klartext wird niemals gespeichert.

Event-Typen

typeSchreibt nachErforderliche props
pageviewvisitors, page_views, user_events, Analytics Engineurl
eventuser_events (event_type='custom')name (Name des benutzerdefinierten Events)
identifyidentified_users (UPSERT auf (website_id, visitor_id))email und/oder eines von: name, customer_id, phone, company, dazu beliebige benutzerdefinierte Attribute
goalgoal_completions (nach Lookup des Ziels über den Namen)name (muss zu einem aktiven Ziel in app.zenovay.com → Ziele passen); optional value
purchasepayment_eventsamount (Zahl), optional currency (Standard USD), payment_provider (stripe/lemonsqueezy/polar/server, Standard server), status (Standard succeeded)

Response

Der Endpunkt antwortet immer mit HTTP 200, sobald Authentifizierung und Validierung erfolgreich sind; die Ergebnisse pro Event werden im Body gemeldet, sodass Sie erfolgreiche und abgelehnte Events in einem Batch mischen können.

{
  "acceptedCount": 4,
  "dedupedCount": 1,
  "rejectedCount": 0,
  "errors": []
}

Wenn Events abgelehnt werden, listet errors jeden Fehlschlag mit dem ursprünglichen Index auf:

{
  "acceptedCount": 0,
  "dedupedCount": 0,
  "rejectedCount": 1,
  "errors": [
    { "index": 0, "reason": "invalid_ts", "message": "ts is missing or beyond ±24h skew tolerance" }
  ]
}

Idempotenz

Übergeben Sie für jedes Event einen idempotencyKey. Innerhalb der nächsten 24 Stunden liefert jedes Event mit demselben (trackingCode, idempotencyKey) ein dedupedCount zurück und wird nicht ein zweites Mal geschrieben. Schlüssel länger als 128 Zeichen oder leere Schlüssel werden mit dem Grund invalid_idempotency_key abgelehnt.

Idempotenz wird sowohl am Edge (Cloudflare-KV-Cache) als auch auf Datenbankebene erzwungen (partielle Unique-Indizes auf (website_id, idempotency_key) für visitors, page_views, user_events). Die KV-Schicht ist schnell; die Datenbankschicht fängt den seltenen Edge Case ab, in dem zwei Events mit demselben Schlüssel innerhalb von Millisekunden in unterschiedlichen Cloudflare-Colos eintreffen.

Rate Limits

Minütliche und monatliche Limits ergeben sich aus Ihrem Abonnement-Tarif:

TarifPro MinutePro Monat
Free101.000
Pro3010.000
Scale60100.000
Enterprise1201.000.000

Wenn Sie ein Limit erreichen, antwortet der Endpunkt mit HTTP 429 sowie den Standard-Antwortheadern X-RateLimit-* und X-Usage-*. Exponentielles Backoff wird empfohlen.

Datenschutz

  • IP-Adressen werden niemals im Klartext gespeichert. Ist serverContext.clientIp vorhanden, wird die Adresse vor der Persistierung mit täglich gesalzenem SHA-256 gehasht. Fehlt sie, wird der Sentinel-Wert unknown gespeichert.
  • GPC wird respektiert. Eine Anfrage mit Sec-GPC: 1 lehnt jedes Event im Batch mit dem Grund gpc_opted_out ab. Es wird keine Zeile geschrieben.
  • Einwilligung pro Event. Ein Event mit consent: 'opt-out' wird einzeln mit dem Grund consent_opted_out abgelehnt; andere Events desselben Batches können dennoch akzeptiert werden.
  • Die serverseitige Einwilligung liegt in der Verantwortung des Kunden. Der Browser-Tracker ist cookiefrei und auch ohne Einwilligung nach ePrivacy/DSGVO rechtmäßig; die serverseitige Erfassung erfolgt definitionsgemäß im Rahmen der Einwilligungslogik Ihrer eigenen Anwendung.

Beispiele

curl — pageviewBash
curl -X POST https://api.zenovay.com/api/v1/events \
-H "Authorization: Bearer $ZENOVAY_API_KEY" \
-H "Content-Type: application/json" \
-d '{
  "trackingCode": "ZV_YourTrackingCode",
  "events": [{
    "type": "pageview",
    "ts": '"$(date +%s%3N)"',
    "visitorId": "11111111-1111-4111-9111-111111111111",
    "sessionId": "22222222-2222-4222-9222-222222222222",
    "props": { "url": "https://yourdomain.com/pricing" },
    "idempotencyKey": "pv-2026-05-02-pricing-1"
  }]
}'
Node.js — Kauf aus einem Stripe-WebhookJavaScript
async function reportPurchaseToZenovay(stripeEvent, visitorId) {
await fetch('https://api.zenovay.com/api/v1/events', {
  method: 'POST',
  headers: {
    'Authorization': `Bearer ${process.env.ZENOVAY_API_KEY}`,
    'Content-Type': 'application/json',
  },
  body: JSON.stringify({
    trackingCode: process.env.ZENOVAY_TRACKING_CODE,
    events: [{
      type: 'purchase',
      ts: Date.now(),
      visitorId,
      props: {
        amount: stripeEvent.data.object.amount_total / 100,
        currency: stripeEvent.data.object.currency.toUpperCase(),
        payment_provider: 'stripe',
        provider_event_id: stripeEvent.id,
        customer_email: stripeEvent.data.object.customer_email,
        plan_name: stripeEvent.data.object.metadata?.plan,
      },
      idempotencyKey: stripeEvent.id,
    }],
  }),
});
}
Python — identify nach der AnmeldungPython
import requests, time, os

def identify_user(visitor_id: str, email: str, attributes: dict):
  requests.post(
      "https://api.zenovay.com/api/v1/events",
      headers={
          "Authorization": f"Bearer {os.environ['ZENOVAY_API_KEY']}",
          "Content-Type": "application/json",
      },
      json={
          "trackingCode": os.environ["ZENOVAY_TRACKING_CODE"],
          "events": [{
              "type": "identify",
              "ts": int(time.time() * 1000),
              "visitorId": visitor_id,
              "props": {"email": email, **attributes},
              "idempotencyKey": f"id-{visitor_id}-{int(time.time())}",
          }],
      },
      timeout=5,
  ).raise_for_status()
Ruby — benutzerdefiniertes Event aus einem Rails-HintergrundjobRuby
require 'net/http'
require 'json'
require 'securerandom'

ZENOVAY_API_KEY = ENV.fetch('ZENOVAY_API_KEY')
ZENOVAY_TRACKING_CODE = ENV.fetch('ZENOVAY_TRACKING_CODE')

def track_event_zenovay(event_name, visitor_id, properties = {})
uri = URI('https://api.zenovay.com/api/v1/events')
http = Net::HTTP.new(uri.host, uri.port)
http.use_ssl = true
http.read_timeout = 5

request = Net::HTTP::Post.new(uri)
request['Authorization'] = "Bearer #{ZENOVAY_API_KEY}"
request['Content-Type'] = 'application/json'
request.body = {
  trackingCode: ZENOVAY_TRACKING_CODE,
  events: [{
    type: 'event',
    ts: (Time.now.to_f * 1000).to_i,
    visitorId: visitor_id,
    props: { name: event_name, **properties },
    idempotencyKey: "#{event_name}-#{visitor_id}-#{SecureRandom.hex(8)}"
  }]
}.to_json

response = http.request(request)
raise "Zenovay tracking failed: #{response.code}" unless response.code.to_i == 200
JSON.parse(response.body)
end

# Beispiel: aus einem Sidekiq-Job oder Rails-Controller
track_event_zenovay('newsletter_subscribed', visitor_id, source: 'footer_form')

Ablehnungsgründe

reasonWann er auftritt
gpc_opted_outDie Anfrage enthielt Sec-GPC: 1. Der gesamte Batch wird abgelehnt.
consent_opted_outDas einzelne Event hatte consent: 'opt-out'.
invalid_tsts fehlt, ist nicht numerisch oder weicht um mehr als ±24 Stunden von der Serverzeit ab.
invalid_event_typetype gehört nicht zu den fünf unterstützten Werten.
invalid_idempotency_keyidempotencyKey ist leer oder länger als 128 Zeichen.
invalid_visitor_idErforderlicher visitorId fehlt oder hat bei einem pageview keine UUID-Form.
invalid_session_idErforderlicher sessionId fehlt oder hat bei einem pageview keine UUID-Form.
unknown_goalprops.name passt zu keinem aktiven benutzerdefinierten Ziel auf dieser Website.
invalid_payment_providerprops.payment_provider ist keiner von stripe/lemonsqueezy/polar/server.
internal_errorUnerwarteter Datenbank- oder Laufzeitfehler. Ein erneuter Versuch mit demselben idempotencyKey ist gefahrlos möglich.

Wo diese Ereignisse im Dashboard erscheinen

Jeder Ereignistyp landet auf einer bestimmten Zenovay-Oberfläche:

typeWo es erscheint
pageviewSeiten-Tab, Echtzeit-Aktivität, Besucher-Sitzungstimeline
eventListe der benutzerdefinierten Ereignisse, Besucher-Timeline (nach event_name filtern, um ein bestimmtes Ereignis zu finden)
identifySeite „Identifizierte Nutzer", Besucherprofil-Detail
goalZiele-Tab, Statistiken zu Funnel-Abschlüssen
purchaseUmsatz-Dashboard, Umsatzattribution, Profil identifizierter Nutzer

Jeder API-Schlüssel hat zudem ein eigenes Serverseitige Aktivität-Panel unter app.zenovay.com → Einstellungen → API-Schlüssel → [auf einen Schlüssel klicken]. Es zeigt für den Website-Bereich des Schlüssels die Zahl akzeptierter Ereignisse der letzten 24 Stunden und 7 Tage, die häufigsten Ereignistypen sowie die 20 zuletzt eingegangenen Ereignisse.

Dort werden nur akzeptierte Ereignisse aufgelistet. Ablehnungsgründe werden synchron im Antwort-Body von POST /api/v1/events zurückgegeben — prüfen Sie diese beim Debuggen Ihres Senders.

Was dieser Endpunkt NICHT tut

  • Er erstellt keine benutzerdefinierten Ziele spontan. Definieren Sie diese zuerst unter app.zenovay.com → Ziele.
  • Er ordnet keine Offline-Aktivität Online-Besuchern zu. Diese Funktion steht als separater Endpunkt auf der Roadmap.
  • Er ersetzt nicht den Browser-Tracker. Pageviews aus Ihrem Backend enthalten keine Geräte-, Betriebssystem-, Browser- oder Geo-Daten, sofern Sie diese nicht in props mitliefern.
  • Er erzeugt keine Visitor-IDs für Sie. Verwenden Sie die UUID, die Ihnen für den Besucher bereits vorliegt; bei pageview-Events muss diese UUID eine echte RFC-4122-UUID sein.
War diese Seite hilfreich?