# FX quotes and rates

> Learn how to get exchange rates and create FX quotes before making cross-border payments.

Before creating a cross-border payment, you'll typically want to check the exchange rate and get a quote that shows the payer exactly how much they'll pay. Nexpay provides two endpoints for this: the **FX Rate API** for quick rate lookups, and the **Quotes API** for detailed quotes with fee breakdowns per settlement method.

---

## Before you start

You will need:

- A valid [API key](/docs/api-keys.md) to authenticate your requests.
- A payee `id` (for quotes) — use the [Payees API](/docs/guides/creating-payees.md) to create or retrieve one.

## Checking the exchange rate

To get the current exchange rate between two currencies, use the FX Rate API. This is useful for displaying indicative rates before the payer commits to a payment.

**cURL**

```bash
curl -X POST 'https://api.nexpay.com.au/v2/fx-rate' \
  -H 'X-API-Key: nxp_ck_your-client-id:nxp_sk_your-secret' \
  -H 'Content-Type: application/json' \
  -d '{
    "fromCurrency": "AUD",
    "toCurrency": "USD",
    "amount": 1,
    "fromCountryCode": "AUS",
    "toCountryCode": "USA",
    "paymentType": "provider"
  }'
```

**JavaScript**

```javascript
try {
  const response = await fetch('https://api.nexpay.com.au/v2/fx-rate', {
    method: 'POST',
    headers: {
      'Content-Type': 'application/json',
      'X-API-Key': 'nxp_ck_your-client-id:nxp_sk_your-secret',
    },
    body: JSON.stringify({
      "fromCurrency": "AUD",
      "toCurrency": "USD",
      "amount": 1,
      "fromCountryCode": "AUS",
      "toCountryCode": "USA",
      "paymentType": "provider"
    }),
  });

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

**PHP (Laravel)**

```php
use Illuminate\Support\Facades\Http;

$response = Http::withHeaders([
    'X-API-Key' => 'nxp_ck_your-client-id:nxp_sk_your-secret',
])->post('https://api.nexpay.com.au/v2/fx-rate', [
    'fromCurrency' => 'AUD',
    'toCurrency' => 'USD',
    'amount' => 1,
    'fromCountryCode' => 'AUS',
    'toCountryCode' => 'USA',
    'paymentType' => 'provider',
]);

$data = $response->json('data');
echo $data['rate'];
```

The response includes the current rate for the corridor:

```javascript
{
  "data": {
    "fromCurrencyCode": "AUD",
    "toCurrencyCode": "USD",
    "rate": 0.7031508029966657,
    "taxPercentage": 0,
    "settlementMethod": 5,
    "expiresOn": "2026-06-02T18:36:29.516Z"
  }
}
```

> **Note — Country codes are ISO 3166-1 alpha-3**
>
> On the FX Rate endpoint, `fromCountryCode` and `toCountryCode` are **3-letter** country codes (`AUS`, `USA`, `BRA`) — different from the 2-letter `countryCode` used on the Quotes endpoint and on payment-intent payloads. See [Currencies & countries](/docs/currencies-countries.md).

### FX Rate request fields

| Field | Type | Required | Description |
| --- | --- | --- | --- |
| `fromCurrency` | string | Yes | 3-letter [ISO 4217](/docs/currencies-countries.md#currencies) source currency code (e.g. `AUD`). |
| `toCurrency` | string | Yes | 3-letter [ISO 4217](/docs/currencies-countries.md#currencies) destination currency code (e.g. `USD`). |
| `amount` | number | Yes | Amount to convert, in [decimal form](/docs/currencies-countries.md#currency-unit). |
| `fromCountryCode` | string | Yes | 3-letter ISO 3166-1 alpha-3 source country code (e.g. `AUS`). |
| `toCountryCode` | string | Yes | 3-letter ISO 3166-1 alpha-3 destination country code (e.g. `USA`). |
| `paymentType` | string | Yes | The type of payment: `provider`, `company`, or `private`. |

---

## Creating a quote

While the FX Rate API gives you a quick rate check, the Quotes API provides a full breakdown with fees and multiple settlement method options — one per available payment rail (bank transfer, card, installment, Pix, etc.). Use quotes when you're ready to show the payer their final costs and to commit a `quoteId` to a payment.

The Quotes endpoint accepts a `payouts` array so you can quote multiple recipients in a single call (used by split and batch flows). For a single-recipient quote, send one item:

> **Warning — Multi-payout quotes need the split-payment entitlement**
>
> A quote with **more than one `payouts` entry** (any tenant split or batch) requires the `AllowSplitPayment` permission on your user / API key — it maps to the `multiPayoutQuote` payment-access capability. Single-payout quotes do not.
>
> Without it, `POST /v2/quotes` returns `403 [GEN9004]`:
>
> ```json
> {
>   "statusCode": 403,
>   "code": "[GEN9004]",
>   "message": "[GEN9004] Forbidden",
>   "details": {
>     "reason": "Payment access does not allow this quote creation",
>     "legacyConstraint": "payment_access.multiPayoutQuote",
>     "failedRequirements": ["User.AllowSplitPayment"]
>   }
> }
> ```
>
> Grant `AllowSplitPayment` in the Dashboard (or ask your Nexpay account manager) before going live with split payments. The change is synced to your API access asynchronously — allow a short propagation delay.

**cURL**

```bash
curl -X POST 'https://api.nexpay.com.au/v2/quotes' \
  -H 'X-API-Key: nxp_ck_your-client-id:nxp_sk_your-secret' \
  -H 'Content-Type: application/json' \
  -d '{
    "payouts": [
      { "payeeId": 12141, "amount": 1000 }
    ],
    "countryCode": "AU",
    "paymentType": "provider"
  }'
```

**JavaScript**

```javascript
try {
  const response = await fetch('https://api.nexpay.com.au/v2/quotes', {
    method: 'POST',
    headers: {
      'Content-Type': 'application/json',
      'X-API-Key': 'nxp_ck_your-client-id:nxp_sk_your-secret',
    },
    body: JSON.stringify({
      "payouts": [
        { "payeeId": 12141, "amount": 1000 }
      ],
      "countryCode": "AU",
      "paymentType": "provider"
    }),
  });

  if (response.ok) {
    const { data: quote } = await response.json();
    console.log('Quote ID:', quote.quoteId);
    console.log('Expires on:', quote.expiresOn);
    console.log('Variants:', quote.variants.length);
  } else {
    throw new Error(`Request failed with status: ${response.status}`);
  }
} catch (error) {
  console.error(error);
}
```

**PHP (Laravel)**

```php
use Illuminate\Support\Facades\Http;

$response = Http::withHeaders([
    'X-API-Key' => 'nxp_ck_your-client-id:nxp_sk_your-secret',
])->post('https://api.nexpay.com.au/v2/quotes', [
    'payouts' => [
        ['payeeId' => 12141, 'amount' => 1000],
    ],
    'countryCode' => 'AU',
    'paymentType' => 'provider',
]);

$quote = $response->json('data');
echo 'Quote ID: ' . $quote['quoteId'];
echo 'Expires on: ' . $quote['expiresOn'];
echo 'Variants: ' . count($quote['variants']);
```

The response wraps the quote in the standard `data` envelope and includes an array of **variants** — one per available settlement method — so the payer can compare options:

```javascript
{
  "data": {
    "quoteId": "902b1624-d2d4-46e5-b087-bfcb311fb128",
    "countryCode": "AU",
    "paymentType": "provider",
    "expiresOn": "2026-06-03T08:30:00.000Z",
    "hasInstallments": false,
    "variants": [
      {
        "id": 1175,
        "settlementMethod": "dmt",
        "settlementChannel": "bank",
        "fromCurrency": "AUD",
        "toCurrency": "AUD",
        "fromAmount": 1005,
        "fxRate": 1.005,
        "fee": 10,
        "minAmount": 100,
        "maxAmount": 50000,
        "spread": 0.005,
        "marketRate": 1,
        "commissionBeneficiaryId": 51,
        "commission": 0.5,
        "meta": {
          "category": "bank-transfer",
          "uiFlow": "standard",
          "eta": { "disbursement": { "unit": "days", "min": 1, "max": 3 } },
          "noticeKeys": ["notice.dmt.proof_required"]
        }
      }
    ],
    "payouts": [
      {
        "payeeId": 12141,
        "fromCurrency": "AUD",
        "toCurrency": "AUD",
        "payerAmount": 1000,
        "payeeAmount": 1000,
        "fxRate": 1
      }
    ]
  }
}
```

### Quote request fields

| Field | Type | Required | Description |
| --- | --- | --- | --- |
| `payouts` | array | Yes | One or more `{ payeeId, amount }` pairs. `amount` is the amount each payee will receive, in [decimal form](/docs/currencies-countries.md#currency-unit). The payee's currency is inferred from the payee record. |
| `countryCode` | string | Yes (Provider/Private) | 2-letter ISO 3166-1 alpha-2 code of the country the payer is paying FROM. Company quotes derive this server-side from the linked-school bank, so it may be omitted on `company` quotes. |
| `paymentType` | string | Yes | The type of payment: `provider`, `company`, or `private`. (Note: payments themselves call this field `transactionType`.) |

### Payment types

| Type | Description |
| --- | --- |
| `provider` | Provider/institution payments (e.g. university tuition, school fees). |
| `company` | Business-to-business payments. |
| `private` | Individual/personal transfers (e.g. family remittances). |

---

## Understanding quote variants

Each variant in the response represents a different way the payer can send money — bank transfer (`dmt`), card capture, Pix, BPAY, installments, and so on. To commit a quote to a payment intent, send the chosen variant's `id` in `instructions.manualPayment.selectedQuoteVariantId` — it is a server-assigned numeric id, NOT an array index.

Key variant fields:

| Field | Description |
| --- | --- |
| `id` | Server-assigned numeric id of this variant. Use this value as `selectedQuoteVariantId` when creating a payment intent. |
| `settlementMethod` | The payment rail (e.g. `dmt`, `card`, `installment`, `pix`). |
| `settlementChannel` | The processing channel (e.g. `bank`, `checkout`, `volt`). |
| `fromCurrency` / `toCurrency` | The currency the payer sends and the payee receives. |
| `fromAmount` | Total the payer pays in `fromCurrency` (already includes `fee`). |
| `fxRate` | The exchange rate used for this variant (payer-facing rate, includes spread). |
| `fee` | The fixed fee for this settlement method, in `fromCurrency`. |
| `minAmount` / `maxAmount` | Limits enforced by this rail, in `fromCurrency`. |
| `marketRate` / `spread` | The mid-market rate and the spread applied to derive `fxRate`. Display-only. |
| `meta` | Connector-specific metadata: `category` (`bank-transfer`, `card`, `cash`, `wallet`), `uiFlow` hint, ETA, card networks, etc. Use for rendering the payment-method picker. |

The top-level `payouts` array mirrors the per-recipient settlement amounts in payee currency:

- `payerAmount` — what the payer pays toward this recipient (per the selected variant)
- `payeeAmount` — what the recipient receives
- `fxRate` — the corridor rate for this leg

---

## Retrieving a quote

You can retrieve a previously created quote by its `quoteId`:

**cURL**

```bash
curl 'https://api.nexpay.com.au/v2/quotes/902b1624-d2d4-46e5-b087-bfcb311fb128' \
  -H 'X-API-Key: nxp_ck_your-client-id:nxp_sk_your-secret'
```

**JavaScript**

```javascript
try {
  const response = await fetch(
    'https://api.nexpay.com.au/v2/quotes/902b1624-d2d4-46e5-b087-bfcb311fb128',
    {
      method: 'GET',
      headers: { 'X-API-Key': 'nxp_ck_your-client-id:nxp_sk_your-secret' },
    },
  );

  if (response.ok) {
    const { data: quote } = await response.json();
    console.log(quote);
  } else {
    throw new Error(`Request failed with status: ${response.status}`);
  }
} catch (error) {
  console.error(error);
}
```

**PHP (Laravel)**

```php
use Illuminate\Support\Facades\Http;

$response = Http::withHeaders([
    'X-API-Key' => 'nxp_ck_your-client-id:nxp_sk_your-secret',
])->get('https://api.nexpay.com.au/v2/quotes/902b1624-d2d4-46e5-b087-bfcb311fb128');

$quote = $response->json('data');
print_r($quote);
```

---

## Things to know

* Quotes have an expiry time (`expiresOn`). Always check this before submitting a payment — using an expired quote returns a `[QOT0001]` error from `/submit` (HTTP `422`).
* Different settlement methods may have different rates and fees. Present all variants to the payer so they can choose the best option.
* The `amount` in the quote request is the amount the **payee receives** (destination amount), not what the payer sends.
* If the currency pair is unavailable, the API returns a `[QOT0003]` error. Use the FX Rate API to check supported pairs first.
* FX rates fluctuate — always create a fresh quote close to when the payer is ready to pay.
* The Quotes endpoint uses `paymentType`; the FX Rate endpoint uses the same value spelled `transactionType`. This is a known inconsistency; the values themselves are interchangeable.
