PayZuDocs
Best practices

Money and precision

Explains that amounts travel as reais with decimals and how to preserve precision in storage.

The PayZu Pix API uses reais with decimal places. Heads up: many PSPs work in cents, so if you're migrating or comparing integrations, the decimal point here is not optional.

{ "amount": 99.90 }

Decimal precision in code

In JavaScript, 0.1 + 0.2 !== 0.3. In Python Decimal is safe but float is not. In SQL, FLOAT loses precision.

LanguageUse
JavaScriptInteger in cents, or decimal.js library.
Pythondecimal.Decimal when calculating, float only at the API.
Goshopspring/decimal or integer in cents.
JavaBigDecimal, never double.
PHPbcmath, or integer in cents.
SQL/PostgresNUMERIC(15,2), never FLOAT or REAL.

Store as an integer in cents in your DB and convert only at the API edge:

function centsToReais(cents: number): number {
  return cents / 100;
}

function reaisToCents(reais: number): number {
  return Math.round(reais * 100);
}

await createPixCharge({
  amount: centsToReais(order.totalCents),
  clientReference: `order-${order.id}`,
});

const callbackAmountCents = reaisToCents(callback.amount);
if (callbackAmountCents !== order.totalCents) {
  throw new Error('Amount mismatch between callback and order');
}
from decimal import Decimal

def cents_to_reais(cents: int) -> Decimal:
    return Decimal(cents) / Decimal(100)

def reais_to_cents(reais: float | Decimal) -> int:
    return int((Decimal(str(reais)) * Decimal(100)).quantize(Decimal('1')))

create_pix_charge({
    'amount': float(cents_to_reais(order.total_cents)),
    'clientReference': f'order-{order.id}',
})

Minimum limits per operation

PayZu validates on the server. A request below the minimum returns 400 Bad Request.

OperationMinimum amount
POST /pix (charge)R$ 1.00
POST /withdraw (Pix payment by key)R$ 0.01
POST /withdraw/qrcode (pay QR)R$ 0.10
POST /internal-transferR$ 0.01

These are the API schema minimums. Your account effective minimum and maximum are in GET /user (cashInTicketMin, cashInTicketMax, cashOutTicketMin, cashOutTicketMax) and the call is validated against those — read them before building the amount. The refusal states the amount applied and the errorCode varies per route: PZD600 on POST /pix, PZS600 on POST /withdraw and POST /withdraw/qrcode, PZC602 on POST /internal-transfer.

Fee

The fee charged by PayZu arrives in the callback in the serviceFeeCharged field (in reais).

{
  "amount": 99.90,
  "serviceFeeCharged": 0.99,
  "status": "COMPLETED"
}

For financial reconciliation, consider:

ValueMeaning
amountWhat the customer paid or you withdrew.
serviceFeeChargedPayZu fee on the operation.
Netamount - serviceFeeCharged (incoming) or amount + serviceFeeCharged (total outgoing).

Validate received amount in the callback

Always check that the callback matches the order. The customer may pay a different amount (Pix allows QR without a fixed amount in some cases).

async function handleDepositCallback(tx: PayzuCallback, order: Order) {
  const callbackCents = reaisToCents(tx.amount);
  if (callbackCents !== order.totalCents) {
    log.warn('Amount mismatch', {
      order: order.totalCents,
      received: callbackCents,
    });
    await flagForReview(order, tx);
    return;
  }
  await markOrderPaid(order, tx);
}

Common pitfalls

PitfallSymptom
Sending amount: 9990 thinking it's centsCharges R$ 9,990.00 from the customer
Storing amount as FLOAT in PostgresLoss of cents when summing many rows
Adding Decimal with float in PythonType error or lost precision
Trusting parseFloat(tx.amount) without rounding99.90 becomes 99.9000000000001
Not checking received amount vs expected amountPartial payment goes through as completed

On this page