Implementation guides
Collecting payments
After creating a Payment Intent, you need to collect the actual payment from your contact. Qualy gives you two options: redirect them to the hosted payment portal, or call the API directly and build your own payment experience.
Via payment portal
The simplest way to collect a payment. When you create a Payment Intent, Qualy returns a links object with a portal URL. Redirect your contact there and Qualy handles everything: method selection, payer verification, payment processing, and confirmation.
{
"_id": "6634f56c7dbbc16e07b5025e",
"links": {
"short": "https://qualyhqpay.com/abc123",
"long": "https://yourcompany.qualyhqportal.com/payments/6634f56c7dbbc16e07b5025e/pay"
}
}
The portal URL follows this format:
https://{subdomain}.qualyhqportal.com/payments/{paymentIntentId}/pay
Where {subdomain} is your tenant's nickname or tenant ID. If you have a custom domain configured, the link will use that instead.
Use the links.short URL when sending to contacts (email, SMS, WhatsApp). Use links.long if you need to construct the URL yourself.
When to use the portal
The portal is the best choice when you want a fully managed experience, need to support card payments (credit/debit), or don't want to build a payment UI. Card payments require the hosted portal for PCI compliance.
Via API (direct)
For non-card payment methods, you can call the API directly, get the payment details (a QR code, a barcode, a PayID address, or bank account details), and display them in your own UI. This gives you full control over the payment experience.
How it works
- Create a Payment Intent - defines the amount, currency, and contact.
- Get available payment options - check which methods the contact can use.
- Sign the payment - call the sign endpoint with the chosen method. The response contains everything you need to display to your contact.
- Contact completes payment - they scan a QR code, pay a boleto, transfer to a PayID, etc.
- Webhook confirms payment - Qualy notifies your server when the payment succeeds or fails.
Understanding currency vs. settlement currency
When fetching payment options, you pass a settlementCurrency parameter. This is the currency your contact will actually pay in, which can differ from the Payment Intent's currency (the currency the invoice is denominated in).
For example, you might create a Payment Intent in AUD (your accounting currency), but the contact pays in BRL (their local currency). Qualy handles the FX conversion automatically.
The priority for determining settlement currency is:
- If the Payment Intent has a
settlementCurrencyset, it is always used (forced). - Otherwise, the value you pass to the payment-options endpoint is used.
- If neither is set, the Payment Intent's
currencyis used as default.
Settlement currency matters
The settlement currency determines which payment methods are available. For example, AS_PIX and AS_BOLETO only appear when the settlement currency is BRL. ZAI_PAYID and ZAI_BSBACC only appear for AUD.
Getting available payment options
const paymentIntentId = '6634f56c7dbbc16e07b5025e';
// Use the PI's settlementCurrency, or fall back to its currency
const settlementCurrency = 'BRL';
try {
const response = await fetch(
`https://api.qualyhq.com/v1/payment-gateways/payment-options/${paymentIntentId}/${settlementCurrency}`,
{
method: 'GET',
headers: {
'Content-Type': 'application/json',
'Authorization': 'ApiKey your-api-key-here',
'X-TENANT-ID': 'your-tenant-id-here',
},
}
);
const data = await response.json();
console.log(data);
} catch (error) {
console.error(error);
}
The response lists every available method with its status, fees, and estimated processing time:
{
"options": [
{
"type": "AS_PIX",
"gateway": "asaas",
"status": "active",
"currency": "BRL",
"amount": 135000,
"eta": "seconds",
"fees": { "gateway": 199 }
},
{
"type": "AS_BOLETO",
"gateway": "asaas",
"status": "active",
"currency": "BRL",
"amount": 135000,
"eta": "3-days",
"fees": { "gateway": 349 }
}
],
"settlementCurrency": "BRL",
"settlementCurrencies": ["BRL"]
}
Only methods with "status": "active" can be used. The amount is in cents (minor currency units), and fees show any additional charges.
Signing the payment
Use the POST /v1/payment-gateways/sign endpoint with the chosen method. The response gives you everything you need to display to your contact.
Payer object
Every sign request requires a payer object that identifies who is making the payment.
payer.type | When to use |
|---|---|
myself | The contact is paying for themselves. Qualy can resolve address and ID from the contact record, so email, profile, and address are optional. |
parent | A parent or guardian is paying on behalf of the contact. |
partner | A business partner is paying. |
family | A family member is paying. |
friend | A friend is paying. |
colleague | A work colleague is paying. |
other | Someone else not fitting the above categories. |
When type is anything other than myself, the email and profile (firstName, lastName, phone) fields become required since Qualy cannot resolve the payer's identity from the contact record.
Handling 424 errors (missing information)
The sign endpoint may return an HTTP 424 status when it needs more information to process the payment. This is common for Brazilian methods (AS_PIX, AS_BOLETO) which require a valid address and tax ID.
{
"statusCode": 424,
"message": "We need more information to complete your payment.",
"data": {
"missingInformation": ["address", "id"]
}
}
The missingInformation array tells you exactly which fields to collect from the payer. Common values:
| Missing field | What to provide |
|---|---|
full-payer | The full payer object is missing. Collect all payer details (email, profile, address, id). |
address | A valid address is required. For Brazilian methods, the postalCode (CEP) must be a valid format. |
id | A tax identification number is required. For Brazil this is CPF (individuals) or CNPJ (companies) in the payer.id.number field. |
When you receive a 424, collect the missing information from the payer and retry the sign request with the complete data.
PIX (Brazil)
PIX payments are instant. The sign response returns a QR code and a copy-paste code that your contact can use in any Brazilian banking app.
try {
const response = await fetch('https://api.qualyhq.com/v1/payment-gateways/sign', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'Authorization': 'ApiKey your-api-key-here',
'X-TENANT-ID': 'your-tenant-id-here',
},
body: JSON.stringify({
gateway: 'asaas',
signatureType: 'AS_PIX',
entity: 'payment-intent',
paymentIntentId: '6634f56c7dbbc16e07b5025e',
contact: {
contactId: '6606ab8ffb9085579f1b5844',
},
payer: {
type: 'myself',
email: 'john@example.com',
profile: {
firstName: 'John',
lastName: 'Doe',
phone: '+5511999999999',
},
address: {
country: 'BR',
line1: 'Rua Example 123',
city: 'Sao Paulo',
state: 'SP',
postalCode: '01001000',
},
id: {
country: 'BR',
number: '12345678901',
},
},
}),
});
if (response.ok) {
const data = await response.json();
// data.encodedQrCode - Base64 QR code image
// data.qrCodeLink - PIX copy-paste code (copia e cola)
console.log(data);
} else {
throw new Error(`Request failed with status: ${response.status}`);
}
} catch (error) {
console.error(error);
}
PIX response
{
"encodedQrCode": "iVBORw0KGgoAAAANSUhEUg...",
"qrCodeLink": "00020126580014br.gov.bcb.pix..."
}
| Field | Description |
|---|---|
encodedQrCode | Base64-encoded QR code image. Render it as an <img> tag: <img src="data:image/png;base64,${encodedQrCode}" /> |
qrCodeLink | The PIX payload string (known as "copia e cola"). Your contact can paste this directly in their banking app. |
Show the QR code image and a "copy code" button with the qrCodeLink value. PIX payments typically confirm within seconds. Supports partial payments via the amount field.
Boleto (Brazil)
Boleto generates a bank slip that can be paid at any bank, lottery house, or banking app in Brazil. Processing takes 1-3 business days.
try {
const response = await fetch('https://api.qualyhq.com/v1/payment-gateways/sign', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'Authorization': 'ApiKey your-api-key-here',
'X-TENANT-ID': 'your-tenant-id-here',
},
body: JSON.stringify({
gateway: 'asaas',
signatureType: 'AS_BOLETO',
entity: 'payment-intent',
paymentIntentId: '6634f56c7dbbc16e07b5025e',
contact: {
contactId: '6606ab8ffb9085579f1b5844',
},
payer: {
type: 'myself',
email: 'john@example.com',
profile: {
firstName: 'John',
lastName: 'Doe',
phone: '+5511999999999',
},
address: {
country: 'BR',
line1: 'Rua Example 123',
city: 'Sao Paulo',
state: 'SP',
postalCode: '01001000',
},
id: {
country: 'BR',
number: '12345678901',
},
},
}),
});
if (response.ok) {
const data = await response.json();
// data.bankSlipUrl - URL to view/download the boleto PDF
// data.identificationField - 47-digit boleto number
// data.barCode - Barcode number
console.log(data);
} else {
throw new Error(`Request failed with status: ${response.status}`);
}
} catch (error) {
console.error(error);
}
Boleto response
{
"bankSlipUrl": "https://www.asaas.com/b/pdf/abc123",
"identificationField": "23793.38128 60000.000003 00000.000400 1 84340000010000",
"nossoNumero": "1234567",
"barCode": "23791843400000100000038126000000000000000040"
}
| Field | Description |
|---|---|
bankSlipUrl | URL to download or view the boleto PDF. You can link or embed this directly. |
identificationField | The formatted 47-digit boleto number. This is what payers type into their banking app. |
nossoNumero | The bank's internal reference number for the boleto. |
barCode | The raw barcode number. Use this to render a barcode image if needed. |
Show the identificationField with a copy button, and link to the bankSlipUrl so they can download the full bank slip. Boleto payments take 1-3 business days to confirm. Supports partial payments via the amount field.
PayID (Australia)
PayID is Australia's real-time payment addressing system. The sign response returns a PayID email address that your contact can pay using their banking app.
PayID availability
PayID is only available for AUD payments in Australia.
try {
const response = await fetch('https://api.qualyhq.com/v1/payment-gateways/sign', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'Authorization': 'ApiKey your-api-key-here',
'X-TENANT-ID': 'your-tenant-id-here',
},
body: JSON.stringify({
gateway: 'zai',
signatureType: 'ZAI_PAYID',
entity: 'payment-intent',
paymentIntentId: '6634f56c7dbbc16e07b5025e',
contact: {
contactId: '6606ab8ffb9085579f1b5844',
},
payer: {
type: 'myself',
email: 'jane@example.com',
profile: {
firstName: 'Jane',
lastName: 'Smith',
phone: '+61412345678',
},
},
}),
});
if (response.ok) {
const data = await response.json();
// data.email - PayID email address to pay to
// data.details - Merchant name info
console.log(data);
} else {
throw new Error(`Request failed with status: ${response.status}`);
}
} catch (error) {
console.error(error);
}
PayID response
{
"email": "pay@merchant.payid",
"userId": "abc123",
"details": {
"name": "Merchant Pty Ltd",
"legalName": "Merchant Pty Ltd"
}
}
| Field | Description |
|---|---|
email | The PayID address. Your contact enters this in their banking app to send the payment. |
details.name | The merchant display name that will appear when the contact looks up the PayID. |
details.legalName | The legal name of the receiving entity. |
Show the PayID email with a copy button, the expected amount, and the merchant name so they can verify the recipient. PayID transfers are typically instant during business hours. Does not support partial payments.
BSB & Account Number (Australia)
For contacts who prefer traditional bank transfers, you can provide BSB and account number details.
try {
const response = await fetch('https://api.qualyhq.com/v1/payment-gateways/sign', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'Authorization': 'ApiKey your-api-key-here',
'X-TENANT-ID': 'your-tenant-id-here',
},
body: JSON.stringify({
gateway: 'zai',
signatureType: 'ZAI_BSBACC',
entity: 'payment-intent',
paymentIntentId: '6634f56c7dbbc16e07b5025e',
contact: {
contactId: '6606ab8ffb9085579f1b5844',
},
payer: {
type: 'myself',
email: 'jane@example.com',
profile: {
firstName: 'Jane',
lastName: 'Smith',
phone: '+61412345678',
},
},
}),
});
if (response.ok) {
const data = await response.json();
// data.accountName - Account name to pay to
// data.routingNumber - BSB number
// data.accountNumber - Account number
console.log(data);
} else {
throw new Error(`Request failed with status: ${response.status}`);
}
} catch (error) {
console.error(error);
}
BSB & Account response
{
"accountName": "Merchant Pty Ltd",
"routingNumber": "062000",
"accountNumber": "12345678"
}
| Field | Description |
|---|---|
accountName | The name of the receiving bank account. |
routingNumber | The BSB (Bank-State-Branch) number. Display this formatted as XXX-XXX. |
accountNumber | The bank account number. |
Show all three fields along with the exact amount to transfer. Remind the contact to use the payment reference if applicable. Bank transfers can take 1-2 business days to clear. Does not support partial payments.
Bank Transfer (International)
Bank transfers via Transfermate support 60+ currencies, making this the most versatile method for international payments. The sign response returns bank account details (IBAN, SWIFT, etc.) that your contact uses to make a wire transfer.
try {
const response = await fetch('https://api.qualyhq.com/v1/payment-gateways/sign', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'Authorization': 'ApiKey your-api-key-here',
'X-TENANT-ID': 'your-tenant-id-here',
},
body: JSON.stringify({
gateway: 'transfermate',
signatureType: 'TM_BANK_TRANSFER',
entity: 'payment-intent',
paymentIntentId: '6634f56c7dbbc16e07b5025e',
countryOfPayment: 'DE',
contact: {
contactId: '6606ab8ffb9085579f1b5844',
},
payer: {
type: 'myself',
email: 'hans@example.com',
profile: {
firstName: 'Hans',
lastName: 'Mueller',
phone: '+4915123456789',
},
},
}),
});
if (response.ok) {
const data = await response.json();
console.log(data);
} else {
throw new Error(`Request failed with status: ${response.status}`);
}
} catch (error) {
console.error(error);
}
Country of payment
TM_BANK_TRANSFER requires the countryOfPayment field (ISO 3166-1 alpha-2 code). This determines which bank details are returned - for example, a German payer gets IBAN + BIC, while a Brazilian payer may get PIX or ISPB details.
Bank Transfer response
The response fields vary depending on the payer's country and the settlement currency, but the structure is:
{
"reference": "QLY-PYMT-1234",
"amount": 135000,
"currency": "EUR",
"bankName": "Deutsche Bank AG",
"bankAddress": "Taunusanlage 12, 60325 Frankfurt, Germany",
"iban": "DE89370400440532013000",
"swiftCode": "DEUTDEFF",
"accountNumber": "0532013000",
"accountName": "TransferMate Global Payments",
"sortCode": "",
"routingNumber": "",
"fees": [{ "name": "Transfer fee", "amount": 500 }],
"documents": ["receipt"]
}
| Field | Description |
|---|---|
reference | The payment reference your contact must include in their bank transfer. This is how Qualy matches the incoming payment. |
amount | Amount to transfer, in minor currency units (cents). |
currency | The currency the contact should send. |
iban | IBAN (International Bank Account Number). Available for most European and international transfers. |
swiftCode | SWIFT/BIC code for international wire transfers. |
accountNumber | Bank account number. |
accountName | Account holder name. |
sortCode | Sort code (for GBP transfers). |
routingNumber | Routing number (for specific regions). |
documents | Array of required document types (e.g., "receipt") that must be uploaded for compliance. |
Display the bank details, the exact amount and currency, and prominently show the reference - without the correct reference, the payment cannot be automatically matched. Bank transfers typically take 2-5 business days depending on the corridor. Does not support partial payments.
Partial payments
Some payment methods support partial payments. Pass the amount field (in cents) in the sign payload to allow a contact to pay less than the full amount:
const signPayload = {
gateway: 'asaas',
signatureType: 'AS_PIX',
entity: 'payment-intent',
paymentIntentId: '6634f56c7dbbc16e07b5025e',
contact: {
contactId: '6606ab8ffb9085579f1b5844',
},
amount: 50000, // Pay 500.00 of the total (in cents)
payer: {
// ... payer details
},
};
| Method | Partial payments |
|---|---|
AS_PIX | Supported |
AS_BOLETO | Supported |
ZAI_PAYID | Not supported |
ZAI_BSBACC | Not supported |
TM_BANK_TRANSFER | Not supported |
Handling webhooks
After the contact completes the payment, Qualy sends a webhook to your server. Set up your webhook endpoint to listen for transaction events. See Setting up webhooks for details.
A Payment Intent can have multiple transactions (e.g. partial payments or retries). Always check the Payment Intent status to determine if the full amount has been collected:
try {
const response = await fetch(
'https://api.qualyhq.com/v1/payment-intents/6634f56c7dbbc16e07b5025e',
{
method: 'GET',
headers: {
'Content-Type': 'application/json',
'Authorization': 'ApiKey your-api-key-here',
'X-TENANT-ID': 'your-tenant-id-here',
},
}
);
const paymentIntent = await response.json();
console.log(paymentIntent.status);
} catch (error) {
console.error(error);
}
Method summary
| Method | Gateway | signatureType | Region | Speed | Partial | What you display |
|---|---|---|---|---|---|---|
| PIX | asaas | AS_PIX | Brazil | Instant | Yes | QR code + copy-paste code |
| Boleto | asaas | AS_BOLETO | Brazil | 1-3 days | Yes | Bank slip link + 47-digit code |
| PayID | zai | ZAI_PAYID | Australia | Instant | No | PayID email address |
| BSB & Account | zai | ZAI_BSBACC | Australia | 1-2 days | No | BSB + account number |
| Bank Transfer | transfermate | TM_BANK_TRANSFER | 60+ currencies | 2-5 days | No | IBAN, SWIFT, reference |