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

  1. Create a Payment Intent - defines the amount, currency, and contact.
  2. Get available payment options - check which methods the contact can use.
  3. Sign the payment - call the sign endpoint with the chosen method. The response contains everything you need to display to your contact.
  4. Contact completes payment - they scan a QR code, pay a boleto, transfer to a PayID, etc.
  5. 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:

  1. If the Payment Intent has a settlementCurrency set, it is always used (forced).
  2. Otherwise, the value you pass to the payment-options endpoint is used.
  3. If neither is set, the Payment Intent's currency is 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.typeWhen to use
myselfThe contact is paying for themselves. Qualy can resolve address and ID from the contact record, so email, profile, and address are optional.
parentA parent or guardian is paying on behalf of the contact.
partnerA business partner is paying.
familyA family member is paying.
friendA friend is paying.
colleagueA work colleague is paying.
otherSomeone 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 fieldWhat to provide
full-payerThe full payer object is missing. Collect all payer details (email, profile, address, id).
addressA valid address is required. For Brazilian methods, the postalCode (CEP) must be a valid format.
idA 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..."
}
FieldDescription
encodedQrCodeBase64-encoded QR code image. Render it as an <img> tag: <img src="data:image/png;base64,${encodedQrCode}" />
qrCodeLinkThe 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"
}
FieldDescription
bankSlipUrlURL to download or view the boleto PDF. You can link or embed this directly.
identificationFieldThe formatted 47-digit boleto number. This is what payers type into their banking app.
nossoNumeroThe bank's internal reference number for the boleto.
barCodeThe 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"
  }
}
FieldDescription
emailThe PayID address. Your contact enters this in their banking app to send the payment.
details.nameThe merchant display name that will appear when the contact looks up the PayID.
details.legalNameThe 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"
}
FieldDescription
accountNameThe name of the receiving bank account.
routingNumberThe BSB (Bank-State-Branch) number. Display this formatted as XXX-XXX.
accountNumberThe 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"]
}
FieldDescription
referenceThe payment reference your contact must include in their bank transfer. This is how Qualy matches the incoming payment.
amountAmount to transfer, in minor currency units (cents).
currencyThe currency the contact should send.
ibanIBAN (International Bank Account Number). Available for most European and international transfers.
swiftCodeSWIFT/BIC code for international wire transfers.
accountNumberBank account number.
accountNameAccount holder name.
sortCodeSort code (for GBP transfers).
routingNumberRouting number (for specific regions).
documentsArray 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
  },
};
MethodPartial payments
AS_PIXSupported
AS_BOLETOSupported
ZAI_PAYIDNot supported
ZAI_BSBACCNot supported
TM_BANK_TRANSFERNot 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

MethodGatewaysignatureTypeRegionSpeedPartialWhat you display
PIXasaasAS_PIXBrazilInstantYesQR code + copy-paste code
BoletoasaasAS_BOLETOBrazil1-3 daysYesBank slip link + 47-digit code
PayIDzaiZAI_PAYIDAustraliaInstantNoPayID email address
BSB & AccountzaiZAI_BSBACCAustralia1-2 daysNoBSB + account number
Bank TransfertransfermateTM_BANK_TRANSFER60+ currencies2-5 daysNoIBAN, SWIFT, reference
Previous
Public demo