Discover MMORPG private servers

Multiple Games • Upcoming Servers • Top Rankings • Real Players

Your world.
Your rules.

Explore game servers

Choose your favorite game and find the best private servers.

YOUR WORLD. YOUR RULES.

Owner postback

Reward players automatically when they vote for your server: connect your endpoint, verify the HMAC signature and credit WCoin in your MS SQL database.

1) What you need to do (admin)

  1. On your hosting, create a public folder, for example /muogg-postback/.
  2. Upload two files there: config.php and postback.php (examples below).
  3. Make sure the final endpoint is reachable from the internet, for example https://your-domain.com/muogg-postback/postback.php.
  4. Open MUOGG → Account → Postback, select the server and save:
    • Server Postback URL — the full URL to your uploaded postback.php,
    • Postback Secret — the same value as secret in your config.php.
  5. Use Send Test Postback with a real account name from your MU database.

After a successful setup, players will automatically receive your configured currency for a vote. Amounts come from rewards in config.php.

Automatically fill the player's account

On your game website, build the vote link using the currently logged-in game account:

https://muogg.com/vote.php?server=YOUR_SERVER_ID&user=PLAYER_ACCOUNT

Replace YOUR_SERVER_ID with your listing ID or slug and URL-encode the account (in PHP: rawurlencode($gameAccount)). The aliases account, username and custom are also supported. Never include a password or your postback secret in this link.

The player sees the account before confirming the vote and can correct it. They can also select Remember my account to fill it on future visits in the same browser, separately for each server. A new account supplied by a link takes priority over a remembered account. Uncheck the option to forget it.

Postback setup alone cannot identify a player's game login. Your game website must supply it in the link, or the player can enter and remember it once. The selected account is sent in the existing signed postback after a successful vote.

2) Required settings

  • Public URL: use a real http or https domain. Localhost, private IPs and internal network URLs are not accepted.
  • HMAC Secret: must be 8-128 characters in MUOGG and must match your config.php exactly.
  • Database connection: enter the correct MS SQL host, port, database, username and password for your MU server.
  • Cash table: set the table that stores player currency, for example CashShopData.
  • Account field: set the column used to find the player account, usually AccountID.
  • Currency column: choose the column to credit, for example WCoinC, WCoinP or GoblinPoint.
  • Vote reward: set the amount for vote. Set an event amount to 0 to log it without crediting coins.

3) What MUOGG sends

When a player votes, MUOGG sends a signed JSON POST request to your Postback URL.

  • event — currently vote for real vote rewards.
  • server_id — the server ID from MUOGG.
  • success — must be true for a reward to be credited.
  • username — the main value to use as the MU account/nickname from the vote form.
  • account, character and nickname — compatibility aliases with the same player name.
  • user_id — MUOGG user ID when available. It can be 0, so do not use it as the primary MU account field.
  • ip and voted_at — vote IP and UTC timestamp for logging.
  • X-Signature header — HMAC-SHA256 signature. Your endpoint verifies it with the same secret.

The example endpoint accepts both the current fields and legacy aliases like userId and ts for compatibility.

4) Example config.php (upload to your hosting)

<?php

return [
  'timezone' => 'Europe/Berlin',
  'secret'   => 'testsecret', // HMAC for X-Signature

  // Connecting to the database
  'db' => [
    'driver'  => 'pdo_dblib',   // 'pdo_sqlsrv' | 'pdo_dblib'
    'host'    => '11.22.333.44',
    'port'    => 1433,
    'dbname'  => 'MuOnline',
    'user'    => 'sa',
    'pass'    => 'DBPassword',
    'charset' => 'UTF-8',
  ],

  // Tables
  'tables' => [
    'cash' => [
      'table'         => '[Louis].[dbo].[CashShopData]',
      'account_field' => 'AccountID',
    ],
    'log'  => '[Louis].[dbo].[MMOPlusPostbackLog]',
  ],

  // Which currency column to deposit into
  'credit' => [
    'column' => 'WCoinC', // WCoinC, WCoinP, GoblinPoint, ...
  ],

  // Amounts to deposit
  'rewards' => [
    'vote'   => 10,  // reward for voting
    'review' => 0,   // disabled
    'test'   => 5,   // test ping
  ],

  // Where to read the account from in the JSON payload
  // New format first, legacy fields kept for compatibility.
  'account_from' => ['username', 'account', 'character', 'nickname', 'user_id', 'userId'],

  // Optional IP allowlist
  'ip_allow' => [
    // '1.2.3.4',
  ],
];
Replace DB credentials and currency column/amounts as you need.

5) Example postback.php (upload to your hosting)

<?php
declare(strict_types=1);

header('Content-Type: application/json; charset=utf-8');

$cfg = require __DIR__ . '/config.php';
date_default_timezone_set((string)($cfg['timezone'] ?? 'UTC'));

function out(int $code, array $data): never {
  http_response_code($code);
  echo json_encode($data, JSON_UNESCAPED_UNICODE);
  exit;
}

if (($_SERVER['REQUEST_METHOD'] ?? '') !== 'POST') {
  out(405, ['ok'=>false,'error'=>'Method Not Allowed']);
}

/* Optional IP allowlist */
if (!empty($cfg['ip_allow']) && is_array($cfg['ip_allow'])) {
  $ip = (string)($_SERVER['REMOTE_ADDR'] ?? '');
  if (!in_array($ip, $cfg['ip_allow'], true)) {
    out(403, ['ok'=>false,'error'=>'Forbidden IP']);
  }
}

/* Read JSON */
$raw  = file_get_contents('php://input') ?: '';
$data = json_decode($raw, true);
if (!is_array($data)) out(400, ['ok'=>false,'error'=>'Invalid JSON']);

/* Fields */
$event     = trim((string)($data['event'] ?? ''));
$server_id = (int)($data['server_id'] ?? 0);
$success   = (bool)($data['success'] ?? false);
$userId    = trim((string)($data['user_id'] ?? ($data['userId'] ?? '')));
$username  = trim((string)($data['username'] ?? ''));
$account   = trim((string)($data['account'] ?? ''));
$character = trim((string)($data['character'] ?? ''));
$nickname  = trim((string)($data['nickname'] ?? ''));
$reqIp     = (string)($data['ip'] ?? ($_SERVER['REMOTE_ADDR'] ?? ''));
if ($event === '' || $server_id <= 0) out(422, ['ok'=>false,'error'=>'Missing fields (event/server_id)']);
if ($event !== 'test' && !$success)   out(200, ['ok'=>true,'credited'=>false,'msg'=>'success=false']);

/* HMAC */
$secret = (string)($cfg['secret'] ?? '');
if ($secret !== '') {
  $sigHeader = (string)($_SERVER['HTTP_X_SIGNATURE'] ?? '');
  $calc      = hash_hmac('sha256', $raw, $secret);
  if (!hash_equals($calc, $sigHeader)) out(401, ['ok'=>false,'error'=>'Bad signature']);
}

/* Resolve account */
$accountId = '';
foreach ((array)($cfg['account_from'] ?? ['username','account','character','nickname','user_id','userId']) as $f) {
  if ($f === 'username' && $username !== '') { $accountId = $username; break; }
  if ($f === 'account' && $account !== '') { $accountId = $account; break; }
  if ($f === 'character' && $character !== '') { $accountId = $character; break; }
  if ($f === 'nickname' && $nickname !== '') { $accountId = $nickname; break; }
  if (($f === 'user_id' || $f === 'userId') && $userId !== '' && $userId !== '0') { $accountId = $userId; break; }
}
if ($accountId === '') out(422, ['ok'=>false,'error'=>'Missing account id (username/user_id)']);

/* Reward & credit column */
$amount = (int)(($cfg['rewards'] ?? [])[$event] ?? 0);
if ($amount < 0) $amount = 0;
$creditCol = preg_replace('~[^A-Za-z0-9_]+~', '', (string)($cfg['credit']['column'] ?? 'WCoinC'));
if ($creditCol === '') $creditCol = 'WCoinC';

/* Tables */
$tables   = (array)($cfg['tables'] ?? []);
$cashCfg  = (array)($tables['cash'] ?? []);
$cashTbl  = (string)($cashCfg['table'] ?? '');
$accField = preg_replace('~[^A-Za-z0-9_]+~', '', (string)($cashCfg['account_field'] ?? 'AccountID'));
$logTable = (string)($tables['log'] ?? '[dbo].[MMOPlusPostbackLog]');
if ($cashTbl === '' || $accField === '') out(500, ['ok'=>false,'error'=>'Table configuration error']);

/* DB connect */
function make_pdo(array $db): PDO {
  $driver  = (string)($db['driver'] ?? 'pdo_sqlsrv');
  $host    = (string)($db['host']   ?? '127.0.0.1');
  $port    = (int)   ($db['port']   ?? 1433);
  $dbname  = (string)($db['dbname'] ?? '');
  $user    = (string)($db['user']   ?? '');
  $pass    = (string)($db['pass']   ?? '');
  $charset = (string)($db['charset']?? 'UTF-8');
  $dsn     = (string)($db['dsn']    ?? '');
  if ($dsn === '') {
    if ($driver === 'pdo_sqlsrv') $dsn = "sqlsrv:Server={$host},".(int)$port.";Database={$dbname}";
    else $dsn = "dblib:host={$host}:".(int)$port.";dbname={$dbname};charset={$charset}";
  }
  return new PDO($dsn, $user, $pass, [
    PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION,
    PDO::ATTR_DEFAULT_FETCH_MODE => PDO::FETCH_ASSOC,
  ]);
}


/* Helpers */

function obj_name(string $x): string { return str_replace(['[',']'], '', $x); }

function log_has_note(PDO $pdo, string $logTable): bool {

  $obj = obj_name($logTable);

  $sql = "SELECT 1 FROM sys.columns WHERE object_id = OBJECT_ID(N'{$obj}') AND name = 'note'";

  try { return (bool)$pdo->query($sql)->fetchColumn(); } catch(Throwable $e){ return false; }

}



try {

  $pdo = make_pdo((array)($cfg['db'] ?? []));



  /* Ensure log table */

  $tblBare = basename(str_replace('.', '/', obj_name($logTable)));

  $pdo->exec("

    IF NOT EXISTS (SELECT * FROM sys.tables WHERE name = '{$tblBare}')

    BEGIN

      CREATE TABLE {$logTable}(

        id BIGINT IDENTITY(1,1) PRIMARY KEY,

        server_id INT NOT NULL,

        event NVARCHAR(24) NOT NULL,

        account_id NVARCHAR(64) NOT NULL,

        user_id NVARCHAR(128) NOT NULL,

        ip NVARCHAR(64) NULL,

        amount INT NOT NULL,

        credited BIT NOT NULL DEFAULT 0,

        payload NVARCHAR(MAX) NOT NULL,

        created_at DATETIME2 NOT NULL

      );

    END

  ");



  $hasNote = log_has_note($pdo, $logTable);

  if (!$hasNote) {

    try {

      $pdo->exec("IF COL_LENGTH('".obj_name($logTable)."','note') IS NULL

                  BEGIN ALTER TABLE {$logTable} ADD note NVARCHAR(200) NULL END");

      $hasNote = log_has_note($pdo, $logTable);

    } catch (Throwable $e) { $hasNote = false; }

  }



  /* 24h duplicate guard */

  $dup = $pdo->prepare("

    SELECT TOP 1 id FROM {$logTable}

     WHERE server_id=:sid AND event=:ev AND account_id=:acc

       AND created_at >= DATEADD(hour,-24,SYSUTCDATETIME())

  ");

  $dup->execute([':sid'=>$server_id, ':ev'=>$event, ':acc'=>$accountId]);

  if ($dup->fetch()) {

    $sql = $hasNote

      ? "INSERT INTO {$logTable}(server_id,event,account_id,user_id,ip,amount,credited,payload,created_at,note)

         VALUES(:sid,:ev,:acc,:uid,:ip,0,0,:payload,SYSUTCDATETIME(),:note)"

      : "INSERT INTO {$logTable}(server_id,event,account_id,user_id,ip,amount,credited,payload,created_at)

         VALUES(:sid,:ev,:acc,:uid,:ip,0,0,:payload,SYSUTCDATETIME())";

    $stmt = $pdo->prepare($sql);

    $stmt->execute([

      ':sid'=>$server_id, ':ev'=>$event, ':acc'=>$accountId, ':uid'=>$userId,

      ':ip'=>$reqIp, ':payload'=>$raw, ':note'=>'duplicate_24h'

    ]);

    out(200, ['ok'=>true,'credited'=>false,'msg'=>'Already credited in last 24h']);

  }



  /* Check account */

  $check = $pdo->prepare("SELECT TOP 1 {$accField} AS acc FROM {$cashTbl} WHERE {$accField}=:acc");

  $check->execute([':acc'=>$accountId]);

  if (!$check->fetch()) {

    $sql = $hasNote

      ? "INSERT INTO {$logTable}(server_id,event,account_id,user_id,ip,amount,credited,payload,created_at,note)

         VALUES(:sid,:ev,:acc,:uid,:ip,0,0,:payload,SYSUTCDATETIME(),:note)"

      : "INSERT INTO {$logTable}(server_id,event,account_id,user_id,ip,amount,credited,payload,created_at)

         VALUES(:sid,:ev,:acc,:uid,:ip,0,0,:payload,SYSUTCDATETIME())";

    $stmt = $pdo->prepare($sql);

    $stmt->execute([

      ':sid'=>$server_id, ':ev'=>$event, ':acc'=>$accountId, ':uid'=>$userId,

      ':ip'=>$reqIp, ':payload'=>$raw, ':note'=>'account_not_found'

    ]);

    out(200, ['ok'=>true,'credited'=>false,'msg'=>'Account not found','accountId'=>$accountId]);

  }



  /* Credit or log */

  $credited = false;

  if ($amount > 0) {

    $pdo->beginTransaction();

    $upd = $pdo->prepare("

      UPDATE {$cashTbl}

         SET {$creditCol} = ISNULL({$creditCol},0) + :amt

       WHERE {$accField} = :acc

    ");

    $upd->execute([':amt'=>$amount, ':acc'=>$accountId]);

    if ($upd->rowCount() < 1) {

      $pdo->rollBack();

      $sql = $hasNote

        ? "INSERT INTO {$logTable}(server_id,event,account_id,user_id,ip,amount,credited,payload,created_at,note)

           VALUES(:sid,:ev,:acc,:uid,:ip,0,0,:payload,SYSUTCDATETIME(),:note)"

        : "INSERT INTO {$logTable}(server_id,event,account_id,user_id,ip,amount,credited,payload,created_at)

           VALUES(:sid,:ev,:acc,:uid,:ip,0,0,:payload,SYSUTCDATETIME())";

      $stmt = $pdo->prepare($sql);

      $stmt->execute([

        ':sid'=>$server_id, ':ev'=>$event, ':acc'=>$accountId, ':uid'=>$userId,

        ':ip'=>$reqIp, ':payload'=>$raw, ':note'=>'update_failed'

      ]);

      out(500, ['ok'=>false,'error'=>'Update failed']);

    }



    $sql = $hasNote

      ? "INSERT INTO {$logTable}(server_id,event,account_id,user_id,ip,amount,credited,payload,created_at,note)

         VALUES(:sid,:ev,:acc,:uid,:ip,:amt,1,:payload,SYSUTCDATETIME(),NULL)"

      : "INSERT INTO {$logTable}(server_id,event,account_id,user_id,ip,amount,credited,payload,created_at)

         VALUES(:sid,:ev,:acc,:uid,:ip,:amt,1,:payload,SYSUTCDATETIME())";

    $log = $pdo->prepare($sql);

    $log->execute([

      ':sid'=>$server_id, ':ev'=>$event, ':acc'=>$accountId, ':uid'=>$userId,

      ':ip'=>$reqIp, ':amt'=>$amount, ':payload'=>$raw,

    ]);



    $pdo->commit();

    $credited = true;

  } else {

    $sql = $hasNote

      ? "INSERT INTO {$logTable}(server_id,event,account_id,user_id,ip,amount,credited,payload,created_at,note)

         VALUES(:sid,:ev,:acc,:uid,:ip,0,0,:payload,SYSUTCDATETIME(),:note)"

      : "INSERT INTO {$logTable}(server_id,event,account_id,user_id,ip,amount,credited,payload,created_at)

         VALUES(:sid,:ev,:acc,:uid,:ip,0,0,:payload,SYSUTCDATETIME())";

    $log = $pdo->prepare($sql);

    $log->execute([

      ':sid'=>$server_id, ':ev'=>$event, ':acc'=>$accountId, ':uid'=>$userId,

      ':ip'=>$reqIp, ':payload'=>$raw, ':note'=>'zero_reward'

    ]);

  }



  out(200, [

    'ok'=>true,

    'credited'=>$credited,
    'amount'=>$credited ? $amount : 0,
    'currency'=>$creditCol,
    'accountId'=>$accountId,
  ]);

} catch (Throwable $e) {
  try {
    if (isset($pdo)) {
      $stmt = $pdo->prepare("
        INSERT INTO {$logTable}(server_id,event,account_id,user_id,ip,amount,credited,payload,created_at)
        VALUES(:sid,:ev,:acc,:uid,:ip,0,0,:payload,SYSUTCDATETIME())
      ");
      $stmt->execute([
        ':sid'=>$server_id ?? 0,
        ':ev'=>$event ?? '',
        ':acc'=>$accountId ?? '',
        ':uid'=>$userId ?? '',
        ':ip'=>$reqIp ?? '',
        ':payload'=>$raw ?? '',
      ]);
    }
  } catch(Throwable $ignored){}
  out(500, ['ok'=>false,'error'=>'Server error','detail'=>$e->getMessage()]);
}
Return {"credited": true} on success; otherwise {"credited": false}. The account test response will show the HTTP code and endpoint response body.

6) Expected response and test result

  • On success, return HTTP 200 and a JSON response with ok: true and credited: true.
  • If the account does not exist, return a successful response with credited: false so the owner can see the reason without retry loops.
  • If the HMAC signature is wrong, return HTTP 401. This usually means the secret in MUOGG does not match config.php.
  • If the URL is blocked, private, invalid or unreachable, the test will show bad_url or network_error.
  • If the same account already received the same event recently, the example endpoint logs it as duplicate and does not credit again.

For the test, use a real MU account name that exists in the configured cash table. If the username is not found, the endpoint is working but no coins will be added.

7) Create the log table (T-SQL)

If you prefer to create it manually, run this in SQL Server:

IF NOT EXISTS (SELECT * FROM sys.tables WHERE name = 'MMOPlusPostbackLog')
BEGIN
  CREATE TABLE [MuOnline].[dbo].[MMOPlusPostbackLog](
    [id] BIGINT IDENTITY(1,1) PRIMARY KEY,
    [server_id] INT NOT NULL,
    [event] NVARCHAR(24) NOT NULL,
    [account_id] NVARCHAR(64) NOT NULL,
    [user_id] NVARCHAR(128) NOT NULL,
    [ip] NVARCHAR(64) NULL,
    [amount] INT NOT NULL,
    [credited] BIT NOT NULL DEFAULT 0,
    [payload] NVARCHAR(MAX) NOT NULL,
    [created_at] DATETIME2 NOT NULL
  );
END