Buy ad spot $20/30d (1200×150)
Postback — Quick Setup
Last update: Feb 25, 20261) What you need to do (admin)
- On your hosting, create a folder (any path on your domain).
- Upload two files there:
config.phpandpostback.php(examples below). - (Optional) Add
.htaccessfrom this page to allow only POST and optionally restrict by IP. - Open your server card on MMOPLUS.PRO → Owner Postback:
- Enable Postback,
- Set Postback URL to your
postback.php, - Set HMAC Secret the same as in your
config.php(secret).
- Click Save and test with Send test.
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]', // deposit table (change if another table)
'account_field' => 'AccountID', // player login (change if another column)
],
'log' => '[Louis].[dbo].[MMOPlusPostbackLog]', // postback log
],
// Which currency column to deposit into
'credit' => [
'column' => 'WCoinC', // WCoinC, WCoinP, GoblinPoint, ... (change if another column)
],
// Amounts to deposit
'rewards' => [
'vote' => 10, // reward for voting
'review' => 0, // reward for review (disabled) don't tounch
'test' => 5, // TEST - don't tounch
],
// Where to read the account from in the JSON payload
// If the postback carries AccountID: ['userId']
// If it carries character nickname: ['nickname']
'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']);
}
/* ---------- Basic fields ---------- */
$event = (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'] ?? ''));
$ts = (string)($data['ts'] ?? gmdate('c'));
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 (if configured) ---------- */
$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 accountId from payload ---------- */
$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)']);
}
/* ---------- Rewards & 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 config ---------- */
$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 === '') {
out(500, ['ok'=>false,'error'=>'Cash table not configured']);
}
if ($accField === '') {
out(500, ['ok'=>false,'error'=>'Account field not configured']);
}
/* ---------- DB connect (driver switch) ---------- */
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 for log table/columns ---------- */
function object_name_for_sys(string $maybeBracketed): string {
return str_replace(['[',']'], '', $maybeBracketed);
}
function log_has_note(PDO $pdo, string $logTable): bool {
$obj = object_name_for_sys($logTable);
$sql = "SELECT 1
FROM sys.columns c
WHERE c.object_id = OBJECT_ID(N'{$obj}')
AND c.name = 'note'";
try {
$r = $pdo->query($sql);
return (bool)$r->fetchColumn();
} catch (Throwable $e) {
return false;
}
}
try {
$pdo = make_pdo((array)($cfg['db'] ?? []));
/* ---------- Ensure log table exists ---------- */
$pdo->exec("
IF NOT EXISTS (SELECT * FROM sys.tables WHERE name = 'MMOPlusPostbackLog')
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
");
/* ---------- Try to add note column if missing ---------- */
$hasNote = log_has_note($pdo, $logTable);
if (!$hasNote) {
try {
$pdo->exec("IF COL_LENGTH('".object_name_for_sys($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 presence ---------- */
$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 in CashShopData','accountId'=>$accountId]);
}
/* ---------- Credit or just log ---------- */
$credited = false;
if ($amount > 0) {
$pdo->beginTransaction();
$col = $creditCol;
$upd = $pdo->prepare("
UPDATE {$cashTbl}
SET {$col} = ISNULL({$col},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 PB column will show ✅ or ❌ accordingly.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 [Louis].[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
5) Optional security: .htaccess (Apache)
<Files "postback.php">
<RequireAll>
Require method POST
Require all granted
# Optional allowlist:
# Require ip 1.2.3.4
# Require ip 5.6.7.8
</RequireAll>
</Files>
Options -Indexes
For Nginx, restrict to POST (and IP allowlist) at the server/location level.
What players get
- On a vote: the amount from
rewards.vote(e.g., 10 WCoinC). - On a review (if you set a non-zero amount):
rewards.review. - You (owner) see PB status on the server page: OFF / Pending / ✅ / ❌.