Events
Track engagement events — the primary write path for Amba.events.track().
Every Amba.events.track() call sends a POST here. Events drive streak qualification and XP award evaluation server-side; the SDK additionally forwards events to first-party analytics client-side, so this endpoint never double-writes to analytics.
Endpoints
| Method | Path | Description |
|---|---|---|
| POST | /client/events | Track one or many events. |
| POST | /client/events/explain | Dry-run an event: preview which rules would fire. No side effect. |
POST /client/events
Accepts either a single event or a { events: […] } batch.
Request — single event
Request — batch
Per-event fields (TrackEventInput):
| Field | Type | Required | Description |
|---|---|---|---|
event | string | yes | Event name. |
properties | object | no | Arbitrary JSONB properties. |
timestamp | ISO-8601 | no | Defaults to now(). |
Limits
- Max batch size: 500 events per request. Exceeding returns
413 BATCH_TOO_LARGE. - Streak qualification is checked asynchronously after the write — it cannot fail the request.
- The user's
last_seen_atis updated fire-and-forget.
Response 200
Empty batch returns { "data": { "tracked": 0 } }.
Errors
413 BATCH_TOO_LARGE— batch exceeds 500 events.500 TRACK_FAILED.
Try it:
/client/eventscurl -X POST 'https://api.amba.dev/v1/client/events' \
-H 'Content-Type: application/json' \
-d '{
"event": "workout_completed",
"properties": {
"duration_minutes": 30
},
"timestamp": "2026-04-24T12:00:00Z"
}'Curl:
POST /client/events/explain
Dry-run an event against the rule engine and get back which rules would fire and what each would produce — XP awards, currency grants, feed items, streak qualification, and webhook deliveries — without committing any side effect. Use it to confirm your rules are wired the way you intend before sending the real track call.
This route needs only the client API key (no signed-in user). Supply the candidate user as event.user_id to also preview per-user gating (daily limits, cooldowns, balance caps); omit it for the user-independent base match. Nothing is written either way.
Request
| Field | Type | Required | Description |
|---|---|---|---|
event.name | string | yes | Candidate event name to evaluate. |
event.properties | object | no | Candidate properties — read by property-scaled grant rules. |
event.user_id | string | no | App user to evaluate per-user gating against. Omit for the base match. |
Response 200
Each matched entry may carry a note when the effect matched but a per-user gate (daily limit, cooldown, balance cap) would currently suppress or reduce it. When nothing matches, matched is empty and unmatched_reason_samples explains why.
Errors
400 INVALID_BODY— body is not JSON or omits theeventobject.400 INVALID_EVENT_NAME—event.nameis missing or empty.400 INVALID_PROPERTIES/400 INVALID_USER_ID— wrong type for those fields.500 EXPLAIN_FAILED.
Curl: