# Collecting payments

> Two ways to collect payments - redirect to Qualy's payment portal, or call the API directly for PIX, Boleto, PayID, bank transfers, and more.

After [creating a Payment Intent](/docs/guides/creating-payment-intents.md), 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.

```json
{
  "_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.

> **Note — 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.

> **Warning — 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

```javascript
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:

```json
{
  "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.

```json
{
  "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.

```javascript
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

```json
{
  "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.

```javascript
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

```json
{
  "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.

> **Note — PayID availability**
>
> PayID is only available for AUD payments in Australia.

```javascript
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

```json
{
  "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.

```javascript
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

```json
{
  "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.

```javascript
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);
}
```

> **Note — 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:

```json
{
  "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:

```javascript
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](/docs/webhooks.md) 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:

```javascript
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 { data: 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 |
