Conquer OnlinePrivate Servers
Server list Features Community Log in Create account
For Gold members

Vote callback guide

Give players credit in your own game after Conquer Online Private Servers confirms their vote.

What this is: after a confirmed vote, Conquer Online Private Servers sends one server-to-server HTTPS GET request to your callback URL. This is an owner integration for Gold membership.

01 Before you start

  1. Your listing must be approved and have an active Gold membership.
  2. Open your listing’s edit page and enter a publicly reachable HTTPS callback URL. HTTP callbacks are not accepted.
  3. Generate or copy the listing’s API key when it is displayed in the owner tools. Store it securely: the full key is shown only after creation or rotation.
  4. Send players to your vote page with their game account identifier in the userid query parameter.

The callback field applies only to the individual server covered by Gold. If you have just created the listing, submit it first, purchase Gold, then edit the listing to add the callback URL.

02 Player vote link

Replace PLAYER_ID with a stable external identifier from your own game. Do not expose a database primary key when a separate public account identifier is available.

https://conqueronlineprivateservers.com/server/YOUR-SLUG/vote?userid=PLAYER_ID

Example PHP link generation:


<?php
$slug = 'tempestco';
$playerId = 'player-12345';

$voteUrl = 'https://conqueronlineprivateservers.com/server/'
    . rawurlencode($slug)
    . '/vote?userid='
    . rawurlencode($playerId);

header('Location: ' . $voteUrl);
exit;

03 Callback request

For a confirmed vote, the directory makes a GET request to your saved HTTPS callback URL. Authentication is sent in the HTTP header, never in the URL:

Authorization: Bearer YOUR_PRIVATE_KEY

ParameterExampleMeaning
useridplayer-12345The exact external player identifier supplied in the vote link.
statussuccessSent for a confirmed vote. Receivers must still validate it.
site_id123The numeric Conquer Online Private Servers listing ID.
vote_id456789The stable, immutable identifier of this confirmed directory vote.

A request is sent only when the vote includes a non-empty userid. Votes without a player identifier still count on the directory but cannot be credited to a game account.

https://your-server.example/vote-callback?userid=player-12345&status=success&site_id=123&vote_id=456789

Create a database uniqueness rule equivalent to UNIQUE(site_id, vote_id). Insert that event record and grant the reward in the same transaction so duplicate delivery can never award twice.

04 Plain PHP receiver

The database functions below are application-specific placeholders. They must look up a player by a safe external identifier and process the event transactionally.


<?php
declare(strict_types=1);

$expectedApiKey = (string) getenv('COPS_API_KEY');
$expectedSiteId = 123;
$authorization = (string) ($_SERVER['HTTP_AUTHORIZATION']
    ?? $_SERVER['REDIRECT_HTTP_AUTHORIZATION']
    ?? '');
$receivedApiKey = preg_match('/^Bearer\s+(.+)$/i', $authorization, $matches)
    ? trim($matches[1])
    : '';

$siteId = filter_input(INPUT_GET, 'site_id', FILTER_VALIDATE_INT);
$playerExternalId = trim((string) ($_GET['userid'] ?? ''));
$voteId = trim((string) ($_GET['vote_id'] ?? ''));
$status = (string) ($_GET['status'] ?? '');

if ($expectedApiKey === ''
    || $receivedApiKey === ''
    || !hash_equals($expectedApiKey, $receivedApiKey)
    || $siteId !== $expectedSiteId
    || $status !== 'success'
    || $playerExternalId === ''
    || !ctype_digit($voteId)
    || (int) $voteId < 1) {
    http_response_code(403);
    exit('Invalid callback');
}

$pdo->beginTransaction();
try {
    $player = findPlayerByExternalId($pdo, $playerExternalId);
    if ($player === null) {
        $pdo->rollBack();
        http_response_code(404);
        exit('Player not found');
    }

    // recordVoteRewardIfNew must rely on UNIQUE(site_id, vote_id).
    $isNew = recordVoteRewardIfNew($pdo, $expectedSiteId, $voteId, $player['id']);
    if ($isNew) {
        grantVoteReward($pdo, $player['id']);
    }

    $pdo->commit();
} catch (Throwable $exception) {
    if ($pdo->inTransaction()) {
        $pdo->rollBack();
    }
    http_response_code(500);
    exit('Callback processing failed');
}

http_response_code(204);

Provide your own configured $pdo, findPlayerByExternalId, recordVoteRewardIfNew, and grantVoteReward implementations. Keep COPS_API_KEY in the environment, not in source control.

05 Laravel receiver

Add a route to your game’s Laravel application:


// routes/web.php
use App\Http\Controllers\ConquerVoteCallbackController;
use Illuminate\Support\Facades\Route;

Route::get('/integrations/conquer-vote', [
    ConquerVoteCallbackController::class,
    'handle',
]);

Then validate and process the callback:


<?php

namespace App\Http\Controllers;

use App\Models\GamePlayer;
use App\Models\VoteReward;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\DB;
use Symfony\Component\HttpFoundation\Response;

class ConquerVoteCallbackController extends Controller
{
    public function handle(Request $request): Response
    {
        $receivedKey = (string) $request->bearerToken();
        $expectedKey = (string) config('services.conquer.api_key');
        $playerExternalId = trim((string) $request->query('userid', ''));
        $voteId = trim((string) $request->query('vote_id', ''));

        abort_unless(
            $expectedKey !== ''
                && $receivedKey !== ''
                && hash_equals($expectedKey, $receivedKey)
                && (int) $request->query('site_id') === 123
                && $request->query('status') === 'success'
                && $playerExternalId !== ''
                && ctype_digit($voteId)
                && (int) $voteId > 0,
            403
        );

        $player = GamePlayer::query()
            ->where('external_id', $playerExternalId)
            ->firstOrFail();

        DB::transaction(function () use ($player, $voteId): void {
            // The database must enforce UNIQUE(site_id, vote_id).
            $inserted = VoteReward::query()->insertOrIgnore([
                'site_id' => 123,
                'vote_id' => $voteId,
                'player_id' => $player->getKey(),
                'created_at' => now(),
                'updated_at' => now(),
            ]);

            if ($inserted === 1) {
                // Application-specific method: grant the reward in this transaction.
                $player->grantVoteReward();
            }
        });

        return response()->noContent();
    }
}

GamePlayer, VoteReward, and grantVoteReward() are application-specific placeholders. Configure services.conquer.api_key from an environment variable and add a unique database index on site_id, vote_id.

06 Python / Flask receiver

The database helpers are application-specific placeholders. record_vote_reward_if_new must use a unique site_id, vote_id constraint.


import hmac
import os

from flask import Flask, abort, request

app = Flask(__name__)

@app.get('/integrations/conquer-vote')
def conquer_vote():
    expected_key = os.environ.get('COPS_API_KEY', '')
    scheme, separator, token = request.headers.get('Authorization', '').partition(' ')
    received_key = token.strip() if separator and scheme.lower() == 'bearer' else ''
    site_id = request.args.get('site_id', type=int)
    player_external_id = request.args.get('userid', '').strip()
    vote_id = request.args.get('vote_id', '').strip()

    if (
        not expected_key
        or not received_key
        or not hmac.compare_digest(expected_key, received_key)
        or site_id != 123
        or request.args.get('status') != 'success'
        or not player_external_id
        or not vote_id.isdecimal()
        or int(vote_id) < 1
    ):
        abort(403)

    with database.transaction():
        player = find_player_by_external_id(player_external_id)
        if player is None:
            abort(404)

        is_new = record_vote_reward_if_new(site_id, vote_id, player.id)
        if is_new:
            grant_vote_reward(player.id)

    return ('', 204)

Provide your own database, player lookup, idempotency insert, and reward functions. Keep the API key in the environment and return quickly after the transaction commits.

07 C# / ASP.NET Core receiver

This minimal API example rejects empty or differently sized secrets before using CryptographicOperations.FixedTimeEquals. Replace the two application-specific functions with your own database implementation.


using System.Globalization;
using System.Security.Cryptography;
using System.Text;

var builder = WebApplication.CreateBuilder(args);
var app = builder.Build();

app.MapGet("/integrations/conquer-vote", async (
    HttpRequest request,
    IConfiguration config) =>
{
    const int expectedSiteId = 123;
    var expectedKey = config["COPS_API_KEY"] ?? "";
    var authorization = request.Headers.Authorization.ToString();
    var receivedKey = authorization.StartsWith("Bearer ", StringComparison.OrdinalIgnoreCase)
        ? authorization["Bearer ".Length..].Trim()
        : "";

    var expectedBytes = Encoding.UTF8.GetBytes(expectedKey);
    var receivedBytes = Encoding.UTF8.GetBytes(receivedKey);
    var keysMatch = expectedBytes.Length > 0
        && expectedBytes.Length == receivedBytes.Length
        && CryptographicOperations.FixedTimeEquals(expectedBytes, receivedBytes);

    var validSiteId = int.TryParse(
        request.Query["site_id"],
        NumberStyles.None,
        CultureInfo.InvariantCulture,
        out var siteId) && siteId == expectedSiteId;
    var validVoteId = long.TryParse(
        request.Query["vote_id"],
        NumberStyles.None,
        CultureInfo.InvariantCulture,
        out var voteId) && voteId > 0;
    var playerExternalId = request.Query["userid"].ToString().Trim();

    if (!keysMatch
        || !validSiteId
        || !validVoteId
        || request.Query["status"].ToString() != "success"
        || playerExternalId.Length == 0)
    {
        return Results.StatusCode(StatusCodes.Status403Forbidden);
    }

    var player = await FindPlayerByExternalIdAsync(playerExternalId);
    if (player is null)
    {
        return Results.NotFound();
    }

    // Must atomically enforce UNIQUE(site_id, vote_id) and grant only if new.
    await ProcessVoteRewardOnceAsync(siteId, voteId, player.Id);
    return Results.NoContent();
});

app.Run();

// Application-specific placeholders:
static Task<ExamplePlayer?> FindPlayerByExternalIdAsync(string externalId) =>
    throw new NotImplementedException();

static Task ProcessVoteRewardOnceAsync(int siteId, long voteId, long playerId) =>
    throw new NotImplementedException();

public sealed record ExamplePlayer(long Id);

Implement both placeholders using your game database. The idempotency insert and reward update must occur in one transaction, backed by a unique index on site_id, vote_id.

08 Security and reliability

  • The callback URL must use HTTPS and resolve only to publicly routable addresses. Redirects are not followed.
  • The API key is sent only as Authorization: Bearer <API_KEY>. Do not put it in a callback URL, query string, log message, or error response.
  • Compare the key using a constant-time comparison and reject empty credentials.
  • Validate the expected site_id, exact status=success, non-empty external userid, and positive vote_id before looking up a player.
  • Do not treat userid as a trusted database primary key. Resolve it through your own external-identifier column.
  • Enforce UNIQUE(site_id, vote_id) in the database. Insert the event and grant the reward atomically.
  • Return a quick 2xx response after successful processing. The directory uses a two-second connection timeout and a three-second total timeout.
  • The directory records the legitimate vote before attempting the owner callback. A timeout, rejected URL, redirect, non-2xx response, or receiver failure never rolls back the directory vote.
  • The current integration makes one delivery attempt and does not queue or retry failed callbacks.
  • Gold is checked when delivery is attempted. When that server’s Gold membership expires, callbacks stop until Gold is renewed.

09 Troubleshooting

  • No callback arrives: confirm the vote link includes ?userid=..., the listing has active Gold, and the saved callback is publicly reachable over HTTPS.
  • Player is not identified: URL-encode the player identifier when generating the vote link, then look it up through your external-ID field.
  • 403 from your receiver: verify the Bearer key, expected numeric site_id, exact status=success, non-empty userid, and positive vote_id.
  • Duplicate rewards: add UNIQUE(site_id, vote_id) and keep the idempotency insert and reward grant in one transaction.
  • Redirect response: save the final public HTTPS endpoint directly; the sender does not follow redirects.

List a server or return to the server directory.

Conquer Online Private Servers

Independent discovery, transparent rankings, and community voting.

Server list Features Community Submit a server Terms Privacy Gold API guide Sitemap
© 2026 Conquer Online Private Servers. Independent of the official game publisher.