Multi-currency and FX quotes
Quick Reference
Why offer multi-currency
Your customers abandon checkout when they see prices in a foreign currency. Multi-currency solves this.
| Without multi-currency | With A55 multi-currency |
|---|---|
| Customer sees R$ 500.00 but pays in USD | Customer sees US$ 87.26 — the exact amount they pay |
| Customer does not know the real exchange rate | You show the mid-market rate with full transparency |
| Customer calls their bank to dispute the amount | No surprises — the price matches the charge |
| You lose cross-border sales | You convert international visitors into paying customers |
The business impact is measurable:
- Higher conversion: Customers buy when they understand the price in their own currency.
- Fewer chargebacks: Transparent pricing reduces "I did not recognize this amount" disputes.
- Competitive edge: You offer the same experience as global platforms like Amazon, Shopify, and Stripe.
- LATAM coverage: 8 currencies across 7 countries — USD, BRL, EUR, MXN, ARS, COP, CLP, PEN.
A merchant in Brazil can display prices in USD, EUR, or MXN to international buyers. A SaaS platform can show subscription prices in each customer's local currency. An e-commerce site can let buyers switch currencies at checkout.
How it works — three steps
The multi-currency flow has three steps. You call the FX endpoint, display the converted price, and create the charge.
Standard flow (2-decimal currency — BRL):
Zero-decimal flow (CLP — integer amounts):
Step 1 — Get the exchange rate
Call the FX endpoint to get the current mid-market rate between two currencies.
Endpoint: POST /api/v1/bank/wallet/fx/rate/
Request:
| Field | Type | Required | Description |
|---|---|---|---|
from_currency | string | Yes | Source currency (ISO 4217). Example: USD |
to_currency | string | Yes | Target currency (ISO 4217). Example: BRL |
Response:
| Field | Type | Description |
|---|---|---|
price | float | The exchange rate, rounded to 2 decimal places. When converting to a zero-decimal currency (CLP, COP), round the final amount to an integer |
- cURL
- Python
- JavaScript
curl -X POST 'https://sandbox.api.a55.tech/api/v1/bank/wallet/fx/rate/' \
-H 'Authorization: Bearer YOUR_ACCESS_TOKEN' \
-H 'Content-Type: application/json' \
-d '{
"from_currency": "USD",
"to_currency": "BRL"
}'
import requests
url = "https://sandbox.api.a55.tech/api/v1/bank/wallet/fx/rate/"
headers = {
"Authorization": "Bearer YOUR_ACCESS_TOKEN",
"Content-Type": "application/json",
}
payload = {
"from_currency": "USD",
"to_currency": "BRL",
}
response = requests.post(url, json=payload, headers=headers)
rate = response.json()["price"]
print(f"1 USD = {rate} BRL")
const response = await fetch(
"https://sandbox.api.a55.tech/api/v1/bank/wallet/fx/rate/",
{
method: "POST",
headers: {
"Authorization": "Bearer YOUR_ACCESS_TOKEN",
"Content-Type": "application/json",
},
body: JSON.stringify({
from_currency: "USD",
to_currency: "BRL",
}),
}
);
const { price } = await response.json();
console.log(`1 USD = ${price} BRL`);
Response:
{
"price": 5.73
}
This means 1 USD = 5.73 BRL at this moment.
Step 2 — Display the converted price to your customer
Use the rate to show your customer the price in their currency. This happens in your application — no A55 API call is needed.
Example (BRL — 2 decimals): Your product costs US$ 100.00. The rate is 5.73.
Price in BRL = US$ 100.00 × 5.73 = R$ 573.00
| What the customer sees | Value |
|---|---|
| Product price | US$ 100.00 |
| Exchange rate | 1 USD = 5.73 BRL |
| Amount to pay | R$ 573.00 |
Example (CLP — zero-decimal): Your product costs US$ 100.00. The rate is 950.73.
Price in CLP = US$ 100.00 × 950.73 = CLP 95,073 (round to integer)
| What the customer sees | Value |
|---|---|
| Product price | US$ 100.00 |
| Exchange rate | 1 USD = 950.73 CLP |
| Amount to pay | CLP $95,073 |
For zero-decimal currencies, the amount you send in the charge request must be an integer. 95073, not 95073.00. The backend truncates decimal places for CLP and COP.
Display the original price AND the converted price. This builds trust. Your customer knows the exact rate and the exact amount their card or bank account will be charged.
Step 3 — Create the charge
Create the charge in the settlement currency (the currency of the wallet). The charge amount is the converted value from Step 2.
curl -X POST 'https://sandbox.api.a55.tech/api/v1/bank/wallet/charge/' \
-H 'Authorization: Bearer YOUR_ACCESS_TOKEN' \
-H 'Content-Type: application/json' \
-d '{
"wallet_uuid": "f47ac10b-58cc-4372-a567-0e02b2c3d479",
"merchant_id": "11111111-1111-1111-1111-111111111111",
"payer_name": "John Smith",
"payer_email": "john@example.com",
"payer_tax_id": "12345678909",
"payer_cell_phone": "+5511999999999",
"installment_value": 573.00,
"currency": "BRL",
"due_date": "2026-04-30",
"description": "Order #12345 (US$ 100.00 at 5.73)",
"type_charge": "credit_card",
"installment_count": 1,
"payer_address": {
"street": "Av. Paulista",
"address_number": "1000",
"complement": "Sala 101",
"neighborhood": "Bela Vista",
"city": "São Paulo",
"state": "SP",
"postal_code": "01310-100",
"country": "BR"
}
}'
Store the rate in the charge description field (for example: "Order #12345 (US$ 100.00 at 5.73)"). This creates an audit trail for reconciliation and dispute resolution.
The charge response includes automatic multi-currency conversion:
{
"charge_uuid": "a1b2c3d4-e5f6-7890-abcd-ef1234567890",
"local_currency": 573.00,
"currency": "BRL",
"usd_currency": 100.00,
"eur_currency": 91.45,
"type": "credit_card",
"status": "confirmed",
"installment_count": 1,
"installments": [
{
"local_currency": 573.00,
"currency": "BRL",
"usd_currency": 100.00,
"eur_currency": 91.45,
"due_date": "2026-04-30",
"status": "confirmed",
"installment_number": 1
}
]
}
Every charge response includes three currency values:
| Field | Description | Example |
|---|---|---|
local_currency | Amount in the wallet's settlement currency | 573.00 (BRL) |
usd_currency | Equivalent amount in US dollars | 100.00 (USD) |
eur_currency | Equivalent amount in euros | 91.45 (EUR) |
You can use these values for multi-currency reporting, reconciliation, and analytics dashboards without calling the FX endpoint again.
Complete integration example
This example shows the full flow — authenticate, get the FX rate, calculate the price, and create the charge.
- Python
- JavaScript
from decimal import Decimal, ROUND_HALF_UP
import requests
BASE = "https://sandbox.api.a55.tech/api/v1"
AUTH_URL = "https://auth.a55.tech/oauth2/token"
# Authenticate
token_resp = requests.post(AUTH_URL, data={
"grant_type": "client_credentials",
"client_id": "YOUR_CLIENT_ID",
"client_secret": "YOUR_CLIENT_SECRET",
}, headers={"Content-Type": "application/x-www-form-urlencoded"})
token = token_resp.json()["access_token"]
headers = {"Authorization": f"Bearer {token}", "Content-Type": "application/json"}
# Step 1: Get the exchange rate
target_currency = "BRL" # Change to "CLP" for Chilean Peso
fx_resp = requests.post(f"{BASE}/bank/wallet/fx/rate/", json={
"from_currency": "USD",
"to_currency": target_currency,
}, headers=headers)
rate = fx_resp.json()["price"]
print(f"Exchange rate: 1 USD = {rate} {target_currency}")
# Step 2: Calculate the converted price with Decimal precision
product_price_usd = 100.00
ZERO_DECIMAL_CURRENCIES = {"CLP", "COP"}
amount = Decimal(str(product_price_usd)) * Decimal(str(rate))
if target_currency in ZERO_DECIMAL_CURRENCIES:
charge_amount = int(amount.quantize(Decimal("1"), rounding=ROUND_HALF_UP))
else:
charge_amount = float(amount.quantize(Decimal("0.01"), rounding=ROUND_HALF_UP))
print(f"Charge amount: {charge_amount} {target_currency}")
# Step 3: Create the charge
charge_resp = requests.post(f"{BASE}/bank/wallet/charge/", json={
"wallet_uuid": "f47ac10b-58cc-4372-a567-0e02b2c3d479",
"merchant_id": "11111111-1111-1111-1111-111111111111",
"payer_name": "John Smith",
"payer_email": "john@example.com",
"payer_tax_id": "12345678909",
"payer_cell_phone": "+5511999999999",
"installment_value": charge_amount,
"currency": target_currency,
"due_date": "2026-04-30",
"description": f"Order #12345 (US$ {product_price_usd} at {rate})",
"type_charge": "credit_card",
"installment_count": 1,
"payer_address": {
"street": "Av. Paulista", "address_number": "1000",
"complement": "Sala 101", "neighborhood": "Bela Vista",
"city": "São Paulo", "state": "SP",
"postal_code": "01310-100", "country": "BR",
},
}, headers=headers)
charge = charge_resp.json()
print(f"Charge UUID: {charge['charge_uuid']}")
print(f"Local ({target_currency}): {charge['local_currency']}")
print(f"USD: {charge['usd_currency']}")
print(f"EUR: {charge['eur_currency']}")
const BASE = "https://sandbox.api.a55.tech/api/v1";
const AUTH_URL = "https://auth.a55.tech/oauth2/token";
// Authenticate
const tokenResp = await fetch(AUTH_URL, {
method: "POST",
headers: { "Content-Type": "application/x-www-form-urlencoded" },
body: new URLSearchParams({
grant_type: "client_credentials",
client_id: "YOUR_CLIENT_ID",
client_secret: "YOUR_CLIENT_SECRET",
}),
});
const { access_token } = await tokenResp.json();
const headers = {
Authorization: `Bearer ${access_token}`,
"Content-Type": "application/json",
};
// Step 1: Get the exchange rate
const targetCurrency = "BRL"; // Change to "CLP" for Chilean Peso
const fxResp = await fetch(`${BASE}/bank/wallet/fx/rate/`, {
method: "POST",
headers,
body: JSON.stringify({ from_currency: "USD", to_currency: targetCurrency }),
});
const { price: rate } = await fxResp.json();
console.log(`Exchange rate: 1 USD = ${rate} ${targetCurrency}`);
// Step 2: Calculate with correct precision per currency type
const productPriceUsd = 100.0;
const ZERO_DECIMAL = new Set(["CLP", "COP"]);
const chargeAmount = ZERO_DECIMAL.has(targetCurrency)
? Math.round(productPriceUsd * rate)
: Math.round(productPriceUsd * rate * 100) / 100;
console.log(`Charge amount: ${chargeAmount} ${targetCurrency}`);
// Step 3: Create the charge
const chargeResp = await fetch(`${BASE}/bank/wallet/charge/`, {
method: "POST",
headers,
body: JSON.stringify({
wallet_uuid: "f47ac10b-58cc-4372-a567-0e02b2c3d479",
merchant_id: "11111111-1111-1111-1111-111111111111",
payer_name: "John Smith",
payer_email: "john@example.com",
payer_tax_id: "12345678909",
payer_cell_phone: "+5511999999999",
installment_value: chargeAmount,
currency: targetCurrency,
due_date: "2026-04-30",
description: `Order #12345 (US$ ${productPriceUsd} at ${rate})`,
type_charge: "credit_card",
installment_count: 1,
payer_address: {
street: "Av. Paulista", address_number: "1000",
complement: "Sala 101", neighborhood: "Bela Vista",
city: "São Paulo", state: "SP",
postal_code: "01310-100", country: "BR",
},
}),
});
const charge = await chargeResp.json();
console.log(`Charge UUID: ${charge.charge_uuid}`);
console.log(`Local (${targetCurrency}): ${charge.local_currency}`);
console.log(`USD: ${charge.usd_currency}`);
console.log(`EUR: ${charge.eur_currency}`);
CLP deep dive — precise quoting for zero-decimal currencies
The Chilean Peso (CLP) is a zero-decimal currency (ISO 4217 exponent 0). It has no centavos — the smallest unit is CLP 1. This creates unique challenges when converting from USD, EUR, or any 2-decimal currency. This section explains the computational traps, the correct rounding technique, and provides a complete end-to-end example.
Why CLP requires special attention
Three properties make CLP different from BRL, MXN, or other 2-decimal currencies:
| Property | BRL (2-decimal) | CLP (zero-decimal) | Impact |
|---|---|---|---|
| Smallest unit | R$ 0.01 (centavo) | CLP $1 (peso) | CLP amounts must be integers |
| Magnitude per USD | ~5.7 BRL | ~950 CLP | Rounding errors are amplified ~167x |
| Rate × amount precision | 100.00 × 5.73 = 573.00 (exact) | 100.00 × 950.73 = 95073.0 (may not be exact in floating-point) | Floating-point arithmetic can produce fractional results |
| Backend behavior | Stored as Numeric(11,2) | Truncated to integer before acquirer | 95072.7 becomes 95072 — you lose CLP 1 |
A 1-cent USD rounding error (~CLP 10) is invisible in BRL (R$ 0.01) but produces a visible CLP discrepancy. Across millions of transactions, systematic rounding bias compounds into material pricing errors — the same pattern documented in the Belgian Franc-to-Euro conversion of 2001, where accumulated rounding caused 0.54–0.72% consumer price inflation.
The precision trap — IEEE 754 and floating-point arithmetic
Computers represent decimal numbers in binary (IEEE 754). Most decimal fractions cannot be represented exactly:
// IEEE 754 demonstration
0.1 + 0.2 // 0.30000000000000004 (not 0.3)
99.99 * 950.73 // 95063.4927 — may have trailing digits
100.00 * 950.73 // 95073.0 (happens to be exact here)
149.99 * 950.73 // 142599.9927 — NOT an integer, must round to 142600
Math.floor(142599.9927) // 142599 — WRONG, lost CLP 1 (correct is 142600)
The danger is intermittent: some multiplications are exact, others are not. You cannot rely on floating-point being "close enough" — you must round explicitly.
The Belgian Franc lesson (2001): When Belgium converted prices from BEF to EUR at the fixed rate of 40.3399, merchants applied floor() to the converted price. Across millions of transactions, this systematic truncation transferred ~0.01 EUR per 101 EUR from consumers to merchants. The Belgian Consumer Organization (Test-Achats) documented the pattern. The lesson: never truncate currency conversions — always round to nearest.
The "round late, not often" principle: Perform all intermediate arithmetic at full floating-point precision (15–17 significant digits in IEEE 754 double). Round only once, at the final step, to the target currency's decimal places. Every intermediate rounding introduces error that accumulates. This is the approach used by Stripe, Wise, and Adyen in production.
Correct rounding for CLP — why round() is not enough
Python and JavaScript implement different rounding strategies for the 0.5 midpoint:
| Language | Expression | Result | Rounding mode |
|---|---|---|---|
| Python | round(95072.5) | 95072 | Banker's rounding (half-to-even) |
| JavaScript | Math.round(95072.5) | 95073 | Half-up |
| Python | round(95073.5) | 95074 | Banker's rounding (rounds up to even) |
| JavaScript | Math.round(95073.5) | 95074 | Half-up |
Banker's rounding minimizes statistical bias over large datasets, but for individual payment transactions, the customer and merchant expect half-up rounding (0.5 rounds away from zero). If your backend is Python and your frontend is JavaScript, the same multiplication can produce different integer amounts.
Recommended approach — use Decimal types:
- Python (Decimal)
- JavaScript (Math.round)
from decimal import Decimal, ROUND_HALF_UP
rate = 950.73
product_price = 149.99
amount = Decimal(str(product_price)) * Decimal(str(rate))
charge_amount = int(amount.quantize(Decimal("1"), rounding=ROUND_HALF_UP))
# charge_amount = 142600 (CLP) — deterministic, no floating-point surprise
const rate = 950.73;
const productPrice = 149.99;
// Math.round uses half-up — safe for CLP
const chargeAmount = Math.round(productPrice * rate);
// chargeAmount = 142600 (CLP) — matches Python Decimal result
Math.floor(142599.9927) = 142599 — but the correct charge is CLP 142,600. parseInt("142599.9927") = 142599. Both silently drop the fractional part, systematically undercharging by CLP 1. Over millions of transactions, this creates a measurable revenue leak. The A55 backend also truncates (not rounds) — if you send 142599.99, it becomes 142599.
The truncation trap — A55 backend behavior for CLP
The A55 backend truncates (not rounds) decimal places for CLP and COP before routing to the acquirer. This means:
| You send | Backend receives | Acquirer receives | Problem |
|---|---|---|---|
95073 | 95073 | 95073 | None — correct |
95073.00 | 95073 | 95073 | None — truncation has no effect |
95072.7 | 95072 | 95072 | Lost CLP 1 — customer was quoted CLP 95,073 |
95072.999 | 95072 | 95072 | Lost CLP 1 — floating-point produced wrong value |
Defensive validation before submitting:
- Python
- JavaScript
def validate_clp_amount(amount: float) -> int:
if amount != int(amount):
raise ValueError(f"CLP amount must be integer, got {amount}")
return int(amount)
function validateClpAmount(amount) {
if (!Number.isInteger(amount)) {
throw new Error(`CLP amount must be integer, got ${amount}`);
}
return amount;
}
Complete CLP example — USD to Chilean Peso
This example shows the complete flow for quoting a USD product and charging in CLP with precise rounding.
- cURL
- Python
- JavaScript
# Step 1: Get USD → CLP rate
curl -X POST 'https://sandbox.api.a55.tech/api/v1/bank/wallet/fx/rate/' \
-H 'Authorization: Bearer YOUR_ACCESS_TOKEN' \
-H 'Content-Type: application/json' \
-d '{"from_currency": "USD", "to_currency": "CLP"}'
# Response: {"price": 950.73}
# Step 2: Calculate — US$ 100.00 × 950.73 = CLP 95,073 (integer)
# Step 3: Create charge in CLP
curl -X POST 'https://sandbox.api.a55.tech/api/v1/bank/wallet/charge/' \
-H 'Authorization: Bearer YOUR_ACCESS_TOKEN' \
-H 'Content-Type: application/json' \
-d '{
"wallet_uuid": "YOUR_CLP_WALLET_UUID",
"merchant_id": "YOUR_MERCHANT_ID",
"payer_name": "Carlos Muñoz",
"payer_email": "carlos@example.cl",
"payer_tax_id": "12.345.678-9",
"payer_cell_phone": "+56911111111",
"installment_value": 95073,
"currency": "CLP",
"due_date": "2026-04-30",
"description": "Order #12345 (US$ 100.00 at 950.73)",
"type_charge": "credit_card",
"installment_count": 1,
"payer_address": {
"street": "Av. Providencia",
"address_number": "1234",
"complement": "Depto 501",
"neighborhood": "Providencia",
"city": "Santiago",
"state": "RM",
"postal_code": "7500000",
"country": "CL"
}
}'
from decimal import Decimal, ROUND_HALF_UP
import requests
BASE = "https://sandbox.api.a55.tech/api/v1"
AUTH_URL = "https://auth.a55.tech/oauth2/token"
token_resp = requests.post(AUTH_URL, data={
"grant_type": "client_credentials",
"client_id": "YOUR_CLIENT_ID",
"client_secret": "YOUR_CLIENT_SECRET",
}, headers={"Content-Type": "application/x-www-form-urlencoded"})
token = token_resp.json()["access_token"]
headers = {"Authorization": f"Bearer {token}", "Content-Type": "application/json"}
# Step 1: Get fresh USD → CLP rate
fx_resp = requests.post(f"{BASE}/bank/wallet/fx/rate/", json={
"from_currency": "USD",
"to_currency": "CLP",
}, headers=headers)
rate = fx_resp.json()["price"]
# Step 2: Convert with Decimal precision — round HALF_UP to integer
product_price_usd = 100.00
amount_decimal = Decimal(str(product_price_usd)) * Decimal(str(rate))
charge_amount = int(amount_decimal.quantize(Decimal("1"), rounding=ROUND_HALF_UP))
# Step 3: Validate and create charge
assert charge_amount == int(charge_amount), "CLP amount must be integer"
charge_resp = requests.post(f"{BASE}/bank/wallet/charge/", json={
"wallet_uuid": "YOUR_CLP_WALLET_UUID",
"merchant_id": "YOUR_MERCHANT_ID",
"payer_name": "Carlos Muñoz",
"payer_email": "carlos@example.cl",
"payer_tax_id": "12.345.678-9",
"payer_cell_phone": "+56911111111",
"installment_value": charge_amount,
"currency": "CLP",
"due_date": "2026-04-30",
"description": f"Order #12345 (US$ {product_price_usd} at {rate})",
"type_charge": "credit_card",
"installment_count": 1,
"payer_address": {
"street": "Av. Providencia", "address_number": "1234",
"complement": "Depto 501", "neighborhood": "Providencia",
"city": "Santiago", "state": "RM",
"postal_code": "7500000", "country": "CL",
},
}, headers=headers)
charge = charge_resp.json()
print(f"Charge UUID: {charge['charge_uuid']}")
print(f"CLP: {charge['local_currency']}")
print(f"USD: {charge['usd_currency']}")
print(f"EUR: {charge['eur_currency']}")
const BASE = "https://sandbox.api.a55.tech/api/v1";
const AUTH_URL = "https://auth.a55.tech/oauth2/token";
const tokenResp = await fetch(AUTH_URL, {
method: "POST",
headers: { "Content-Type": "application/x-www-form-urlencoded" },
body: new URLSearchParams({
grant_type: "client_credentials",
client_id: "YOUR_CLIENT_ID",
client_secret: "YOUR_CLIENT_SECRET",
}),
});
const { access_token } = await tokenResp.json();
const headers = {
Authorization: `Bearer ${access_token}`,
"Content-Type": "application/json",
};
// Step 1: Get fresh USD → CLP rate
const fxResp = await fetch(`${BASE}/bank/wallet/fx/rate/`, {
method: "POST",
headers,
body: JSON.stringify({ from_currency: "USD", to_currency: "CLP" }),
});
const { price: rate } = await fxResp.json();
// Step 2: Convert — Math.round uses half-up, safe for CLP
const productPriceUsd = 100.0;
const chargeAmount = Math.round(productPriceUsd * rate);
// Step 3: Validate integer and create charge
if (!Number.isInteger(chargeAmount)) {
throw new Error(`CLP amount must be integer, got ${chargeAmount}`);
}
const chargeResp = await fetch(`${BASE}/bank/wallet/charge/`, {
method: "POST",
headers,
body: JSON.stringify({
wallet_uuid: "YOUR_CLP_WALLET_UUID",
merchant_id: "YOUR_MERCHANT_ID",
payer_name: "Carlos Muñoz",
payer_email: "carlos@example.cl",
payer_tax_id: "12.345.678-9",
payer_cell_phone: "+56911111111",
installment_value: chargeAmount,
currency: "CLP",
due_date: "2026-04-30",
description: `Order #12345 (US$ ${productPriceUsd} at ${rate})`,
type_charge: "credit_card",
installment_count: 1,
payer_address: {
street: "Av. Providencia", address_number: "1234",
complement: "Depto 501", neighborhood: "Providencia",
city: "Santiago", state: "RM",
postal_code: "7500000", country: "CL",
},
}),
});
const charge = await chargeResp.json();
console.log(`Charge UUID: ${charge.charge_uuid}`);
console.log(`CLP: ${charge.local_currency}`);
console.log(`USD: ${charge.usd_currency}`);
console.log(`EUR: ${charge.eur_currency}`);
Expected response:
{
"charge_uuid": "a1b2c3d4-e5f6-7890-abcd-ef1234567890",
"local_currency": 95073,
"currency": "CLP",
"usd_currency": 100.00,
"eur_currency": 91.45,
"type": "credit_card",
"status": "confirmed",
"installment_count": 1,
"installments": [
{
"local_currency": 95073,
"currency": "CLP",
"usd_currency": 100.00,
"eur_currency": 91.45,
"due_date": "2026-04-30",
"status": "confirmed",
"installment_number": 1
}
]
}
Note that local_currency is an integer (95073, not 95073.00) for CLP.
CLP chargeback scenario — stale rates magnify overcharges
CLP's large magnitude (~950 per USD) means rate drift creates proportionally larger discrepancies than BRL (~5.7 per USD).
Scenario: Your product costs US$ 100.00. A Chilean customer pays with a USD credit card. Your wallet currency is CLP.
| Time | Event | Rate (USD→CLP) | Amount |
|---|---|---|---|
| 10:00 AM | You fetch the rate and display the price | 960.00 | Page shows "CLP $96,000" |
| 10:45 AM | Customer clicks "Confirm" — you create the charge using the 10:00 AM rate | Real rate is now 940.00 | You submit CLP 96,000 |
| 10:45 AM | Issuing bank converts CLP 96,000 back to USD at current rate | Issuer uses 940.00 | Statement shows US$ 102.13 |
| 10:45 AM | Issuer adds 3% foreign transaction fee | — | Statement shows US$ 105.19 |
| Next day | Cardholder checks statement | — | "I agreed to US$ 100.00, why was I charged US$ 105.19?" |
| 3 days later | Cardholder disputes | — | Mastercard 4834: "Transaction Amount Differs" |
The math: Rate drifted 2.1% (960→940). With CLP amounts in the tens of thousands, a 2.1% drift = CLP 2,000 = US$ 2.13. Add a 3% bank fee and the total overcharge reaches US$ 5.19 — well above the chargeback threshold.
The fix: Re-fetch the rate at 10:45 AM (940.00), compute CLP 94,000, submit CLP 94,000. Issuer converts back to ~US$ 100.00. No dispute.
Zero-decimal currency decision tree
Use this flowchart when implementing currency conversion for any A55-supported currency:
Supported currencies
The FX API supports 8 currencies across 7 LATAM countries plus USD and EUR, producing 56 conversion pairs.
| Code | Currency | Country / Region | Decimals | Wallet supported |
|---|---|---|---|---|
USD | US Dollar | United States | 2 | — |
BRL | Brazilian Real | Brazil | 2 | Yes |
EUR | Euro | Eurozone | 2 | — |
MXN | Mexican Peso | Mexico | 2 | Yes |
ARS | Argentine Peso | Argentina | 2 | Yes |
COP | Colombian Peso | Colombia | 0 | — |
CLP | Chilean Peso | Chile | 0 | Yes |
PEN | Peruvian Sol | Peru | 2 | — |
When converting to CLP or COP, the final charge amount must be an integer. After multiplying by the FX rate, round to the nearest whole number using ROUND_HALF_UP (not floor(), not banker's rounding). Example: USD 100.00 × 950.73 = CLP 95,073. The backend truncates any decimal — 95072.7 becomes 95072. See the CLP deep dive for complete precision guidance.
Key cross-border corridors
| Pair | Use case |
|---|---|
| USD → BRL | US companies selling to Brazilian customers |
| USD → MXN | US companies selling to Mexican customers |
| EUR → BRL | European companies entering Brazil |
| BRL → USD | Brazilian SaaS, exports, remittances |
| USD → CLP | US companies selling to Chilean customers |
| USD → ARS | US companies selling to Argentine customers |
| USD → COP | US companies selling to Colombian customers |
| USD → PEN | US companies selling to Peruvian customers |
Rate source and freshness
| Property | Value |
|---|---|
| Rate source | Mid-market (interbank) — the Bid/Ask midpoint as reported by third-party data sources, with zero A55 markup |
| Cache window | ~17 minutes (1,000 seconds) |
| Data sources | Cascading fallback — the system queries multiple external providers in sequence to ensure rate availability |
| Precision | Rate rounded to 2 decimal places; final charge amount follows currency decimals (0 for CLP/COP, 2 for all others) |
| API availability | 24/7 — the endpoint responds at all times, including weekends and holidays |
| Weekend rates | Outside forex market hours (weekends, holidays), data sources return the last available trading rate |
The mid-market rate is the midpoint between the buy and sell price in the interbank market. It has zero markup. Central banks, regulators, and fintech companies use it as the fairest reference for currency conversion. The Forex market trades over US$ 9.6 trillion per day (BIS 2025) — no other financial market offers this level of liquidity and price accuracy.
How card networks and issuers add fees to the exchange rate
The A55 FX endpoint returns the mid-market rate with zero markup. But between A55 and the cardholder's credit card statement, two additional layers add fees. Understanding this chain helps you distinguish expected bank fees from integration errors.
The conversion chain
When a US cardholder pays on a BRL merchant site, five steps determine the final statement amount:
| Step | Actor | What happens |
|---|---|---|
| 1 | A55 FX endpoint | Returns mid-market rate (for example, 5.50 BRL/USD) |
| 2 | Merchant | Charges R$ 550.00 in BRL (settlement currency) |
| 3 | Card network (Visa or Mastercard) | Converts R$ 550.00 to USD at their published daily rate — mid-market plus network markup (~1%) |
| 4 | Issuing bank | Adds foreign transaction fee (0–3%, most cards charge 1–2%) |
| 5 | Cardholder statement | Shows the final amount: approximately US$ 101–104 |
Fee breakdown by layer
| Layer | What it is | Who pays | Typical range |
|---|---|---|---|
| A55 FX quote | Mid-market interbank rate, no markup | — | 0% |
| Card network assessment | Visa ISA or Mastercard Cross-Border Assessment | Merchant (passed through processor) | 0.6–1.4% |
| Network currency conversion | Embedded in the card network's published exchange rate | Cardholder | ~1% |
| Issuer foreign transaction fee | Fee charged by the cardholder's bank for foreign currency transactions | Cardholder | 0–3% |
| Total impact on cardholder | Difference between mid-market rate and statement amount | Cardholder | ~1.6–4.4% |
Visa vs Mastercard fee comparison
| Visa | Mastercard | |
|---|---|---|
| Cross-border assessment (merchant side) | ISA: 1.00% (USD settlement) / 1.40% (non-USD) | 0.60% (USD settlement) / 1.00% (non-USD) |
| Conversion markup (cardholder side) | ~1% over mid-market | ~1% over mid-market |
| Rate application timing | Typically at settlement (1–2 business days after authorization) | At authorization (point of sale) |
| Published rate checker | Visa exchange rate calculator | Mastercard currency converter |
What you control vs what you cannot control
| You control | You cannot control |
|---|---|
| When to fetch the FX rate (freshness) | The card network's published exchange rate |
| The BRL amount submitted in the charge | The issuer's foreign transaction fee (0–3%) |
| The price and rate displayed to the customer | Whether the cardholder's card charges 0% or 3% FX fee |
| The audit trail stored for dispute defense | The conversion date the network applies |
Real-world scenarios
| Scenario | Expected statement amount | Risk |
|---|---|---|
| Fresh rate + card with 0% foreign fee | ~US$ 101.00 | None |
| Fresh rate + card with 2% foreign fee | ~US$ 103.00 | None |
| Fresh rate + card with 3% foreign fee | ~US$ 104.00 | None — within expected range |
| Stale rate (45 min old) + 2% foreign fee | ~US$ 106–108 | High — Mastercard 4834 chargeback |
The first three rows show expected, non-disputable variance caused by bank fees. The last row shows the stale-rate problem compounded by fees — this combination produces the chargebacks.
Conversion timing and weekends
Mastercard applies the exchange rate at the time of authorization (when the cardholder pays). Visa typically applies the rate at the time of settlement (1–2 business days later). This means:
- Mastercard charges: The rate applied is very close to the rate at the time of purchase.
- Visa charges: The rate may shift between authorization and settlement. In volatile markets, this creates additional variance.
For weekend transactions, both networks use the last available Friday rate (New York 5:00 PM EST) until Forex markets reopen Sunday evening. Weekend transactions carry higher risk of rate divergence if the market moves significantly over the 48-hour closure.
Rate freshness best practices
Leading payment platforms (Stripe, Wise, Adyen) distinguish between two uses of exchange rates. The A55 FX endpoint serves both, but the refresh strategy differs.
Display pricing — product pages and catalogs
When showing multi-currency prices on product listing pages or pricing pages, you can cache the rate server-side and refresh periodically.
| Recommendation | Details |
|---|---|
| Refresh frequency | Call the FX endpoint every 10–15 minutes |
| Caching | Cache the rate server-side to avoid an API call on every page load |
| Price disclaimer | Label prices as "approximate — final amount determined at checkout" |
Transactional checkout — creating actual charges
When the customer confirms payment and you create the charge, use the freshest rate possible to ensure the displayed amount matches the charged amount.
| Recommendation | Details |
|---|---|
| Refresh timing | Re-fetch the rate when the customer clicks "Confirm payment" |
| Display the rate | Show the original amount, exchange rate, and converted amount on the payment confirmation page |
| Record the rate | Store the rate in the charge description field and in your database |
| Amount consistency | Ensure the amount shown to the customer matches the installment_value exactly |
The exchange rate remains constant within the cache window (~17 minutes). If your customer spends time on the checkout page, re-fetch the rate before creating the charge. Using a stale rate may cause the charged amount to differ from the displayed amount.
How a stale rate causes chargebacks — a real scenario
If you are new to payments, this section explains why rate freshness matters. Here is a real chargeback scenario:
Scenario: Your product costs US$ 100.00. The customer pays with a USD credit card. Your wallet settlement currency is BRL.
| Time | Event | Rate | Amount |
|---|---|---|---|
| 10:00 AM | You fetch the rate and display the price | 1 USD = 5.73 BRL | Page shows "R$ 573.00" |
| 10:45 AM | Customer clicks "Confirm" — you create the charge using the 10:00 AM rate | Real rate is now 5.50 | You submit R$ 573.00 |
| 10:45 AM | Issuing bank converts R$ 573.00 back to USD at current rate | Issuer uses 5.50 | Statement shows US$ 104.18 |
| Next day | Cardholder checks statement | — | "I agreed to pay US$ 100.00, why was I charged US$ 104.18?" |
| 3 days later | Cardholder files dispute with issuer | — | Mastercard reason code 4834: "Transaction Amount Differs" |
What happened: You calculated the BRL amount using a 45-minute-old rate (5.73). But the cardholder's issuing bank converted BRL back to USD at the current rate (5.50), resulting in US$ 104.18 instead of US$ 100.00. The US$ 4.18 difference triggered the chargeback.
The fix: At the moment the customer clicks "Confirm" (10:45 AM), re-call the FX endpoint to get the current rate (5.50), calculate R$ 550.00, and submit R$ 550.00. The issuer converts back to approximately US$ 100.00 — the statement matches the displayed price, no dispute.
Even with a perfectly timed rate, the cardholder's issuing bank adds its own FX markup (typically 1–3% over mid-market). This is standard across Visa and Mastercard networks — the cardholder's bank agreement covers this markup. A charge that converts to exactly US$ 100.00 at mid-market may appear as US$ 101.00–103.00 on the statement. This small, expected variance rarely triggers disputes. The critical issue — and the one that causes Mastercard 4834 chargebacks — is the large discrepancy caused by stale rates, as shown in the scenario above.
Fetch the rate at the last possible moment before creating the charge. The shorter the gap between fetching the rate and creating the charge, the smaller the discrepancy on the cardholder's statement.
Why re-fetching at confirm is the best approach
Three strategies exist for preventing FX-related chargebacks in cross-border payments:
| Strategy | How it works | Available in A55 |
|---|---|---|
| Re-fetch at confirm | Get the freshest mid-market rate before creating the charge | Yes — call the FX endpoint at the last moment |
| DCC (Dynamic Currency Conversion) | Charge the cardholder in their own currency so the issuer does not convert | No — requires DCC certification and multi-currency settlement |
| Rate lock | Lock the quoted rate for a guaranteed period | No — the FX endpoint returns a real-time quote, not a locked rate |
Re-fetching at confirm minimizes rate drift (the largest source of chargebacks). Combined with transparent disclosure and record-keeping, this is the industry-standard approach used by Stripe, Adyen, and Wise for merchants who settle in a single currency.
Chargeback prevention checklist
For every cross-border charge, verify:
- You fetched a fresh rate immediately before creating the charge
- The payment confirmation page shows the original amount, exchange rate, and converted amount together
- The payment confirmation page includes a note: "Final amount on your card statement may vary slightly due to your bank's exchange rate"
- The rate and original amount are stored in the charge
descriptionfield - The
installment_valueexactly matches the amount displayed to the customer - Your database records: rate value, fetch timestamp, displayed amount, charged amount
- For zero-decimal currencies (CLP, COP), the amount is an integer
Use cases
| Scenario | How to use the FX endpoint |
|---|---|
| Cross-border checkout | Get the rate, display the converted price to the buyer before they confirm payment |
| Dynamic pricing | Update product prices on your website based on current exchange rates |
| Multi-currency dashboard | Convert all transaction values to USD or EUR for consolidated reporting |
| Financial reconciliation | Compare the rate at transaction time with the rate at settlement time |
| Subscription pricing | Show subscribers their monthly charge in their local currency |
| Invoice generation | Include the exchange rate and both currency amounts on invoices |
Important notes
For currencies with capital controls — particularly the Argentine Peso (ARS) — the mid-market rate may differ from official or parallel rates used locally. The rate returned reflects the international interbank market.
The FX endpoint returns the current interbank market rate for display and calculation. It does not lock the rate. The actual conversion applied to a charge depends on the acquirer at the time the transaction is processed. To minimize discrepancy, fetch a fresh quote immediately before creating the charge.
For every cross-border charge, record: the time you fetched the rate, the rate value, the amount displayed to the customer, and the amount charged. Include the rate in the charge description field (for example: "Order #12345 (US$ 100.00 at 5.73)") and persist the full record in your system. This data is essential for reconciliation and dispute resolution.