PayZuPayZu Docs
Best practices

Handling callbacks

PayZu sends real-time callbacks to your callbackUrl every time a transaction changes status. The main rule: respond 2xx within 5 seconds. Anyone who doesn't respond quickly ends up in the retry queue (up to 72 attempts, exponential backoff).

Do heavy processing outside the handler. The handler only receives, enqueues, and responds.

import express from 'express';
const app = express();

app.post('/webhooks/payzu', express.json(), async (req, res) => {
  await queue.enqueue('payzu-callback', req.body);
  res.status(204).end();
});
from flask import Flask, request

app = Flask(__name__)

@app.post('/webhooks/payzu')
def payzu_webhook():
    queue.enqueue('payzu_callback', request.get_json())
    return '', 204
http.HandleFunc("/webhooks/payzu", func(w http.ResponseWriter, r *http.Request) {
    var payload map[string]any
    if err := json.NewDecoder(r.Body).Decode(&payload); err != nil {
        w.WriteHeader(http.StatusBadRequest)
        return
    }
    queue.Enqueue("payzu-callback", payload)
    w.WriteHeader(http.StatusNoContent)
})
<?php
$payload = json_decode(file_get_contents('php://input'), true);
$queue->enqueue('payzu-callback', $payload);
http_response_code(204);

Testing locally

Expose your localhost via ngrok or Cloudflare Tunnel and trigger the payload manually:

curl -X POST https://your-tunnel.ngrok.io/webhooks/payzu \
  -H "Content-Type: application/json" \
  -d '{
    "id": "PAYZU20251123104518DF75D20A8F",
    "type": "DEPOSIT",
    "status": "COMPLETED",
    "amount": 99.90,
    "clientReference": "order-1234",
    "virtualAccount": "loja-rj-01",
    "paidAt": "2025-11-23T10:46:26.986Z"
  }'
await fetch('https://your-tunnel.ngrok.io/webhooks/payzu', {
  method: 'POST',
  headers: { 'Content-Type': 'application/json' },
  body: JSON.stringify({
    id: 'PAYZU20251123104518DF75D20A8F',
    type: 'DEPOSIT',
    status: 'COMPLETED',
    amount: 99.90,
    clientReference: 'order-1234',
    virtualAccount: 'loja-rj-01',
    paidAt: '2025-11-23T10:46:26.986Z',
  }),
});
import requests

requests.post(
    'https://your-tunnel.ngrok.io/webhooks/payzu',
    headers={'Content-Type': 'application/json'},
    json={
        'id': 'PAYZU20251123104518DF75D20A8F',
        'type': 'DEPOSIT',
        'status': 'COMPLETED',
        'amount': 99.90,
        'clientReference': 'order-1234',
        'virtualAccount': 'loja-rj-01',
        'paidAt': '2025-11-23T10:46:26.986Z',
    },
)

Resending a real callback

To reprocess a callback that failed on your side (after fixing the handler), use the resend endpoints:

Inspecting history

PayZu stores every delivery attempt. Useful for investigating failures:

Common pitfalls

PitfallSymptom
Synchronous processing in the handlerTimeouts, retries, double bookkeeping
Returning 4xx due to internal validation errorPayZu doesn't retry, callback lost
Dedupe by id onlyRefund (REFUNDED) is ignored
Endpoint with no IP protectionEndpoint may receive forged payloads
Logger prints payload without masking payerDocumentLGPD risk

On this page