Amba

Events

Project-wide event log tail (engagement and telemetry events), aggregate counts, and server-side ingest.

These endpoints expose the raw event log — the source of truth for everything Amba.events.track() writes. The list endpoint is a cursor-paginated tail keyed on (occurred_at, id) so deep pages stay fast even with millions of rows; the count endpoint aggregates server-side and refuses ranges greater than 90 days. The POST endpoint emits events on behalf of any user (high-trust events that the device should not be able to forge) with optional caller-supplied idempotency.

Engagement and telemetry events

Amba.events.track() stores an event in one of two places:

  • Engagement events — the default. They drive segments, XP, streaks, currency grants, push, and webhooks.
  • Telemetry events — events tracked with { telemetry: true }, such as page views, funnel steps, and UI breadcrumbs. They are stored and billed at the telemetry rate and trigger no rules.

Deleting an app user deletes its engagement events and its telemetry events. After the delete, none of the user's events appear in event lists, counts, analytics, funnels, or exports, whichever source you read.

The list and count endpoints read engagement events by default. Pass source to choose:

sourceReads
engagementEngagement events (the default).
telemetryTelemetry events.
allBoth, under the same filters. List rows carry source; counts add by_source.

The same source parameter works on GET /admin/projects/:projectId/analytics and on the funnel query. Over MCP, pass source: "telemetry" to amba_events_list, amba_events_count, amba_analytics_get, or amba_funnels_query. From the CLI, use amba analytics export --type=events --source=telemetry or amba logs tail --source=telemetry.

Endpoints

MethodPathDescription
GET/admin/projects/:projectId/eventsCursor-paginated event log tail, newest first.
GET/admin/projects/:projectId/events/countAggregate event counts with optional buckets.
POST/admin/projects/:projectId/eventsServer-side event ingest with optional event_id idempotency.

GET /admin/projects/:projectId/events

Cursor pagination on (occurred_at, id) — pass next_cursor from a previous page back as cursor. The cursor is base64url-encoded JSON { ts, id }.

since defaults to 24 hours ago when omitted; the default protects the API from a query that scans the full table on an unindexed range.

Query

ParamTypeDefaultDescription
sinceISO-8601now − 24hLower bound on occurred_at.
untilISO-8601—Upper bound on occurred_at.
event_namestring—Filter to a specific event_name.
exclude_event_namestring—Leave out one event_name.
user_iduuid—Filter to a single app_user.
limitint100Page size, capped at 1000.
cursorstring—Opaque cursor returned as next_cursor on a previous page.
sourceengagement | telemetry | allengagementWhich events to read. See above.

Response 200

{
  "data": [
    {
      "id": "…",
      "app_user_id": "…",
      "event_name": "workout_completed",
      "properties": { "duration_min": 30 },
      "occurred_at": "2026-04-23T14:02:11.000Z"
    }
  ],
  "next_cursor": "eyJ0cyI6IjIwMjYtMDQtMjNUMTQ6MDI6MTEuMDAwWiIsImlkIjoiLi4uIn0"
}

next_cursor is null when the page is the last one. When the page is full (rows.length === limit), the API computes a cursor from the last row.

Response 200 (source=telemetry or source=all)

Each row also carries source, naming where the event was stored. With all, rows from both stores interleave newest first, and one cursor pages through both.

{
  "data": [
    {
      "id": "…",
      "app_user_id": "…",
      "event_name": "quiz_started",
      "properties": { "quiz": "intake" },
      "occurred_at": "2026-10-06T14:02:11.000Z",
      "source": "telemetry"
    },
    {
      "id": "…",
      "app_user_id": "…",
      "event_name": "signup_completed",
      "properties": {},
      "occurred_at": "2026-10-06T14:01:58.000Z",
      "source": "engagement"
    }
  ],
  "next_cursor": null
}

Errors

  • 400 INVALID_SINCE / INVALID_UNTIL — unparseable timestamps.
  • 400 INVALID_LIMIT — limit is not a positive integer.
  • 400 INVALID_CURSOR — cursor is malformed.
  • 400 INVALID_FILTER — user_id is not a UUID.
  • 400 INVALID_SOURCE — source is something other than engagement, telemetry, or all.
  • 500 LIST_FAILED.

Try it:

GET/admin/projects/%7B%7BprojectId%7D%7D/events
developer auth
curl -X GET 'https://api.amba.dev/v1/admin/projects/%7B%7BprojectId%7D%7D/events'
Loading auth… Configure auth in the settings drawer (top-right) to run this request.

Curl:

curl -X GET "${BASE_URL}/admin/projects/${PROJECT_ID}/events" \
  -H "Authorization: Bearer ${DEV_TOKEN}"
 
# Telemetry events from the last 7 days
curl -X GET "${BASE_URL}/admin/projects/${PROJECT_ID}/events?source=telemetry&since=2026-09-29T00:00:00Z" \
  -H "Authorization: Bearer ${DEV_TOKEN}"

GET /admin/projects/:projectId/events/count

Aggregate counts over a time window. since is required; the API refuses ranges greater than 90 days because a full-table COUNT(*) on a high-volume tenant can be expensive. Optional group_by returns per-day or per-event_name buckets in addition to the total.

Query

ParamTypeRequiredDescription
sinceISO-8601yesLower bound on occurred_at.
untilISO-8601noUpper bound on occurred_at. Defaults to now.
event_namestringnoFilter to a specific event_name.
group_byday | event_namenoBucket results by calendar day (UTC) or event_name. Omit for total only.
sourceengagement | telemetry | allnoWhich events to count. Defaults to engagement.

Response 200 (no group_by)

{ "data": { "total": 1234567 } }

Response 200 (group_by=day)

{
  "data": {
    "total": 1234567,
    "buckets": [
      { "key": "2026-04-22", "count": 412345 },
      { "key": "2026-04-23", "count": 822222 }
    ]
  }
}

Response 200 (group_by=event_name)

{
  "data": {
    "total": 1234567,
    "buckets": [
      { "key": "session_started", "count": 600000 },
      { "key": "workout_completed", "count": 420000 },
      { "key": "achievement_unlocked", "count": 214567 }
    ]
  }
}

Buckets are ordered ascending by key for group_by=day and descending by count (then key ascending) for group_by=event_name.

Response 200 (source=all)

total covers both stores, by_source splits it, and buckets merge both stores into one bucket per key.

{
  "data": {
    "total": 5210,
    "by_source": { "engagement": 410, "telemetry": 4800 }
  }
}

Errors

  • 400 MISSING_SINCE — since is required.
  • 400 INVALID_SINCE / INVALID_UNTIL — unparseable timestamps.
  • 400 RANGE_TOO_LARGE — window exceeds 90 days. error.details includes max_days and requested_days.
  • 400 INVALID_RANGE — until is before since.
  • 400 INVALID_SOURCE — source is something other than engagement, telemetry, or all.
  • 500 COUNT_FAILED.

Try it:

GET/admin/projects/%7B%7BprojectId%7D%7D/events/count
developer auth
curl -X GET 'https://api.amba.dev/v1/admin/projects/%7B%7BprojectId%7D%7D/events/count'
Loading auth… Configure auth in the settings drawer (top-right) to run this request.

Curl:

curl -X GET "${BASE_URL}/admin/projects/${PROJECT_ID}/events/count" \
  -H "Authorization: Bearer ${DEV_TOKEN}"

POST /admin/projects/:projectId/events

Server-side event ingest. The client-plane equivalent (POST /client/events) takes the app_user_id from the inbound session token; this admin variant takes it from the request body so a partner backend can emit events on behalf of any user — required for high-trust events (mission_completed, xp_awarded) that the device must not be able to forge.

Accepts a single event or a batch of up to 500 events per request. The batch is committed in a single transaction — if any slot fails validation or write, the whole request rolls back.

Idempotency (event_id)

Pass an optional caller-supplied event_id per event. If supplied, the API guarantees that re-POSTing an event with the same event_id is a silent no-op: the second call returns inserted: 0, skipped: 1 and no new event is recorded. Streak / XP / badge fan-out only fires for events that actually inserted, so a network-retry loop credits each event exactly once.

Per-project isolation is automatic — the same event_id reused across two projects never collides. Events submitted without an event_id are unconstrained (legacy / SDK calls keep working).

event_id is a free-form non-empty string of at most 255 characters. Common patterns: a partner-generated UUID, a hash of (user_id, event_name, server_timestamp), or a webhook delivery id.

Body — single event

{
  "app_user_id": "5d0a…",
  "event_name": "mission_completed",
  "properties": { "xp": 100, "mission_id": "m1" },
  "occurred_at": "2026-05-03T14:00:00Z",
  "event_id": "partner-evt-abc-123"
}

Body — batch

{
  "events": [
    { "app_user_id": "5d0a…", "event_name": "mission_completed", "event_id": "evt-1" },
    { "app_user_id": "5d0a…", "event_name": "level_up", "event_id": "evt-2" },
    { "app_user_id": "9b22…", "event_name": "mission_completed", "event_id": "evt-3" }
  ]
}
FieldTypeRequiredDescription
app_user_iduuidyesTarget user inside the project's your database.
event_namestringyesNon-empty event name.
propertiesobjectnoArbitrary JSON properties bag (defaults to {}).
occurred_atISO-8601noDefaults to server now().
event_idstringnoIdempotency key. ≤ 255 chars. Re-POSTing same id is a no-op.

Response 200

{
  "data": {
    "inserted": 2,
    "skipped": 1,
    "tracked": 2
  }
}

tracked mirrors inserted and is retained for backwards compatibility with callers written against the pre-idempotency response shape.

Errors

  • 400 INVALID_JSON — body is not valid JSON.
  • 400 INVALID_EVENT — a slot is not an object.
  • 400 INVALID_APP_USER_ID — missing or non-UUID app_user_id.
  • 400 INVALID_EVENT_NAME — missing or empty event_name.
  • 400 INVALID_OCCURRED_AT — unparseable timestamp.
  • 400 INVALID_EVENT_ID — empty string or longer than 255 characters.
  • 413 BATCH_TOO_LARGE — more than 500 events in one request.
  • 500 TRACK_FAILED — the transaction rolled back; safe to retry.

Try it:

POST/admin/projects/%7B%7BprojectId%7D%7D/events
developer auth
curl -X POST 'https://api.amba.dev/v1/admin/projects/%7B%7BprojectId%7D%7D/events' \
  -H 'Content-Type: application/json' \
  -d '{
  "app_user_id": "00000000-0000-0000-0000-000000000000",
  "event_name": "mission_completed",
  "properties": {
    "xp": 100
  },
  "event_id": "partner-evt-001"
}'
Loading auth… Configure auth in the settings drawer (top-right) to run this request.

Curl — single event with idempotency key:

curl -X POST "${BASE_URL}/admin/projects/${PROJECT_ID}/events" \
  -H "Authorization: Bearer ${DEV_TOKEN}" \
  -H 'Content-Type: application/json' \
  -d '{
    "app_user_id": "5d0a…",
    "event_name": "mission_completed",
    "properties": { "xp": 100 },
    "event_id": "partner-evt-abc-123"
  }'

Curl — batch:

curl -X POST "${BASE_URL}/admin/projects/${PROJECT_ID}/events" \
  -H "Authorization: Bearer ${DEV_TOKEN}" \
  -H 'Content-Type: application/json' \
  -d '{
    "events": [
      { "app_user_id": "5d0a…", "event_name": "mission_completed", "event_id": "evt-1" },
      { "app_user_id": "5d0a…", "event_name": "level_up",          "event_id": "evt-2" }
    ]
  }'