Amba

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

MethodPathDescription
POST/client/eventsTrack one or many events.
POST/client/events/batchDurably keyed mobile batch with per-event acknowledgments.
POST/client/events/explainDry-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

{
  "event": "workout_completed",
  "properties": { "duration_minutes": 30 },
  "timestamp": "2026-04-24T12:00:00Z"
}

Request — batch

{ "events": [{ "event": "…" }, { "event": "…" }] }

Per-event fields (TrackEventInput):

FieldTypeRequiredDescription
eventstringyesEvent name.
propertiesobjectnoArbitrary JSONB properties.
timestampISO-8601noDefaults 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_at is updated fire-and-forget.

Response 200

{ "data": { "tracked": 30 } }

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.

{
  "events": [
    {
      "event": "playback_started",
      "properties": { "episode": "42" },
      "timestamp": "2026-10-07T12:00:00Z",
      "idempotency_key": "evt-123"
    },
    {
      "event": "shelf_impression",
      "timestamp": "2026-10-07T12:00:01Z",
      "idempotency_key": "evt-124",
      "telemetry": true
    }
  ]
}

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.

{
  "data": {
    "accepted": [
      { "idempotency_key": "evt-123", "status": "inserted" },
      { "idempotency_key": "evt-124", "status": "duplicate" }
    ],
    "rejected": []
  }
}

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:

POST/client/events
client auth
curl -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"
}'
Loading auth… Configure auth in the settings drawer (top-right) to run this request.

Curl:

curl -X POST "${BASE_URL}/client/events" \
  -H "X-Api-Key: ${CLIENT_API_KEY}" \
  -H "Authorization: Bearer ${SESSION_TOKEN}" \
  -H 'Content-Type: application/json' \
  -d '{"event":"workout_completed","properties":{"duration_minutes":30},"timestamp":"2026-04-24T12:00:00Z"}'

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_id must be the signed-in user (send their session token). Any other id answers 403 USER_ID_NOT_ALLOWED. Webhook subscriptions are left out of a client-key preview.
  • With a server key, event.user_id can 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

{
  "event": {
    "name": "lesson_completed",
    "properties": { "score": 90 },
    "user_id": "f3b1…"
  }
}
FieldTypeRequiredDescription
event.namestringyesCandidate event name to evaluate.
event.propertiesobjectnoCandidate properties — read by property-scaled grant rules.
event.user_idstringnoApp user to evaluate per-user gating against (the signed-in user with a client key). Omit for the base match.

Response 200

{
  "data": {
    "event": { "name": "lesson_completed", "properties": { "score": 90 }, "user_id": "f3b1…" },
    "matched": [
      { "rule_type": "xp", "rule_id": "…", "name": "Lesson XP", "would": { "xp_delta": 25 } },
      {
        "rule_type": "currency_grant",
        "rule_id": "…",
        "name": "grant coins on lesson_completed",
        "would": { "currency": "coins", "amount": 10 }
      },
      {
        "rule_type": "feed",
        "rule_id": "…",
        "name": "feed item \"completed_lesson\" on lesson_completed",
        "would": { "feed_item": { "action": "completed_lesson", "target_type": "lesson" } }
      },
      {
        "rule_type": "streak",
        "rule_id": "…",
        "name": "Daily Lessons",
        "would": { "period": "daily" }
      },
      {
        "rule_type": "webhook",
        "rule_id": "…",
        "name": "webhook → https://hooks.example.com",
        "would": { "webhook_delivery": { "matched_via": "exact" } }
      }
    ],
    "evaluated_surfaces": ["xp", "currency_grant", "feed", "streak", "webhook"]
  }
}

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 the event object.
  • 400 INVALID_EVENT_NAME — event.name is 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_id is not the signed-in user.
  • 500 EXPLAIN_FAILED.

Curl:

curl -X POST "${BASE_URL}/client/events/explain" \
  -H "X-Api-Key: ${CLIENT_API_KEY}" \
  -H 'Content-Type: application/json' \
  -d '{"event":{"name":"lesson_completed","properties":{"score":90}}}'

On this page