Console
Guide

Achievements

Add achievements to your app — unlock them from your server backend, display them in-game, and let users compare progress on the leaderboard.

Concepts

FieldTypeDescription
slugstringUnique identifier within your app. Only [a-z0-9_-], no spaces. E.g. first_kill, win-streak-10
namestringDisplay name shown to users
descriptionstringWhat the user must do to unlock it
iconstringURL of the achievement icon
rarityenumcommon, uncommon, rare, epic, legendary
xpintExperience points granted on unlock (0 = no XP)
secretboolHidden until unlocked — shows as ??? in the public list

Rarity guide

RaritySuggested use
commonEasy, accessible to most players
uncommonRequires some effort
rareChallenging — few players will have it
epicVery hard, requires real skill
legendaryExtremely rare, historic feat

Authentication

All endpoints require Authorization: AppID:AppKey. Management calls (create, update, delete, grant, revoke) only work for achievements belonging to your own app.

For read-only calls (list, user, check, leaderboard), a user session token may be passed as the Authorization header instead of AppID:AppKey — the response will include per-user unlock status automatically.

Endpoints overview

EndpointAction
POST /api/achievements/create.phpCreate an achievement
POST /api/achievements/update.phpUpdate an achievement
POST /api/achievements/delete.phpDelete an achievement
POST /api/achievements/grant.phpUnlock an achievement for a user
POST /api/achievements/revoke.phpRemove an unlock from a user
POST /api/achievements/list.phpList all achievements for an app
POST /api/achievements/user.phpList achievements unlocked by a user
POST /api/achievements/check.phpCheck if a user has a specific achievement
POST /api/achievements/leaderboard.phpXP-based user ranking

1. Grant an achievement (server-side)

Call grant.php from your game server after a condition is met. The call is idempotent — granting the same achievement twice returns "already_unlocked", not an error.

// From your game backend (Node.js)
const res = await fetch('https://vaneltonmedia.com/api/achievements/grant.php', {
  method: 'POST',
  headers: {
    'Authorization': `${APP_ID}:${APP_KEY}`,
    'Content-Type': 'application/json'
  },
  body: JSON.stringify({
    slug:  'first_goal',
    token: userToken   // session token received from the game client
  })
});
const { result } = await res.json();
// result: "granted" or "already_unlocked"
const res = await fetch('https://vaneltonmedia.com/api/achievements/grant.php', {
  method: 'POST',
  headers: {
    'Authorization': `${APP_ID}:${APP_KEY}`,
    'Content-Type': 'application/json'
  },
  body: JSON.stringify({ slug: 'first_goal', token: userToken })
});
const data = await res.json() as { success: boolean; result: 'granted' | 'already_unlocked' };
function grantAchievement(string $slug, string $userToken): array {
    $ch = curl_init('https://vaneltonmedia.com/api/achievements/grant.php');
    curl_setopt_array($ch, [
        CURLOPT_RETURNTRANSFER => true,
        CURLOPT_POST           => true,
        CURLOPT_HTTPHEADER     => [
            'Authorization: ' . APP_ID . ':' . APP_KEY,
            'Content-Type: application/json',
        ],
        CURLOPT_POSTFIELDS => json_encode(['slug' => $slug, 'token' => $userToken]),
    ]);
    $result = json_decode(curl_exec($ch), true);
    curl_close($ch);
    return $result;
}

$r = grantAchievement('first_goal', $userToken);
// $r['result'] === 'granted' or 'already_unlocked'
import requests

HEADERS = {'Authorization': f'{APP_ID}:{APP_KEY}', 'Content-Type': 'application/json'}

def grant_achievement(slug: str, user_token: str) -> dict:
    return requests.post(
        'https://vaneltonmedia.com/api/achievements/grant.php',
        headers=HEADERS,
        json={'slug': slug, 'token': user_token}
    ).json()

r = grant_achievement('first_goal', user_token)
# r['result'] == 'granted' or 'already_unlocked'
using System.Net.Http;
using System.Text;
using System.Text.Json;

using var client = new HttpClient();
client.DefaultRequestHeaders.Add("Authorization", $"{APP_ID}:{APP_KEY}");

var payload = JsonSerializer.Serialize(new { slug = "first_goal", token = userToken });
var res = await client.PostAsync(
    "https://vaneltonmedia.com/api/achievements/grant.php",
    new StringContent(payload, Encoding.UTF8, "application/json"));

using var doc = JsonDocument.Parse(await res.Content.ReadAsStringAsync());
string result = doc.RootElement.GetProperty("result").GetString(); // "granted" or "already_unlocked"
curl -s -X POST "https://vaneltonmedia.com/api/achievements/grant.php" \
  -H "Authorization: $APP_ID:$APP_KEY" \
  -H "Content-Type: application/json" \
  -d "{\"slug\":\"first_goal\",\"token\":\"$USER_TOKEN\"}"

2. List achievements with unlock status (client-side)

Authenticate with the user's session token to get unlocked and unlocked_at per achievement. Secret achievements that haven't been unlocked appear as ???.

// Authenticated as the user — pass their token as Authorization
const res = await fetch('https://vaneltonmedia.com/api/achievements/list.php', {
  method: 'POST',
  headers: {
    'Authorization': userToken,
    'Content-Type': 'application/json'
  },
  body: JSON.stringify({ app_id: 'com.myapp.game' })
});
const { achievements } = await res.json();
// achievements[].unlocked, achievements[].unlocked_at
interface Achievement {
  id: number; slug: string; name: string; description: string;
  icon: string; rarity: string; xp: number; secret: boolean;
  unlocked?: boolean; unlocked_at?: string | null;
}

const res = await fetch('https://vaneltonmedia.com/api/achievements/list.php', {
  method: 'POST',
  headers: { 'Authorization': userToken, 'Content-Type': 'application/json' },
  body: JSON.stringify({ app_id: 'com.myapp.game' })
});
const { achievements } = await res.json() as { achievements: Achievement[] };
// Authenticate with the user token as Authorization header
var _headers = ds_map_create();
ds_map_add(_headers, "Authorization", global.user_token);
ds_map_add(_headers, "Content-Type",  "application/json");

global.ach_req = http_request(
    "https://vaneltonmedia.com/api/achievements/list.php",
    "POST", _headers,
    json_stringify({ app_id: "com.myapp.game" })
);
ds_map_destroy(_headers);

// --- In Async HTTP Event ---
if (async_load[? "id"] == global.ach_req) {
    var _d = json_parse(async_load[? "result"]);
    // _d.achievements is an array — each entry has .slug, .name, .unlocked, .unlocked_at
    show_debug_message("Achievements: " + string(_d.count));
}
// Pass the user token as Authorization
using var userClient = new HttpClient();
userClient.DefaultRequestHeaders.Add("Authorization", userToken);

var payload = JsonSerializer.Serialize(new { app_id = "com.myapp.game" });
var res = await userClient.PostAsync(
    "https://vaneltonmedia.com/api/achievements/list.php",
    new StringContent(payload, Encoding.UTF8, "application/json"));

using var doc = JsonDocument.Parse(await res.Content.ReadAsStringAsync());
var achievements = doc.RootElement.GetProperty("achievements");

3. Check a specific achievement

const res = await fetch('https://vaneltonmedia.com/api/achievements/check.php', {
  method: 'POST',
  headers: { 'Authorization': `${APP_ID}:${APP_KEY}`, 'Content-Type': 'application/json' },
  body: JSON.stringify({ slug: 'first_goal', app_id: 'com.myapp.game', token: userToken })
});
const { has, unlocked_at } = await res.json();
if (has) console.log('Unlocked on', unlocked_at);
r = requests.post(
    'https://vaneltonmedia.com/api/achievements/check.php',
    headers=HEADERS,
    json={'slug': 'first_goal', 'app_id': 'com.myapp.game', 'token': user_token}
).json()
print('has:', r['has'], '| unlocked_at:', r.get('unlocked_at'))

4. Leaderboard

Returns users ranked by total XP earned from achievements. Supports global ranking, per-app ranking, friends-only ranking, and pagination. See the Leaderboard reference for the full parameter list.

// Top 10 for your app
const res = await fetch('https://vaneltonmedia.com/api/achievements/leaderboard.php', {
  method: 'POST',
  headers: { 'Authorization': `${APP_ID}:${APP_KEY}`, 'Content-Type': 'application/json' },
  body: JSON.stringify({ app_id: 'com.myapp.game', limit: 10 })
});
const { entries, my_position } = await res.json();
// entries[].rank, entries[].username, entries[].xp_total, entries[].achievements_count

// Friends-only leaderboard (use user token as Authorization)
const res2 = await fetch('https://vaneltonmedia.com/api/achievements/leaderboard.php', {
  method: 'POST',
  headers: { 'Authorization': userToken, 'Content-Type': 'application/json' },
  body: JSON.stringify({ app_id: 'com.myapp.game', scope: 'friends' })
});
const { entries: friendEntries, my_position: myPos } = await res2.json();
// my_position.rank is always the global rank, regardless of scope
# Top 10 global
r = requests.post(
    'https://vaneltonmedia.com/api/achievements/leaderboard.php',
    headers=HEADERS,
    json={'app_id': 'com.myapp.game', 'limit': 10}
).json()
for e in r['entries']:
    print(f"#{e['rank']} {e['username']} — {e['xp_total']} XP")