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

Payment webhook integration for your MU Online server's shop

Set up payment webhooks (Mercado Pago, PagSeguro, Stripe) in your MU Online server's online shop, with signature validation, idempotency, and automatic VIP/Zen/Cash Points crediting.

GA Gabriel · Updated on Apr 21, 2024 · ⏱ 16 min read
Quick answer

The Cash Points shop is one of the main revenue sources for a private MU Online server, and its weakest link is usually payment confirmation itself. Relying solely on the "payment approved" screen in the player's browser is a recipe for chargebacks, fraud, and duplicate credits. The correct solution

The Cash Points shop is one of the main revenue sources for a private MU Online server, and its weakest link is usually payment confirmation itself. Relying solely on the "payment approved" screen in the player's browser is a recipe for chargebacks, fraud, and duplicate credits. The correct solution is to integrate webhooks: asynchronous notifications the payment gateway sends directly to your server, confirming that the money actually came in. This tutorial covers setting up webhooks with Mercado Pago, PagSeguro, and Stripe, signature validation, idempotency handling, and automatically crediting VIP, Zen, or Cash Points to the player's account, along with the most common mistakes first-timers make.

Why browser confirmation isn't enough

When the player finishes paying, the gateway redirects their browser to a "success" URL on your site. That redirect isn't reliable as a source of truth: the player might close the tab before it completes, lose their connection, or in rare cases try to manipulate the return URL to forge a ?status=approved. The webhook, by contrast, is a server-to-server HTTP call, cryptographically signed, fired by the gateway itself when the payment status actually changes on their end.

Overview of the full flow

  1. The player picks a Cash Points package in the web shop and is redirected to the gateway's checkout.
  2. The gateway processes the payment (Pix, card, boleto).
  3. The gateway sends a webhook POST to a URL of yours, reporting the transaction status.
  4. Your backend validates the signature, checks idempotency, and credits the amount to the player's credit table (read by the MU server at login or via an in-game command).
  5. The gateway also redirects the player's browser to the success page — purely for UX, not as a source of truth.

Comparing the main gateways used by Brazilian MU servers

GatewaySignature headerPayload formatNote
Mercado Pagox-signature (HMAC SHA256)JSON with the payment's data.idNeeds to fetch the full payment via API after the webhook
PagSeguro/PagBankToken in the notification URLXML/JSON depending on the API (legacy vs. new)Legacy API sends only the code; new API sends the full payload
StripeStripe-Signature (HMAC SHA256)Full event JSONOfficial library already validates the signature (stripe.webhooks.constructEvent)
Pagar.mex-hub-signature (HMAC SHA1)Full JSONFormat similar to Mercado Pago

Step 1 — Create the endpoint that receives the webhook

Use a dedicated route, outside of any user session authentication (the gateway has no session cookie):

<?php
// webhook_pagamento.php
$payload = file_get_contents('php://input');
$headers = getallheaders();

// never trust the payload before validating the signature
if (!validarAssinatura($payload, $headers['x-signature'] ?? '')) {
    http_response_code(401);
    exit('invalid signature');
}

$evento = json_decode($payload, true);
processarEvento($evento);
http_response_code(200);

Step 2 — Validate the signature (HMAC)

Never process a webhook without confirming it really came from the gateway. The principle is the same across all of them: recompute the HMAC with your secret key and compare it against the received header, using a constant-time comparison to avoid timing attacks:

function validarAssinatura(string $payload, string $assinaturaRecebida): bool {
    $chaveSecreta = getenv('WEBHOOK_SECRET');
    $assinaturaCalculada = hash_hmac('sha256', $payload, $chaveSecreta);
    return hash_equals($assinaturaCalculada, $assinaturaRecebida);
}

Store WEBHOOK_SECRET in an environment variable, never in versioned source code.

Step 3 — Fetch the real transaction status (when needed)

Some gateways (Mercado Pago is the classic case) send only an ID in the webhook, and you need to query their API to confirm the actual status — this prevents a forged webhook claiming an "approved" status from being accepted without cross-checking:

$paymentId = $evento['data']['id'];
$resposta = httpGet("https://api.mercadopago.com/v1/payments/{$paymentId}", [
    'Authorization: Bearer ' . getenv('MP_ACCESS_TOKEN')
]);
$pagamento = json_decode($resposta, true);

if ($pagamento['status'] !== 'approved') {
    http_response_code(200); // acknowledge the event, but don't credit
    exit;
}

Step 4 — Ensure idempotency

Gateways resend the same webhook multiple times if they don't get a 200 in time, or due to retry policy. Record the transaction ID before crediting, and reject duplicates:

CREATE TABLE webhook_eventos (
    id_transacao VARCHAR(100) PRIMARY KEY,
    processado_em DATETIME,
    valor_creditado DECIMAL(10,2)
);
if (transacaoJaProcessada($paymentId)) {
    http_response_code(200);
    exit; // already credited, avoid duplicating
}

Step 5 — Credit VIP, Zen, or Cash Points

Once validated and confirmed not to be a duplicate, credit the table the GameServer reads (directly in the MU database, or an intermediary table that a service credits periodically):

UPDATE AccountCash SET CashPoints = CashPoints + @valorComprado
WHERE AccountID = @conta;

INSERT INTO webhook_eventos (id_transacao, processado_em, valor_creditado)
VALUES (@paymentId, GETDATE(), @valorComprado);

If your emulator uses a Web Cash Shop with its own table (e.g., IGCN_CashShop), credit the corresponding structure and make sure the GameServer rereads the balance at the next login or via a real-time event, if supported.

Step 6 — Notify the player

Send a confirmation email and, if possible, an in-game notification (mailbox, system message) confirming the credit. This drastically reduces "I paid and didn't receive it" support tickets, since many cases are just a few seconds of delay between payment and webhook.

Testing webhooks locally

Gateways can't reach localhost. Use a public tunnel during development:

ngrok http 8443
# copy the generated https URL and register it as the webhook endpoint in the gateway's dashboard

All the gateways mentioned offer a sandbox/test mode — use test cards and accounts before pointing to production.

Extra security: IP whitelisting and rate limiting

In addition to signature validation, restrict the webhook endpoint by source IP when the gateway publishes a fixed list (Stripe and PagSeguro publish ranges), and apply rate limiting on the endpoint to mitigate brute-force attempts against signature validation.

Logs and auditing

Log every payload received (even those rejected for invalid signature) in a separate log, with timestamp and source IP. This is essential for investigating chargeback disputes and auditing whether any credit was applied incorrectly.

Common errors and fixes

SymptomLikely causeFix
Cash Points credited twiceWebhook resent without an idempotency checkRecord the transaction ID and ignore duplicates
Webhook never arrivesEndpoint without valid HTTPS or wrong URL in the dashboardSet up a valid certificate and review the URL registered with the gateway
Signature always invalidWrong secret key or payload altered before validation (e.g., framework parsing it)Validate against the raw body, before any decoding
Player paid but didn't receive anythingCredit failure mistakenly returned 200 to the gatewayReturn 500 on credit failure to force a resend
Test (sandbox) webhook processed in productionSandbox and production sharing the same endpoint without distinctionUse separate endpoints/keys for sandbox and production
Chargeback isn't reflected in the gameNo handling for refund/chargeback eventsImplement a handler for refund events and remove credit/apply a ban per policy

Payment integration checklist

  • Dedicated HTTPS endpoint for webhooks, outside normal session authentication.
  • HMAC signature validation implemented and tested with a real payload.
  • Real status lookup via the gateway's API when the webhook only carries an ID.
  • Idempotency table recording already-processed transaction IDs.
  • VIP/Zen/Cash Points crediting automated and validated in sandbox.
  • Player notification (email and/or in-game) after crediting.
  • Logs of all payloads received, including rejected ones.
  • Handling of refund/chargeback events implemented.

With reliable webhooks and the shop crediting automatically, it's worth reviewing the overall site and server architecture to make sure the rest of your infrastructure can handle the transaction volume safely — start with the MU Online server setup guide if you haven't reviewed the base yet.

Frequently asked questions

Why can't I just trust the payment success redirect?

Because the redirect happens in the player's browser, which can be manipulated, interrupted, or never completed (closed tab, dropped connection). The webhook comes directly from the payment gateway's server to your backend, asynchronously and reliably, and is the only source of truth for releasing credit.

How do I know a webhook really came from the payment gateway and not a scammer?

Every serious gateway signs the payload with a secret key (HMAC) or sends a signature header. You recompute that signature on your backend with your secret key and compare it — if it doesn't match, discard the request. Never process a webhook without this validation.

What is idempotency and why is it mandatory here?

It's making sure processing the same event twice doesn't credit Cash Points in duplicate. Gateways resend webhooks when they don't get a confirmation in time, so you need to record the event/transaction ID and ignore duplicates before crediting.

Do I need HTTPS to receive webhooks?

Yes, it's required by practically every gateway (Mercado Pago, Stripe, PagSeguro) — they reject or warn about HTTP endpoints. Use a valid certificate (Let's Encrypt is enough) on your shop's domain.

What do I do if crediting fails after the payment was already approved?

Log the failure in a retry queue and return HTTP 500 to the gateway so it automatically resends the webhook. Never return 200 if the credit wasn't applied — that makes the gateway stop retrying, and the player never gets what they paid for.

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