Money and precision
The Saq Pix API uses reais with decimal places, not cents. 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.
| Language | Use |
|---|---|
| JavaScript | Integer in cents, or decimal.js library. |
| Python | decimal.Decimal when calculating, float only at the API. |
| Go | shopspring/decimal or integer in cents. |
| Java | BigDecimal, never double. |
| PHP | bcmath, or integer in cents. |
| SQL/Postgres | NUMERIC(15,2), never FLOAT or REAL. |
Recommended pattern: cents internally
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}',
})import "github.com/shopspring/decimal"
func CentsToReais(cents int64) decimal.Decimal {
return decimal.NewFromInt(cents).Div(decimal.NewFromInt(100))
}
func ReaisToCents(reais decimal.Decimal) int64 {
return reais.Mul(decimal.NewFromInt(100)).Round(0).IntPart()
}<?php
function centsToReais(int $cents): float {
return $cents / 100;
}
function reaisToCents(float $reais): int {
return (int) round($reais * 100);
}
createPixCharge([
'amount' => centsToReais($order->totalCents),
'clientReference' => 'order-' . $order->id,
]);
$callbackAmountCents = reaisToCents($callback['amount']);
if ($callbackAmountCents !== $order->totalCents) {
throw new RuntimeException('Amount mismatch between callback and order');
}Minimum limits per operation
Saq validates on the server. A request below the minimum returns 400 Bad Request.
| Operation | Minimum amount |
|---|---|
POST /pix (charge) | R$ 1.00 |
POST /withdraw (withdrawal by key) | R$ 0.01 |
POST /withdraw/qrcode (pay QR) | R$ 0.10 |
POST /internal-transfer | R$ 0.01 |
Fee
The fee charged by Saq arrives in the callback in the serviceFeeCharged field (in reais).
{
"amount": 99.90,
"serviceFeeCharged": 0.99,
"status": "COMPLETED"
}For financial reconciliation, consider:
| Value | Meaning |
|---|---|
amount | What the customer paid or you withdrew. |
serviceFeeCharged | Saq fee on the operation. |
| Net | amount - 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: saqCallback, 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
| Pitfall | Symptom |
|---|---|
Sending amount: 9990 thinking it's cents | Charges R$ 9,990.00 from the customer |
Storing amount as FLOAT in Postgres | Loss of cents when summing many rows |
Adding Decimal with float in Python | Type error or lost precision |
Trusting parseFloat(tx.amount) without rounding | 99.90 becomes 99.9000000000001 |
| Not checking received amount vs expected amount | Partial payment goes through as completed |
Paginação
Endpoints de listagem (transações, callbacks, infrações) usam paginação clássica por page + limit com flag hasNextPage. Para janelas grandes prefira o relatório assíncrono em CSV.
Segurança
Token Bearer é a única credencial da Saq. Trate como senha. Esta página cobre armazenamento, rotação, mascaramento em log, proteção do webhook por IP e validação DICT antes de pagar.