Player tools
Live-ops tools for when something goes wrong in production: force-complete, retry roll, manual grants, force-claim, and a read-only player profile endpoint.
The Player lookup screen (inside any game's nav) is the single search for everything Kraty knows about a player: profile snapshot, recent attempts, reward and crate grants, lobby memberships. It is also where you fix things when they break.
The four admin actions
Every action is permission-gated, audited (with reason captured where applicable), and idempotent where possible. Use them sparingly, because they bypass the normal player flow, so reach for them only to fix broken state, not as a routine operation.
Force-complete an attempt
When a player's attempt is stuck in in_progress (client crashed
between progress reports, network partition, etc.), force-complete
from the attempt row.
You can optionally override the final score. The leaderboard catches up with the new score automatically. Rewards are not auto-rolled; chain with Retry roll if the attempt should produce a grant.
- Permission:
attempt.force_complete(GAME_ADMIN,GAME_DEBUGGER) - Audit:
attempt.force_completewith the reason in metadata.
Retry the reward roll
When the original roll failed or never happened (e.g. after a force-complete), re-run the reward pipeline from the attempt row.
The route refuses to roll a second time if any grants are already linked to the attempt: there is no duplicate-rewards path.
- Permission:
grant.retry_reward_roll(GAME_ADMIN,GAME_DEBUGGER) - Audit:
grant.retry_reward_rollwith{ grantCount }in metadata.
Issue a manual grant
Make-goods, promotional one-offs, and recovery from edge cases.
Compose the grant's contents as JSON in the Issue a manual
grant card. The grant fires a normal grant.created webhook so
your studio backend picks it up the same way as an
event-completion grant.
- Permission:
grant.manual_create(GAME_ADMIN) - Audit:
grant.manual_createwith the reason and the external player id in metadata.
Force-claim a grant
Use after verifying server-side that a claim succeeded but the ack never came back (network hiccup mid-deploy on the receiver, etc.). Marks the grant claimed regardless of current status.
- Permission:
grant.manual_force_claim(GAME_ADMIN,GAME_DEBUGGER) - Audit:
grant.manual_force_claimwith full before/after diff.
From your backend (without the portal)
For support workflows that need to read player state
programmatically (a customer-service tool, a fraud-review job, a
Slack bot), call the player profile endpoint directly with a
server_integration API key:
GET /server/v1/players/{externalPlayerId}
Authorization: Bearer <your-server-integration-key>Returns the same shape the portal's Player lookup uses:
{
"data": {
"player": {
"id": "...",
"externalPlayerId": "alice_42",
"firstSeenAt": "...",
"lastSeenAt": "...",
"lastContextSnapshot": { "country": "PT", "level": 7 }
},
"attempts": [ /* recent N */ ],
"grants": [ /* recent N */ ],
"lobbies": [ /* recent N */ ],
"summary": {
"attemptCount": 12,
"attemptsCompleted": 8,
"attemptsExpired": 1,
"grantsPending": 2,
"grantsClaimed": 9,
"lobbiesActive": 0
}
}
}The endpoint is read-only: no force-complete, no manual
grant, no force-claim from the server side. Those mutations
need a member session through the portal so they leave an actor
trail. If your support team needs to issue make-goods
programmatically, use
POST /server/v1/players/:p/grants
(which already audits the grant and fires the same
grant.created webhook).
- Auth:
server_integrationAPI key (aclient_sdkkey gets 403). - Scope: the key's
(studio, game); the path does not carry them. - Limits:
?limit=caps each of the three lists (1–200, default 50).
Synthetic identity
Every player row carries two values Kraty derives for you, both shown on the player-detail page and returned to your game:
-
Country: resolved server-side (your CDN's geo header —
CloudFront-Viewer-Country,CF-IPCountry, … — falling back to an offline GeoIP lookup on the client IP) on register, attempt-start, and leaderboard join. Stored on the player and usable as acountryleaderboard segment with no client changes: you never send it (aplayerContext.countryyou do send, or an operator override, wins). It comes back to the client in two places so you can render a flag: onregister(ascountry) and on every leaderboard entry (entry.country; null for bots). -
Synthetic identity: a stable fake
{ name, avatar }composed once from the game's default identity pool (or the built-in system pool) and keyed off the player, so it never changes on its own. Use it to show a player under a privacy-preserving alias (e.g. on a public board or a "recent players" feed) without exposing their real profile. Returned fromPOST /sdk/v1/players/:externalId/registerassyntheticIdentity.
Two identities per player: display + anonymized
Every player carries two identities in parallel:
- Display identity (
displayIdentity) — whatsetIdentitywrites. Optional; falls back to the anonymized pool value when the player never renamed themselves. This is the "real" name that leaderboards render as the primary label. - Anonymized identity (
anonymizedIdentity) — the immutable synthetic pool value.setIdentitynever touches this. Safe to surface on public boards or cross-game aggregations where the real name shouldn't leak.
Both are enriched with the player's country (identical on either view — a
player has one real country regardless of which name you render). The
register response carries both
envelopes; every LeaderboardEntry mirrors them via name / avatar
(display) and anonymizedName / anonymizedAvatar (immutable pool value).
Client SDKs expose two accessors for reading the calling player's own identity fresh from the server:
// TypeScript client SDK
const real = await kraty.players.getIdentity(); // display, fallback to pool
const anon = await kraty.players.getAnonymizedIdentity(); // always the pool value// Flutter client SDK
final real = await kraty.players.getIdentity();
final anon = await kraty.players.getAnonymizedIdentity();// Unity client SDK
var real = await kraty.Players.GetIdentityAsync();
var anon = await kraty.Players.GetAnonymizedIdentityAsync();Customize a player's identity
The display identity is optional, but you can set it with a custom name and avatar — e.g. to honor a player's chosen handle. The anonymized identity stays untouched. From your backend with the server SDK:
// TypeScript server SDK
await kraty.players.setIdentity('player_42', {
name: 'ShadowStrike',
avatar: 'https://cdn.example.com/avatars/42.png', // URL, asset key, or id
});# Python server SDK
kraty.players.set_identity("player_42", name="ShadowStrike", avatar="hero_blue")curl -X PUT https://api.kraty.io/server/v1/players/player_42/identity \
-H "Authorization: Bearer $KRATY_SERVER_KEY" \
-H "Content-Type: application/json" \
-d '{"name":"ShadowStrike","avatar":"hero_blue"}'The new name + avatar flow onto leaderboard entries on the next read. Operators can also edit it from the player-detail page (Fake identity → Edit), and set a player's country there too.
Let players rename themselves (from the game client)
If your game has a "choose your username" screen, the player can set their own identity directly from the client SDK — no backend round-trip. It's authorized by the player's own secret, so a client can only ever rename itself, never another player:
// TypeScript client SDK — changes the active player's own identity
await kraty.players.setIdentity({ name: 'ShadowStrike', avatar: 'hero_blue' });
// kraty.syntheticIdentity now reflects it immediately.// Flutter client SDK
await kraty.players.setIdentity('ShadowStrike', avatar: 'hero_blue');// Unity client SDK
await kraty.Players.SetIdentityAsync("ShadowStrike", avatar: "hero_blue");This trusts the client. If you need to moderate display names (profanity,
impersonation, length rules), don't expose the client path — set names from
your backend with the server SDK players.setIdentity above, after your own
validation. Both write the same field; pick the one that matches how much you
trust the caller.
Custom player metadata
Anything else you want to attach to a player — level, cohort, VIP tier, a
chosen region — lives in player context, a free-form JSON bag. The client
sends it on events.start (playerContext), and operators can edit it from
the player-detail page (Edit context). Context keys double as
leaderboard segments, so sending
playerContext: { league: 'gold' } lets you rank players per league with no
extra config.
Dev triggers: fast-forward end-of-period (test keys only)
While you're wiring Kraty into your game you'll want to see what happens when an event window closes or a weekly/monthly leaderboard rolls over (the grants, the promotion/relegation, the webhooks) without waiting for the real clock. Two server endpoints trigger those paths on demand, scoped to your key's game:
POST /server/v1/dev/events/{eventKey}/end-now
POST /server/v1/dev/leaderboards/{leaderboardKey}/rollover-now
Authorization: Bearer <your-TEST-server-integration-key>events/{eventKey}/end-nowcloses the event's currently-active window(s) immediately, finalizing leaderboards, expiring in-progress attempts, dispatching window-end rewards, and closing lobbies, exactly as the scheduler would at the real end time. Returns the windows that were closed.leaderboards/{leaderboardKey}/rollover-nowrolls one shared leaderboard forward by a period right now, snapshotting it and paying out per-rank (and per-division) rewards plus any promotion/relegation. ReturnsrolledOver: falsefor an all-time board (nothing to roll).
These require a test-environment server_integration key; a
live key gets a 403. Test and live are fully isolated: each
environment gets its own event windows, leaderboard rankings, and
period rollovers, so a test trigger only fast-forwards your test
window / period and never touches live state.
The operations are idempotent: re-firing against an already-closed window or an already-rolled period is a safe no-op.
When NOT to use these
These tools are for fixing broken state. They are not for routine operations:
- For periodic make-goods, generate them server-to-server via
POST /server/v1/players/:p/grants, not by hand in the portal. - If an attempt regularly needs force-completing, the event config is probably wrong (e.g. target unreachable); fix the config rather than the symptom.
- If grants regularly need force-claiming, your webhook receiver is unreliable; fix retries on your side instead of papering over with manual claims.
Resolve a "lost my reward" support ticket
Your support engineer gets a ticket: "I finished the daily race but the gold never arrived." The full path from ticket to resolution, all auditable:
Open the game's Players tab and search for the player by their external id (or the email they signed up with; both are indexed).
The profile shows their last reported context, ban status, and totals. Open the Attempts card and find the one they are complaining about. Its status is one of:
completedwith grants attached → look at the Grants card; ifpendingthey are waiting for the SDKcollectAll()call. Click Force-claim to deliver immediately.completedwith no grants attached → the reward roll failed. Click Retry reward roll; it's idempotent, so the fix lands exactly once even if you double-click.in_progressorexpired→ the attempt never completed server-side. Click Force-complete with an override score and a reason; the engine rolls rewards as if the attempt finished normally.
For a make-good on top of recovering the original payout, open
the Wallet card and click + Credit. Enter the currency
key, amount, and an audit reason. The credit appears instantly
and emits a wallet.changed webhook to your CRM.
Every action lands in the studio Audit log with the actor, timestamp, and before/after diff. Linking the audit row in your ticketing system gives you a permanent paper trail.
Merge a guest player into an authenticated account
When a guest who has been playing locally signs in with a social provider for the first time, you usually want their progress to follow them, not start over. Run this from your backend, not the portal:
// `@kraty/server-sdk`: server_integration key required.
await server.players.merge('guest_device_abc', 'auth_user_42');What happens, in one transaction:
- Attempts and grants are reassigned to the authenticated id.
- Item quantities are summed (guest had 2 potions, authed had 1 → 3).
- Wallet balances are summed.
- Lobby seats are re-pointed.
- The guest's
externalPlayerIdis anonymised so the slot can be reused on the next guest signup from the same device.
A single player.merged webhook fires with the full counts so
your analytics pipeline can stitch the two journeys together.