Brazil's biggest MU Online portal — since 2003
Tutorial Advanced Website

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.

GA Gabriel · Updated on Jun 30, 2025 · ⏱ 14 read
Quick answer

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 composer to 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 localhost or 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.

  1. The player logs into the site and picks a package (e.g., R$ 20 = 5,000 WCoin + 30 days of VIP).
  2. The backend creates a pending order in the site database, with a unique identifier (order_id).
  3. The player is redirected to PayPal or receives a PIX QR Code.
  4. After paying, the provider sends a webhook to your server confirming the payment.
  5. The backend validates the signature, checks amount/currency, marks the order as paid and credits the reward in the game database — all idempotently.
  6. The player sees the updated balance in the panel.
LayerResponsibilityMust never
Front endShow packages and start the paymentDecide whether something was paid
Backend (orders)Create/update orders, generate the chargeCredit without a confirmed webhook
Webhook handlerValidate the signature and confirmTrust return parameters (?status=success)
Crediting (game DB)Add VIP/WCoin atomicallyRun 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

SymptomLikely causeFix
Donation paid but not creditedWebhook not configured or URL without valid HTTPSRegister the URL in the provider panel and test with a valid certificate
WCoin credited twiceMissing idempotency; webhook resentUse UNIQUE(provider_txn) and UPDATE ... WHERE status='pending'
Credited the wrong amountTrusted the amount sent by the browserAlways reconcile against the order saved in the database
Chargeback after creditNew account + PayPalLog IP/account, deliver as a virtual good, monitor recent accounts
Webhook returns 500 and the provider resends endlesslyUnhandled exception before http_response_code(200)Wrap it in try/catch and respond 200 even on a business error, logging separately
PIX does not confirmSignature/mTLS validation failingConfirm 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
  • donations table created with UNIQUE(provider_txn)
  • API credentials in .env outside 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 paid but not credited orders 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.

GA
Guides & builds editor

Gabriel covers gameplay, class builds, PvP and progression. He tests every strategy on a live server before publishing.

Keep reading

Related articles