API and Examples

API Reference

Practical reference for the current Logga backend route surface.

This page is a practical guide to the route surface currently wired in logga-backend/src/index.ts.

Base Concepts

Auth modes

Mode Typical caller Notes
Dashboard auth Web dashboard or native user flows Human user JWT-based access.
API key auth Backend or service integration Used for ingest and other workspace-scoped automation.
Hybrid auth Either of the above Route decides from the auth context which workspace and environment are active.
MCP token An AI assistant A lg_mcp_ key on the /mcp endpoint. See AI Connectors.

Scopes

An API key carries an explicit list of scopes. A request whose key lacks the scope its route needs is answered 403 insufficient_scope, naming the scope that was missing. Session tokens are not scoped — their authorisation is workspace membership.

Keys created before scopes existed carry an empty list, which means unrestricted.

Scope Grants
events:read / events:write GET / everything else on /v1/events
channels:read / channels:write /v1/channels
sessions:read / sessions:write /v1/sessions
rules:read / rules:write /v1/rules
analytics:read /v1/stats
notifications:read /v1/notifications
users:read tracked-user data through MCP
users:link POST /v1/users/link. Never on an app key
devices:write /v1/device-tokens
flags:read GET /v1/flags, POST /v1/flags/:key/exposure
flags:manage /v1/feature-flags: flags with their rules, changes and A/B results. Never on an app key

Common route families

Route family Auth
/health none
/auth/* mixed, depending on route
/v1/dashboard/* dashboard auth
/v1/events hybrid auth
/v1/channels hybrid auth
/v1/sessions hybrid auth
/v1/stats hybrid auth
/v1/rules hybrid auth
/v1/notifications hybrid auth
/v1/device-tokens hybrid auth
/v1/reports dashboard auth
/mcp MCP token

Health

GET /health

Returns a basic liveness payload.

Example response:

json
{
  "status": "ok",
  "timestamp": "2026-03-19T12:00:00.000Z"
}

Authentication

POST /auth/register

Creates a user, workspace, workspace membership, and a default project for the production environment.

Example body:

json
{
  "email": "[email protected]",
  "password": "very-secret-password",
  "name": "Alex",
  "workspaceName": "Acme"
}

POST /auth/login

Returns a user token plus a basic user payload.

GET /auth/me

Returns the authenticated user plus workspace memberships and environment summaries.

Events

POST /v1/events

Create an event in the authenticated workspace and environment.

Headers:

text
Authorization: Bearer lg_sk_xxxx
Content-Type: application/json

Example body:

json
{
  "channel": "payments",
  "event": "invoice_sent",
  "level": "success",
  "tags": ["stripe", "pro"],
  "metadata": {
    "amount": 49,
    "currency": "EUR"
  },
  "actor": {
    "id": "user_123",
    "name": "Francesco",
    "email": "[email protected]",
    "role": "admin",
    "properties": {
      "plan": "pro",
      "seats": 5
    }
  },
  "idempotencyKey": "invoice_abc123"
}

Notable validation behavior:

  • channel is required
  • event is required
  • level defaults to info
  • metadata is capped at 10KB
  • idempotencyKey is optional but globally unique when provided
  • actor.id is preferred over userId when both are sent
  • actor.properties values must be strings, numbers, booleans, or null; keys ≤64 chars, string values ≤1024 chars, max 50 keys per user
  • events upsert a row in the tracked users table (TrackedUser), keyed on (projectId, actor.id), and merge any identity fields carried in the event
  • actor.properties is a patch: sent keys are written, missing keys are kept, null removes a key. Properties that break the limits, or would take the user past 50 keys, are not stored; the event still is, and the 201 body carries warnings: ["actor.properties ignored: ..."]

GET /v1/events

Supported query filters include:

  • channel
  • level
  • userId
  • sessionId
  • from
  • to
  • limit
  • cursor
  • tags
  • projectId

GET /v1/events/:id

Fetch a single event scoped to the authenticated workspace and environment.

DELETE /v1/events/:id

Delete one event.

DELETE /v1/events

Bulk delete up to 200 event ids in one request.

Sessions

POST /v1/sessions

Creates a new session.

Example body:

json
{
  "channel": "system",
  "name": "nightly_build",
  "metadata": {
    "branch": "main",
    "commit": "abc123"
  },
  "ttlSeconds": 1800
}

PATCH /v1/sessions/:id

Updates session progress, current step, or metadata.

POST /v1/sessions/:id/complete

Marks the session complete and may include a summary.

POST /v1/sessions/:id/fail

Marks the session failed and may include a reason.

POST /v1/sessions/:id/close

Closes the session without marking it successful.

Rules

GET /v1/rules

Lists alert rules for the authenticated workspace and environment.

Optional filters:

  • channelId
  • isActive
  • cursor pagination fields

POST /v1/rules

Creates an alert rule.

Example body:

json
{
  "name": "Alert on payment failures",
  "channelId": "channel_uuid",
  "condition": {
    "type": "on_event",
    "level": "error"
  }
}

PATCH /v1/rules/:id

Updates name, target channel, condition, or isActive.

Reports

Reports are dashboard-auth-only today.

GET /v1/reports

Query parameters:

  • workspaceId
  • optional environmentId
  • pagination fields

POST /v1/reports

Creates a scheduled report.

Example body:

json
{
  "workspaceId": "workspace_uuid",
  "environmentId": "workspace_uuid_production",
  "name": "Daily payment volume",
  "schedule": "daily",
  "metric": "sum",
  "aggregateField": "amount",
  "filters": {
    "eventType": "payment_succeeded",
    "tags": ["payments"]
  },
  "groupBy": null,
  "channelIds": ["channel_uuid"]
}

PATCH /v1/reports/:id

Updates report configuration. Aggregate metrics require aggregateField.

DELETE /v1/reports/:id

Deletes a report config.

Tracked Users

POST /v1/users/link

Hybrid auth, scope users:link. Body { "previousId": "v_8f3…", "userId": "user_123" }: the anonymous id a visitor had and the user id they got when they signed up are one person from now on. Call it from your backend at signup, where the account is created.

What it does, once: previousId's events, profile (the earlier first-seen, the visitor's country and properties where the user has none) and feature-flag exposures move to userId. Afterwards events and flag reads still sent with previousId land on userId, and looking up previousId returns the user, with linkedIds listing the ids that were linked to it.

In an A/B test the first exposure counts: if the visitor saw the new version, the signed-in user keeps seeing it and stays in that group, and the goal they reach after signing up is counted there. A goal like user_signed_up can therefore be measured on logged-out visitors.

json
{ "previousId": "v_8f3…", "userId": "user_123", "alreadyLinked": false, "eventsMoved": 14, "exposuresMoved": 1 }

The rules that keep one person from taking another's history:

  • a link is permanent: calling it again with the same pair is harmless (alreadyLinked: true), linking the same previousId to a different user is 409 already_linked;
  • an id that other ids were linked to is a known user and can never be linked away: 409 previous_id_is_identified;
  • users:link is never on an app key: a browser or an app can claim any id, the server that created the account cannot.

Two ids that never went through a link look alike to Logga, so pass the anonymous id as previousId, never a real user id. previousId and userId equal is 422.

Dashboard Project Users

Project users are dashboard-auth-only and scoped to a single project inside the selected environment.

GET /v1/dashboard/projects/:projectId/users

Returns a paginated tracked-user list plus page summary.

Query parameters:

  • workspaceId
  • optional environmentId
  • limit
  • optional opaque cursor
  • optional query
  • optional range: 24h, 7d, 30d, all
  • optional sort: currently last_seen_desc

Response shape:

json
{
  "summary": {
    "trackedUsers": 42,
    "activeUsersInRange": 17,
    "eventsInRange": 382,
    "unattributedEventsInRange": 21
  },
  "data": [
    {
      "userId": "user_123",
      "name": "Francesco",
      "email": "[email protected]",
      "avatarUrl": null,
      "role": "admin",
      "label": null,
      "properties": {
        "plan": "pro",
        "seats": 5
      },
      "lastSeenAt": "2026-04-09T09:15:00.000Z",
      "eventCount": 18,
      "sessionCount": 4,
      "channelCount": 3,
      "lastEventType": "signed_in"
    }
  ],
  "nextCursor": "opaque_cursor"
}

GET /v1/dashboard/projects/:projectId/users/detail

Returns one tracked user detail payload.

Query parameters:

  • workspaceId
  • optional environmentId
  • required userId
  • optional range: 24h, 7d, 30d, all
  • optional eventsLimit
  • optional eventsCursor

The detail response includes:

  • stable user identity, including properties (custom key-value metadata)
  • all-time firstSeenAt
  • all-time lastSeenAt
  • range-scoped totals
  • top channels
  • top event types
  • recent events with event-style pagination

PATCH /v1/dashboard/projects/:projectId/users

Updates identity fields and custom properties for a tracked user. All fields except workspaceId and userId are optional; only the fields you pass are updated.

Body:

json
{
  "workspaceId": "workspace_uuid",
  "environmentId": "environment_id",
  "userId": "user_123",
  "name": "Francesco",
  "email": "[email protected]",
  "avatarUrl": "https://…",
  "role": "admin",
  "label": "Pro tier",
  "properties": {
    "plan": "pro",
    "seats": 5,
    "trial": false
  }
}

Response mirrors the request's identity fields and the full stored properties object.

Validation:

  • name ≤255, email must parse as email, avatarUrl must parse as URL, role ≤128, label ≤255
  • properties keys ≤64 chars, values must be strings/numbers/booleans/null, string values ≤1024 chars, max 50 keys
  • empty strings are normalized to null so the editor can clear a field
  • properties here is a full replacement: send the complete object. Events work the other way, patching only the keys they carry

Note: subsequent events carrying actor.name, actor.email, etc. will overwrite what you save here. See Tracking Users In The Dashboard for how to reason about the source of truth.

Analytics

Dashboard auth. Every body carries workspaceId (and optionally environmentId); the project must belong to that workspace and environment or the call returns 404. Queries time out after 10 seconds.

POST /v1/analytics/query

Aggregates over events: count, count_distinct, sum, avg, min, max, p50, p95, p99, grouped by channel, eventType, level, userId, actor, metadata.<path>, time:<bucket> or user.<property>.

userFilters keeps only events whose user matches, using the same operators as metadata rules (exists, not_exists, equals, not_equals, contains, gt, gte, lt, lte):

json
{
  "workspaceId": "…",
  "projectId": "…",
  "aggregations": [{ "op": "count", "alias": "reports" }],
  "filters": { "eventType": "report_sent" },
  "userFilters": [{ "field": "mrr", "operator": "gt", "value": 100 }],
  "groupBy": ["user.plan", "time:week"]
}

User properties are read as they are now, not as they were when the event happened.

POST /v1/analytics/funnel

How many users did a sequence of events, in order. A user enters at their first step-1 event between from and to; each later step must happen strictly after the previous one and within conversionWindowSeconds of step 1 (default 7 days, max 90). Users are followed by the event's user id, so events without one are ignored.

json
{
  "workspaceId": "…",
  "projectId": "…",
  "steps": [
    { "eventType": "user_signed_up" },
    { "eventType": "account_added" },
    { "eventType": "report_created", "metadataRules": [{ "source": "metadata", "field": "type", "operator": "equals", "value": "weekly" }] }
  ],
  "from": "2026-08-01T00:00:00Z",
  "to": "2026-09-01T00:00:00Z",
  "conversionWindowSeconds": 604800,
  "userFilters": [{ "field": "is_agency", "operator": "equals", "value": true }]
}

Response: one entry per step with users, conversionFromStart, conversionFromPrevious and medianSecondsFromPrevious.

POST /v1/analytics/retention

Do users come back. Each user joins the cohort of the day, week or month in which they first did startEvent, ever; cohorts are the ones whose first occurrence falls between from and to (at most 60). For each later period it counts the users who did returnEvent; omit returnEvent to count any event.

json
{
  "workspaceId": "…",
  "projectId": "…",
  "startEvent": { "eventType": "user_signed_up" },
  "returnEvent": { "eventType": "dashboard_viewed" },
  "period": "week",
  "from": "2026-07-01T00:00:00Z",
  "to": "2026-09-01T00:00:00Z",
  "periods": 8
}

Response: cohorts[] with cohortStart, size, retained[] and rates[], where index 0 is the cohort itself, plus average[] weighted by cohort size. A null means that period has not started yet; it is not zero retention.

The same three questions are available to AI agents over MCP as run_analytics_query, run_funnel_query and run_retention_query.

Device Tokens

POST /v1/device-tokens

Registers or updates an iOS device token for the active workspace and environment.

Example body:

json
{
  "token": "apns_device_token_hex",
  "workspaceId": "workspace_uuid",
  "environmentId": "workspace_uuid_production",
  "platform": "ios"
}

Important behavior:

  • workspaceId must match the authenticated workspace
  • if environmentId is supplied, it must match the authenticated environment
  • platform is currently ios only

API Keys

GET /v1/dashboard/api-keys

Lists the keys for a workspace and environment. Returns keyPrefix, never the key: the server stores a SHA-256 digest and cannot produce the original.

POST /v1/dashboard/api-keys

Creates a key. The response is the only place the raw value ever appears.

Example body:

json
{
  "workspaceId": "workspace_uuid",
  "environmentId": "workspace_uuid_production",
  "type": "mcp",
  "name": "Claude Desktop",
  "scopes": ["events:read", "analytics:read"],
  "expiresInDays": 90
}
Field Notes
environmentId Optional. Defaults to the workspace's default environment.
projectId Optional. Confines the key to one project. Omitted means every project in the environment, which is what every key created before this field is.
type secret (default) for an ingest key, mcp for an AI connector. Decides the key's prefix.
name Human label. label is accepted as the older name for this field.
scopes Optional. Omitted means events:write/sessions:write/devices:write/flags:read when projectId is set, everything for a secret without one, and read plus events:write/sessions:write/rules:write for mcp. Unknown scopes are dropped rather than rejected.
expiresInDays Optional hard expiry. An expired key is rejected exactly like a revoked one.

Keys for a distributed client

A key compiled into a mobile or desktop app is handed to everyone who downloads it, so treat it as public. Give it a projectId and let the scopes default: it can then write its own project's events and read its own project's flags, and read no data at all.

Scopes alone are not enough. They say what a key may do and never whose data it may do it to: before projectId existed, a key made for one app could read every other project in the workspace, which is how a key created for one iOS app returned another app's events from /v1/stats on 2026-08-31.

A bound key behaves like this:

  • an unqualified request resolves to its project, not to the environment's default;
  • ?projectId= naming any other project answers 403 project_forbidden;
  • over MCP it sees its project alone, and list_projects returns only that one.

DELETE /v1/dashboard/api-keys/:id

Soft revocation. The key stops authenticating immediately; the row survives so lastUsedAt and the creation date stay available after a rotation.

Feature Flags

Product guide: Feature Flags.

GET /v1/flags

Hybrid auth, scope flags:read. Every flag of the project, evaluated for one user.

Query Notes
userId Optional. The same id sent with events. Empty or absent means a logged-out visitor: only flags at 100% are on. An id linked with POST /v1/users/link reads as the user it was linked to, and a user already in an A/B test keeps the version recorded for them.
projectId Optional for a key bound to a project; naming another project answers 403 project_forbidden.
expose Optional. false reads without recording an A/B exposure; see POST /v1/flags/:key/exposure.
json
{ "userId": "u_123", "flags": { "new_onboarding": true, "paywall_v2": false } }

Answered with Cache-Control: no-store. Rate limited per key: 300 requests per 15 minutes per userId (per client address without one), and 6000 per 15 minutes per address; past either, 429.

GET /v1/flags/:key

Same auth and query. An unknown key is not an error: { "key": "x", "enabled": false, "exists": false }.

POST /v1/flags/:key/exposure

Same auth, scope and limits. Body { "userId": "..." }. Enters the user in the flag's A/B test with the version the server assigns (partial rollouts only, never hand-picked users, first exposure wins). Answers 200 { "key", "enabled", "exists", "recorded" }; recorded is false when the user was already counted or the flag is not a test, and an unknown key answers exists: false. Use it with expose=false reads when only part of the users belong to the experiment.

Managing flags: /v1/dashboard/flags and /v1/feature-flags

The routes below exist twice with the same bodies and answers: under /v1/dashboard/flags for a signed-in person (any project of their workspaces), and under /v1/feature-flags for a key with flags:manage (its own environment, and only its project when bound to one; projectId may then be left out, it defaults to the key's project).

GET /v1/dashboard/flags?projectId=

Dashboard auth. The flags with their rules (enabled, rolloutPercent, userIds, lastEvaluatedAt).

POST /v1/dashboard/flags

Body: projectId, key, optional description, enabled (default false), rolloutPercent (default 100), userIds. 409 if the project already has the key, 422 limit_reached past 200 flags.

PATCH /v1/dashboard/flags/:id

Any of description, enabled, rolloutPercent, userIds, goalEventType (null clears it). The key cannot change. A changed rolloutPercent stamps rolloutChangedAt. A new share while the flag splits users, or a split switched back on, opens a new phase; stopping the split (100%, 0%, off) sets phaseEndedAt.

GET /v1/dashboard/flags/:id/results

The A/B result. newVersion and oldVersion each carry users, converted and rate; plus relativeLift, probabilityNewBetter, verdict (not_enough_data, new_better, old_better, no_clear_difference), phase (the numbers cover this phase only), startedAt (its first user), endedAt (when the split stopped, goals after it are not counted), isRunning, rolloutChangedDuringTest (deprecated, always false), and goalOptions, the project's event types from the last 30 days. See A/B results.

DELETE /v1/dashboard/flags/:id

204. Code still checking the key reads it as off.

GET /v1/dashboard/flags/prompt?projectId=&flagKey=

The prompt for a coding agent, as { "prompt": "..." }. flagKey adds the task of wiring that one flag.

MCP

POST /mcp

JSON-RPC 2.0 over HTTP, stateless. Authenticated with a lg_mcp_ token as a bearer, or at POST /mcp/k/:token for clients that accept only a URL. Full detail in AI Connectors (MCP).

Example Integration Flow

bash
curl -X POST https://api.logga.sh/v1/events \
  -H "Authorization: Bearer lg_sk_test_1234567890" \
  -H "Content-Type: application/json" \
  -d '{
    "channel": "auth",
    "event": "login_failed",
    "level": "warning",
    "actor": {
      "id": "user_123",
      "email": "[email protected]"
    },
    "metadata": {
      "reason": "invalid_password"
    }
  }'