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 folder (any path on your domain).
  2. Upload two files there: config.php and postback.php (examples below).
  3. (Optional) Add .htaccess from this page to allow only POST and optionally restrict by IP.
  4. Open your server card on MUOGG → Owner Postback:
    • Enable Postback,
    • Set Postback URL to your postback.php,
    • Set HMAC Secret the same as in your config.php (secret).

After a successful setup, players will automatically receive your configured currency for a vote (and optionally for a review) — amounts come from rewards in config.php.

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

<?php

return [
  'timezone' => 'Europe/Vilnius',
  '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
  'account_from' => ['nickname', 'userId'],

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

3) 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['userId'] ?? ''));
$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'] ?? ['nickname','userId']) as $f) {
  if ($f === 'nickname' && $nickname !== '') { $accountId = $nickname; break; }
  if ($f === 'userId'   && $userId   !== '') { $accountId = $userId;   break; }
}
if ($accountId === '') out(422, ['ok'=>false,'error'=>'Missing account id (nickname/userId)']);

/* 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 server page postback status will reflect it.

4) 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