Skip to content

BUILT FOR YOUR BACKEND

One vote.
A direct connection.

Your code. Your rewards. ShardHop sends signed vote events to your callback URL and records the HTTP result.

Signed HTTPS callbacksHTTP delivery statusPrivate JSON APIWrite your own endpoint

Connect in three steps

  1. Create a callback endpoint on your own backend using the contract and example below. Keep your server ID and signing secret in server-side configuration.
  2. In My servers → Vote rewards, save the endpoint’s HTTPS URL. ShardHop does not install anything on your website. Use Send test callback to check the connection.
  3. Verify each incoming vote, save the reward once, then return HTTP 200. Your owner panel shows HTTP 200 accepted or a Callback error, with the most recent attempt and delivery history.

Use a public HTTPS hostname on port 443 with a public IPv4 DNS record. Private/internal addresses, ShardHop itself and redirects are blocked. Responses have a five-second timeout and a 64 KiB body limit; a small JSON response is sufficient. Saving a URL enables pending vote delivery. Clearing it stops future requests; a request already in progress may finish.

Callback request

ShardHop sends an HTTPS POST to your saved URL. The IDs below are examples.

Content-Type: application/json
X-ShardHop-Timestamp: 1792590000
X-ShardHop-Signature: sha256=HEX_HMAC_DIGEST

{
  "api_version": 1,
  "event": "vote.accepted",
  "server_id": 123,
  "vote_id": 456,
  "player_id": "PlayerName",
  "voted_at": 1792590000
}
FieldMeaning
api_versionInteger 1. Reject unsupported versions.
eventvote.accepted for a counted vote; callback.test for a connectivity test.
server_idYour ShardHop listing ID. Check it against your configured ID.
vote_idStable positive integer, unchanged across retries. Unique with server_id.
player_idPlayer-entered game identity, 1–100 characters. Resolve it to your game account; ShardHop does not verify ownership of that identity.
voted_atVote time, Unix seconds. The signature timestamp is the delivery time.

Verify the signature

Compute lowercase hex HMAC-SHA256(timestamp + "." + raw_request_body, callback_signing_secret). Compare "sha256=" + digest with X-ShardHop-Signature using a constant-time comparison. Check the timestamp is within five minutes of your server clock. Verify the exact raw bytes before decoding JSON. Each retry has a fresh delivery timestamp/signature but the same vote ID. The signing secret is separate from your private API key and is never sent in the callback.

Return HTTP 200 only after accepting the event

For a vote, commit the reward and a unique receipt in one database transaction before returning HTTP 200. If the receipt already exists, return 200 without rewarding again. ShardHop treats only HTTP 200 as accepted; 201, 202, 204, redirects, 4xx, 5xx, connection errors and timeouts retry. Response JSON is optional and is not interpreted or stored. HTTP 200 means your endpoint accepted the event; it is not independent proof of an in-game reward.

HTTP/1.1 200 OK
Content-Type: application/json

{"message":"Reward accepted"}

Test callback

{
  "api_version": 1,
  "event": "callback.test",
  "server_id": 123,
  "test_id": "UNIQUE_TEST_ID",
  "sent_at": 1792590000
}

Verify the same signature and server ID, then return HTTP 200. Never award a reward for callback.test. A successful test verifies connectivity and your test handler only. Tests do not create votes, change ranking or enter the reward queue.

Retries and delivery records

ShardHop stores confirmed votes and delivery state in its database. The worker runs every minute, processing up to 20 events per batch. After failure, the next four attempts become eligible after 1, 5, 15 and 60 minutes; queue load can delay delivery. After five unsuccessful attempts the event is marked failed. Fix your endpoint, then choose Retry failed deliveries. Failed events remain retrievable through the API. Do not combine callbacks and polling unless both paths use the same unique receipt.

Example callback.php

This is an example to copy into a file you create on your own backend. It requires PHP 8.1+, PDO MySQL and the receipt table below in the same database as your reward data. Configure the environment variables and replace grantReward() with your game-specific code. Until that function is implemented, real votes return HTTP 503 rather than falsely reporting a reward. Tests can still return HTTP 200.

View PHP callback example
<?php
// Documentation example: owners create and host their own endpoint.
// PHP 8.1+, PDO MySQL. Configure these environment variables on YOUR backend:
// SHARDHOP_CALLBACK_SECRET, SHARDHOP_SERVER_ID, GAME_DB_DSN,
// GAME_DB_USER, GAME_DB_PASSWORD. Never place secrets in public HTML.
declare(strict_types=1);

function reply(int $status, string $message): never {
    http_response_code($status);
    header('Content-Type: application/json');
    echo json_encode(['message' => $message]);
    exit;
}

if ($_SERVER['REQUEST_METHOD'] !== 'POST') reply(405, 'POST required');
$secret = getenv('SHARDHOP_CALLBACK_SECRET') ?: '';
$serverId = (int) getenv('SHARDHOP_SERVER_ID');
if (strlen($secret) < 32 || $serverId < 1) reply(503, 'Configure the callback first');

$raw = file_get_contents('php://input', false, null, 0, 65537);
if ($raw === false || strlen($raw) > 65536) reply(413, 'Invalid body size');
$timestamp = $_SERVER['HTTP_X_SHARDHOP_TIMESTAMP'] ?? '';
$signature = $_SERVER['HTTP_X_SHARDHOP_SIGNATURE'] ?? '';
if (!ctype_digit($timestamp) || abs(time() - (int)$timestamp) > 300) reply(401, 'Expired timestamp');
$expected = 'sha256=' . hash_hmac('sha256', $timestamp . '.' . $raw, $secret);
if (!hash_equals($expected, $signature)) reply(401, 'Invalid signature');

try { $event = json_decode($raw, true, 32, JSON_THROW_ON_ERROR); }
catch (JsonException $e) { reply(400, 'Invalid JSON'); }
if (!is_array($event) || ($event['api_version'] ?? null) !== 1
    || ($event['server_id'] ?? null) !== $serverId) reply(400, 'Wrong version or server');

// Connectivity tests must NEVER award a reward.
if (($event['event'] ?? '') === 'callback.test') reply(200, 'Test accepted; no reward awarded');
if (($event['event'] ?? '') !== 'vote.accepted'
    || !is_int($event['vote_id'] ?? null) || $event['vote_id'] < 1
    || !is_string($event['player_id'] ?? null) || strlen($event['player_id']) > 100
    || $event['player_id'] === '') reply(400, 'Invalid vote event');

function grantReward(PDO $db, string $playerId): void {
    // REPLACE this function with your own parameterized game-database UPDATE.
    // Resolve $playerId to the correct account. Throw if it does not exist.
    // Use THIS $db connection and transactional tables for the reward write.
    // Do not commit here, run DDL or send non-idempotent remote game commands.
    // Example shape: UPDATE your_accounts SET your_coins = your_coins + ? WHERE ...
    throw new RuntimeException('Implement your game reward here');
}

$db = null;
try {
    $db = new PDO(getenv('GAME_DB_DSN'), getenv('GAME_DB_USER'), getenv('GAME_DB_PASSWORD'),
        [PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION, PDO::ATTR_EMULATE_PREPARES => false]);
    $db->beginTransaction();
    $receipt = $db->prepare('INSERT INTO shardhop_reward_receipts (server_id, vote_id) VALUES (?, ?)');
    try { $receipt->execute([$serverId, $event['vote_id']]); }
    catch (PDOException $e) {
        $db->rollBack();
        // A concurrent request may have committed this receipt already.
        if ($e->getCode() === '23000') {
            $check = $db->prepare('SELECT 1 FROM shardhop_reward_receipts WHERE server_id = ? AND vote_id = ?');
            $check->execute([$serverId, $event['vote_id']]);
            if ($check->fetchColumn()) reply(200, 'Already rewarded');
        }
        throw $e;
    }
    grantReward($db, $event['player_id']);
    $db->commit();
    reply(200, 'Reward accepted');
} catch (Throwable $e) {
    if ($db instanceof PDO && $db->inTransaction()) $db->rollBack();
    // Log diagnostics privately; never expose database credentials to callers.
    reply(503, 'Reward unavailable; retry later');
}

Example receipt table

Create this once in your game database. Keep receipts as long as vote IDs can be replayed. Your reward tables must also use transactional storage such as InnoDB.

CREATE TABLE shardhop_reward_receipts (
  server_id BIGINT UNSIGNED NOT NULL,
  vote_id BIGINT UNSIGNED NOT NULL,
  created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
  PRIMARY KEY (server_id, vote_id)
) ENGINE=InnoDB;

The receipt and reward must use the same database connection and transaction. For remote game commands, use a durable local outbox and an idempotent command consumer instead; do not mark a receipt as completed before an unreliable remote command succeeds.

Private API reference

You can build your own polling integration or inspect delivery status. All requests require Authorization: Bearer YOUR_PRIVATE_API_KEY and version v=1. Keep keys on the backend; query-string keys and browser CORS access are not supported. ShardHop exposes JSON endpoints, not direct access to its database.

GET /vote-api/pending

GET https://shardhop.com/vote-api/pending?v=1&server_id=123
Authorization: Bearer YOUR_PRIVATE_API_KEY

{"api_version":1,"server_id":123,"votes":[{"vote_id":456,"player_id":"PlayerName","voted_at":1792590000}],"has_more":false}

Returns up to 100 oldest unacknowledged votes, including failed callbacks. Fetching does not consume them. Drain further batches when has_more is true.

POST /vote-api/acknowledge

POST https://shardhop.com/vote-api/acknowledge
Authorization: Bearer YOUR_PRIVATE_API_KEY
Content-Type: application/json

{"v":1,"server_id":123,"vote_id":456}

Response: {"api_version":1,"vote_id":456,"acknowledged":true}

Use after your polling integration commits the reward. Repeated acknowledgements are safe. Callback integrations do not need this extra request: returning HTTP 200 acknowledges the vote automatically.

GET /vote-api/status

GET https://shardhop.com/vote-api/status?v=1&server_id=123
Authorization: Bearer YOUR_PRIVATE_API_KEY

{
  "api_version":1,"server_id":123,
  "callback_configured":true,"callbacks_enabled":true,
  "last_attempt":{"event_type":"callback.test","http_code":200,"error_message":"","attempt_date":1792590000},
  "pending":0,"failed":0
}

last_attempt is null until a test or vote delivery occurs for the current configuration. HTTP code 0 means no HTTP response. pending counts queued events; failed counts events that exhausted automatic retries.

API errors

HTTPMeaning
400Unsupported version or invalid vote ID.
401Missing, incorrect or rotated private API key.
404No matching vote event for your server.
405Wrong HTTP method.
503Reward processing temporarily disabled; retain your receipt and retry later.

Errors use {"error":{"code":"..."}}. Handle timeouts and non-JSON infrastructure responses too. API response codes are separate from the HTTP codes returned by your callback.

Voting rules and data

Votes require a valid signed-in ShardHop account, native form verification and an approved listing in an active game. A rolling 24-hour cooldown checks account, hashed network address and case-insensitive player identity per server. Owners receive only their own server’s vote IDs, entered game identities and vote times. Network hashes and ShardHop user IDs are not sent. Delivered player identities are cleared after 90 days; pending and failed identities remain available for delivery.

Back
Top