Skip to main content

Anonymous key patterns for game session identification

The common UX goal for game mini-apps is straightforward: "let the player start immediately, only ask for login when they want to post a score." getAnonymousKey is the foundation that makes this possible. It identifies users at the device level without requiring a Toss account, and you only call appLogin at the moment leaderboard submission is needed.

This guide covers the nature and lifetime of anonymous keys, two standard implementation patterns, anonymous→logged-in migration, server-side considerations, and common mistakes.

getAnonymousKey vs getUserKeyForGame vs appLogin

getAnonymousKeygetUserKeyForGameappLogin
Identifier scopeDevice / mini-appGame-category mini-apps onlyToss user account
Login requiredNoNoYes (consent dialog shown)
Return shape{ type: 'HASH'; hash: string }{ key: string }{ authorizationCode; referrer }
After reinstallNew key likely issuedNew key likely issuedSame account, re-login possible
Different deviceDifferent keyDifferent keySame account login possible
Server verificationSelf-managed (rate-limit, pattern monitoring)SameServer-side code exchange available
StatusCurrent — recommendedDeprecated — use getAnonymousKeyCurrent — recommended
getUserKeyForGame is deprecated

getUserKeyForGame is no longer recommended. Use getAnonymousKey in new code. The return shapes differ ({ key: string } vs { type: 'HASH'; hash: string }) but the purpose and lifetime characteristics are the same. Note that calling getUserKeyForGame outside a game-category mini-app returns 'INVALID_CATEGORY'.

Pattern 1 — saving anonymous progress

The basic pattern for persisting game progress server-side without login.

[Mini-app launches]


getAnonymousKey() → hash


Register / look up hash as userId on your server


Save score, unlocks, inventory

Implementation

import { getAnonymousKey } from '@apps-in-toss/web-framework';
import { useEffect, useState } from 'react';

// Replace with your actual server API.
declare function initGameSession(anonymousId: string): Promise<{
score: number;
level: number;
}>;

type SessionState =
| { status: 'loading' }
| { status: 'ready'; anonymousId: string; score: number; level: number }
| { status: 'unsupported' }
| { status: 'error'; message: string };

function useGameSession() {
const [session, setSession] = useState<SessionState>({ status: 'loading' });

useEffect(() => {
(async () => {
const result = await getAnonymousKey();

if (result === undefined) {
setSession({ status: 'unsupported' });
return;
}

if (result === 'ERROR') {
setSession({ status: 'error', message: 'Session initialization failed.' });
return;
}

const { hash } = result;
const gameData = await initGameSession(hash);
setSession({ status: 'ready', anonymousId: hash, ...gameData });
})();
}, []);

return session;
}

Using hash as a substitute for a server-side user_id lets you save scores, unlocked items, and inventory without login. Do not rely solely on the anonymous key for security-sensitive data (payment info, personal data) — the client can forge any hash value it sends to your server.

Pattern 2 — trigger appLogin at score-submission time

The moment the user wants their name on the leaderboard, they need an identity for the first time. Only call appLogin then.

[Score achieved → leaderboard submit button tapped]


appLogin() → authorizationCode


Server: authorizationCode → Toss token exchange → confirmed userId


Server: map anonymous hash → userId (one-time migration)


Post nickname and score to leaderboard

Implementation

import { appLogin, getAnonymousKey } from '@apps-in-toss/web-framework';

// Replace with your actual server API.
declare function exchangeAndMigrate(params: {
authorizationCode: string;
referrer: string;
anonymousId: string;
}): Promise<{ userId: string }>;

declare function submitScore(params: {
userId: string;
score: number;
}): Promise<void>;

async function handleLeaderboardSubmit(score: number): Promise<void> {
// 1. Capture the current anonymous ID first.
const keyResult = await getAnonymousKey();
if (keyResult === undefined || keyResult === 'ERROR') {
// Key retrieval failed — proceed with login only or show an error.
return;
}

// 2. Show the login dialog.
const { authorizationCode, referrer } = await appLogin();

// 3. Server-side code exchange + anonymous progress migration.
const { userId } = await exchangeAndMigrate({
authorizationCode,
referrer,
anonymousId: keyResult.hash,
});

// 4. Submit score under the confirmed userId.
await submitScore({ userId, score });
}

authorizationCode is single-use and short-lived. For the full server-side token exchange flow, see Guides — Toss login flow.

Where getUserKeyForGame fits

getUserKeyForGame is a game-specific predecessor that only works inside game-category mini-apps. Calling it from a non-game mini-app returns 'INVALID_CATEGORY'. If you are already using it, migrate as follows.

// Old
import { getUserKeyForGame } from '@apps-in-toss/web-framework';
const result = await getUserKeyForGame();
// result: { key: string } | 'INVALID_CATEGORY' | 'ERROR' | undefined

// Recommended
import { getAnonymousKey } from '@apps-in-toss/web-framework';
const result = await getAnonymousKey();
// result: { type: 'HASH'; hash: string } | 'ERROR' | undefined

Only the field name changes (keyhash). If your server stores existing key values, write a one-time mapping endpoint before switching.

Lifetime and reinstall cases

Anonymous key lifetime is determined internally by the SDK and platform — you cannot control or observe the expiry directly. What you need to know in practice:

  • Same device, app intact: the key stays stable.
  • App reinstalled: a new key is likely issued. Previous anonymous progress cannot be recovered through the key alone.
  • Different device: the key differs. The same person appears as a different user.

Because of these characteristics, anonymous keys alone cannot protect long-term user progress. When a user reinstalls or switches devices, the only way to preserve progress is through appLogin-based account linking. Pattern 2 is designed to nudge users toward that moment naturally.

Server-side validation

Do not trust the hash value sent from the client without additional safeguards. A client can send any string as the anonymous ID.

MeasureDescription
Rate-limitingBlock or throttle requests from the same IP or session within a time window.
Behavior monitoringFlag anomalies such as sudden score spikes or repeated submissions for further review.
appLogin required for impactful actionsPayments, leaderboard submissions, and other high-stakes actions must require account authentication.
Separate security-sensitive dataPersonal information and payment records must not be reachable through the anonymous key path.

The SDK does not provide a server-side verification endpoint for anonymous keys. Combine the above measures to build your own protection layer.

anonymous → logged-in migration

Run this once on your server when the user completes their first appLogin.

Server pseudocode:

function migrateAnonymousToUser(anonymousId, userId):
if user(userId).hasExistingData():
// Conflict: this userId already has data from another device.
// Define a policy — e.g. keep the higher score, or let the user choose.
mergeOrPickWinner(user(userId).data, anonymous(anonymousId).data)
else:
// Clean migration: attribute anonymous data to userId.
assign(anonymous(anonymousId).data, userId)

// Mark the anonymous record as migrated and schedule for expiry / deletion.
markAsMigrated(anonymousId)

Implementation considerations:

  • Idempotency: calling with the same (anonymousId, userId) pair a second time must not duplicate or corrupt data.
  • Define a conflict policy upfront: if the same userId already has data from another device, decide in advance which record takes priority. Letting the user choose is the safest default when automatic merging is ambiguous.
  • Migration runs on the server: the client is just a trigger. Perform the actual data move inside a server-side transaction.

Common mistakes checklist

  • Treating the anonymous key as a permanent identifier — it can change after reinstall.
  • Trusting the hash sent from the client without server-side protection (rate-limiting, monitoring).
  • Not handling all three outcomes of getAnonymousKey'ERROR' and undefined must be handled explicitly.
  • Forgetting to update the server-side field name when migrating from getUserKeyForGame (keyhash).
  • Non-idempotent migration logic — a duplicate call can corrupt data.
  • Storing security-sensitive data in the anonymous path — restrict it to authenticated routes.

Environment differences

EnvironmentBehavior
Toss appgetAnonymousKey works as expected. The returned hash stays stable as long as the device and app state are unchanged.
devtools mock@ait-co/devtools returns a stub key immediately. Sufficient for testing the game flow locally, but lifetime characteristics may differ from the real SDK.
External browsergetAnonymousKey throws or returns 'ERROR'. Do not expose the anonymous key path outside the mini-app environment.

External references