Owner Postback (Vote Rewards)

Reward players automatically when they vote for your server. Follow the steps below to connect your endpoint, verify 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 → AccountPostback and save:
    • Account 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.

2) Required settings

3) What MUOGG sends

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

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

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