Skip to main content

Vote webhook guide

Reward verified votes automatically on your website or game server.

PlayMU sends a signed JSON POST to your endpoint after a vote is accepted. Your endpoint must use HTTPS, verify the signature against the raw request body, process each vote once, and return any 2xx response.

1. Configure PlayMU

  1. Open My Servers, edit your server, and save the HTTPS endpoint under Webhook URL.
  2. Generate or regenerate the webhook secret and store it as an environment variable on your website.
  3. Implement the receiver below, then click Test webhook. The integration becomes active only after a successful test.

2. Verify every request

PlayMU signs the exact raw body with HMAC-SHA256. Do not decode and re-encode the JSON before checking the signature.

Node.js

Install Express with npm install express. Register this route before any express.json() middleware.

import crypto from 'node:crypto';
import express from 'express';

const app = express();

app.post('/webhooks/playmu', express.raw({ type: 'application/json' }), (req, res) => {
    const secret = process.env.PLAYMU_WEBHOOK_SECRET ?? '';
    const body = req.body;
    const provided = req.get('X-PlayMu-Signature') ?? '';
    const expected = 'sha256=' + crypto.createHmac('sha256', secret).update(body).digest('hex');
    const providedBuffer = Buffer.from(provided);
    const expectedBuffer = Buffer.from(expected);
    const valid = secret !== ''
        && providedBuffer.length === expectedBuffer.length
        && crypto.timingSafeEqual(providedBuffer, expectedBuffer);

    if (!valid) return res.sendStatus(401);

    let payload;
    try {
        payload = JSON.parse(body.toString('utf8'));
    } catch {
        return res.sendStatus(400);
    }

    if (payload.event === 'webhook.test') return res.sendStatus(204);
    if (payload.event !== 'vote.completed' || payload.status !== 'success') {
        return res.sendStatus(202);
    }

    // In one database transaction:
    // 1. Reject an already processed (server_slug, vote_id).
    // 2. Resolve external_hash to the expected account.
    // 3. Credit that account and mark the vote as processed.

    return res.sendStatus(204);
});

app.listen(3000);
Python

Install Flask with pip install flask.

import hashlib
import hmac
import json
import os

from flask import Flask, request

app = Flask(__name__)

@app.post('/webhooks/playmu')
def playmu_webhook():
    secret = os.environ.get('PLAYMU_WEBHOOK_SECRET', '')
    body = request.get_data(cache=False)
    provided = request.headers.get('X-PlayMu-Signature', '')
    expected = 'sha256=' + hmac.new(
        secret.encode('utf-8'), body, hashlib.sha256
    ).hexdigest()

    if not secret or not hmac.compare_digest(expected, provided):
        return '', 401

    try:
        payload = json.loads(body)
    except (TypeError, ValueError):
        return '', 400

    if payload.get('event') == 'webhook.test':
        return '', 204

    if payload.get('event') != 'vote.completed' or payload.get('status') != 'success':
        return '', 202

    # In one database transaction:
    # 1. Reject an already processed (server_slug, vote_id).
    # 2. Resolve external_hash to the expected account.
    # 3. Credit that account and mark the vote as processed.

    return '', 204
PHP
$secret = $_ENV['PLAYMU_WEBHOOK_SECRET'] ?? '';
$body = file_get_contents('php://input');
$provided = $_SERVER['HTTP_X_PLAYMU_SIGNATURE'] ?? '';
$expected = 'sha256=' . hash_hmac('sha256', $body, $secret);

if ($secret === '' || !hash_equals($expected, $provided)) {
    http_response_code(401);
    exit;
}

$payload = json_decode($body, true);
if (!is_array($payload)) {
    http_response_code(400);
    exit;
}

if (($payload['event'] ?? '') === 'webhook.test') {
    http_response_code(204);
    exit;
}

if (($payload['event'] ?? '') !== 'vote.completed'
    || ($payload['status'] ?? '') !== 'success') {
    http_response_code(202);
    exit;
}

// In one database transaction:
// 1. Reject an already processed (server_slug, vote_id).
// 2. Resolve external_hash to the expected account.
// 3. Credit that account and mark the vote as processed.

http_response_code(204);

Request headers

  • Content-Type: application/json
  • X-PlayMu-Signature: sha256=<hex-hmac>
  • X-PlayMu-Event: vote.completed or webhook.test

3. Vote payload

{
  "event": "vote.completed",
  "vote_id": 12345,
  "server_slug": "your-server",
  "userid": "PlayerAccount",
  "external_hash": "your-single-use-reference",
  "status": "success",
  "voted_at": "2026-08-17T14:30:00+00:00",
  "test": false
}

Treat vote_id as the delivery id and make processing idempotent. A callback may be delivered more than once when a response is lost or your endpoint returns a non-2xx status.

4. Identify the player safely

The userid query value is entered by the visitor and must not be trusted by itself. Before sending the player to the vote page, create a random, single-use external_hash on your website, store it with the authenticated account and an expiry time, then place it in the vote link shown in your server panel.

On vote.completed, atomically consume that reference and reward the stored account. Reject expired, unknown, already consumed, or account-mismatched references.

5. Test payload and delivery

{
  "event": "webhook.test",
  "test": true,
  "server_slug": "your-server",
  "timestamp": "2026-08-17T14:30:00+00:00",
  "nonce": "32-character-hex-value"
}

Return a 2xx response within five seconds. Failed vote deliveries are retried after approximately 1, 5, and 15 minutes. Webhook failure never removes the vote from the ranking.

Production checklist

  • Secret stored outside source control and logs.
  • Signature checked with the language's constant-time comparison helper against the raw body.
  • Reward and processed-vote record committed in one database transaction.
  • Unique database key on (server_slug, vote_id).
  • external_hash random, short-lived, single-use, and bound to the account.
  • Endpoint returns quickly; slow secondary work runs in your own queue.

Vote & Support

Vote every 12 hours and help your favorite server reach the top.

Find the Best

Filter by season, EXP, reset type, country, opening date and listing quality.

Join & Play

Visit the server website, read details and start your next adventure.

Advertise

Use premium slots and banner placements to grow your community.