XTREMETOP300

Vote reward API

Hand your players an in-game reward for voting. Two ways to receive votes, both using one key, and both designed to work on a server that is behind NAT or restarts twice a day.

Which one do I use?

Pull — start here

Your server asks us every minute or two what it owes. Needs no open port, no domain and no TLS certificate. Works from a home connection.

Push — if you have a web endpoint

We POST to your URL the moment a vote lands, signed so you can prove it is us. Instant, but only if we can reach you.

You can run both. A reward pushed to your callback stays claimable by pulling, so a missed push is never a lost reward.

1. Get your key

Open your listing under My servers → Vote rewards, switch rewards on, and copy the API key. Send it as a header on every call:

Authorization: Bearer YOUR_API_KEY

Never put the key in a URL. It ends up in access logs, proxy logs and browser history.

2. Read what you owe

POST /api/v1/rewards/pending returns unclaimed rewards, oldest first.

curl -X POST https://xtremetop300.com/api/v1/rewards/pending \
  -H "Authorization: Bearer YOUR_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{"limit": 50}'

{
  "server":  { "id": 1234, "name": "My Server" },
  "count":   2,
  "more":    false,
  "rewards": [
    { "token": "8f2c...", "player": "Steve",  "server_id": 1234, "voted_at": "2026-08-25T09:14:02+00:00" },
    { "token": "1a90...", "player": "Alexia", "server_id": 1234, "voted_at": "2026-08-25T09:31:44+00:00" }
  ]
}

Reading does not consume anything. Grant the rewards, then confirm them:

curl -X POST https://xtremetop300.com/api/v1/rewards/claim \
  -H "Authorization: Bearer YOUR_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{"tokens": ["8f2c...", "1a90..."]}'

{ "claimed": ["8f2c...", "1a90..."], "already_claimed": [], "unknown": [] }
Why two calls? If your server crashes between reading and paying out, the reward is still there on the next tick. Claiming is what says “the player has it”. If you would rather make one call, send "claim": true with the pending request — you get the rewards and they are marked given immediately, and a crash at the wrong moment loses them.

3. Or just ask a yes/no question

Most plugins only want to know whether to nag someone at login. GET /api/v1/rewards/check answers by player name or by IP address.

curl "https://xtremetop300.com/api/v1/rewards/check?player=Steve&hours=24" \
  -H "Authorization: Bearer YOUR_API_KEY"

{ "voted": true, "voted_at": "2026-08-25T09:14:02+00:00", "window_hours": 24 }

4. Push, if you want it instant

Set a callback URL in your reward settings and we POST this to it as each vote lands:

POST /your-callback HTTP/1.1
Content-Type: application/json
X-Xtreme300-Signature: sha256=4f1e...
X-Xtreme300-Timestamp: 1787654321
X-Xtreme300-Delivery: 8f2c...

{
  "event":   "vote.reward",
  "sent_at": 1787654321,
  "reward":  { "token": "8f2c...", "player": "Steve", "server_id": 1234, "voted_at": "..." }
}

Reply with any 2xx status. Anything else is retried 5 times over the following hour, and after that the reward simply waits to be pulled. A 4xx is treated as a refusal and not retried, so do not return one for a temporary problem.

5. Verify the signature

Anyone can POST to your callback URL. The signature is what separates a real vote from a stranger handing themselves diamonds. Compute HMAC-SHA256(timestamp + "." + rawBody) with your API key and compare it to the header.

// PHP
$body      = file_get_contents('php://input');   // the raw body, before json_decode
$timestamp = (int) $_SERVER['HTTP_X_XTREME300_TIMESTAMP'];
$signature = $_SERVER['HTTP_X_XTREME300_SIGNATURE'];

// Reject old requests first. A valid signature on a captured request replayed
// a thousand times is the whole attack.
if (abs(time() - $timestamp) > 300) {
    http_response_code(400);
    exit;
}

$expected = 'sha256=' . hash_hmac('sha256', $timestamp . '.' . $body, $apiKey);

// hash_equals, not ==. A normal comparison returns faster the earlier it finds
// a difference, which is enough to guess the signature one byte at a time.
if (! hash_equals($expected, $signature)) {
    http_response_code(401);
    exit;
}
// Java (Bukkit / Paper / Spigot)
Mac mac = Mac.getInstance("HmacSHA256");
mac.init(new SecretKeySpec(apiKey.getBytes(StandardCharsets.UTF_8), "HmacSHA256"));

byte[] digest = mac.doFinal((timestamp + "." + rawBody).getBytes(StandardCharsets.UTF_8));

StringBuilder hex = new StringBuilder("sha256=");
for (byte b : digest) hex.append(String.format("%02x", b));

if (!MessageDigest.isEqual(hex.toString().getBytes(), signatureHeader.getBytes())) {
    return; // not from us
}

6. Send players a link that already knows them

Add the player’s name to the vote link and the field arrives filled in, so they only have to click. Any of player, pingUsername, username or nick works — the extras are there so a button you already built for another toplist can be repointed here unchanged.

https://xtremetop300.com/vote/1234?player=Steve

Rules worth knowing

  • A reward is only created for a vote that counted. Votes our fraud checks flag earn nothing, so a farm cannot harvest your in-game currency while leaving your ranking untouched.
  • One vote per player per 24 hours, the same limit as the toplist itself.
  • Treat every token as at-least-once. Store the ones you have paid and ignore repeats. This is the one thing a plugin has to get right.
  • Never paste a player name straight into a console command. We restrict names to letters, numbers, spaces, dots, dashes and underscores, but your server should still check the name belongs to a real account before paying it.
  • 50 rewards per pull by default, 200 maximum. Watch the more field and call again if it is true.
  • 60 requests per minute per listing. Polling once a minute uses one of them.
  • Uncollected rewards are deleted after 14 days.

Errors

401
Key missing or wrong. Check the Bearer prefix.
403
The key is fine, but the listing is not live or rewards are switched off.
422
A parameter is missing or malformed; the body says which.
429
Too many requests. Back off and retry — nothing is lost.

Already listed? Open your server settings to switch rewards on. Not listed yet? Add your server.