Authentication#
Every project has two API keys, created for you automatically. There is nothing to set up and nothing to choose — the only decision is which one goes where, and that decision is the whole of Gamearly's API security model.
| Public key | Private key | |
|---|---|---|
| Token looks like | gak_pub_… |
gak_prv_… |
| Belongs in | your game build | your server, CI, backend jobs |
| Can | send events, run the account-link flow, read your project's public info | everything the public key can, plus everything below |
| Cannot | complete quests, grant or revoke entitlements, verify launch tokens, mark events trusted, import history, wipe data | — |
| Safe to ship to players | yes | never |
Both keys are on your project dashboard, under the API integration section. You can reveal either one as often as you like, and regenerate either one independently.
Request headers
| Header | Value |
|---|---|
X-Api-Key |
[YOUR_PROJECT_API_KEY] |
Content-Type |
application/json |
Authorization: ApiKey [YOUR_PROJECT_API_KEY] works identically if that suits your HTTP
client better.
Why two keys#
A key inside a game build is not a secret. Anyone who owns your game can extract it from the binary, from memory, or from the network traffic — this is not a hypothetical, it is routine, and no amount of obfuscation changes it.
So the question is never "how do I hide the key in my build", it is "what happens when someone finds it". With a public key the answer is: they can send you fake telemetry about themselves. That is annoying and it is why trusted events exist, but it costs nobody anything real.
With a full-power key the answer is: they can complete quests to grant themselves rewards, and
revoke other players' access to your game. That is why quests/complete_task,
entitlements/* and launch_tokens/verify are private-key only. It is a deliberate boundary,
not an oversight.
If you already shipped a build containing your key
The key your project had before this change is now your private key, and it still works exactly as it did — nothing has broken. But if you embedded it in a game build, that build is holding a key that can grant rewards.
Fix it in two steps: ship the public key in your next build, then regenerate the private key. Regenerating gives you a grace window (7 days by default), so your servers keep working while you roll out the new value.
Scopes#
Scopes are the mechanism underneath public/private. You never assign them — they follow from
the key's kind — but they are what the API actually checks, and they are what a 403 names.
| Scope | Grants | Public key |
|---|---|---|
project:read |
Read your project's public information | ✅ |
link:write |
Start an account-linking session | ✅ |
link:read |
Poll a linking session's status | ✅ |
users:read |
Look up a linked user by partner_user_id |
✅ |
events:write |
Ingest events, timers, device identification, player tags and traits | ✅ |
events:read |
Counts, trends, funnels, event search, per-player lookups | opt-in |
quests:read |
List quests, read one, check completion | private only |
quests:write |
Complete a task and grant its reward | private only |
contests:read |
List and read contests | private only |
votes:read |
List and read referendums | private only |
leaderboard:read |
Read the project leaderboard and a player's position | private only |
entitlements:read |
Check whether a player owns your game | private only |
entitlements:write |
Revoke a player's entitlement | private only |
launch:verify |
Verify and consume a launch token | private only |
events:trusted |
Mark ingested events as trusted | private only |
events:import |
Bulk historical backfill with arbitrary timestamps | private only |
events:wipe |
Delete your project's test data | private only |
A private key holds every scope, including any added in future — you never have to regenerate it to pick up a new capability.
events:read on the public key#
This is the one capability you can add to your public key, from the key card on your dashboard. It is off by default, and you should leave it off unless your game genuinely needs to display its own analytics to players.
Turning it on means that anyone who extracts the key from your build can read: your event counts and trends across the whole project, your funnels, your event search results, your per-player event totals and leaderboards, and your player tags and traits. None of it is per-player-scoped — it is your project's analytics, and the key is public.
Reading a 403#
A scope failure returns 403 and tells you exactly what happened:
{
"error": "insufficient_scope",
"required": ["quests:write"],
"granted": ["events:write", "link:read", "link:write", "project:read", "users:read"],
"key_kind": "public"
}
key_kind: "public" on a route that needs a private scope means the call is coming from the
wrong place — move it to your server rather than looking for a way to widen the key.
Regenerating a key#
Regenerating mints a replacement and leaves the old key working for a grace period, so you
are never in a state where half your traffic is 401ing mid-deploy.
| Default grace | Allowed range | |
|---|---|---|
| Private key | 7 days | 0–90 days |
| Public key | 90 days | 7–365 days |
The public key's grace is long and its minimum cannot be waived, because you cannot update a build that is already on a player's machine. A player who has not launched your game in two months still has the old key in their install, and console certification alone can take weeks. Ninety days is the smallest window that reliably covers a real release cycle.
While a key is inside its grace window, every response it authenticates carries:
X-Gamearly-Key-Expires-At: 2026-11-16T09:00:00Z
Log it. A key that is about to stop working shows up in your own logs long before it shows up as an incident.
Errors#
| Status | error |
Meaning |
|---|---|---|
401 |
missing_api_key |
No X-Api-Key header and no Authorization: ApiKey … |
401 |
invalid_api_key |
No key matches, the key was revoked, or the caller's IP is not allowed |
401 |
expired_api_key |
The key was real, but its regeneration grace period has ended |
403 |
insufficient_scope |
Valid key, wrong kind for this route — see above |
403 |
inactive_client |
The project is inactive on Gamearly |
expired_api_key is deliberately distinct from invalid_api_key: it means you regenerated
this key and something is still using the old value. Search your config for the previous
token rather than opening a support ticket.
Revealing a key is repeatable
Both tokens are recoverable from your dashboard at any time. You will not be shown a "copy this now, you will never see it again" screen, so there is no reason to paste a Gamearly key into a notes app.