Progress, XP and levels
Read a player's level, XP, streak and mission progress, and replay retained history after publishing a mission.
Read a player's progress
| Route | Scope | Rate class |
|---|---|---|
GET /v1/gamification/players/:playerId/progress | gamification-progress:read | read |
Set PLAYER_ID to your identifier for the player.
curl "$GAMIFICATION_API_URL/v1/gamification/players/$PLAYER_ID/progress" \
-H "Authorization: Bearer $TANITAR_API_KEY"{
"playerId": "player-2231",
"level": 4,
"experience": 120,
"totalExperience": 1320,
"currentStreak": 3,
"longestStreak": 9,
"lastActivityDate": "2026-09-30",
"curve": {
"revision": 2,
"earnedUnderRevision": 2,
"source": "tenant",
"maxLevel": 30,
"atMaxLevel": false,
"currentLevelXpRequired": 400,
"nextLevelXpRequired": 550,
"thresholds": [{ "level": 1, "xpRequired": 100 }]
},
"missions": [
{
"missionKey": "win-three-ranked",
"currentCount": 2,
"requiredCount": 3,
"status": "active",
"occurrence": 1,
"periodStart": "2026-09-30T00:00:00.000Z",
"completedAt": null
}
]
}A player you have never sent an event for reads as an empty progression (level 1, no XP, no missions); the read creates nothing. The read is always scoped to your tenant, so naming a player that belongs to another organizer returns your own empty record for that identifier, never theirs.
Fields
| Field | Meaning |
|---|---|
level | The player's current level |
experience | XP earned toward leaving the current level |
totalExperience | All XP the player has ever earned |
currentStreak, longestStreak | Consecutive UTC days with at least one mission completion, now and at best |
lastActivityDate | The last UTC day that counted toward the streak |
curve.revision | The level-curve revision in effect now (0 is the default curve) |
curve.earnedUnderRevision | The revision under which the player's current level was reached. It differs from revision after you change the curve; that is expected, because curve changes apply forward only. |
curve.source | pinned (default curve) or tenant (your curve) |
curve.currentLevelXpRequired | The XP needed to leave the current level; null at the top |
curve.nextLevelXpRequired | The price of the level after that: a preview. It is already null one level below the top, so do not read it as "maximum level reached". |
curve.atMaxLevel | true only when the player stands on the highest level the curve reaches |
curve.maxLevel | The highest reachable level, or null for the unbounded default curve |
curve.thresholds | The curve's table; for the default curve, its first 100 levels |
missions[] | The player's progress rows: count, target, status (active, completed, expired), the occurrence number, the UTC period start (or null for missions without a period) and completion time |
XP and levels
XP reaches a player from three places: an xp mission reward, an XP drop from a chest, and an xp
reward on a battle-pass tier. Each grant adds to experience and totalExperience; when
experience passes the current level's price the player levels up, possibly by several levels at
once. Each grant publishes gamification.xp.granted with the amount, the new total, the previous
and new level and the levels gained.
You set the prices with the level curve.
Streaks
A player's streak advances on the first mission completion of each UTC day. Completing a mission on
the next UTC day extends the streak; a gap of two days or more restarts it at 1 on the next
completion. There is no nightly reset: a lapsed streak is noticed when the player next completes a
mission. Each advance publishes gamification.streak.advanced.
Reaching a streak of 3, 7, 14 or 30 days is a milestone, recorded once per player. Each milestone
pays a chest: common at 3 days, rare at 7, epic at 14 and legendary at 30.
Replay a player's history
When you publish a mission after the activity it counts has already happened, replay lets that activity count.
| Route | Scope | Idempotency |
|---|---|---|
POST /v1/gamification/replay | gamification-progress:replay | Idempotency-Key required |
curl -X POST "$GAMIFICATION_API_URL/v1/gamification/replay" -d '{ "playerId": "player-2231" }' \
-H "Authorization: Bearer $TANITAR_API_KEY" \
-H "Idempotency-Key: $(uuidgen)" \
-H "Content-Type: application/json"{ "playerId": "player-2231", "replayed": 500, "completed": 2, "skipped": 0, "nextOffset": 500 }Replay re-evaluates the player's events retained from the last 90 days, oldest first, through the same path as live ingestion, and can complete missions and pay their rewards.
- It is safe to repeat. An event that already counted toward a mission never counts again, so re-running a replay over the same history completes nothing and pays nothing twice.
- It is bounded. One call processes at most 500 events. When
nextOffsetis notnull, call again with"offset": <nextOffset>under a newIdempotency-Keyto continue. skippedcounts events that are not replayable: built-in events, which are consequences of work Tanitar already did.- Tier-restricted missions are not backfilled. A mission with
allowedLevelKeysis never advanced by replay, because the tier a player held when the event happened cannot be reconstructed.
Replay is also the scope that authorizes granting a chest to a player, since both change a player's state directly.