Events and Integration
This guide explains how to send gameplay or engagement events into Phoenix so they can be scored against active leaderboards. It covers the ingestion pipeline, payload schema, authentication, retries, and best practices for keeping data consistent at scale.
Events and Integration
This guide explains how to send gameplay or engagement events into Phoenix so they can be scored against active leaderboards. It covers the ingestion pipeline, payload schema, authentication, retries, and best practices for keeping data consistent at scale.
1. Pipeline Overview
- Client or game backend sends events (with JWT in Authorization header) to the Ingestion Gateway.
- Ingestion Gateway validates the JWT and request body (JSON schema), persists it to Phoenix's event log, and acknowledges once the event is durable.
- Leaderboard Engine consumes the log, evaluates events against each active leaderboard for the tenant, and updates the live standings store plus longer-term history.
- Query & WebSocket Gateways expose the resulting standings to your applications.
Because the event log deduplicates on event_id, you can retry failed HTTP requests safely—duplicates within the configured window (default 300 s) are ignored.
2. Endpoint Summary
| Method | Path | Description | Auth | Body Limit | Notes |
|---|---|---|---|---|---|
| POST | /v1/events | Ingest a single event | JWT (Bearer) | 1 MB | event_id must be unique per tenant |
| POST | /v1/events/bulk | Ingest up to 100 events in one call | JWT (Bearer) | 1 MB | Fails fast if list is empty or >100 |
| GET | /health, /ready | Health/readiness status | None | — | Useful for deployment checks |
All ingestion endpoints are scoped per tenant. The gateway overrides any tenant_id field in the payload with the tenant ID derived from your JWT (e.g. Zitadel org or platform gamer token), preventing cross-tenant injection.
3. Authentication
Ingestion uses JWT (Bearer token) only:
- Obtain a JWT that encodes your tenant (e.g. from your IdP or the platform gamer-token endpoint). The gateway accepts either Zitadel-style JWTs (tenant resolved from
org_idclaim) or platform gamer tokens. - Send the token on every request:
Authorization: Bearer <JWT>Important: The gateway enforces a 1 MB body limit and rejects malformed UTF-8. Tenant is derived from the JWT; you can send a placeholder
tenant_idin the event body—it will be overwritten.
4. Event Schema (IngestionEventV1)
Phoenix provides a stable event contract for all ingestion calls. Required fields:
| Field | Type | Description |
|---|---|---|
tenant_id | string | Automatically set by the gateway; include any placeholder value on your side. |
event_id | string | Globally unique ID per event (UUID recommended). Used for deduplication and idempotency. |
type | string | Logical event type, e.g., match.completed, purchase.settled, xp.granted. |
actor.user_id | string | The player/account receiving points on leaderboards. |
subject | object (optional) | { "type": "quest", "id": "quest-77", ... } describing the entity involved. |
occurred_at | string (ISO 8601) | When the event actually happened; cannot be more than 1 hour in the future. |
attrs | object | Arbitrary key/value bag used by scopes, conditions, and scoring expressions. |
Sample payload:
{
"tenant_id": "placeholder",
"event_id": "evt_01JBQ56ZGTKNC3XN8R8KZZR4N5",
"type": "match.completed",
"actor": {
"user_id": "user-123",
"metadata": {
"region": "na-east",
"platform": "ios"
}
},
"subject": {
"type": "match",
"id": "match-987"
},
"occurred_at": "2025-11-18T12:34:56Z",
"attrs": {
"score": 1550,
"duration_seconds": 480,
"victory": true
}
}Best practices:
- Event IDs: Use ULIDs/UUIDs to guarantee uniqueness; duplicates detected within the dedupe window are dropped automatically.
- Occurred at: Send the real event time so Phoenix can place scores into the correct hourly/daily windows.
- Attributes: Keep attribute names short but descriptive (
score,kills,coins_spent)—they are referenced in leaderboard scoring expressions (attrs.score * 0.1,attrs.victoryetc.).
5. How Events Drive Leaderboards
Behind the scenes Phoenix applies every event to the leaderboards you configure:
- Each leaderboard declares scoring rules with:
event_type(exact match or*wildcard).- Optional
conditionsusing attribute paths (e.g.,{"attrs.mode": {"eq": "ranked"}}). points_expressionthat can referenceattrs.*, do arithmetic, and callmin,max,abs.
- When an ingested event matches a rule, the resulting points are applied to the user for the relevant time window.
- Aggregation (
sum,count,max,min,avg) determines how multiple events combine within a window. - Scope filters (configured per leaderboard) can inspect any field on the event JSON before scoring.
Implication: Make sure every attribute your scoring/filters reference is present in attrs or the standard fields above—otherwise events will be ignored silently (no rule match).
6. Bulk Ingestion
POST /v1/events/bulk accepts:
{
"events": [ { ...IngestionEventV1... }, ... ]
}Constraints enforced by the gateway:
- Minimum 1 event, maximum 100 events per request.
- Total body (after JSON encoding) must be ≤ 1 MB.
- Each event is validated independently; the response returns per-event status:
{
"status": "partial",
"total": 3,
"accepted": 2,
"failed": 1,
"results": [
{ "event_id": "evt_1", "status": "accepted" },
{
"event_id": "evt_2",
"status": "failed",
"error": "Validation failed: event_type cannot be empty"
},
{ "event_id": "evt_3", "status": "accepted" }
]
}Retry only the failed events; re-sending accepted ones with the same event_id will be deduplicated but wastes bandwidth.
7. Retry, Ordering, and Idempotency
- HTTP retries: Safe as long as you reuse the same
event_id. The event log ignores duplicates within the configured 300 s dedupe window. - Ordering: Events are processed roughly in publish order per tenant, but you should not rely on strict ordering; leaderboard windows are keyed by
occurred_at. - Clock skew: Ensure JWT expiration and system time are aligned; expired tokens are rejected.
- Idempotent consumer: The leaderboard service also double-checks window/timestamps, so late events may still land in the correct historical window as long as it is still open.
8. Error Handling
| HTTP Status | Code | Meaning / Resolution |
|---|---|---|
| 400 | VALIDATION_ERROR | Malformed JSON, missing fields, batch size >100, occurred_at too far in future, etc. Fix payload. |
| 401 | UNAUTHORIZED | JWT missing or invalid. Ensure Authorization: Bearer <JWT> is sent and the token encodes the correct tenant. |
| 403 | FORBIDDEN | Tenant is inactive/suspended. Contact Phoenix support. |
| 500 | INTERNAL_ERROR | Transient ingestion/storage issue. Wait and retry. |
Responses always follow:
{
"error": {
"code": "VALIDATION_ERROR",
"message": "Bulk request can contain maximum 100 events",
"status": 400
}
}Log both the HTTP status and error.code in your observability stack for faster debugging.
9. Monitoring & Observability Hooks
GET /health– basic liveness (service started).GET /ready– confirms the gateway can persist incoming events; returns503if downstream persistence is unavailable.- Emit logs/metrics on your side for each
event_idsent plus the HTTP status; this simplifies reconciliation with Phoenix support.
10. Checklist Before Moving On
- Generated HMAC signatures successfully in your language of choice.
- Ingested at least one single event (
202 Accepted). - Ingested a bulk batch and handled per-event failures.
- Verified the leaderboard you care about updates after sending events.
Once you have dependable ingestion, continue to Leaderboard Overview to understand how scopes, timeframes, scoring, and ranking settings interact with the events you just wired up.