← Complete SDK reference

Browser; server clients

Achievements and progress

@silicon-jungle/inkwell-sdk/achievements

Persistent account milestones, stat-linked progress, public discovery and notifications.

Exact source · npm package · Reference version 0.0.8

API at a glance

  • achievements.games({ after?, query? }?), summary({ game?, username? }?), count({ game? }?)
  • achievements.list(query?), get(name, query?), percentages({ game?, offset? }?)
  • achievements.percentage(name, { game? }?): Promise<percentage result | null>
  • achievements.unlock(name), clear(name)
  • achievements.indicateProgress(name, current, max, { locale? }?)
  • achievements.onNotification(listener): unsubscribe
  • achievements.onChange(listener): unsubscribe
  • onAchievementNotification(listener): unsubscribe
  • context.achievements.definitions(offset?), define(definition), update(definition), delete(name)
  • context.achievements.unlockFor(username, name), clearFor(username, name), indicateProgressFor(username, name, current, max, options?)
  • createAchievements(request?), createServerAchievements(request)

Queries can select another visible game or public username; writes stay scoped to the current game and authorized player. Unlock is idempotent and preserves the first unlock date.

indicateProgress requests a notification, not a durable progress write or unlock. Use the linked stat to persist progress.

Definitions support hidden/disabled achievements, locked/unlocked icons, translations and server-only writes. Hidden locked details are withheld by the platform.

With offline mode enabled, unlock can return { queued: true, requestId }; do not treat this as server-confirmed.

onChange is a browser-only invalidation hint with id, kind (updated/reset/refresh) and names, not an award notice or durable event log. Re-read after backend unlock/clear, linked stat changes or reconnect; empty names can mean a broader refresh. Unsubscribe and handle errors inside your callbacks.

Examples, policies and operational limits →

All exported symbols

  • Achievement
  • AchievementChange
  • AchievementDefinition
  • AchievementNotification
  • AchievementPercentage
  • AchievementQuery
  • AchievementUnlock
  • achievements
  • createAchievements
  • createServerAchievements
  • onAchievementChange
  • onAchievementNotification

Exact TypeScript API

Generated from the SDK source, including input options, return values, public types and overloads. Relative imports below are links between SDK source modules, not additional package subpaths. Only the entrypoints listed in the reference index are supported import paths.

import { type GameServiceRequest } from "./game-services.js";
import type { CachedGameRead, QueuedGameWrite } from './offline.js';
export type AchievementNotification = {
    id: string;
    kind: 'unlocked' | 'progress';
    name: string;
    title: string;
    description: string;
    iconUrl: string | null;
    current?: number;
    max?: number;
};
export type AchievementChange = {
    id: string;
    kind: 'updated' | 'reset' | 'refresh';
    names: string[];
};
/** Browser hint to re-read achievements after backend changes or reconnect. */
export declare function onAchievementChange(listener: (change: AchievementChange) => void): () => void;
export declare function onAchievementNotification(listener: (notice: AchievementNotification) => void): () => void;
export type AchievementDefinition = {
    name: string;
    title: string;
    description: string;
    iconUrl?: string | null;
    lockedIconUrl?: string | null;
    hidden?: boolean;
    serverWritesOnly?: boolean;
    enabled?: boolean;
    translations?: Record<string, {
        title: string;
        description: string;
    }>;
    progressStat?: string | null;
    progressMin?: number;
    progressTarget?: number | null;
};
export type Achievement = {
    name: string;
    title: string;
    description: string;
    iconUrl: string | null;
    hidden: boolean;
    unlocked: boolean;
    unlockedAt: string | null;
    progress: {
        current: number;
        min: number;
        target: number;
        percent: number;
    } | null;
};
export type AchievementQuery = {
    game?: string;
    username?: string;
    offset?: number;
    locale?: string;
};
export type AchievementUnlock = {
    queued?: false;
    name: string;
    unlocked: true;
    newlyUnlocked: boolean;
    unlockedAt: string;
};
export type AchievementPercentage = {
    name: string;
    percent: number;
    unlockedPlayers: number;
    unlocked: boolean;
};
export declare function createAchievements(request?: GameServiceRequest): Readonly<{
    onChange: typeof onAchievementChange;
    games: (options?: {
        after?: string;
        query?: string;
    }) => Promise<{
        games: {
            slug: string;
            title: string;
            publisherUsername: string;
            achievementCount: number;
        }[];
        nextCursor: string | null;
    }>;
    summary: (options?: Pick<AchievementQuery, "game" | "username">) => Promise<{
        total: number;
        unlocked: number;
    }>;
    count(options?: Pick<AchievementQuery, "game">): Promise<number>;
    onNotification: typeof onAchievementNotification;
    indicateProgress: (name: string, current: number, max: number, options?: {
        locale?: string;
    }) => Promise<{
        displayed: boolean;
    }>;
    list: (options?: AchievementQuery) => Promise<{
        achievements: Achievement[];
        nextOffset: number | null;
    } & CachedGameRead>;
    get(name: string, options?: AchievementQuery): Promise<{
        offline: boolean | undefined;
        cachedAt: number | undefined;
        pendingWrites: number | undefined;
        name: string;
        title: string;
        description: string;
        iconUrl: string | null;
        hidden: boolean;
        unlocked: boolean;
        unlockedAt: string | null;
        progress: {
            current: number;
            min: number;
            target: number;
            percent: number;
        } | null;
    } | null>;
    unlock: (name: string) => Promise<QueuedGameWrite | AchievementUnlock>;
    clear: (name: string) => Promise<{
        success: true;
    }>;
    percentage(name: string, options?: {
        game?: string;
    }): Promise<AchievementPercentage | null>;
    percentages: (options?: {
        game?: string;
        offset?: number;
    }) => Promise<{
        achievements: AchievementPercentage[];
        nextOffset: number | null;
    }>;
}>;
export declare function createServerAchievements(request: GameServiceRequest): Readonly<{
    indicateProgressFor: (username: string, name: string, current: number, max: number, options?: {
        locale?: string;
    }) => Promise<{
        displayed: boolean;
    }>;
    definitions: (offset?: number) => Promise<{
        achievements: (Achievement & AchievementDefinition)[];
        nextOffset: number | null;
    }>;
    define: (definition: AchievementDefinition) => Promise<{
        achievement: AchievementDefinition;
    }>;
    update: (definition: AchievementDefinition) => Promise<{
        achievement: AchievementDefinition;
    }>;
    delete: (name: string) => Promise<{
        success: true;
    }>;
    unlockFor: (username: string, name: string) => Promise<AchievementUnlock>;
    clearFor: (username: string, name: string) => Promise<{
        success: true;
    }>;
    onChange: typeof onAchievementChange;
    games: (options?: {
        after?: string;
        query?: string;
    }) => Promise<{
        games: {
            slug: string;
            title: string;
            publisherUsername: string;
            achievementCount: number;
        }[];
        nextCursor: string | null;
    }>;
    summary: (options?: Pick<AchievementQuery, "game" | "username">) => Promise<{
        total: number;
        unlocked: number;
    }>;
    count: (options?: Pick<AchievementQuery, "game">) => Promise<number>;
    onNotification: typeof onAchievementNotification;
    indicateProgress: (name: string, current: number, max: number, options?: {
        locale?: string;
    }) => Promise<{
        displayed: boolean;
    }>;
    list: (options?: AchievementQuery) => Promise<{
        achievements: Achievement[];
        nextOffset: number | null;
    } & CachedGameRead>;
    get: (name: string, options?: AchievementQuery) => Promise<{
        offline: boolean | undefined;
        cachedAt: number | undefined;
        pendingWrites: number | undefined;
        name: string;
        title: string;
        description: string;
        iconUrl: string | null;
        hidden: boolean;
        unlocked: boolean;
        unlockedAt: string | null;
        progress: {
            current: number;
            min: number;
            target: number;
            percent: number;
        } | null;
    } | null>;
    unlock: (name: string) => Promise<QueuedGameWrite | AchievementUnlock>;
    clear: (name: string) => Promise<{
        success: true;
    }>;
    percentage: (name: string, options?: {
        game?: string;
    }) => Promise<AchievementPercentage | null>;
    percentages: (options?: {
        game?: string;
        offset?: number;
    }) => Promise<{
        achievements: AchievementPercentage[];
        nextOffset: number | null;
    }>;
}>;
export declare const achievements: Readonly<{
    onChange: typeof onAchievementChange;
    games: (options?: {
        after?: string;
        query?: string;
    }) => Promise<{
        games: {
            slug: string;
            title: string;
            publisherUsername: string;
            achievementCount: number;
        }[];
        nextCursor: string | null;
    }>;
    summary: (options?: Pick<AchievementQuery, "game" | "username">) => Promise<{
        total: number;
        unlocked: number;
    }>;
    count(options?: Pick<AchievementQuery, "game">): Promise<number>;
    onNotification: typeof onAchievementNotification;
    indicateProgress: (name: string, current: number, max: number, options?: {
        locale?: string;
    }) => Promise<{
        displayed: boolean;
    }>;
    list: (options?: AchievementQuery) => Promise<{
        achievements: Achievement[];
        nextOffset: number | null;
    } & CachedGameRead>;
    get(name: string, options?: AchievementQuery): Promise<{
        offline: boolean | undefined;
        cachedAt: number | undefined;
        pendingWrites: number | undefined;
        name: string;
        title: string;
        description: string;
        iconUrl: string | null;
        hidden: boolean;
        unlocked: boolean;
        unlockedAt: string | null;
        progress: {
            current: number;
            min: number;
            target: number;
            percent: number;
        } | null;
    } | null>;
    unlock: (name: string) => Promise<QueuedGameWrite | AchievementUnlock>;
    clear: (name: string) => Promise<{
        success: true;
    }>;
    percentage(name: string, options?: {
        game?: string;
    }): Promise<AchievementPercentage | null>;
    percentages: (options?: {
        game?: string;
        offset?: number;
    }) => Promise<{
        achievements: AchievementPercentage[];
        nextOffset: number | null;
    }>;
}>;
Supporting internal type declarations

These clarify types referenced by the generated signatures; do not import these internal paths directly.

game-services.d.ts

export declare class GameServiceError extends Error {
    readonly status: number;
    readonly code: string;
    constructor(message: string, status?: number, code?: string);
}
export type GameServiceRequest = <T>(service: string, request: Record<string, unknown>) => Promise<T>;
export declare const requestGameService: GameServiceRequest;
/** Server-only adapter. Credentials must never be bundled into browser games. */
export declare function createGameServiceRequest(options: {
    baseUrl: string;
    token: string;
    fetch?: typeof fetch;
}): GameServiceRequest;

protocol.d.ts

export declare const SDK_SOURCE: "inkwell-sdk";
export declare const SDK_VERSION: 1;
export type InkwellMessageType = "ready" | "loading.progress" | "loading.error" | "session.complete" | "analytics.track" | "assets.progress" | "player.get" | "presence.get" | "performance.sample" | "backend.connect" | "backend.fetch";
export type InkwellMessage = {
    source: typeof SDK_SOURCE;
    version: typeof SDK_VERSION;
    type: InkwellMessageType;
    payload?: Record<string, unknown>;
    sentAt: number;
};
export declare function parentOrigin(): string | null;
export declare function requestId(): string;
export declare function emit(type: InkwellMessageType, payload?: Record<string, unknown>): boolean;