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:
source | Reads |
|---|---|
engagement | Engagement events (the default). |
telemetry | Telemetry events. |
all | Both, 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
| Method | Path | Description |
|---|---|---|
| GET | /admin/projects/:projectId/events | Cursor-paginated event log tail, newest first. |
| GET | /admin/projects/:projectId/events/count | Aggregate event counts with optional buckets. |
| POST | /admin/projects/:projectId/events | Server-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
| Param | Type | Default | Description |
|---|---|---|---|
since | ISO-8601 | now − 24h | Lower bound on occurred_at. |
until | ISO-8601 | — | Upper bound on occurred_at. |
event_name | string | — | Filter to a specific event_name. |
exclude_event_name | string | — | Leave out one event_name. |
user_id | uuid | — | Filter to a single app_user. |
limit | int | 100 | Page size, capped at 1000. |
cursor | string | — | Opaque cursor returned as next_cursor on a previous page. |
source | engagement | telemetry | all | engagement | Which events to read. See above. |
Response 200
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.
Errors
400 INVALID_SINCE/INVALID_UNTIL— unparseable timestamps.400 INVALID_LIMIT—limitis not a positive integer.400 INVALID_CURSOR— cursor is malformed.400 INVALID_FILTER—user_idis not a UUID.400 INVALID_SOURCE—sourceis something other thanengagement,telemetry, orall.500 LIST_FAILED.
Try it:
/admin/projects/%7B%7BprojectId%7D%7D/eventscurl -X GET 'https://api.amba.dev/v1/admin/projects/%7B%7BprojectId%7D%7D/events'Curl:
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
| Param | Type | Required | Description |
|---|---|---|---|
since | ISO-8601 | yes | Lower bound on occurred_at. |
until | ISO-8601 | no | Upper bound on occurred_at. Defaults to now. |
event_name | string | no | Filter to a specific event_name. |
group_by | day | event_name | no | Bucket results by calendar day (UTC) or event_name. Omit for total only. |
source | engagement | telemetry | all | no | Which events to count. Defaults to engagement. |
Response 200 (no group_by)
Response 200 (group_by=day)
Response 200 (group_by=event_name)
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.
Errors
400 MISSING_SINCE—sinceis required.400 INVALID_SINCE/INVALID_UNTIL— unparseable timestamps.400 RANGE_TOO_LARGE— window exceeds 90 days.error.detailsincludesmax_daysandrequested_days.400 INVALID_RANGE—untilis beforesince.400 INVALID_SOURCE—sourceis something other thanengagement,telemetry, orall.500 COUNT_FAILED.
Try it:
/admin/projects/%7B%7BprojectId%7D%7D/events/countcurl -X GET 'https://api.amba.dev/v1/admin/projects/%7B%7BprojectId%7D%7D/events/count'Curl:
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
Body — batch
| Field | Type | Required | Description |
|---|---|---|---|
app_user_id | uuid | yes | Target user inside the project's your database. |
event_name | string | yes | Non-empty event name. |
properties | object | no | Arbitrary JSON properties bag (defaults to {}). |
occurred_at | ISO-8601 | no | Defaults to server now(). |
event_id | string | no | Idempotency key. ≤ 255 chars. Re-POSTing same id is a no-op. |
Response 200
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-UUIDapp_user_id.400 INVALID_EVENT_NAME— missing or emptyevent_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:
/admin/projects/%7B%7BprojectId%7D%7D/eventscurl -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"
}'Curl — single event with idempotency key:
Curl — batch: