ClanStop API V1 — Documentation
Add ClanStop gaming clans, members, and webhooks to your game, Discord bot, or integration. This page walks you from zero to your first successful API call in 3 minutes.
Quick start
Register your app
Email staff with your game name. We approve the app and send you a key like csk_a3f7…. Goes on your server, kept secret. One key per game.
Provision your players
When a player first uses a clan feature in your game, your server hits POST /game-users with their in-game ID. We return a per-player token. Cache it server-side, keyed by your player ID.
Make requests as that player
Every clan call from then on uses the cached token. Player never sees it. Player never logs in to clanstop.com unless they want to.
Integration architecture in one picture
┌─────────────────┐
Player Your game server │ ClanStop API │
│ │ └─────────────────┘
│ joins clan │ ▲
│ ─────────────────▶ │ lookup player's token │
│ │ ─────────────────────────────────────▶ │ X-API-Key: csk_…
│ │ │ X-API-Token: csu_…
│ │ (first time?) POST /game-users │
│ │ ─────────────────────────────────────▶ │
│ │ ◀──── { user_id, token } │
│ │ cache, then call POST /clans │
│ │ ─────────────────────────────────────▶ │
│ ◀── clan created │ ◀──── { clan_id } │The player's game ID becomes a ClanStop user. The token authenticates every subsequent call for that player. If you serve 10,000 players, that's 10,000 tokens your server holds.
CanIssueGameOnlyAccounts flag on a paid-tier app; email staff to enable it. Without either, players must register on the website first and manually link their game.
Your first request
Paste this into a terminal (replace YOUR_KEY):
curl -H "X-API-Key: YOUR_KEY" \
"https://clanstop.com/APIs/V1/games"You should get back a list of supported games as JSON. If you got an error, see error codes.
https://clanstop.com/APIs/V1. The base URL never changes inside V1; we'll bump to /APIs/V2 only for breaking changes.
Authentication
There are two kinds of credentials. Most calls need just the first.
1. App Key X-API-Key
Identifies your game. Use it on every request. Looks like csk_a3f7…. Sent as a header:
X-API-Key: csk_a3f7b9c4d8e1f3a6c5b2a1d9e8f7c4b32. User Token X-API-Token
Identifies a specific player. Required whenever an endpoint acts on someone's behalf — creating a clan, editing it, viewing private profile data. Looks like csu_a3f7…. Sent alongside the app key:
X-API-Key: csk_…
X-API-Token: csu_…X-API-Token instead of Authorization: Bearer?
Both are accepted by the server. Many shared-hosting setups (Apache + PHP-FPM, LiteSpeed defaults, some reverse proxies) silently strip the Authorization header at the FastCGI boundary before PHP can read it — including ClanStop's own production stack. Sending the token via the custom X-API-Token header bypasses that strip entirely. If your client lives somewhere with a well-behaved Apache/Nginx config, Authorization: Bearer csu_… works identically — use whichever fits your stack.
3. Server Key cssrv_
A server key is a third credential, used in place of the app key as X-API-Key for a single community-hosted game server. It belongs to a game (not your studio app), is minted self-service by the person running the server, and carries its own rate limit and quota. Looks like cssrv_a3f7….
This exists for the "many people, same game, different servers" world — modded/community dedicated servers where each host wants to credential their own box without sharing your studio key. A server key can provision players and act on a player's behalf for clan read/create/manage; it cannot edit game metadata, mint other keys, or dissolve clans.
How games get user tokens (the production path)
Your server calls POST /game-users with the player's in-game ID. We return a token. Cache it server-side indexed by the player's game ID. Every future API call for that player reuses the cached token — players never see anything.
// First time a player triggers a clan feature in your game
POST /APIs/V1/game-users
{ "external_user_id": "steamid:76561197960265728" }
// We respond with their ClanStop user_id + a token
{ "data": { "user_id": "846858771", "token": "csu_…", … } }
// Store on your server, keyed by your player id. From now on, when
// that player wants to do anything ClanStop-related, you look up the
// token and use it — without ever asking the player.This requires the CanIssueGameOnlyAccounts flag on your app. A sandbox app (create one at /account/api-apps) has it on by default for testing; production apps get it flipped by staff.
Linking an existing ClanStop account (the player isn't new)
The auto-provision flow above creates a fresh ClanStop account every time a new external_user_id appears. Players who already have a ClanStop account would end up with two separate profiles. The fix: have the player generate a short link code on their existing account and type it into the game.
// 1. Player goes to https://clanstop.com/gameprofiles (signed in)
// and clicks "Generate link code". They see something like:
// ABCD-1234 (expires in 9:58)
// 2. Player types ABCD-1234 (or ABCD1234) into your game's link UI.
// 3. Your server calls /game-users with the code on the same body.
POST /APIs/V1/game-users
{
"external_user_id": "steamid:76561197960265728",
"code": "ABCD1234",
"display_name": "PlayerOnSteam"
}
// We respond with the EXISTING user's id (no new account created).
// existing=true and is_game_only=false tell you this was a link, not a provision.
{
"data": {
"user_id": "846858771",
"existing": true,
"is_game_only": false,
"token": "csu_…",
"claim_token": null
}
}Codes are 8 chars from an unambiguous alphabet, single-use, 10-minute TTL. The code field is optional on POST /game-users — if absent, behavior is unchanged (new game-only account is auto-provisioned). If present, the link path runs instead.
Failure modes specific to linking:
| HTTP | Code | Meaning |
|---|---|---|
| 404 | code_not_found | Code doesn't match anything. Typo or stale. |
| 409 | code_already_used | Code was already consumed (one-shot) or cancelled. |
| 410 | code_expired | Code is past its 10-minute window. Have the player generate a new one. |
| 409 | already_linked_other_user | This external_user_id is already linked to a different ClanStop account. Player needs to use the other account's code, or unlink first. |
The manual token path (indie tools, Discord bots, dev scripts)
For projects where the operator is the player — running a Discord bot for their own clan, a personal dashboard, a modder utility — there's a self-service path: sign in to ClanStop, go to Account → API Tokens, pick your app, pick scopes, copy the token, paste into the tool. Not appropriate for distributing to non-technical players.
clan:create, clan:manage) — actions the token is allowed to take. The auto-provision path picks scopes via the scopes field on POST /game-users; the manual path picks them in the UI. Hitting an endpoint without the right scope returns scope_missing — re-provision (or have the player re-issue) with the missing scope.
Rotating an API key
If a key leaks, rotate it from /account/api-apps → your app → Rotate API key. We mint a fresh csk_… and slot the current key into a 7-day grace window: during that window both keys authenticate. Your integration can swap to the new one without dropping a single call.
While a request comes in on the prior key, the response carries:
X-API-Key-Status: prior; expires_in_seconds=453221; expires_in=5d6hThat's the signal to update your config — after the grace expires, the prior key returns auth_invalid.
Sample code
The same call (GET /games) in different languages. All other endpoints follow the same pattern.
curl -H "X-API-Key: csk_YOUR_KEY" \
"https://clanstop.com/APIs/V1/games"const resp = await fetch("https://clanstop.com/APIs/V1/games", {
headers: { "X-API-Key": "csk_YOUR_KEY" }
});
const data = await resp.json();
console.log(data);import requests
resp = requests.get(
"https://clanstop.com/APIs/V1/games",
headers={"X-API-Key": "csk_YOUR_KEY"}
)
print(resp.json())using UnityEngine;
using UnityEngine.Networking;
using System.Collections;
public class ClanStopExample : MonoBehaviour
{
IEnumerator Start()
{
using (var req = UnityWebRequest.Get("https://clanstop.com/APIs/V1/games"))
{
req.SetRequestHeader("X-API-Key", "csk_YOUR_KEY");
yield return req.SendWebRequest();
Debug.Log(req.downloadHandler.text);
}
}
}Endpoint reference
Every successful response has this shape:
{
"status": "success",
"data": { /* the actual payload */ },
"meta": { "pagination": { /* … */ } }
}Every error has this shape:
{
"status": "error",
"error": {
"code": "scope_missing",
"message": "…human readable…"
}
}Number. Always treat them as opaque strings in your code. Use "32777684916580", not 32777684916580.
GET /clans App key only
Search public clans. Returns up to 100 per page.
| Query | Type | Notes |
|---|---|---|
q | string | Searches Name + Tag. |
page | int | 1-indexed (default 1). |
limit | int | 1–100 (default 20). |
Example
curl -H "X-API-Key: csk_…" \
"https://clanstop.com/APIs/V1/clans?q=team&limit=5"GET /clans/{clan_id} App key only
One clan's public profile: name, tag, member count, banner, icon, games, owners, roster preview.
curl -H "X-API-Key: csk_…" \
"https://clanstop.com/APIs/V1/clans/32777684916580"GET /clans/{clan_id}/members App key only
Paginated public roster. Each row has user_id, display_name, joined_at, icon_url, and is_owner.
GET /games App key only
Catalog of every game ClanStop knows about. Use this when you let players tag their clan with games during creation.
GET /me me:read
Basic info about the player whose token you're using. Useful for greeting them by name in-game.
curl -H "X-API-Key: csk_…" \
-H "X-API-Token: csu_…" \
"https://clanstop.com/APIs/V1/me"GET /me/clans me:read
Clans the player is in, with an is_owner flag. Paginated like other lists.
POST /clans clan:create
Creates a new clan with the token-holder as founder. Required: name, tag, at least one game, at least one platform.
Example
curl -X POST "https://clanstop.com/APIs/V1/clans" \
-H "X-API-Key: csk_…" \
-H "X-API-Token: csu_…" \
-H "Content-Type: application/json" \
-d '{
"name": "Bob Squad",
"tag": "BSQ",
"short_description": "Just a chill clan.",
"games": ["apx"],
"platforms": ["pc"]
}'Returns { data: { clan_id, name, tag } }. The clan_id is what you'll use in every subsequent call.
PATCH /clans/{clan_id} clan:manage
Updates any subset of name, tag, short_description. Only provided fields change.
POST /clans/{clan_id}/members clan:read or clan:manage
Behavior is inferred from who is being added — there is no direct flag:
- Self-join (
user_id= the token-holder,clan:read): follows the clan's recruiting policy, exactly like the website apply flow. Open / invite-auto clans add the member active immediately (app_status: 1); approval-required clans create a pending application the owner can accept (app_status: 2); closed clans are rejected. - Admin-add (
user_id= someone else,clan:manage): instantly adds another player as an active member. Requires the token-holder to own/manage the clan; overrides the recruiting/closed gate.
Banned users (clan block list) are rejected in both cases.
// player joins (outcome depends on the clan's recruiting setting)
{ "user_id": "846858771" }
// owner adds another player directly (user_id != caller)
{ "user_id": "846858771" }DELETE /clans/{clan_id}/members/{user_id} clan:read (self) or clan:manage (kick)
If user_id matches the token-holder, this is "leave clan." If different, this is "kick" and requires clan:manage plus ownership.
PATCH /clans/{clan_id}/members/{user_id} clan:manage
Sets an active member's rank. The rank must belong to the clan and cannot be the owner rank — to move ownership, use Transfer. Same bar as transfer (clan:manage plus clan ownership). Queues the member's Discord role re-sync.
{ "rank_id": "42" }POST /clans/{clan_id}/transfer clan:manage
Hands ownership to another member. The current owner becomes a regular member; the new owner gains full admin rights.
{ "new_owner_id": "846858771" }DELETE /clans/{clan_id} clan:dissolve
Permanently closes the clan. This cannot be undone. Everyone is removed, the clan stops appearing in searches, and the name can't be reused.
clan:dissolve for your app to be able to do this. Tokens with just clan:manage can edit but not destroy.
POST /game-users App key only (requires staff approval)
Auto-creates a ClanStop account for a player who doesn't have one. Use it when a player wants to use clan features without leaving the game to sign up.
Testing? A sandbox app (/account/api-apps → "Sandbox") can call this immediately — no approval — up to 50 wipeable test accounts. For production, email staff and we'll flip CanIssueGameOnlyAccounts on your app. Without a sandbox or that flag you get a game_users_not_allowed error; past the sandbox cap you get sandbox_cap_reached.
Example
curl -X POST "https://clanstop.com/APIs/V1/game-users" \
-H "X-API-Key: csk_…" \
-H "Content-Type: application/json" \
-d '{
"external_user_id": "steamid:76561197960265728",
"display_name": "BobInGame",
"scopes": ["me:read", "clan:read", "clan:create"]
}'First call returns:
{
"data": {
"existing": false,
"user_id": "846858771",
"is_game_only": true,
"token": "csu_…", // store this
"claim_token": "a3f7…" // show this to player
}
}Subsequent calls for the same external_user_id return existing: true and token: null — store the token on the first call, you only get it once.
Show the claim_token to the player in your UI ("type this code on clanstop.com/claim-game-account to promote your account"). When they do, the same user_id becomes a full website account — every clan they joined survives the upgrade.
Games (write)
If your app owns a game in our catalog, you can keep its public metadata fresh from your own server — description, store/dev URLs, and a live concurrent-online counter. Ownership is assigned by staff at /superadmin/apis/{id}: only one app owns a given game at a time. Reading any game's metadata stays public (just X-API-Key); writes require the game:manage scope on the user token AND your app must be the owner.
GET /games/{slug} App key
Full metadata for one game. slug is the lowercase short_name from /games.
curl -H "X-API-Key: csk_…" \
"https://clanstop.com/APIs/V1/games/apx"Returns { id, name, short_name, slug, about, store_page_url, dev_page_url, current_online, online_reported_at, owned_by_app_id, stable_identity }. stable_identity (boolean) tells a server mod whether the game exposes a stable, game-wide player id — see Game servers.
PATCH /games/{slug} game:manage (and you must own it)
Update any subset of name, short_name, about, store_page_url, dev_page_url. Pass "" (empty string) to clear an optional field. Returns the updated row.
curl -X PATCH "https://clanstop.com/APIs/V1/games/apx" \
-H "X-API-Key: csk_…" \
-H "X-API-Token: csu_…" \
-H "Content-Type: application/json" \
-d '{
"about": "Apex Legends — battle royale by Respawn",
"store_page_url": "https://store.steampowered.com/app/1172470/",
"dev_page_url": "https://respawn.com/"
}'POST /games/{slug}/online game:manage (and you must own it)
Lightweight heartbeat. Designed for once-a-minute calls from your game backend. Bumps current_online + online_reported_at; nothing else moves. The website's game pages fade the value after about 30 minutes of silence so a dropped integration doesn't show a stale count forever.
curl -X POST "https://clanstop.com/APIs/V1/games/apx/online" \
-H "X-API-Key: csk_…" \
-H "X-API-Token: csu_…" \
-H "Content-Type: application/json" \
-d '{ "current_online": 138420 }'Returns { id, current_online, online_reported_at }. Failure code not_owner if your app isn't the assigned owner.
Game servers
For community-hosted dedicated servers running the same game. Each server credentials itself with a server key (cssrv_…), reports who's online, and clans automatically appear on the servers their members play on.
The studio model (one app, one key, one game) doesn't fit a game with hundreds of independently-run servers. So beneath a game sits a server layer:
- Game-scoped identity. A player's in-game id maps to one ClanStop user per game — the app key and every server key resolve the same player to the same
user_id. A clan follows the player across servers with no extra work. - Self-service server keys. A host mints a
cssrv_…key for the game with their own user token. No staff approval — the gate is a verified-email account and a per-host cap. - Per-server tokens. The first time a given server key provisions a given player it gets a fresh player token; later calls from that key for that player return
token: null. One player can hold several tokens (one per server they've touched), each independently revocable. - Last-seen presence. Membership and roles stay clan-wide. The only per-server thing tracked is "this clan was recently active on this server," fed by a heartbeat.
Scopes
| Scope | On | Grants |
|---|---|---|
server:manage | a user token | Register and delete your own game servers. Pick it when issuing a token at Account → API Tokens. |
game_user:provision | the server key (fixed) | Provision/resolve players for the game. Frozen onto the key at creation. |
server:telemetry | the server key (fixed) | Write the server's own heartbeat. Frozen onto the key at creation. |
A server key also carries clan:read, clan:create, and clan:manage so it can act for the player whose token it holds. It deliberately does not carry game:manage, clan:dissolve, or the ability to mint other keys.
Mod provisioning branch
On player join, the server makes one decision driven by the game's stable_identity flag (read it from GET /games/{slug}):
// stable_identity = true → a game-wide id exists (SteamID64, Epic id, …)
external_user_id = "steamid:76561197960265728" // same on every server
// → provision under (game, id). Clan shows up everywhere. No link step, ever.
// stable_identity = false → only session/slot ids exist
external_user_id = "local:{server_id}:{slot}" // namespaced to this server
// → provision, and if the player expects a clan, prompt for a link code.
// Linking is per-server in this case — stable ids are strongly preferred.POST /games/{slug}/servers server:manage (user token)
Registers a server for the game and returns a server key, shown once. Authenticate with the host's own user token (any valid app key + their csu_… with server:manage). The host account must have a verified email and may hold at most 5 active server keys; the game's plan also caps the total active community servers across all hosts.
| Body | Type | Notes |
|---|---|---|
name | string | Required, 1–120 chars. Shown in the public server list. |
host_note | string | Optional, ≤255 chars. e.g. "us-east, modded". |
Example
curl -X POST "https://clanstop.com/APIs/V1/games/7d2d/servers" \
-H "X-API-Key: csk_…" \
-H "X-API-Token: csu_…" \
-H "Content-Type: application/json" \
-d '{ "name": "Bob'\''s 24/7", "host_note": "us-east, modded" }'{
"data": {
"server_id": "5",
"game_id": 138,
"name": "Bob's 24/7",
"api_key": "cssrv_…", // store it — shown only once
"scopes": ["game_user:provision", "clan:read", "clan:create", "clan:manage", "server:telemetry"],
"rate_limit_per_min": 120,
"daily_quota": 50000
}
}Failure codes: email_unverified (403), server_cap_reached (409), game_not_owned (409, the game has no owning app).
POST /game-users Server key (cssrv_)
Same endpoint as Game users — but sent with a cssrv_ key as X-API-Key (no user token). Resolves the player game-wide and, the first time this server key sees them, mints a player token scoped to the key. The response adds token_minted so the mod can tell "I already have a token for this player on this server" from "this player is brand-new to me."
curl -X POST "https://clanstop.com/APIs/V1/game-users" \
-H "X-API-Key: cssrv_…" \
-H "Content-Type: application/json" \
-d '{ "external_user_id": "steamid:76561197960265728" }'First call → { user_id, token: "csu_…", token_minted: true }. Repeat from the same key → { user_id, token: null, token_minted: false }. The minted token is clamped to me:read + the clan scopes — a server can't hand a player a token that edits game metadata.
POST /servers/{server_id}/heartbeat Server key (cssrv_)
Called ~once a minute by the running server with its own key. Reports who's online; ClanStop derives each active player's clan(s) and stamps clan ↔ server presence. Also records the server's own online count + tick rate (per-server telemetry — the studio's game-wide current_online is separate).
| Body | Type | Notes |
|---|---|---|
active_user_ids | array | ClanStop user ids currently online. Max 500 per call. |
current_online | int | Required, ≥ 0. This server's player count. |
tick_rate | number | Optional, 0–1000. Server sim tick rate. |
curl -X POST "https://clanstop.com/APIs/V1/servers/5/heartbeat" \
-H "X-API-Key: cssrv_…" \
-H "Content-Type: application/json" \
-d '{ "active_user_ids": ["846858771","771203…"], "current_online": 24, "tick_rate": 63.8 }'Returns { server_id, current_online, clans_touched, reported_at }. A key may only heartbeat its own server_id; a plain app key is rejected with forbidden.
GET /games/{slug}/servers App key only
Public server browser for a game — active servers, most-recently-online first. Paginated. Never exposes key material or the host's user id.
curl -H "X-API-Key: csk_…" \
"https://clanstop.com/APIs/V1/games/7d2d/servers"Each row: { server_id, name, host_note, current_online, online_reported_at, tick_rate, is_active_now, created_at }. is_active_now is true when the last heartbeat was within ~30 minutes.
GET /clans/{clan_id}/servers App key only
The servers a clan plays on, most-recently-seen first. Honors clan visibility — a key whose app can't see the clan gets a 404.
curl -H "X-API-Key: csk_…" \
"https://clanstop.com/APIs/V1/clans/32777684916580/servers"Returns an array of { server_id, name, last_seen_at, is_active_now }. "Plays on" = the row exists; "active now" = seen within ~30 minutes.
DELETE /games/{slug}/servers/{server_id} server:manage (owner)
Revokes a server key immediately. Must be the host who registered it (your user token's user is the owner). Players, their tokens, and clan presence rows survive — they're game-scoped, not key-scoped.
curl -X DELETE "https://clanstop.com/APIs/V1/games/7d2d/servers/5" \
-H "X-API-Key: csk_…" \
-H "X-API-Token: csu_…"Returns { server_id, status: "revoked" }. The key stops authenticating at once.
Webhooks
Instead of polling for clan / member / link changes, register a URL and ClanStop will POST signed events to it as they happen. Endpoints are registered staff-side at /superadmin/apis/{id} — pick a URL, pick the events you want, copy the secret once.
Event types
| Event | Fires when |
|---|---|
clan.created | A clan was created via your app's API key. |
clan.updated | Clan name / tag / short_description changed (via API or website). |
clan.dissolved | Clan was dissolved. |
clan.member_joined | User became an active member. |
clan.member_left | User left or was removed. |
clan.transferred | Clan ownership transferred to another user. |
clan.member_rank_changed | A member's rank was changed via PATCH /clans/{id}/members/{user_id}. |
game_user.created | A new game-only ClanStop account was auto-provisioned via POST /game-users. |
link.completed | A player linked an existing ClanStop account to your game via an OTP code. |
game.metadata_updated | A game your app owns had its metadata changed via PATCH /games/{slug}. |
game.online_reported | A game your app owns posted an online-count heartbeat. Throttled to once per 5 min per game so a minute-frequency cron doesn't flood subscribers. |
Untick events at registration time to opt out of noise. Untick all (or pass *) to subscribe to everything.
Payload shape
POST <your URL>
Content-Type: application/json
User-Agent: ClanStop-Webhook/1
X-ClanStop-Event: clan.member_joined
X-ClanStop-Event-Id: <32-hex> // stable across retries — dedupe on this
X-ClanStop-Delivery: <attempt id>
X-ClanStop-Attempt: 1 // 1..6
X-ClanStop-Signature: sha256=<hex> // HMAC-SHA256 of the raw body
{
"event": "clan.member_joined",
"event_id": "6f3a…",
"sent_at": 1717059823,
"app_id": 42,
"data": {
"clan_id": "29401576424788",
"user_id": "846858771",
"actor_id": "846858771",
"direct": false // true when an admin added someone else
}
}Verifying the signature
On every delivery we send X-ClanStop-Signature: sha256=<hex> where hex is the HMAC-SHA256 of the raw request body, using the endpoint's secret as the key. Recompute and compare in constant time before trusting the payload — never just ==.
const crypto = require("crypto");
function verify(rawBody, signatureHeader, secret) {
const expected = "sha256=" + crypto.createHmac("sha256", secret)
.update(rawBody)
.digest("hex");
const a = Buffer.from(expected, "utf8");
const b = Buffer.from(signatureHeader, "utf8");
return a.length === b.length && crypto.timingSafeEqual(a, b);
}import hmac, hashlib
def verify(raw_body: bytes, signature_header: str, secret: str) -> bool:
expected = "sha256=" + hmac.new(secret.encode(), raw_body, hashlib.sha256).hexdigest()
return hmac.compare_digest(expected, signature_header)function verify(string $raw_body, string $signature_header, string $secret): bool {
$expected = "sha256=" . hash_hmac("sha256", $raw_body, $secret);
return hash_equals($expected, $signature_header);
}Rotating the signing secret
If the secret leaks, rotate it on /account/api-apps → your app → webhook row → Rotate. A fresh secret is minted; the old one keeps signing for 7 days. During that window every delivery carries two signature headers:
X-ClanStop-Signature: sha256=<hex with new secret>
X-ClanStop-Signature-Prior: sha256=<hex with old secret>Update your verifier to accept either during a rotation. After the grace, only the new header is sent. Pseudocode:
function verify(raw_body, secret_now, secret_prior_or_null) {
const sig = header("X-ClanStop-Signature");
const sig_prior = header("X-ClanStop-Signature-Prior");
if (verify_one(raw_body, sig, secret_now)) return true;
if (secret_prior_or_null && sig_prior &&
verify_one(raw_body, sig_prior, secret_prior_or_null)) return true;
return false;
}Retries
Your endpoint must respond 2xx within 10 seconds. Any non-2xx (or no response) triggers a retry. Schedule:
- Attempt 1: immediate (≤1 min)
- Attempt 2: 1 minute later
- Attempt 3: 5 minutes
- Attempt 4: 30 minutes
- Attempt 5: 2 hours
- Attempt 6: 6 hours
- Attempt 7 (final): 24 hours — if this fails, the delivery is dead-lettered and we stop retrying
Dead-letters are visible in /superadmin/apis/{id} → Recent Deliveries panel so staff can spot endpoints that are silently broken. Use X-ClanStop-Event-Id to dedupe — same event id across all retries of the same logical event.
Error codes
Every error response has a stable error.code string. Branch on this, not the human-readable message (which we may polish over time).
| HTTP | Code | What to do |
|---|---|---|
| 401 | auth_required | Missing X-API-Key, or the endpoint needs a user token and you didn't send one. Add X-API-Token: csu_… (or Authorization: Bearer csu_…). |
| 401 | auth_invalid | The key or token doesn't exist (or was revoked). Get a new one. |
| 403 | app_suspended | Your app is paused. Email staff. |
| 403 | app_not_active | App is still pending approval. Wait for the OK. |
| 403 | scope_missing | The user's token doesn't grant the scope this endpoint needs. Ask them to re-issue with the right scope. |
| 403 | permission_denied | The user is authenticated but doesn't have permission on this clan (e.g. trying to edit a clan they don't own). |
| 403 | game_users_not_allowed | Your app isn't approved to auto-create accounts. Use a sandbox app to test, or ask staff to enable it for production. |
| 403 | sandbox_cap_reached | A sandbox app hit its test-account cap (50). Wipe the app's sandbox data from /account/api-apps, or upgrade for production auto-provisioning. |
| 403 | not_owner | Games-write call from an app that doesn't own the game, or a server-delete from someone who isn't the server's host. Ask staff to assign game ownership at /superadmin/apis. |
| 403 | email_unverified | Registering a server needs a ClanStop account with a verified email. Game-only / unclaimed accounts can't mint server keys. |
| 403 | forbidden_for_server_key | A cssrv_ key tried something reserved for app keys / user tokens (editing game metadata, minting or deleting keys). |
| 403 | forbidden | Server-key call on the wrong target — e.g. heartbeating a server_id the key doesn't own, or a plain app key hitting a server-only endpoint. |
| 409 | server_cap_reached | A server-key cap was hit: either your personal limit (5 active keys), or the game's plan limit on total active community servers. Delete one, or the game owner can raise it by upgrading the game's API tier. |
| 409 | game_not_owned | Tried to register a server for a game that has no owning app yet. |
| 409 | game_not_resolved | The key isn't tied to a game, so a player can't be provisioned. Staff assigns game ownership. |
| 400 | validation_error | Your payload is malformed. The message tells you which field. |
| 404 | not_found | The clan, user, or other resource doesn't exist. |
| 409 | reserved_name | The clan name or tag is reserved (e.g. a pro team's name). Pick another. |
| 409 | already_member | The user is already in the clan. |
| 409 | application_pending | There's already a pending application for this user. |
| 429 | rate_limited | Too many requests in the last minute. Wait retry_after_seconds. |
| 429 | quota_exceeded | You hit today's daily call cap. Resumes at UTC midnight. |
| 500 | internal_error | Our problem. If it persists, ping staff with the timestamp. |
| 404 | not_implemented | The path exists in our route table but the endpoint isn't built yet. Wait for the changelog. |
Rate limits
Two limits run in parallel:
- Per-minute: 600 requests/minute by default. Going over returns
rate_limited. - Daily quota: 100,000 calls/day by default. Going over returns
quota_exceededuntil UTC midnight.
Both numbers can be raised per-app — ask staff if your game needs more.
Headers on every response
X-RateLimit-Limit: 600 # your cap
X-RateLimit-Remaining: 487 # left in this window
X-RateLimit-Reset: 1717012345 # unix time when the window resetsCache and reuse responses when you can. The /games catalog rarely changes — cache it for an hour and you'll save thousands of calls.
FAQ
Wait — do players really have to paste a token into my game?
No, and they shouldn't. That's the manual path, useful for Discord bots and modder utilities, not for commercial games. For a real game the flow is server-side: your game's backend calls POST /game-users the first time a player triggers a clan feature, gets back a token, caches it. The player never sees it, never logs in to clanstop.com unless they want to. See the quick start diagram.
What if my game doesn't have a backend server?
If the game is entirely client-side, the X-API-Key would end up shipped inside the client binary, where it can be extracted by anyone. That's a problem — your key authenticates every call as your game, and an extracted key lets a third party impersonate your game's traffic. Two reasonable options: (1) stand up a thin proxy server that holds the key and brokers calls, or (2) accept the risk and rotate keys when they leak. For anything published commercially, option 1 is the right answer.
Why do I get a different clan_id than what I see in the URL bar?
You shouldn't. Clan IDs are 14-digit strings. If a number looks shorter, you might be parsing them as JavaScript Numbers and losing precision. Treat IDs as strings end-to-end.
My player created a clan but it's not showing in /search?
The public search excludes brand-new clans for a few minutes while indexes catch up. Hitting GET /clans/{id} directly should work immediately.
What's the difference between "game-only" and "real" accounts?
Mechanically — nothing. Both can join clans, post, get banned, etc. The differences:
- A real account has an email + password and can sign in to
clanstop.com. - A game-only account exists only inside your game until the player claims it via /claim-game-account using the
claim_token. - Once claimed, it's a real account. Same UserID. No data lost.
Can I see clans my player created but hasn't joined?
The creator becomes founder automatically — they're always a member of their own clan. GET /me/clans will list it.
How do I test without burning quota?
Read endpoints are cheap and won't blow through your quota during dev. Use them to sanity-check your integration before wiring up writes.
How do community-hosted game servers authenticate?
A server host mints a server key (cssrv_…) for a game via POST /games/{slug}/servers using their own user token. The server then sends that key as X-API-Key. It belongs to the game, carries its own rate limit and quota, and can be revoked without affecting players or clans. See Game servers.
Does a player's clan follow them across different servers?
Yes. Player identity is keyed to the game, not to an individual key, so the game's app key and every server key resolve the same player to the same ClanStop user. A clan shows up automatically on any server its members are active on — the next heartbeat that sees an active member adds the clan to that server's presence list.
What is a stable identity and why does it matter?
If a game exposes a stable, game-wide player id (SteamID64, Epic account id, etc.), servers provision under (game_id, external_user_id) with no link step and the clan appears everywhere automatically. Without a stable id, servers namespace ids per server (local:{server_id}:{slot}) and fall back to one-time link codes — which is why stable ids are strongly preferred.
How do I get my key revoked / quota raised / app approved for game-only accounts?
Email ClanStop staff. We'll respond within a business day.