ClanStopAPI
Back to site My tokens

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.

Who's this for? Game developers building an integration. Your game's server talks to this API on behalf of players — players themselves don't see tokens or touch URLs. The server-to-server model is the primary path; the manual-token path further down is for personal tools, Discord bots, and indie/modder scripts.

Quick start

1

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.

2

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.

3

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.

Test it free in Sandbox first. Create a sandbox app at /account/api-apps (tick "Sandbox") and auto-provisioning works immediately — no staff approval — capped at 50 wipeable test accounts. For production, auto-creating ClanStop users needs the 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.

About URLs. All endpoints sit under 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_a3f7b9c4d8e1f3a6c5b2a1d9e8f7c4b3

2. 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_…
Why 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.

One player, one identity, every server. Player identity is keyed to the game, so your app key and every server key under that game resolve the same in-game id to the same ClanStop user. That's what lets a clan follow a player across servers automatically. Full details and endpoints in Game servers.

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:

HTTPCodeMeaning
404code_not_foundCode doesn't match anything. Typo or stale.
409code_already_usedCode was already consumed (one-shot) or cancelled.
410code_expiredCode is past its 10-minute window. Have the player generate a new one.
409already_linked_other_userThis 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.

Scopes. Every token carries a list of scopes (e.g. 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=5d6h

That'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…"
  }
}
IDs are strings. Clan IDs and user IDs are 14-digit numbers that don't fit in a JavaScript 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.

QueryTypeNotes
qstringSearches Name + Tag.
pageint1-indexed (default 1).
limitint1–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.

We made dissolve a separate scope on purpose. A player has to explicitly grant 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

ScopeOnGrants
server:managea user tokenRegister and delete your own game servers. Pick it when issuing a token at Account → API Tokens.
game_user:provisionthe server key (fixed)Provision/resolve players for the game. Frozen onto the key at creation.
server:telemetrythe 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.

BodyTypeNotes
namestringRequired, 1–120 chars. Shown in the public server list.
host_notestringOptional, ≤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).

BodyTypeNotes
active_user_idsarrayClanStop user ids currently online. Max 500 per call.
current_onlineintRequired, ≥ 0. This server's player count.
tick_ratenumberOptional, 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

EventFires when
clan.createdA clan was created via your app's API key.
clan.updatedClan name / tag / short_description changed (via API or website).
clan.dissolvedClan was dissolved.
clan.member_joinedUser became an active member.
clan.member_leftUser left or was removed.
clan.transferredClan ownership transferred to another user.
clan.member_rank_changedA member's rank was changed via PATCH /clans/{id}/members/{user_id}.
game_user.createdA new game-only ClanStop account was auto-provisioned via POST /game-users.
link.completedA player linked an existing ClanStop account to your game via an OTP code.
game.metadata_updatedA game your app owns had its metadata changed via PATCH /games/{slug}.
game.online_reportedA 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).

HTTPCodeWhat to do
401auth_requiredMissing 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_…).
401auth_invalidThe key or token doesn't exist (or was revoked). Get a new one.
403app_suspendedYour app is paused. Email staff.
403app_not_activeApp is still pending approval. Wait for the OK.
403scope_missingThe user's token doesn't grant the scope this endpoint needs. Ask them to re-issue with the right scope.
403permission_deniedThe user is authenticated but doesn't have permission on this clan (e.g. trying to edit a clan they don't own).
403game_users_not_allowedYour app isn't approved to auto-create accounts. Use a sandbox app to test, or ask staff to enable it for production.
403sandbox_cap_reachedA sandbox app hit its test-account cap (50). Wipe the app's sandbox data from /account/api-apps, or upgrade for production auto-provisioning.
403not_ownerGames-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.
403email_unverifiedRegistering a server needs a ClanStop account with a verified email. Game-only / unclaimed accounts can't mint server keys.
403forbidden_for_server_keyA cssrv_ key tried something reserved for app keys / user tokens (editing game metadata, minting or deleting keys).
403forbiddenServer-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.
409server_cap_reachedA 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.
409game_not_ownedTried to register a server for a game that has no owning app yet.
409game_not_resolvedThe key isn't tied to a game, so a player can't be provisioned. Staff assigns game ownership.
400validation_errorYour payload is malformed. The message tells you which field.
404not_foundThe clan, user, or other resource doesn't exist.
409reserved_nameThe clan name or tag is reserved (e.g. a pro team's name). Pick another.
409already_memberThe user is already in the clan.
409application_pendingThere's already a pending application for this user.
429rate_limitedToo many requests in the last minute. Wait retry_after_seconds.
429quota_exceededYou hit today's daily call cap. Resumes at UTC midnight.
500internal_errorOur problem. If it persists, ping staff with the timestamp.
404not_implementedThe 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_exceeded until 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 resets

Cache 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.