Amba

Events

Project-wide engagement event log tail, aggregate counts, and server-side ingest.

These endpoints expose the raw engagement-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.

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-8601Upper bound on occurred_at.
event_namestringFilter to a specific event_name.
user_iduuidFilter to a single app_user.
limitint100Page size, capped at 1000.
cursorstringOpaque cursor returned as next_cursor on a previous page.

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.

Errors

  • 400 INVALID_SINCE / INVALID_UNTIL — unparseable timestamps.
  • 400 INVALID_LIMITlimit is not a positive integer.
  • 400 INVALID_CURSOR — cursor is malformed.
  • 400 INVALID_FILTERuser_id is not a UUID.
  • 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/{projectId}/events' \
  -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.

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.

Errors

  • 400 MISSING_SINCEsince 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_RANGEuntil is before since.
  • 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/{projectId}/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/{projectId}/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/{projectId}/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" }
    ]
  }'