Analytics Endpoints Index

Overview

The analytics domain forwards events to Google Analytics 4 (GA4) via the Measurement Protocol, for Google Ads conversion tracking and in-app engagement reporting.

Conversion events (server-side, no client endpoint)

These fire automatically from existing endpoints — user_registered from /api/auth/register and the Google/Apple registration flows, trial_started from the subscription trial endpoints, first_payment from the Stripe webhook, and session_active from /api/auth/refresh. There is no separate endpoint to call for these — this doc covers only the client-facing ingest endpoint below.

Batching

POST /api/analytics/track accepts either a single event or a batch of up to 25 (GA4 Measurement Protocol's own hard limit per call) — see the endpoint doc below for the exact shape. As an alternative to a separate request, a batch can also be attached directly to a token refresh call, saving a round trip.

GA4 attribution (gclid / gaClientId / platform)

The following optional fields, when present on POST /api/auth/register, /api/auth/login, and the Google/Apple auth endpoints, are used to attribute conversion events to a Google Ads click and a GA4 client:

Field Type Description
gclid string Google Ads click ID. First-touch — stored once per user, never overwritten by a later request.
gaClientId string GA4 client_id (web, from the _ga cookie) or Firebase app_instance_id (iOS/Android). Last-touch — refreshed on every request that supplies it.
platform "web" | "android" | "ios" Which GA4 Measurement Protocol credentials to use when sending server-initiated events for this user.

All three are optional and additive to the existing request bodies documented in Authentication API — omitting them simply means that request doesn't update attribution.

POST /api/analytics/track

Forward a client-driven engagement event (or app_install) to GA4 — either a single event or a batch. Public endpoint — accepts both anonymous callers (needed for pre-login events like app_install) and authenticated ones (the request is attributed to req.user.id when a valid session is present).

Headers:

Content-Type: application/json
Authorization: Bearer <access_token>   # optional

Request Body — single event:

{
    "eventName": "content_search",
    "gaClientId": "GA1.2.123456789.1234567890",
    "platform": "ios",
    "gclid": "Cj0KCQiA...",
    "params": {
        "searchTerm": "grace"
    }
}

Request Body — batch (up to 25 events):

{
    "gaClientId": "GA1.2.123456789.1234567890",
    "platform": "ios",
    "gclid": "Cj0KCQiA...",
    "events": [
        { "eventName": "scripture_view", "params": { "book": "John", "chapter": 3 } },
        { "eventName": "content_search", "params": { "searchTerm": "grace" } }
    ]
}

gaClientId/platform/gclid are shared across the whole batch — GA4 Measurement Protocol accepts only one client_id (+ optional user_id) per call, so a batch is inherently scoped to one client/device. Each batch item may also carry an optional timestampMicros (Unix epoch microseconds) to report when the event actually occurred, forwarded to GA4 as a per-event timestamp_micros override.

eventName allowlist: app_install, session_active, content_search, book_navigation, scripture_view, topical_index_view, outline_view. Any other value is rejected — event names are not forwarded to GA4 free-form. Applies to both the single-event eventName and every item's eventName in a batch.

Limits:

Response (202 Accepted):

{
    "message": "Event accepted"
}

Error Responses:

// 400 - Validation error (bad eventName, missing gaClientId/platform, batch too large,
//        param value too long, or neither eventName nor events provided)
{
    "message": "Validation error",
    "code": "VALIDATION_ERROR",
    "errors": [ ... ]
}

// 429 - Rate limited
{
    "error": "Too many analytics events",
    "message": "Too many analytics events from this IP, please try again later.",
    "code": "RATE_LIMIT_EXCEEDED"
}

Notes: