Orishare
Concepts

Leagues

Weekly or daily leagues: tiers, cohorts, promotion and demotion, rank rewards in currency or items, standings, settlement and the league events.

A league puts players who are active in the same week (or day) into small groups, a cohort of up to 100, ranks them by score, and at the end of the period moves the top of each cohort up a tier and the bottom down. Rank rewards and a promotion reward pay out automatically. It is the Duolingo league: you join a group when you first play that week.

Operator routes need programs:read to read and programs:write to change. In the dashboard, leagues are under Engagement.

Configuration (league/v1)

{
  "key": "weekly",
  "name": "Weekly league",
  "config": {
    "schema": "league/v1",
    "tiers": [
      {
        "key": "bronze",
        "name": "Bronze",
        "promote_count": 10,
        "rewards": [{ "rank_from": 1, "rank_to": 3, "currency": "gems", "amount": 50 }]
      },
      {
        "key": "silver",
        "name": "Silver",
        "promote_count": 7,
        "demote_count": 5,
        "rewards": [
          {
            "rank_from": 1,
            "rank_to": 1,
            "reward": [
              { "currency": "gems", "amount": 100 },
              { "type": "item", "item": "golden_frame", "quantity": 1 }
            ]
          }
        ],
        "promotion_reward": { "type": "item", "item": "streak_freeze", "quantity": 1 }
      },
      { "key": "gold", "name": "Gold", "demote_count": 5 }
    ],
    "cohort_size": 30,
    "period": { "kind": "week", "start_weekday": 1, "start_minute": 0, "timezone": "environment" },
    "score": { "source": "direct" },
    "matching": "none",
    "settle_delay_minutes": 10,
    "min_score_to_promote": 1
  }
}
FieldMeaning
tiers1 to 20, lowest first. promote_count (top N move up) and demote_count (bottom N move down); the top tier never promotes and the bottom tier never demotes. promote_count + demote_count is at most cohort_size.
tiers[].rewardsUp to 20 rank bands that do not overlap. A band pays { currency, amount }, or a reward list of up to 5 entries mixing currency and items.
promotion_rewardOptional, paid on promotion: one entry or a list of up to 5.
cohort_size5 to 100 players per cohort.
periodkind week or day; start_weekday (ISO, 1 is Monday) and start_minute in local time; timezone an IANA zone or environment. Boundaries are correct across daylight-saving changes.
score{ "source": "direct" } (your backend or a rule contributes) or { "source": "leaderboard", "program_id": "prg_..." } (the sum of the player's valid contributions to that leaderboard program in the period).
matchingnone, or timezone to fill cohorts per 6 hour UTC offset band.
settle_delay_minutes0 to 1440 minutes after the period ends before it settles; late scores inside this window still count.
min_score_to_promoteA player in the promotion zone moves up only with at least this score.

Reward entries. Every reward list in engagement (league bands and promotion rewards, friend quests, friend streak milestones, team quests) takes the same two shapes: a currency entry { "currency": "gems", "amount": 50 } and an item entry { "type": "item", "item": "<item key>", "quantity": 1 } (at most 5 entries per list). An item entry is granted from the environment's item catalogue in the same transaction as the currency entry beside it, with the same idempotency key, so a retry never grants twice. An item that does not exist yet or is archived keeps the reward pending (item_not_found, item_archived) until the item is fixed; an inventory that is full grants what fits.

Lifecycle

POST /v1/leagues creates a league as a draft (at most 20 per environment). Then POST /v1/leagues/{id}/activate, POST /v1/leagues/{id}/pause, POST /v1/leagues/{id}/resume and POST /v1/leagues/{id}/archive, each with { "expected_revision": n } and an Idempotency-Key. PATCH /v1/leagues/{id} with If-Match changes the name or configuration; a new configuration applies from the next period, because each period pins the configuration it started with. On activation every named currency must exist, and for the leaderboard source the program must be a leaderboard.

In a live environment, an agent without auto autonomy proposes the same changes as change-set commands: league.create (the create body), league.update (key, expected_version and the PATCH fields), and league.activate, league.pause, league.resume, league.archive (each key and expected_version). Archiving is irreversible. See Change sets.

Pausing a league freezes it. While it is paused no score is added (direct scores are refused with league_not_active, and leaderboard contributions made during the pause never count), no new period opens, and the current period does not settle; standings stay as they are. A period that had already ended before the pause still settles and pays its rewards. When you resume, the current period's end moves later by exactly the time the league was paused, so players get the time they had left, and the next period starts where it ends.

Scoring

With the direct source, your backend adds score with POST /v1/leagues/{id}/contributions:

{ "customer_id": "cust_...", "amount": 25, "dedupe_key": "lesson:8812" }

The dedupe_key makes a repeat count once (a repeat under a new Idempotency-Key answers replayed: true; a retry with the same Idempotency-Key gets the stored first answer, with the Idempotent-Replayed: true header). amount may be negative; the score never goes below 0. The first contribution of a period creates the player's entry and puts them in a cohort of their tier. A league that is not active, and a league scored from a leaderboard, refuse direct contributions with invalid_state.

Anonymous customers take part in leagues under a pseudonym. Players are shown by their social profile display name (PATCH /v1/customer/social_profile) or, without one, a stable pseudonym such as "Player 4821".

Settlement

Some minutes after a period ends (settle_delay_minutes), each cohort is ranked by score, then by who reached it first. The top promote_count move up (if their score reaches min_score_to_promote), the bottom demote_count move down, everyone else stays. Each entry records final_rank, outcome, the reward it earned and its reward_status; rewards are granted exactly once. A cohort that never filled demotes nobody above its last real rank. Merged customers rank once, as one player, with their summed score.

Reads

ReadWhoReturns
GET /v1/customer/leaguesCustomer sessionPer active league: tier, current period, time left, the entry (rank, score, zone) and the last result
GET /v1/customer/leagues/{id}/standingsCustomer sessionThe player's cohort, by display name only, with zone (promotion, safe, demotion)
GET /v1/customer/leagues/{id}/friend_standingsCustomer sessionThe player and their friends across cohorts
GET /v1/leagues/{id}/periodsOperatorPeriods with their pinned configuration
GET /v1/league_periods/{id}/cohortsOperatorCohorts of a period
GET /v1/league_cohorts/{id}/standingsOperatorA cohort's standings, with customer ids
GET /v1/leagues/{id}/membersOperatorMembers and their tiers
GET /v1/customers/{id}/leaguesOperatorOne customer's leagues

POST /v1/leagues/{id}/members with { "customer_id", "status": "left" } removes a player from future periods (an entry they already have this period still settles).

Events and notifications

Settlement and joining emit league.period_started, league.promoted and league.demoted (data league_name, tier, previous_tier, rank), and reward.ready for a reward earned without a promotion. Each is a webhook event and can become a message: see Notifications. The hosted widget's league surface shows the tier, the cohort standings and the time left (Hosted widget).

A complete setup is in the recipe Weekly league with item rewards.

On this page