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
| 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. |
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. |
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.
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.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. |
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.
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.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: