Postback Guide
Reward your players automatically every time they vote for your server.
When a player votes for your server on SroForge, we send a signed HTTP request (a postback) to your server with the player's in-game identifier. Your server checks the signature and gives the reward. In your account this feature is called Vote rewards and the address that receives the requests is your callback URL.
How to turn on vote rewards
- 1. Create an account and add your server. New servers are reviewed before they appear in the ranking.
- 2. In My servers, open the Vote rewards page of your server.
- 3. Enter the URL of your endpoint and save. A secret is generated and shown only once, so copy it right away.
- 4. Press Send test. We send a request with
event: "test"and the usernameTestUser, so you can check your endpoint without a real vote. - 5. Add a vote link on your website or launcher (see below), so players never have to type anything.
- 6. Every delivery appears in the Delivery history on the Vote rewards page, where failed ones can be retried.
Vote link (recommended)
Send logged-in players to your server page with their in-game identifier in the link:
https://sroforge.com/server/your-server-slug?user=PLAYER_ID
- Replace
your-server-slugwith the last part of your server page address on SroForge, andPLAYER_IDwith the value your server needs to give the reward: account ID (JID), account name or character name. You decide. - The value is sent back to you unchanged, in the
usernamefield of the postback. - With a valid
uservalue, the player only sees the captcha and the Vote button. There is nothing to type. - Allowed characters: letters, digits and underscore (
_), up to 64 characters. Other values are ignored and the player sees the normal form. - Players who open your server page without a vote link can still type their in-game name themselves. If a vote has no identifier at all, no postback is sent for it.
- The link is not secret: a player could change the value. The vote limit still applies (one vote per visitor every 12 hours), so this only changes who receives the reward, never how many votes are counted.
Request
We send an HTTP POST request with a JSON body to your callback URL. Your endpoint checks the signature, gives the reward and answers with any 2xx status.
Headers
X-Toplist-Signature:sha256=followed by the HMAC-SHA256 oftimestamp + "." + raw body, using your secret as the key.X-Toplist-Timestamp: Unix time (seconds) when the request was signed.X-Toplist-Delivery: unique ID of the delivery. It is the same on every retry.X-Toplist-Event:votefor real votes,testfor the "Send test" button.
Body
{
"event": "vote",
"delivery_id": "a97d48b9-5161-41cd-9f04-00c8d91fa085",
"vote_id": 123,
"server": "your-server-slug",
"username": "PlayerName",
"voted_at": "2026-09-21T12:32:06+00:00"
}
Example: verifying the request in PHP
<?php
$secret = 'YOUR_CALLBACK_SECRET';
$body = file_get_contents('php://input');
$timestamp = $_SERVER['HTTP_X_TOPLIST_TIMESTAMP'] ?? '';
$signature = $_SERVER['HTTP_X_TOPLIST_SIGNATURE'] ?? '';
// 1. Reject old requests (replay protection)
if (! ctype_digit($timestamp) || abs(time() - (int) $timestamp) > 300) {
http_response_code(400);
exit('Stale request');
}
// 2. Verify the signature
$expected = 'sha256=' . hash_hmac('sha256', $timestamp . '.' . $body, $secret);
if (! hash_equals($expected, $signature)) {
http_response_code(401);
exit('Invalid signature');
}
$data = json_decode($body, true);
// 3. Test requests must not give rewards
if (($data['event'] ?? '') === 'test') {
exit('OK');
}
// 4. Give the reward only once per delivery_id
$deliveryId = $data['delivery_id'];
$username = $data['username'];
// ... check $deliveryId in your database, then reward $username ...
http_response_code(200);
echo 'OK';
Retries
- Your endpoint has 10 seconds to answer.
- On a timeout, a connection error, a
5xx,408or429, we try again after 30 seconds, 2 minutes, 10 minutes, 30 minutes and 2 hours (6 attempts in total). - Other
4xxanswers and redirects (3xx) are not retried. The delivery is marked as failed and you can retry it from the Delivery history. - Every attempt is signed again with a fresh timestamp.
Good to know
- Always store the
delivery_idand ignore repeated ones, so a retry can never give the reward twice. - Sign and compare against the raw request body, not a re-encoded copy of the JSON.
- Callback URLs that point to private, internal or reserved addresses are rejected.
- Keep your secret private. If it leaks, generate a new one on the Vote rewards page. The old one stops working immediately.