TopSites: API

The directory is readable through the standard XenForo REST API, and a listing's owner can additionally read that listing's votes. Same base path, same authentication, same error shape as the rest of the forum.

Contents

Your listing key

Everything on this page uses one key, and it is yours: you generate it on your own listing, no staff involvement, and it works for that listing only. Treat it like a password - anyone holding it can ask whether a given member voted for you.

Getting your listing key

  1. Open your listing and press Manage listing.
  2. Go to the Rewards tab.
  3. Next to Listing key, press Generate key.

Copy it somewhere safe. Treat it like a password: anyone holding it can ask whether a given member voted for your listing.

If it leaks, press Regenerate key on the same page. The old key stops working immediately, so update your server before you regenerate, or your reward checks will start failing.

Using it

Send it as a header, or as a query parameter if your game server can only manage a URL:

Bash
# header (preferred - stays out of access logs)
curl -H 'RZ-Listing-Key: YOUR_LISTING_KEY' \
  'https://forum.ragezone.com/topsites/51/vote-check?user_id=12'

# query parameter
curl 'https://forum.ragezone.com/topsites/51/vote-check?key=YOUR_LISTING_KEY&user_id=12'

Checking a single voter

The endpoint most reward integrations actually want. One player, one answer, using your listing key.

HTTP
GET https://forum.ragezone.com/topsites/{listing_id}/vote-check
key Your listing key, unless you sent the RZ-Listing-Key header.
ref Your own player reference, exactly as you put it on the vote link. The way to ask about a player with no forum account. Checked first when present.
user_id Forum user id. Preferred over username - a member can change their name but not their id.
username Used only when user_id is absent.
period YYYY-MM in UTC. Defaults to the current period.
JSON
{
    "content_id": 51,
    "user_id": 12,
    "period": "2026-08",
    "voted": true,
    "vote_date": 1787416627,
    "vote_source": "site"
}

voted is the answer; the rest is context. A withdrawn vote returns false.

403 invalid_key - key missing, wrong, or not generated yet.
400 no_user or bad_period.
Fail closed. If the request errors or times out, do not grant the reward. A player waiting a moment longer is better than a free reward every time your network hiccups.

Push or pull: choosing an integration

There are two ways to reward a voter, and they are not mutually exclusive.

Push - the vote callback We call your server the moment somebody votes. Instant, no polling, no API key. But delivery can fail while your server is down, and you have to run a public HTTPS endpoint and verify signatures.
Pull - vote-check Your server asks us when it needs to know. No public endpoint, no signature handling, nothing to miss during an outage. But it only happens when a player asks, so you need a button or a command.
Use both. The callback grants the reward immediately for everyone who is online. The vote-check button is how a player who voted during your maintenance window claims what they are owed, without opening a ticket.

Worked example: an in-game vote reward button

The player clicks "claim vote reward" on your website, in your launcher, or via an in-game command. Your server does this:

PHP
<?php

const RZ_BASE   = 'https://forum.ragezone.com';
const RZ_KEY    = 'your-listing-key';
const RZ_ITEM   = 51;              // your listing id

/**
 * Ask whether this forum member has voted for us this period.
 */
function hasVoted(int $forumUserId): bool
{
    $url = RZ_BASE . '/topsites/' . RZ_ITEM . '/vote-check?user_id=' . $forumUserId;

    $ch = curl_init($url);
    curl_setopt_array($ch, [
        CURLOPT_RETURNTRANSFER => true,
        CURLOPT_HTTPHEADER     => ['RZ-Listing-Key: ' . RZ_KEY],
        CURLOPT_TIMEOUT        => 5,
    ]);

    $body = curl_exec($ch);
    $code = curl_getinfo($ch, CURLINFO_HTTP_CODE);
    curl_close($ch);

    if ($code !== 200) {
        // Never fail open. If you cannot confirm the vote, do not pay out.
        error_log('rz vote-check failed: HTTP ' . $code);
        return false;
    }

    $data = json_decode($body, true);

    return !empty($data['voted']);
}

/**
 * Claim, once per period. The unique key does the work - two clicks in the
 * same second cannot both win.
 */
function claimVoteReward(PDO $db, int $gameAccountId, int $forumUserId): string
{
    $period = gmdate('Y-m');   // must be UTC, to match the vote period

    if (!hasVoted($forumUserId)) {
        return 'not_voted';
    }

    $stmt = $db->prepare(
        'INSERT IGNORE INTO vote_rewards (forum_user_id, vote_period, rewarded_at)
         VALUES (?, ?, ?)'
    );
    $stmt->execute([$forumUserId, $period, time()]);

    if ($stmt->rowCount() === 0) {
        return 'already_claimed';
    }

    $db->prepare('UPDATE accounts SET vote_points = vote_points + 1 WHERE id = ?')
       ->execute([$gameAccountId]);

    return 'granted';
}

The same vote_rewards table serves both integrations, so a player who was already rewarded by the callback gets already_claimed rather than a second payout. That shared table is the whole trick.

Showing the player where they stand

PHP
$period = gmdate('Y-m');

$stmt = $db->prepare('SELECT rewarded_at FROM vote_rewards WHERE forum_user_id = ? AND vote_period = ?');
$stmt->execute([$forumUserId, $period]);

if ($stmt->fetchColumn()) {
    $message = 'You have claimed this month. Next reward on the 1st.';
} else {
    $message = 'Vote now to claim this month’s reward.';
}

Caching and rate limits

  • Do not poll vote-check on a timer. Call it when a player asks. A loop over every account is the fastest way to get a key revoked.
  • Cache a positive answer. Once voted is true for a period it stays true unless the voter withdraws. Store it and stop asking.
  • Use /votes for reconciliation, not for live checks. One call after an outage beats a thousand individual lookups.
  • Set a short timeout - 5 seconds is plenty - and fail closed. A player waiting on a spinner is better than a player getting a free reward because your check timed out.

Errors

Standard XenForo shape:

JSON
{ "errors": [ { "code": "...", "message": "...", "params": [] } ] }
401 Missing or invalid API key.
403 The key is wrong, or the listing is not yours.
400 No ref, user_id or username supplied, or a period that is not YYYY-MM.
404 Unknown listing, or a page past the last one.

What the API deliberately does not do

  • No writing. Listings are managed through the site.
  • No voting with a key. No API key can cast a vote - a key that could would be a vote-stuffing tool with a support contract. The public vote endpoint below is a different thing: it takes no key at all and counts the player's own device as the voter, which is what keeps it honest.
  • No reading other people's voters. Ownership is enforced on every vote endpoint.

Letting players vote from your launcher

POST /topsites/<id>/api-vote casts a vote. It is the one endpoint that needs no account and no key, so a player who has never registered here can still vote for you.

Every vote it accepts fires your vote callback exactly like a vote cast on the site, so whatever you already hand out in game keeps working - you do not need to change anything on your side beyond adding the button.

Bash
curl -X POST \
  -H 'X-RaGEZONE-TopSites: 1' \
  -d 'ref=acct_88214' \
  'https://forum.ragezone.com/topsites/51/api-vote'

Call it from the player's PC, not from your game server

The voter is the address the request arrives from. There is no parameter that changes that, and there will not be one: an endpoint that accepts a claimed identity is one where an owner can manufacture their own votes, and a ranking that can be forged is worth nothing to the players reading it.

The ref parameter is not that. It is your own reference for the player, echoed back to you in the callback and answerable through vote-check - it never decides who is voting, never attributes the vote to a forum member, and buys the caller nothing but its own echo. Up to 64 characters: letters, digits, dot, dash, underscore.

So a game server that calls this for all its players will see every vote arrive from one address, and all but the first refused. That is the endpoint working. Put the call in your launcher, your in-game browser, or simply link players to the site.

The header

X-RaGEZONE-TopSites: 1 is required. It is not a password. It is there because this endpoint takes no CSRF token, and without the header any web page could quietly submit a hidden form and vote from every browser that loaded it. Browsers will not let a form set a custom header; your launcher sets it in one line.

Limits

  • One vote per listing, per address, per vote period - and a period is a UTC calendar month. This is the limit that decides how often your players can vote for you: not a number of hours.
  • A daily cap on how many votes one address may cast across the directory.
  • The board's vote flood check.

A successful vote comes back with next_vote_at, the moment that address becomes eligible again, and next_vote_in_hours, the same thing counted in hours. Both point at the turn of the month. Use them rather than assuming a fixed interval - on the 2nd, the answer is nearly thirty days.

Every attempt is logged, accepted or not.

What comes back

HTTPstatusMeaning
200countedThe vote was recorded
400header_requiredThe header was missing
403disabledTurned off by an administrator
403not_allowedGuests may not vote for this listing
405post_requiredYou used GET
409already_votedAlready voted this period
429refusedA rate limit stopped it - wait and retry

refused does not say which limit caught you, on purpose. Telling a bot what stopped it is telling it what to change.

Back