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 |
devices:write |
/v1/device-tokens |
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:
{
"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:
{
"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:
Authorization: Bearer lg_sk_xxxx
Content-Type: application/jsonExample body:
{
"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:
channelis requiredeventis requiredleveldefaults toinfometadatais capped at 10KBidempotencyKeyis optional but globally unique when providedactor.idis preferred overuserIdwhen both are sentactor.propertiesvalues 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.propertiesis a patch: sent keys are written, missing keys are kept,nullremoves a key. Properties that break the limits, or would take the user past 50 keys, are not stored; the event still is, and the201body carrieswarnings: ["actor.properties ignored: ..."]
GET /v1/events
Supported query filters include:
channelleveluserIdsessionIdfromtolimitcursortagsprojectId
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:
{
"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:
channelIdisActive- cursor pagination fields
POST /v1/rules
Creates an alert rule.
Example body:
{
"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:
{
"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.
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: currentlylast_seen_desc
Response shape:
{
"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:
{
"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,emailmust parse as email,avatarUrlmust parse as URL,role≤128,label≤255propertieskeys ≤64 chars, values must be strings/numbers/booleans/null, string values ≤1024 chars, max 50 keys- empty strings are normalized to
nullso the editor can clear a field propertieshere 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):
{
"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.
{
"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.
{
"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:
{
"token": "apns_device_token_hex",
"workspaceId": "workspace_uuid",
"environmentId": "workspace_uuid_production",
"platform": "ios"
}Important behavior:
workspaceIdmust match the authenticated workspace- if
environmentIdis supplied, it must match the authenticated environment platformis currentlyiosonly
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:
{
"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 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 nothing 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 answers403 project_forbidden;- over MCP it sees its project alone, and
list_projectsreturns 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.
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
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"
}
}'