Boas práticas
Multi-tenant com virtualAccount
Se você opera várias marcas, lojas, filiais ou parceiros em uma só conta PayZu, passe virtualAccount (até 50 caracteres) em cada criação. Esse campo:
- Volta em todo callback, você sabe imediatamente de qual tenant é a transação.
- Pode ser filtrado em
GET /user/transactionseGET /pix, listas isoladas por tenant. - Dispensa contas filhas, uma só conta PayZu serve N tenants.
Convenções de nome
| Padrão | Quando usar |
|---|---|
tenant-{slug} | Plataforma SaaS multi-cliente. |
loja-{cidade}-{numero} | Rede com lojas físicas. |
mkt-{partner} | Marketplace com vários vendedores. |
filial-{codigo} | Filiais de uma mesma empresa. |
branch-{branchId} | Genérico, em inglês. |
Tamanho máximo: 50 caracteres. Use formato estável e legível. Evite caracteres especiais e espaços.
Criar com virtualAccount
{
"amount": 99.90,
"clientReference": "order-1234",
"virtualAccount": "loja-rj-01",
"callbackUrl": "https://seusite.com.br/webhooks/payzu"
}{
"id": "PAYZU20251123104518DF75D20A8F",
"status": "PENDING",
"amount": 99.90,
"clientReference": "order-1234",
"virtualAccount": "loja-rj-01",
"qrCodeText": "00020126870014br.gov.bcb.pix..."
}{
"id": "PAYZU20251123104518DF75D20A8F",
"type": "DEPOSIT",
"status": "COMPLETED",
"amount": 99.90,
"clientReference": "order-1234",
"virtualAccount": "loja-rj-01",
"paidAt": "2025-11-23T10:46:26.986Z"
}Listar só de um tenant
curl "https://api.payzu.processamento.com/v1/user/transactions?virtualAccount=loja-rj-01&dateFrom=2025-11-01" \
-H "Authorization: Bearer $TOKEN" \
-H "Content-Type: application/json"const url = new URL('https://api.payzu.processamento.com/v1/user/transactions');
url.searchParams.set('virtualAccount', 'loja-rj-01');
url.searchParams.set('dateFrom', '2025-11-01');
const res = await fetch(url, {
headers: {
Authorization: `Bearer ${process.env.PAYZU_TOKEN}`,
'Content-Type': 'application/json',
},
});res = requests.get(
'https://api.payzu.processamento.com/v1/user/transactions',
params={'virtualAccount': 'loja-rj-01', 'dateFrom': '2025-11-01'},
headers={
'Authorization': f'Bearer {os.environ["PAYZU_TOKEN"]}',
'Content-Type': 'application/json',
},
)Rotear o callback
async function payzuWebhook(tx: PayzuCallback) {
const tenantId = tx.virtualAccount;
if (!tenantId) {
log.warn('callback sem virtualAccount', { id: tx.id });
return;
}
const handler = tenantHandlers[tenantId];
if (!handler) {
log.error('tenant desconhecido', { tenantId, id: tx.id });
return;
}
await handler.process(tx);
}virtualAccount vs clientReference
Os dois são campos independentes e complementares. Use os dois sempre.
| Campo | Granularidade | Propósito |
|---|---|---|
clientReference | Por transação | Idempotência + lookup por pedido. |
virtualAccount | Por tenant | Roteamento + filtro de listagens. |
Exemplo combinado:
{
"amount": 99.90,
"clientReference": "order-1234",
"virtualAccount": "loja-rj-01",
"callbackUrl": "https://seusite.com.br/webhooks/payzu"
}Armadilhas comuns
| Armadilha | Sintoma |
|---|---|
Usar clientReference pra identificar tenant | Não filtra em listagens, complica lookup |
virtualAccount muda toda vez (timestamp, slug variável) | Listagem fica fragmentada |
Não tratar callback sem virtualAccount | Roteamento crasha em transações legadas |
| Hardcode de tenants no handler | Onboarding manual a cada novo cliente |
Idempotência
clientReference é a sua chave de idempotência e callbacks chegam mais de uma vez. Como combinar os dois para que retries não viram cobranças duplicadas nem baixas em dobro.
Lidando com callbacks
Responda em menos de 5 segundos, enfileire o processamento pesado, deduplique por id + status e teste localmente com ngrok. O retry da PayZu é robusto, mas só funciona se o seu handler se comportar bem.