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
| Field | Type | Description |
|---|---|---|
| slug | string | Unique identifier within your app. Only [a-z0-9_-], no spaces. E.g. first_kill, win-streak-10 |
| name | string | Display name shown to users |
| description | string | What the user must do to unlock it |
| icon | string | URL of the achievement icon |
| rarity | enum | common, uncommon, rare, epic, legendary |
| xp | int | Experience points granted on unlock (0 = no XP) |
| secret | bool | Hidden until unlocked — shows as ??? in the public list |
Rarity guide
| Rarity | Suggested use |
|---|---|
common | Easy, accessible to most players |
uncommon | Requires some effort |
rare | Challenging — few players will have it |
epic | Very hard, requires real skill |
legendary | Extremely 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
| Endpoint | Action |
|---|---|
POST /api/achievements/create.php | Create an achievement |
POST /api/achievements/update.php | Update an achievement |
POST /api/achievements/delete.php | Delete an achievement |
POST /api/achievements/grant.php | Unlock an achievement for a user |
POST /api/achievements/revoke.php | Remove an unlock from a user |
POST /api/achievements/list.php | List all achievements for an app |
POST /api/achievements/user.php | List achievements unlocked by a user |
POST /api/achievements/check.php | Check if a user has a specific achievement |
POST /api/achievements/leaderboard.php | XP-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")