Events
Track engagement events — the primary write path for Amba.events.track().
By default, each Amba.events.track() call sends a POST here. React Native and Expo can opt into a durable event queue that sends keyed batches to /client/events/batch. Events drive streak qualification and XP award evaluation server-side.
Endpoints
| Method | Path | Description |
|---|---|---|
| POST | /client/events | Track one or many events. |
| POST | /client/events/batch | Durably keyed mobile batch with per-event acknowledgments. |
| 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(). |
Reserved event names
Some names belong to events Amba records itself, and a client key can't track
them: the ai. prefix (AI usage), xp_awarded, streak_shielded,
league_promoted, league_demoted, and every name in the platform event
catalog (subscription.started, economy.currency.granted, ...; list them with
GET /v1/admin/projects/:projectId/webhooks/catalog or the
amba_events_catalog MCP tool). Matching ignores case. A request that uses one
answers 400 RESERVED_EVENT_NAME and writes nothing; on the batch route only
that event is rejected. A server key (with X-On-Behalf-Of) and the admin
events route can record these names.
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 } }.
The single-event route also accepts an optional idempotency_key and timestamp together. This is used by a queued mobile event when its runtime kill switch selects the single-event transport. A retried key returns tracked: 0 and does not repeat engagement effects. Unkeyed callers retain the legacy behavior.
POST /client/events/batch
Requires the same client API key and user session as /client/events. The body is limited to 500 events and 512 KB. Each event needs a nonempty event, ISO-8601 timestamp, and unique idempotency_key of at most 255 characters. properties and telemetry are optional.
The response acknowledges every submitted key. duplicate means the event was accepted earlier, so the client may safely remove it from its queue. Invalid events are rejected individually; a transient request failure should be retried with the original keys.
occurred_at records the supplied client timestamp; received_at records server receipt time. Both are available in admin event listings. Telemetry's stored ingested_at is exposed there as received_at.
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 an API key; a signed-in user is optional. Omit event.user_id for the user-independent base match. To also preview per-user gating (daily limits, cooldowns, balance caps), pass event.user_id:
- With a client key,
event.user_idmust be the signed-in user (send their session token). Any other id answers403 USER_ID_NOT_ALLOWED. Webhook subscriptions are left out of a client-key preview. - With a server key,
event.user_idcan be any user in the project, and the preview includes webhook deliveries, each destination shown as its scheme and host.
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 (the signed-in user with a client key). Omit for the base match. |
Response 200
The webhook entry appears only for a server key. 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.403 USER_ID_NOT_ALLOWED— with a client key,event.user_idis not the signed-in user.500 EXPLAIN_FAILED.
Curl: