Skip to content

← Add your server

Developer guide

Vote callback reference

When a player votes for your server, we tell your server by opening a web address you choose, with the player's name on the end. Your script gives the reward and says “OK”. That is the whole thing; this page covers the details.

We send1 GET requestno body, no JSON
Success meansHTTP 200the body is ignored
Answer within3 secondsor it counts as failed
If it fails3 retriesafter 30 s, 2 min, 10 min
Player opens your vote link…/server/293/vote/Zezima — the last part is the player's name.
They solve the captchaand press Vote Now.
We call your callback URLwith the name attached: one GET request.
Your script rewards the playerand answers with status 200.

The request we send

The part after /vote/ in the vote link is called the incentive. It is normally the player's in-game name, but it can be anything your server understands, such as an account id or a one-time token. We add it to the end of the callback URL you saved. How we add it depends only on how your saved URL ends:

Ends with =the most flexible
You savehttps://play.example/vote?user=
We callhttps://play.example/vote?user=Zezima
Ends with .phpclassic vote scripts
You savehttps://play.example/callback.php
We callhttps://play.example/callback.php?postback=Zezima
Anything elsename in the path
You savehttps://play.example/api/vote
We callhttps://play.example/api/vote/Zezima
With the first shape you can put your own parameters in front, for example a secret key: https://play.example/vote?key=YOUR_SECRET&user=

On the wire it looks like this (plus the usual connection headers):

GET /vote?user=Zezima HTTP/1.1
Host: play.example
User-Agent: Mozilla/5.0 (Macintosh; Intel Mac OS X 10_14_3) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/76.0.3809.100 Safari/537.36
Accept: text/html,application/xhtml+xml,…
Accept-Language: en-US,en;q=0.9
  • Always GET. Nothing is sent in a request body, and there is no signature header.
  • Both https:// and http:// work, and redirects are followed. Use https when you can.
  • We identify as a normal web browser, because many hosts block unknown clients. Do not use the User-Agent or our IP address to recognise us; both can change.
  • The incentive is added exactly as it appears in the vote link, up to 255 characters. We do not clean or escape it: a space reaches you as a space, and characters such as &, # or ? in a name would change the URL. Always check the value before you use it.

What your script should answer

  • Answer with status 200 once the reward is stored. It has to be exactly 200; 201, 204 and every other code count as failed.
  • The response body does not matter. Keep it short (ok is perfect): we save the first 500 characters in your callback log so you can debug.
  • You have 3 seconds. A slower answer, a connection error or any other status counts as failed.
  • After a failure we try again 3 more times: after 30 s, then 2 min, then 10 min. Retries wait up to 5 seconds for your answer. After the last one we stop.
You can receive the same vote twice. If your script stores the reward but answers too slowly, we think it failed and call again. Answer fast, and make rewards safe to repeat, for example at most one reward per player every 12 hours.

Player names with spaces

A web address cannot contain a real space, so a name like Iron Man has to be written in a way that survives the trip. You have two good options. Pick one and use it everywhere.

OptionVote link you give the playerWhat we callWhat your script reads
%20 (the standard way)…/vote/Iron%20Man…?user=Iron%20ManIron Man — PHP, Express, Flask, ASP.NET, Go and Sinatra decode it for you
Underscore (the RSPS habit)…/vote/Iron_Man…?user=Iron_ManIron_Man — turn _ into a space yourself before you look the player up
  • Build the link with your language's URL encoder instead of by hand: rawurlencode() in PHP, encodeURIComponent() in JavaScript, urllib.parse.quote() in Python, Uri.EscapeDataString() in C#, url.PathEscape() in Go.
  • Java: URLEncoder.encode(name, UTF_8) writes a space as +, and in this part of a link a plus stays a plus, so the player would be called Iron+Man. Add .replace("+", "%20").
  • If you read the raw query string yourself (as the Java example does), decode it; the example's URLDecoder.decode line does exactly that.
  • A player who types a plain space into the address bar is fine too: the browser turns it into %20 on its own.
  • Names are passed on exactly as written, including upper and lower case. If your game treats iron man, Iron Man and Iron_Man as the same player, normalise the name in your script before you store the reward. The name check in the examples already allows spaces, _ and -.

When we call you, and when we don't

We call your URL
  • For every vote that passes the captcha and has a name in the link.
  • Also when the vote does not count towards your rank because the same device voted before or a VPN was detected. The player did the work, so they still get the reward.
We do not call
  • When the same IP address already voted for you in the last 12 hours.
  • When the vote link has no name (…/vote/ with nothing after it).
  • When one connection sends far too many votes in a few minutes.

Code examples

Every example does the same four things: check the secret key, check that the name looks like a real player name, store the reward, answer 200. Pick your language, change the name rule and the “your own code” line to fit your game.

Callback URL to save: https://play.example/callback.php
<?php
// callback.php
$player = $_GET['postback'] ?? '';

// 1. Only accept names your game could really have.
if (!preg_match('/^[A-Za-z0-9 _-]{1,12}$/', $player)) {
    http_response_code(400);
    exit('bad name');
}

// 2. Store the reward (your own table; the player claims it in game).
$db = new PDO('mysql:host=localhost;dbname=game;charset=utf8mb4', 'user', 'password');
$stmt = $db->prepare(
    'INSERT INTO vote_rewards (player, site, created_at) VALUES (?, ?, NOW())'
);
$stmt->execute([$player, 'runelist']);

// 3. Tell us it worked. Anything other than 200 is retried.
http_response_code(200);
echo 'ok';

Ends in .php, so the name arrives as ?postback=. To use a secret key with PHP, save https://play.example/callback.php?key=YOUR_SECRET&user= instead and read $_GET['user'].

Keep it safe

  • Use a secret key in the URL (see the examples). Without one, anyone who finds the address can call it and hand out rewards.
  • Validate the name. Accept only what your game allows and use parameterised database queries, never string-built SQL.
  • Give the reward in game, not in the script. Store a row and let the player claim it on login or with a command, so a slow database never blocks the answer.
  • Limit rewards per player to one every 12 hours. It makes retries harmless and stops anyone replaying a captured URL.

Allow the callback through Cloudflare

If your website is behind Cloudflare, its bot protection can mistake our request for an attack and answer 403 or a challenge page before your script ever runs. The fix is a rule that says “leave this one address alone”. It takes two minutes. In these steps, replace /callback.php with the path of your own script.

  1. In the Cloudflare dashboard open your domain, then Security → WAF → Custom rules (in the newer dashboard: Security → Security rules) and choose Create rule. Name it RuneList vote callback.
  2. Under When incoming requests match set Field URI Path, Operator equals, Value /callback.php. If you use a secret key, make the rule stricter with Edit expression:
    (http.request.uri.path eq "/callback.php" and http.request.uri.query contains "key=YOUR_SECRET")
  3. Under Then take action choose Skip and tick:
    • All remaining custom rules
    • All rate limiting rules
    • All managed rules
    • All Super Bot Fight Mode rules
  4. Set Place at to First so it runs before your other rules, then Deploy.
  5. Using I'm Under Attack mode, a high Security Level or Browser Integrity Check? Those are not covered by the Skip rule. Go to Rules → Configuration Rules → Create rule, match the same URI Path, and for that path turn I'm Under Attack off (or set Security Level to its lowest) and switch Browser Integrity Check off.
  6. Press Test callback on your listing. If it still fails, open Security → Events (Analytics → Events in the newer dashboard), filter by your path, and the entry tells you which Cloudflare feature blocked the request.
Free plan with Bot Fight Mode on? Cloudflare does not let any rule skip Bot Fight Mode; only Super Bot Fight Mode (Pro and up) can be skipped. On the free plan either switch Bot Fight Mode off under Security → Bots, or put the callback on its own subdomain (for example vote.yourserver.com) and set that DNS record to DNS only (grey cloud) so it does not pass through Cloudflare at all.

Do not allow-list us by IP address: our servers' addresses can change. A rule on the path plus your secret key keeps working.

Test it

On your listing's edit page, the Test callback button sends a real request with the name RuneListTest and shows the exact URL we called, the status code, how long it took and the start of your response. The test waits up to 8 seconds, so a script that passes the test slowly can still be too slow for real votes.

Your server's dashboard lists the latest real callbacks with the same details, including every failed attempt. A test vote of your own looks like this:

https://test.runelist.io/toplist/server/{your id}/vote/YourName

When it does not work

  • Open the callback URL with a test name in your own browser. Do you get your “ok”, without a login page or an error?
  • Is the address reachable from the internet? localhost, a home network address or a port your firewall blocks cannot be reached by us.
  • Cloudflare “Under Attack” mode, bot protection or a CAPTCHA in front of the script answers us with 403 or 503. Exclude the callback path from them: see the Cloudflare steps.
  • Does the saved URL end the way you meant? A missing = turns ?user=Zezima into ?user/Zezima.
  • An expired or self-signed https certificate makes the request fail.
  • Answering 204 or a redirect to an error page is not a 200.

Still stuck? Ask in our Discord and include the line from your callback log.