Vote WebHook

Reward & Track every vote



How does Incentive Voting work?

  1. Send your users to your voting page, adding the voters username or id as a variable to the link:
    {{ voteUrl }}?id=PlayerX&...
  2. After the user votes, a script on your server (called "webhook") receives a request with the vote information.
  3. In response to this request, you can reward the voter.

The webhook script would receive all variables passed to voting page (the QUERY_STRING).
The important thing is for your script to be able to identify the voter and award him based on these variables (eg. PlayerX identifies `PlayerX` in your DB).

Dynamic Webhook URL

In some cases you might want to use a custom webhook URL for some specific votes.

In such a case, you can extend your link with the callback variable "_":

{{ voteUrl }}?_=strtr(base64_encode($customURL),'+/','-_')&id=PlayerX&...

The callback variable "_" can be any custom URL, base64 encoded, safely to be used as a URL variable.

<?php
$customURL = 'https://vote.example.com/vote_webhook.php';
$callback = strtr(base64_encode($customURL), '+/', '-_');
echo '{{voteUrl}}?_='.$callback.'&id='.$playerID;
?>

All other variables would be sent to $customURL as POST fields.

Validate Webhook request (JWT)

Each webhook request contains a security token in the X-JWT header. This is a JSON Web Token which encodes additional vote details, securely signed with your Webhook Secret.

Thus you can make sure the request came from our server and avoid awarding users for fake votes.

The Webhook Secret is generated for your site under your account.

Vote details encoded in JWT

The webhook script would receive the folowing vote details as part of JWT's payload:

Webhook JWT data
Field name Example value Description
jti "5d14ac20e4fd9455f1cf8e5b" A unique vote ID. Use this to keep track of which vote has been rewarded.
ip "44.197.197.23" The voters' IP address.
ref "https://example.com/vote/" Referer with which the voter landed on voting page
aud "example.com" Hostname of the site the vote is for.
sub "5d04f82b515e8b6b43f83c76" Your site's ID on RSPS.Page.
iss "https://rsps.page/" Issuer.
at 1632182523456 Unix timestamp of the vote (ms).
iat 1632182524 Unix timestamp of the webhook request (sec).
exp 1632182644 Expiration timestamp (sec). After this time the JWT is no longer valid.
val 1 Vote value.
qs { "id": "PlayerX" } All variables passed to vote link (the QUERY_STRING).
This value duplicates the request data, and it is signed as part of the JWT.

Example Webhook PHP script to process votes

<?php

/**
 * Example PHP >= 5.4 WebHook Script
 * https://example.com/vote_webhook.php
 *
 * RSPS.Page does not take responsibility or liability for the (miss)use of this php snippet.
 */

// Your site's webhook secret (see https://rsps.page/account/site/)
$myWebhookSecret = 'my-webhook-secret';

// Your Database Connection Details
$host        = 'db.example.com';
$db_name     = 'example_com';
$db_user     = 'example_com';
$db_password = '**************';

// Get the exact request time, default to current system time
$request_time = getenv('REQUEST_TIME_FLOAT') ?: getenv('REQUEST_TIME') ?: microtime(true);

// Decode the JWT token provided through X-JWT request header
list($invalidReason, $vote) = jwt_decode(getenv('HTTP_X_JWT'), $myWebhookSecret, 120, $request_time);

// Validate the request using the JWT
if ($invalidReason) {
    // Request not accepted
    return rsps_webhook_response(406, $invalidReason);
}

// Unique vote ID - could be used to track reward for this specific vote
$voteId = $vote['jti'];
if (!$voteId) {
    // Request not accepted
    return rsps_webhook_response(406, $invalidReason);
}

// All variables passed to the voting page are received as payload ($_POST or $_GET)
$query = rsps_webhook_get_payload() ?: $vote['qs'];

// Player ID - fetch it from the variable you chose in the voting page's URL:
// https://rsps.page/vote/example.com?id=PlayerX -> $query['id'] === 'PlayerX'
$userId = $query['id'] ?: $query['userId'] ?: $query['playerId'] ?: null;

// IP address of the voter
$voterIp = $vote['ip'];

// Coordinates of the site this vote is for
$hostname   = $vote['aud'];
$rspsSiteId = $vote['sub'];

// Referer with which the voter landed of voting page
$referer = $vote['ref'];

// Vote event timestamp in seconds (when the player clicked on vote button)
$votedAt = $vote['at'] / 1e3;

// The timestamp of webhook request (JWT creation) - should be close to $request_time
$webhookTs = $vote['iat'];

// Vote value, should be greated than 1
$value = intval($vote['val']);

// Make sure the user was not yet rewarded on your system
// For example by checking your reward logs
if ($userId && $value > 0 && $voteId) {

    // Connect to the DB
    $connection = mysqli_connect($host, $db_user, $db_password);
    mysqli_select_db($connection, $db_name);

    // Make userId safe to use in query
    $userId = mysqli_real_escape_string($connection, $userId);

    // Check to see whether this vote has been rewarded or not
    $result = mysqli_query(
        $connection,
        'SELECT COUNT(*) FROM `reward_log` ' .
            // track user rewards by userId & last reward time
            ' WHERE `userId`="' . $userId . '" AND `reward_datetime`>=FROM_UNIXTIME(' . $votedAt . ')' .
            // or by checking individual votes
            ' OR `voteId`="' . $voteId . '" AND `votedAt`=' . $votedAt . ';'
    );
    if (!$result) {
        return rsps_webhook_response(500, 'sql-error');
    }
    $rewarded = mysqli_num_rows($result);

    // Already rewarded for this vote?
    if ($rewarded) {
        // Request not accepted
        return rsps_webhook_response(406, 'rewarded');
    }

    // Grant reward and set the reward date/time log
    if (!$rewarded) {
        // Grant the reward, for example points
        $ok = mysqli_query(
            $connection,
            'UPDATE `users` SET `points` = `points` + 1 WHERE `userId`="' . $userId . '";'
        );
        if (!$ok) {
            return rsps_webhook_response(500, 'sql-error');
        }

        // Insert log
        $ok = mysqli_query(
            $connection,
            'INSERT INTO `reward_log` (`voteId`, `votedAt`, `userId`, `reward_datetime`)' .
                ' VALUES (' . implode(', ', [
                    "'$voteId'", $votedAt,
                    "'$userId'", 'FROM_UNIXTIME(' . $request_time . ')'
                ]) . ');'
        );
        if (!$ok) {
            return rsps_webhook_response(500, 'sql-error');
        }
    }

    // Close connection
    mysqli_close($connection);
}

// Request accepted
rsps_webhook_response(200);

/**
 * Send response to webhook caller.
 * @param int $code - HTTP response status code.
 *                    200 - Ok, 406 - Not accepted
 * @param string $reason - If an error, send some error message to caller
 *
 * Note: This function invokes die!
 */
function rsps_webhook_response($code, $reason = null)
{
    http_response_code($code);

    if ($code != 200) {
        $reason and header('x-reason: ' . urlencode($reason));
        die(intval($code));
    }

    die(0);
}

/**
 * Get WebHook request payload
 *
 * @return array $payload
 */
function rsps_webhook_get_payload()
{
    $payload = $_POST ?: $_GET;

    if (empty($payload)) {
        $request_method = strtoupper(getenv('HTTP_X_HTTP_METHOD_OVERRIDE') ?: getenv('REQUEST_METHOD'));

        switch ($request_method) {
            case 'POST':
            case 'PUT':
            case 'PATCH':
                // Only POST, PUT and PATCH requests should have a body.
                if ($contentType = getenv('HTTP_CONTENT_TYPE')) {
                    // Extract content-type without the charset
                    $contentType = explode(';', $contentType);
                    $contentType = trim(reset($contentType));

                    // In the case the request body did not get decoded, for whatever reason, try to decode it here.
                    switch ($contentType) {
                        case 'application/json':
                            // PHP doesn't decode JSON request body
                            $tmp = @file_get_contents('php://input');
                            $payload = json_decode($tmp, true);
                            break;

                        case 'application/x-www-form-urlencoded':
                            // If there is no decoded $_POST super-global, decode the request body here
                            // (eg. PUT or PATCH request)
                            $tmp = @file_get_contents('php://input');
                            parse_str($tmp, $payload);
                            break;
                    }
                }
                break;

            case 'GET':
            case 'HEAD':
                // GET and HEAD requests don't have a body, thus look for payload in the query string
                $tmp = explode('?', getenv('REQUEST_URI'), 2);
                if (count($tmp) === 2) {
                    parse_str($tmp[1], $payload);
                }
                break;
        }
    }

    return $payload;
}

/**
 * Decode a JWT (JSON Web Token) and validate it.
 *
 * See https://jwt.io/ for more details
 *
 * RFC 7519 https://tools.ietf.org/html/rfc7519
 *
 * @param string $jwt - A JWT string
 * @param string $secret - The WebHook Secret
 * @param int $defExpiresIn - Default expiration time, for when there is no 'exp' field in the JWT
 * @param int $requestTime - Unix timestamp of the request. Defaults to current time.
 * @return array [ $invalidReason, $payload, $header, $signature ]
 */
function jwt_decode($jwt, $secret = null, $defExpiresIn = 120, $requestTime = null)
{
    if (empty($jwt)) {
        return ['jwt', null];
    }

    $invalidReason = null; // In the case the JWT is invalid

    list($header, $payload, $signature) = explode('.', $jwt);

    $token = "$header.$payload";

    $header    = base64_decode(strtr($header, '-_', '+/'));
    $payload   = base64_decode(strtr($payload, '-_', '+/'));
    $signature = base64_decode(strtr($signature, '-_', '+/'));

    $header = json_decode($header);

    if ($header->typ != 'JWT') {
        // Wrong JWT type
        $invalidReason = 'typ';
    } elseif ($secret) {
        static $algorithms = [
            'HS256' => ['hash_hmac', 'SHA256'],
            'HS384' => ['hash_hmac', 'SHA384'],
            'HS512' => ['hash_hmac', 'SHA512'],
            'none' => [],
            // 'RS256' => ['openssl', 'SHA256'],
            // 'RS384' => ['openssl', 'SHA384'],
            // 'RS512' => ['openssl', 'SHA512'],
        ];

        if (!isset($algorithms[$header->alg])) {
            // Unsupported signing algorithm
            $invalidReason = 'alg';
        } else {
            $alg = $algorithms[$header->alg];
            if (!$alg || hash_equals($signature, $alg[0]($alg[1], $token, $secret, true))) {
                // Invalid signature ('none' is always invalid)
                $invalidReason = 'sig';
            }
        }
    }

    $payload = json_decode($payload);
    if (!$invalidReason) {
        isset($requestTime) or $requestTime = time();

        if (isset($payload->exp) && $requestTime > $payload->exp) {
            // JWT expired
            $invalidReason = 'exp';
        } elseif (isset($payload->iat) && $defExpiresIn && $requestTime > $payload->iat + $defExpiresIn) {
            // JWT expired by default expiration time, based on .iat
            $invalidReason = 'iat';
        }
    }

    return [
        $invalidReason,
        $payload,
        $header,
        $signature,
    ];
}

if (!function_exists('hash_equals')) {
    // Polyfill for PHP < 5.6
    function hash_equals($known_string, $user_string)
    {
        return hash_hmac('SHA256', $known_string, __FUNCTION__, true) === hash_hmac('SHA256', $user_string, __FUNCTION__, true);
    }
}

Test your WebHook Script to see if it works with our system.

=
▶️ Send request

Status Code: {{ testResponse.vote.whStatus }} {{ testResponse.vote.whStatus != 200 ? testResponse.vote.whStatus != 406 ? '?' : 'rejected' : 'accepted' }}

Response Headers:

  • {{ k }}: {{ v }}

Response Body:

{{ testResponse.body }}

Play with you callback script in the shell (Bash + cURL)

Here is a bash (sh, zsh) script that generates a vote webhook request to test your callback script. You can modify, save and run this script any time from any machine. All you need is bash, cURL, and OpenSSL.