Developer docs

Vote Callback

When a player votes for your server and the vote counts, rsps.org sends one request to your callback address. Your server checks that it came from us and gives the player their in-game reward.

1

What your server needs to do

  1. Have a public https address with a domain name, for example https://yourserver.com/vote, and enter it as the Callback URL of your listing.
  2. Enter a Callback Secret on your listing: up to 100 characters, no spaces.
  3. Accept the request exactly as described below: a GET with query parameters.
  4. Check the secret before doing anything else.
  5. Give the player the reward once per vote.
  6. Answer with a success status, for example 200 OK.
2

Test your callback

When your handler is ready, open your listing's edit page from My Listings and press Test callback next to the Callback URL field. We send one call exactly like a real vote, with the player name callback-test, and show you the result on the same page.

3

Players vote through this link. Put it behind the vote button or command in your game, with the player's name filled in. Replace your-server with the name in your listing's address.

URL
https://rsps.org/server/your-server/vote?callback=Zezima&username=Zezima
ParameterRequiredMeaning
callbackNo, but you need it to know who to rewardSent back to you as callback. Usually the player's in-game name. 1 to 64 characters: letters, digits, space and _ - . @ : (other characters are refused).
usernameNoFills in the player's name on the vote page. If the link has no callback, the name the player types there is sent as callback instead.
4

The request we send

For each counted vote we send an HTTP GET to your Callback URL with the values added as query parameters. There is no request body.

Test calls you send with the "Test callback" button use the player name callback-test.

How we add the values to your address

Your Callback URLWhat we call
https://yourserver.com/votehttps://yourserver.com/vote?callback=Zezima&ip=VOTER_IP&secret=k7Qp2xVb9
https://yourserver.com/vote?game=1https://yourserver.com/vote?game=1&callback=Zezima&ip=VOTER_IP&secret=k7Qp2xVb9
https://yourserver.com/vote.php?id=https://yourserver.com/vote.php?id=Zezima&ip=VOTER_IP&secret=k7Qp2xVb9
https://yourserver.com/vote/{callback}?key={secret}&from={ip}https://yourserver.com/vote/Zezima?key=k7Qp2xVb9&from=VOTER_IP

An address that ends with = gets the callback value right after it. Placeholders {callback} {ip} {secret} are filled in and nothing else is added. The secret is added only when your listing has a Callback Secret. VOTER_IP stands for the voter's address.

Parameters

NameTypeMeaningExample
callbacktext, can be emptyThe callback value from the vote link, or the name the player typed. Empty when the player gave no name.Zezima
iptextThe voter's IP address (IPv4 or IPv6).the voter's address
secrettextYour Callback Secret, exactly as saved. Sent only when you set one.k7Qp2xVb9

Values are URL encoded like an HTML form: a space becomes + and other special characters become %XX (UTF-8). Decode them with your language's normal query decoder before you use or check them. Zezima Pk arrives as Zezima+Pk, and each colon of an IPv6 address as %3A.

Headers

HeaderValue
User-AgentMozilla/5.0 (compatible; VoteNotify/1.0)
Accept*/*

Other headers can be present or missing; do not depend on them. Anyone can copy a User-Agent, so never use it to decide whether a call is real.

Example request

http
GET /vote?callback=Zezima+Pk&ip=VOTER_IP&secret=k7Qp2xVb9 HTTP/1.1
Host: yourserver.com
User-Agent: Mozilla/5.0 (compatible; VoteNotify/1.0)
Accept: */*
5

Check that the call is from us

Check the secret before doing anything else. If the check fails, answer 403 and do nothing else.

  1. Read the decoded secret parameter.
  2. Compare it with your Callback Secret using a constant-time comparison (Java MessageDigest.isEqual, PHP hash_equals). Letter case counts.
  3. Missing or different: reject.

Worked example: your Callback Secret is k7Qp2xVb9. A call with secret=k7Qp2xVb9 passes; secret=k7qp2xvb9 or no secret at all fails.

6

What to answer

  • Any status from 200 to 299 means the vote was delivered. 200 with the body OK is perfect.
  • We do not read the body, but keep it short: a body over 16 KB counts as a failure.
  • Answer within 10 seconds. Store the vote, answer, and do slow work afterwards.
  • Any other status, a redirect we do not follow, or no answer in time means the call failed, and the reward may not be delivered.
  • Answer 403 when the secret is wrong.
7

Rules for your callback address

  • Use https with a valid certificate. Plain http is accepted, but then your secret can be read on the way.
  • Use a domain name, for example yourserver.com. Number addresses are not accepted for new or changed callback addresses.
  • The domain must lead to a public server on the internet. localhost, home and private networks, and names ending in .local or .internal are refused.
  • Port: none, 80, 443, or 1024 to 65535.
  • At most 255 characters, no spaces, no user name or password in the address.
  • Not an address on our own sites, rsps.org and rspslist.com, or any name under them.
  • Do not redirect. We follow a redirect only with status 300, 301, 302, 303 or 307, only to the same scheme (https stays https), never to a number address, and at most two in a row.
  • When you move the callback to another domain, type your Callback Secret again: it is never sent to a new domain on its own.
8

Where our calls come from

Our calls can come from different addresses, so do not allow only one sender address. Check the secret to know a call is from us.

9

Give each reward once

  • A call can occasionally arrive more than once. Reward each vote only once, and answer success to a repeated call without rewarding again.
  • The call has no vote id. A player can vote for your server once per 12 hours, so a safe rule is at most one reward per callback value per 12 hours.
  • If callback is empty there is nobody to reward: answer success and skip the reward.
  • If your server does not answer with success, the reward may not be delivered. If the player is offline, store the reward and give it at their next login.
10

If rewards do not arrive

  • Press Test callback on your listing's edit page to see what your server answers.
  • The Callback URL on your listing is correct and opens from the internet, for example from a phone on mobile data.
  • Your https certificate is valid, not expired, and made for that domain name.
  • Your server answers 200: not a redirect, an error page or a login page.
  • The secret on your server matches the Callback Secret on your listing exactly.
  • Your firewall or protection service is not blocking our calls.

You can also test from a terminal, the way we call it, and check that you get 200 back:

curl
curl -i -A "Mozilla/5.0 (compatible; VoteNotify/1.0)" "https://yourserver.com/vote?callback=TestPlayer&ip=test&secret=YOUR_SECRET"

For AI assistants

The exact contract, for writing a correct vote callback handler. It matches everything above.

  • Trigger: one HTTP GET per counted vote, no request body. The same vote can occasionally be called more than once.
  • Test calls from the "Test callback" button on the listing's edit page carry callback=callback-test; treat them like any other call.
  • URL: the owner's Callback URL plus query parameters. Default: append ?callback=C&ip=I (& instead of ? when the URL already has a query), then &secret=S when a secret is set. A URL ending in = gets C&ip=I (then &secret=S) appended directly. A URL with placeholders {callback} {ip} {secret} has them replaced and nothing else added.
  • Encoding: application/x-www-form-urlencoded (space as +, other bytes of the UTF-8 text as %XX). Decode every value before use.
  • callback: string, 0 to 64 characters from A-Z a-z 0-9, space, _ - . @ :. Empty means no player to reward.
  • ip: string, the voter's IPv4 or IPv6 address.
  • secret: string, 1 to 100 characters without whitespace, sent only when the owner set one.
  • Headers: User-Agent: Mozilla/5.0 (compatible; VoteNotify/1.0) and Accept: */*. Never authenticate by header or by sender address; calls come from different addresses.
  • Verification: constant-time equality of the decoded secret with the configured secret. Missing or different: answer 403, no reward.
  • Response: a status from 200 to 299 means delivered. The body is ignored and must stay under 16 KB. The whole call must finish within 10 seconds. Do not redirect.
  • Idempotency: the call has no vote id; give at most one reward per callback value per 12 hours. Answer 200 to a repeat without rewarding again.
  • Endpoint: https, a domain name (no number address), port 80, 443 or 1024 to 65535, at most 255 characters.

A minimal handler in plain Java (JDK only, Java 8 or newer). It answers on port 8080 at /vote; serve it to the internet over https, for example behind your web server.

java
import com.sun.net.httpserver.HttpExchange;
import com.sun.net.httpserver.HttpServer;
import java.io.IOException;
import java.io.OutputStream;
import java.net.InetSocketAddress;
import java.net.URLDecoder;
import java.nio.charset.StandardCharsets;
import java.security.MessageDigest;
import java.util.HashMap;
import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;

public class VoteCallback {
    static final String SECRET = "YOUR_CALLBACK_SECRET"; // your Callback Secret

    // In memory for the example; keep it in your database so it survives a restart.
    static final Map<String, Long> lastRewardAt = new ConcurrentHashMap<>();

    public static void main(String[] args) throws IOException {
        HttpServer server = HttpServer.create(new InetSocketAddress(8080), 0);
        server.createContext("/vote", VoteCallback::handle);
        server.start();
    }

    static void handle(HttpExchange ex) throws IOException {
        int status = 405;
        if ("GET".equals(ex.getRequestMethod())) {
            Map<String, String> q = query(ex.getRequestURI().getRawQuery());
            if (secretOk(q)) {
                status = 200;
                String player = q.getOrDefault("callback", "");
                long now = System.currentTimeMillis() / 1000;
                Long last = lastRewardAt.get(player);
                if (!player.isEmpty() && (last == null || now - last >= 12 * 3600)) {
                    lastRewardAt.put(player, now);
                    giveReward(player);
                }
            } else {
                status = 403;
            }
        }
        byte[] body = (status == 200 ? "OK" : "NO").getBytes(StandardCharsets.UTF_8);
        ex.sendResponseHeaders(status, body.length);
        try (OutputStream out = ex.getResponseBody()) {
            out.write(body);
        }
    }

    static void giveReward(String player) {
        // Give the in-game reward here (or store it until the player logs in).
    }

    static boolean secretOk(Map<String, String> q) {
        String got = q.get("secret");
        return got != null && !SECRET.isEmpty() && MessageDigest.isEqual(
                SECRET.getBytes(StandardCharsets.UTF_8), got.getBytes(StandardCharsets.UTF_8));
    }

    // Decodes a raw query string like an HTML form ("+" is a space); the first value of a name wins.
    static Map<String, String> query(String raw) throws IOException {
        Map<String, String> out = new HashMap<>();
        if (raw == null) return out;
        for (String pair : raw.split("&")) {
            int i = pair.indexOf('=');
            try {
                String name = URLDecoder.decode(i < 0 ? pair : pair.substring(0, i), "UTF-8");
                String value = i < 0 ? "" : URLDecoder.decode(pair.substring(i + 1), "UTF-8");
                out.putIfAbsent(name, value);
            } catch (IllegalArgumentException e) {
                // a broken %XX: skip this pair
            }
        }
        return out;
    }
}