Best practices
Multi-tenant with virtualAccount
If you operate multiple brands, stores, branches, or partners under a single Saq 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/transactionsandGET /pix, with lists isolated per tenant. - Removes the need for child accounts, a single Saq account serves N tenants.
Naming conventions
| Pattern | When 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/saq"
}{
"id": "SAQ20251123104518DF75D20A8F",
"status": "PENDING",
"amount": 99.90,
"clientReference": "order-1234",
"virtualAccount": "loja-rj-01",
"qrCodeText": "00020126870014br.gov.bcb.pix..."
}{
"id": "SAQ20251123104518DF75D20A8F",
"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.saq.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.saq.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.SAQ_TOKEN}`,
'Content-Type': 'application/json',
},
});res = requests.get(
'https://api.saq.processamento.com/v1/user/transactions',
params={'virtualAccount': 'loja-rj-01', 'dateFrom': '2025-11-01'},
headers={
'Authorization': f'Bearer {os.environ["SAQ_TOKEN"]}',
'Content-Type': 'application/json',
},
)Route the callback
async function saqWebhook(tx: saqCallback) {
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.
| Field | Granularity | Purpose |
|---|---|---|
clientReference | Per transaction | Idempotência + lookup by order. |
virtualAccount | Per tenant | Routing + listing filter. |
Combined example:
{
"amount": 99.90,
"clientReference": "order-1234",
"virtualAccount": "loja-rj-01",
"callbackUrl": "https://seusite.com.br/webhooks/saq"
}Common pitfalls
| Pitfall | Symptom |
|---|---|
Using clientReference to identify the tenant | Does not filter in listings, complicates lookup |
virtualAccount changes every time (timestamp, variable slug) | Listing becomes fragmented |
Not handling callbacks without virtualAccount | Routing crashes on legacy transactions |
| Hardcoding tenants in the handler | Manual onboarding for every new customer |
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 Saq é robusto, mas só funciona se o seu handler se comportar bem.