How to integrate donations with PayPal and PIX on your MU Online server
Learn how to build a complete donation system for your MU Online server, integrating PayPal via IPN/Webhook and PIX through Brazilian gateways, with automatic VIP and WCoin crediting and no manual intervention.
Building a reliable donation system is one of the steps that most sets an amateur MU Online server apart from a sustainable project. Most administrators start by asking for a receipt over Discord and crediting VIP by hand, which does not scale, causes delays and leaves room for human error and scams
Building a reliable donation system is one of the steps that most sets an amateur MU Online server apart from a sustainable project. Most administrators start by asking for a receipt over Discord and crediting VIP by hand, which does not scale, causes delays and leaves room for human error and scams. In this advanced tutorial you will build an end-to-end donation flow: a checkout page on the site, integration with PayPal via IPN/Webhook, integration with PIX through a Brazilian gateway (using the dynamic charge standard with a QR Code and webhook confirmation), and automatic crediting of rewards into the game database. All the table names, columns and values shown are illustrative only and vary by version (Season 6, Season 16, IGCN files, MuEMU, etc.) — always check your distribution's real schema before running any SQL in production.
The core principle to take from this guide is simple: never credit a reward based on what the user's browser tells you. Valid payment confirmation always comes from a signed, verifiable server-to-server notification sent by the payment provider directly to your backend. Everything else is just interface.
Prerequisites
Before starting, make sure you have the environment and access below. Without them, the rest of the tutorial will not run.
- A working site running PHP 8.0 or higher (ideally 8.2+) with access to
composerto install SDKs. - A web server with valid HTTPS (Let's Encrypt already handles this). PayPal and PIX gateway webhooks require a public HTTPS endpoint — no
localhostor self-signed certificate. - Access to the game database (SQL Server, in most MU distributions) and to the site database (MySQL/MariaDB, common in CMSes).
- A PayPal Business account (the personal one does not issue the full REST API credentials).
- An account with a Brazilian payment gateway that offers PIX via API with a webhook (Mercado Pago, Efí/formerly Gerencianet, Asaas, PagSeguro, among others). The examples use the generic "create charge → receive webhook" pattern.
- A grasp of queues/cron or SQL transactions to ensure atomic crediting.
> Security: treat API credentials (Client ID, Secret, tokens) as passwords. Never put them in the Git repository or in front-end JavaScript. Use environment variables (.env) outside the public root whenever possible.
Overall flow architecture
Before writing any code, picture the path of a donation. The player never touches the game database directly; they pass through layers that validate each step.
- The player logs into the site and picks a package (e.g., R$ 20 = 5,000 WCoin + 30 days of VIP).
- The backend creates a pending order in the site database, with a unique identifier (
order_id). - The player is redirected to PayPal or receives a PIX QR Code.
- After paying, the provider sends a webhook to your server confirming the payment.
- The backend validates the signature, checks amount/currency, marks the order as paid and credits the reward in the game database — all idempotently.
- The player sees the updated balance in the panel.
| Layer | Responsibility | Must never |
|---|---|---|
| Front end | Show packages and start the payment | Decide whether something was paid |
| Backend (orders) | Create/update orders, generate the charge | Credit without a confirmed webhook |
| Webhook handler | Validate the signature and confirm | Trust return parameters (?status=success) |
| Crediting (game DB) | Add VIP/WCoin atomically | Run twice for the same transaction |
Modeling the orders table
Create a table in the site database to track every donation. Idempotency depends on it: the provider's transaction_id column must be unique to prevent duplicate credit.
CREATE TABLE donations (
id BIGINT AUTO_INCREMENT PRIMARY KEY,
account VARCHAR(20) NOT NULL, -- game account
method ENUM('paypal','pix') NOT NULL,
package_id INT NOT NULL,
amount_brl DECIMAL(10,2) NOT NULL,
wcoin INT NOT NULL,
vip_days INT NOT NULL DEFAULT 0,
status ENUM('pending','paid','credited','failed','refunded') DEFAULT 'pending',
provider_txn VARCHAR(120) NULL,
ip VARCHAR(45) NULL,
created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
paid_at DATETIME NULL,
UNIQUE KEY uq_provider_txn (provider_txn)
);
Note two details: the status goes through paid (the webhook arrived) and only then credited (the reward was applied in the game). Separating these two states is what saves you when crediting fails because the game database is offline — you reprocess without charging again.
PayPal integration (REST API + Webhook)
Modern PayPal uses the REST API v2 with OAuth2 authentication. You obtain an access_token with Client ID and Secret, create an "order", redirect the user and receive the result via webhook. Install the official SDK via Composer:
composer require paypal/paypal-server-sdk
To create the order in the backend (simplified example with a direct call):
<?php
// get OAuth2 token
$ch = curl_init('https://api-m.paypal.com/v1/oauth2/token');
curl_setopt_array($ch, [
CURLOPT_RETURNTRANSFER => true,
CURLOPT_USERPWD => $clientId . ':' . $secret,
CURLOPT_POSTFIELDS => 'grant_type=client_credentials',
]);
$token = json_decode(curl_exec($ch))->access_token;
// create the payment order
$payload = [
'intent' => 'CAPTURE',
'purchase_units' => [[
'reference_id' => $orderId, // your internal order_id
'amount' => ['currency_code' => 'BRL', 'value' => '20.00'],
]],
];
The critical point is the webhook. In the PayPal Developer panel you register the URL https://yoursite.com/webhook/paypal.php and subscribe to the PAYMENT.CAPTURE.COMPLETED event. When you receive the notification, you must verify the signature by calling the /v1/notifications/verify-webhook-signature endpoint, comparing the PAYPAL-TRANSMISSION-ID and PAYPAL-TRANSMISSION-SIG headers and the webhook_id. Only after that should you trust the content.
<?php
$body = file_get_contents('php://input');
$event = json_decode($body, true);
if ($event['event_type'] === 'PAYMENT.CAPTURE.COMPLETED') {
$txn = $event['resource']['id'];
$value = $event['resource']['amount']['value'];
$curr = $event['resource']['amount']['currency_code'];
$ref = $event['resource']['custom_id'] ?? null;
// 1. verify the signature via API (mandatory!)
// 2. check amount and currency against the saved order
// 3. credit idempotently
if ($curr === 'BRL' && verificarAssinaturaPayPal($body)) {
creditarDoacao($ref, $txn, (float)$value);
}
http_response_code(200);
}
Never credit based on the browser return (return_url). That redirect only serves to show "Thank you!" to the player. The financial truth lives in the verified webhook.
PIX integration via gateway
Pure PIX (a static key) does not notify your server automatically — that is why we use a gateway's dynamic charge (the PIX Cob standard), which generates a QR Code per order and fires a webhook when the payment lands. The generic flow is: authenticate against the API, create a charge with a txid and amount, receive the qrcode (copy-and-paste payload) and the imagemQrcode (base64), show it to the user, and wait for the webhook.
<?php
// generic example of creating a PIX charge (varies by gateway)
$cobranca = [
'calendario' => ['expiracao' => 3600],
'valor' => ['original' => '20.00'],
'chave' => '[email protected]',
'solicitacaoPagador' => "Donation order #{$orderId}",
];
// authenticated POST -> returns { txid, pixCopiaECola, location }
When the payer finishes, the gateway calls your webhook (e.g., https://yoursite.com/webhook/pix.php) with the txid and the status CONCLUIDA. Validation here is usually done through mTLS (the gateway's certificate) or an agreed-upon secret token — confirm in your provider's documentation. Just as with PayPal, check the amount, mark it as paid and credit:
<?php
$dados = json_decode(file_get_contents('php://input'), true);
foreach ($dados['pix'] ?? [] as $pix) {
if (($pix['status'] ?? '') === 'CONCLUIDA') {
creditarPorTxid($pix['txid'], (float)$pix['valor']);
}
}
http_response_code(200);
Atomic crediting in the game database
This is the most sensitive step. The reward (WCoin, VIP) goes to the game database (SQL Server), not the site's. The crediting function must be idempotent and transactional: if it runs twice for the same transaction, it credits only once.
<?php
function creditarDoacao(string $orderId, string $txn, float $valor): void
{
global $siteDb, $gameDb;
// idempotent lock: only proceed if it has not been credited yet
$ok = $siteDb->prepare(
"UPDATE donations SET status='paid', provider_txn=?, paid_at=NOW()
WHERE id=? AND status='pending'"
);
$ok->execute([$txn, $orderId]);
if ($ok->rowCount() === 0) return; // already processed
$row = /* fetch wcoin, vip_days, account from the order */;
// apply in the game DB (table/column names VARY BY VERSION)
$gameDb->beginTransaction();
$gameDb->prepare(
"UPDATE MEMB_INFO SET WCoin = WCoin + ? WHERE memb___id = ?"
)->execute([$row['wcoin'], $row['account']]);
// add VIP days according to your distribution's system
$gameDb->commit();
$siteDb->prepare("UPDATE donations SET status='credited' WHERE id=?")
->execute([$orderId]);
}
The WHERE ... status='pending' combined with rowCount() is the heart of idempotency: if two webhooks arrive (PayPal resends on timeout), only the first changes the row; the second finds zero affected rows and exits without crediting again. The WCoin column and the MEMB_INFO table are typical examples from Season 6 — in your version it could be Cash, zen, a custom Cash_Shop table, or a VIP system in a separate table. Always confirm.
If you want to decouple even further, the webhook only marks paid and a cron runs every minute processing paid orders that have not yet been credited. This makes the system resilient to game database outages.
Donation page and player experience
On the front end, present the packages clearly and offer both methods. An example of the package data structure:
const pacotes = [
{ id: 1, label: "Starter", brl: 10, wcoin: 2000, vip: 7 },
{ id: 2, label: "Warrior", brl: 20, wcoin: 5000, vip: 30 },
{ id: 3, label: "Lord", brl: 50, wcoin: 15000, vip: 90 },
];
On click, the front end calls your backend, which creates the pending order and returns the PayPal URL or the PIX QR Code. Show a PIX expiration countdown and an "Already paid / Check" button that queries the order status (without crediting — it only reads the status). If you are building your server from scratch, it is worth reviewing the rest of the structure in the guide how to build a MU Online server before plugging in the donation module.
Common errors and fixes
| Symptom | Likely cause | Fix |
|---|---|---|
| Donation paid but not credited | Webhook not configured or URL without valid HTTPS | Register the URL in the provider panel and test with a valid certificate |
| WCoin credited twice | Missing idempotency; webhook resent | Use UNIQUE(provider_txn) and UPDATE ... WHERE status='pending' |
| Credited the wrong amount | Trusted the amount sent by the browser | Always reconcile against the order saved in the database |
| Chargeback after credit | New account + PayPal | Log IP/account, deliver as a virtual good, monitor recent accounts |
| Webhook returns 500 and the provider resends endlessly | Unhandled exception before http_response_code(200) | Wrap it in try/catch and respond 200 even on a business error, logging separately |
| PIX does not confirm | Signature/mTLS validation failing | Confirm the gateway certificate and the webhook secret token |
Security and compliance
- Always validate the webhook origin. On PayPal, use the signature verification endpoint; on PIX, mTLS or a secret token.
- Keep logs of every webhook received (raw payload + IP) for at least a few months, for auditing and chargeback disputes.
- Limit attempts and apply rate limiting on the order creation endpoint to prevent abuse.
- Do not expose the Client Secret in JavaScript; all sensitive communication stays in the backend.
- Clear terms: make it explicit that donations are for server maintenance and that virtual items have no refundable monetary value, reducing disputes.
- Taxes: as you go professional, consider registering as a sole proprietor/company and using a gateway that issues receipts. Recurring revenue is taxable.
Launch checklist
- Valid HTTPS on the domain and on the webhook endpoints
donationstable created withUNIQUE(provider_txn)- API credentials in
.envoutside the public root - PayPal webhook registered and the signature verified in code
- PIX gateway webhook registered with validation (mTLS/token)
- Idempotent, transactional crediting function tested
- Real game DB table/column names confirmed for your version
- Reprocessing cron for
paidbut notcreditedorders active - Webhook and IP logging enabled
- End-to-end test in sandbox (PayPal Sandbox + test PIX)
- Donation terms published on the site
With this flow you eliminate manual crediting, reduce fraud and deliver an instant experience to the player — who donates, pays with PIX in seconds and sees the VIP on their character without opening a ticket. Always start in a test environment, validate idempotency with real low-value payments, and only then release it to the whole community.
Frequently asked questions
Are MU Online donations legal in Brazil?
Yes, as long as they are offered as a voluntary donation or the sale of cosmetic virtual items. Transparent terms, issuing receipts and attention to the tax rules that apply to recurring revenue are all recommended.
Do I need a company tax ID to receive PIX donations?
It is not mandatory for small, occasional amounts using an individual's key, but as volume grows the ideal is to register as a sole proprietor or company to formalize the revenue and use gateways that require a business account.
Does PayPal work well for the Brazilian audience?
It works, but many Brazilian players prefer PIX for its instant nature and the absence of high fees. The ideal is to offer both methods side by side in the same donation panel.
How do I avoid PayPal chargeback fraud?
Log IP and account, deliver items marked as virtual goods, keep the terms clear and monitor repeated donations from new accounts. Chargebacks are common on servers and should be anticipated.
Is it safe to credit automatically without human review?
Yes, if you validate the webhook signature, check the amount and currency, and use an idempotency table so the same transaction is not credited twice. Never rely on return-URL parameters alone.