PayZuPayZu Docs
Best practices

Multi-tenant with virtualAccount

If you operate multiple brands, stores, branches, or partners under a single PayZu account, pass virtualAccount (up to 50 characters) on each creation. This field:

  • Returns in every callback, so you immediately know which tenant the transaction belongs to.
  • Can be filtered in GET /user/transactions and GET /pix, with lists isolated per tenant.
  • Removes the need for child accounts, a single PayZu account serves N tenants.

Naming conventions

PatternWhen to use
tenant-{slug}Multi-customer SaaS platform.
loja-{cidade}-{numero}Network with physical stores.
mkt-{partner}Marketplace with multiple sellers.
filial-{codigo}Branches of the same company.
branch-{branchId}Generic, in English.

Maximum length: 50 characters. Use a stable, readable format. Avoid special characters and spaces.

Create with 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"
}

List only one 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',
    },
)

Route the callback

async function payzuWebhook(tx: PayzuCallback) {
  const tenantId = tx.virtualAccount;
  if (!tenantId) {
    log.warn('callback without virtualAccount', { id: tx.id });
    return;
  }

  const handler = tenantHandlers[tenantId];
  if (!handler) {
    log.error('unknown tenant', { tenantId, id: tx.id });
    return;
  }

  await handler.process(tx);
}

virtualAccount vs clientReference

The two are independent and complementary fields. Always use both.

FieldGranularityPurpose
clientReferencePer transactionIdempotência + lookup by order.
virtualAccountPer tenantRouting + listing filter.

Combined example:

{
  "amount": 99.90,
  "clientReference": "order-1234",
  "virtualAccount": "loja-rj-01",
  "callbackUrl": "https://seusite.com.br/webhooks/payzu"
}

Common pitfalls

PitfallSymptom
Using clientReference to identify the tenantDoes not filter in listings, complicates lookup
virtualAccount changes every time (timestamp, variable slug)Listing becomes fragmented
Not handling callbacks without virtualAccountRouting crashes on legacy transactions
Hardcoding tenants in the handlerManual onboarding for every new customer

On this page