Gamearly API v1
Dashboard

In-game events / Player tags & traits

Player tags and traits#

Events tell us what happened. Tags and traits tell us who it happened to — the state you attach to a player rather than to a moment.

That is what makes comparison possible. Tag half your players team_a and half team_b, and every event chart on your dashboard can be split between them. Record class: mage or bought_season_pass: true as a trait, and the same is true of those.

Tag or trait?#

Use a tag Use a trait
Shape a label you either have or don't a key with a value
Examples team_a, beta_tester, whale class: mage, chapter: 7, spend_tier: high
Best for splitting a population in two to compare describing a player, and breaking down by value
Limits 50 per player, 500 per project 50 per player, 200 keys per project

Both are always available as filters. The difference is cost: tags get a materialised cohort in our rollups automatically, so charts split by tag are fast without you doing anything. Traits get the same treatment once they prove to be low-cardinality, or when the studio enables it for a key explicitly in the dashboard.

POST /v1/players/update#

Up to 100 players per call. Requires events:write.

{
  "players": [
    {
      "partner_user_id": "A1B2C3",
      "tags_add": ["team_a", "beta_tester"],
      "traits": { "class": "mage", "chapter": 7 },
      "traits_once": { "first_build": "1.4.2" },
      "traits_increment": { "bosses_killed": 1 }
    }
  ]
}

Identify the player with partner_user_id, or with device_id when they are not signed in — the same rule as events, and the device's tags fold into their real profile once they link.

Field Meaning
traits Merged into what is there. Only the keys you send change.
traits_once Set only if the key is not already present. Good for "first seen" values.
traits_increment Numeric add, applied on our side. Use this for counters — a read-modify-write from two devices loses updates.
traits_unset Remove these keys.
tags_add / tags_remove Add or remove specific tags. Prefer these.
tags Replaces the entire tag list. Only use it when you genuinely mean "these are now all of their tags".

Response

{
  "success": true,
  "updated": [
    { "identity_key": "u:8a8c…", "partner_user_id": "A1B2C3",
      "device_id": null, "gamearly_user_id": "8a8c…", "linked": true,
      "tags": ["team_a", "beta_tester"] }
  ],
  "rejected": [],
  "server_time": "2026-08-20T10:00:03Z"
}

This writes immediately

Unlike /events/track, there is no queue and no lag. A tag set at login exists before the next event is filtered by it. That is why the batch is smaller: this is one row per player, sent when something changes, not one per moment.

Normalisation#

Tags are lowercased and spaces become underscores, so "Team A", "team a" and "team_a" are all the same tag. That is deliberate — three spellings of one cohort would each look a third the size. Trait keys keep their case; trait values are kept exactly as sent.

Anything that cannot be normalised — a tag with punctuation we don't allow, a key starting with _ — is dropped from that entry rather than failing the call. Keys beginning with _ are reserved for our own annotations.

POST /v1/players/get#

{ "partner_user_id": "A1B2C3" }
{
  "found": true,
  "profile": {
    "identity_key": "u:8a8c…",
    "partner_user_id": "A1B2C3", "device_id": null,
    "gamearly_user_id": "8a8c…", "linked": true,
    "tags": ["team_a", "beta_tester"],
    "traits": { "class": "mage", "chapter": 7, "bosses_killed": 12 },
    "first_seen_at": "…", "last_seen_at": "…"
  }
}

found: false is not an error — a player you have never tagged simply has no profile yet.

POST /v1/players/search#

Find everyone in a cohort. Requires events:read.

{ "tags": ["team_a"], "traits": { "class": "mage" }, "page": 1 }

tags matches any of the listed tags by default; send "tags_match": "all" to require all of them. Multiple traits are always ANDed.

Returns { "players": [ …profiles… ], "players_info": { "total", "pages", "page" } }.

POST /v1/players/tags#

The tags your project uses, with how many players carry each. Useful for building your own picker, and for checking a rollout actually landed. Requires events:read.

{ "page": 1, "page_size": 50, "text": "team", "category": null }
{
  "tags": [
    { "id": 7, "tag": "team_a", "label": "Team A", "category": "team",
      "description": null, "is_hidden": false, "players_count": 4102,
      "first_seen_at": "…", "last_seen_at": "…" }
  ],
  "tags_info": { "total": 2, "pages": 1, "page": 1 },
  "categories": ["team"]
}

page is required; page_size caps at 100.

Using cohorts in the event endpoints#

Every read endpoint that takes a date range also takes tags, tags_match and traits, with exactly the shape above. So "how many bosses did team A kill this month" is /events/trends with names: ["killed_boss"] and tags: ["team_a"].

Two things worth knowing#

Cohorts are evaluated as of now, not as of the event. A player who joined team_a yesterday appears in team A's numbers for the whole period, including the days before they joined. That is how product analytics tools treat user properties, and it is the useful answer for "how does team A behave" — but if you need "who was in team A at the time", record it as an event property on each event instead.

Tags are for cohorts, not for identifiers. A tag like team_a is right. A tag like session_9f3a21 is not: the project cap of 500 exists to stop one such mistake turning the tag registry into a second copy of your player table. Put per-player values in traits, and per-event values in event properties.