Multi-Currency ERP Integration: How to Connect Currency APIs to NetSuite, SAP, and Accounting Systems
Most ERPs ship with built-in currency data, but it's rarely sufficient. The rates update once daily at best. Historical depth is limited. You can't configure the source methodology or pull custom date ranges for audit. Finance teams end up maintaining rates manually in spreadsheets, which creates exactly the kind of inconsistency that auditors flag.
This post walks through connecting an external currency exchange API to your ERP — covering NetSuite, SAP, QuickBooks, and generic integration patterns. The focus is on the architecture and implementation, not on selling you on the concept. If you're already convinced you need better rate data, this is the how-to.
Three Integration Patterns
Every ERP accepts currency rates through one or more of these mechanisms. The right choice depends on your system, your refresh requirements, and how much control you need over the process.
| Pattern | How It Works | Best For | Refresh Frequency |
|---|---|---|---|
| CSV / Flat File Import | Export rates to CSV, import via ERP file loader | Legacy ERPs, manual processes, low frequency | Daily or weekly |
| REST API Pull | ERP script calls currency API on schedule, writes rates | NetSuite SuiteScript, SAP CPI, custom integrations | Hourly or daily |
| Webhook / Push | Currency API pushes rate updates via webhook to middleware | Real-time pricing, transactional systems | Real-time or near-real-time |
Most finance teams start with a REST API pull on a daily schedule. It's the most straightforward to implement and debug. Move to webhook-based push only if you need sub-hourly updates for live transaction processing.
NetSuite: SuiteScript Rate Updates
NetSuite's built-in exchange rate service updates once per day and lacks historical depth. For companies with multi-currency subsidiaries or frequent cross-border transactions, that's not enough. SuiteScript — NetSuite's server-side JavaScript runtime — lets you pull rates from an external API and write them directly into NetSuite's currency tables.
/**
* @NApiVersion 2.1
* @NScriptType ScheduledScript
* @NModuleScope SameAccount
*/
define(['N/http', 'N/currency', 'N/runtime', 'N/log'],
(http, currency, runtime, log) => {
const API_KEY = runtime.envVars.CURRENCY_API_KEY;
const API_URL = 'https://currency-exchange.app/api/v1-rates';
const CURRENCY_PAIRS = [
{ from: 'USD', to: 'EUR' },
{ from: 'USD', to: 'GBP' },
{ from: 'USD', to: 'JPY' },
{ from: 'USD', to: 'CAD' },
{ from: 'USD', to: 'AUD' },
];
function execute(context) {
// Fetch latest rates from the API
const response = http.request({
method: http.Method.GET,
url: `${API_URL}?base=USD`,
headers: {
'x-api-key': API_KEY,
'Content-Type': 'application/json',
},
});
if (response.code !== 200) {
log.error({
title: 'Rate fetch failed',
details: `Status: ${response.code}, Body: ${response.body}`,
});
return;
}
const rates = JSON.parse(response.body);
// Write each rate to NetSuite
for (const pair of CURRENCY_PAIRS) {
const rate = rates.rates?.[pair.to];
if (!rate) {
log.error({
title: 'Missing rate',
details: `No rate returned for ${pair.from}/${pair.to}`,
});
continue;
}
try {
currency.exchangeRate({
from: pair.from,
to: pair.to,
date: new Date(),
rate: rate,
});
log.audit({
title: 'Rate updated',
details: `${pair.from}/${pair.to} = ${rate}`,
});
} catch (e) {
log.error({
title: 'Rate write failed',
details: `${pair.from}/${pair.to}: ${e.message}`,
});
}
}
}
return { execute };
});Implementation notes:
- Store the API key in a NetSuite environment variable or credentials store — never hardcode it in the script.
- Schedule the script to run once per business day, ideally 1-2 hours before the close window starts. NetSuite's scheduling supports specific days and times.
- Add error handling that retries failed requests and alerts the finance team if a rate update doesn't complete.
- Log every successful rate update with the timestamp, rate value, and currency pair. These logs serve as your audit trail during period-end review.
SAP: ABAP Report and CPI Integration
SAP stores exchange rates in table TCURR, with fields for currency pair, rate type (e.g., M for average, B for buying), validity date, and rate value. There are two main approaches for pushing external rates into this table.
Approach 1: ABAP Report
For on-premise SAP systems, an ABAP report can call an external REST API using the CL_HTTP_CLIENT class, parse the JSON response, and write rates to TCURR using the BAPI_EXCHANGERATE_CREATE function module. This can be scheduled as a background job.
DATA: lv_from_curr TYPE tcurc-waers VALUE 'USD',
lv_to_curr TYPE tcurc-waers VALUE 'EUR',
lv_rate TYPE bapi1093_0-exch_rate,
lv_valid_from TYPE sy-datum,
lv_return TYPE bapiret2.
* Fetch rate from currency API via HTTP client
lv_rate = zcl_currency_api=>get_exchange_rate(
iv_from = lv_from_curr
iv_to = lv_to_curr
iv_date = sy-datum
).
* Write to SAP exchange rate table
CALL FUNCTION 'BAPI_EXCHANGERATE_CREATE'
EXPORTING
exch_rate = VALUE bapi1093_0(
rate_type = 'M'
from_curr = lv_from_curr
to_currty = lv_to_curr
valid_from = lv_valid_from
exch_rate = lv_rate
)
IMPORTING
return = lv_return.
IF lv_return-type CA 'EA'.
" Log error for review
ENDIF.Approach 2: SAP CPI (Cloud Platform Integration)
For SAP S/4HANA Cloud or companies using SAP Business Technology Platform, SAP CPI (now called Integration Suite) provides a no-code/low-code way to connect external APIs to SAP. The flow is: HTTP call to the currency API, JSON-to-XML transformation, mapping to the TCURR OData API, and scheduled execution via a timer event.
The CPI approach is preferred for cloud deployments because it avoids custom ABAP development and can be maintained by integration specialists without SAP development skills.
QuickBooks Online: API Middleware
QuickBooks Online has a built-in exchange rate service, but it's limited to daily updates with no historical depth and no control over the data source. For companies that need verifiable rates with audit trails, a middleware service that fetches rates from a dedicated provider and pushes them to QBO is the practical solution.
interface QboExchangeRate {
CurrencyCode: string;
RateValue: number;
AsOfDate: string;
}
async function syncRatesToQuickBooks(
currencyPairs: Array<{ from: string; to: string }>
): Promise<void> {
for (const pair of currencyPairs) {
// 1. Fetch rate from currency API
const rateResponse = await fetch(
'https://currency-exchange.app/api/v1-rates',
{
headers: {
'x-api-key': process.env.CURRENCY_API_KEY!,
},
}
);
const rateData = await rateResponse.json();
const rate = rateData.rates?.[pair.to];
if (!rate) continue;
// 2. Push to QuickBooks Online
const qboResponse = await fetch(
`https://quickbooks.api.intuit.com/v3/company/${process.env.QBO_REALM_ID}/exchangerate?currency=${pair.to}&asofdate=${new Date().toISOString().split('T')[0]}`,
{
method: 'PUT',
headers: {
'Authorization': `Bearer ${await getQboToken()}`,
'Content-Type': 'application/json',
'Accept': 'application/json',
},
body: JSON.stringify({
CurrencyCode: pair.to,
RateValue: rate,
AsOfDate: new Date().toISOString().split('T')[0],
} as QboExchangeRate),
}
);
if (!qboResponse.ok) {
console.error(
`QBO rate update failed for ${pair.to}: ${qboResponse.status}`
);
}
}
}Schedule this middleware to run daily before business hours. For QuickBooks Desktop, the integration follows a similar pattern but uses the QuickBooks Web Connector or the QBXML SDK instead of the REST API.
Generic Middleware Architecture
Regardless of which ERP you use, the integration architecture follows the same pattern. Building a middleware layer between the currency API and your ERP gives you validation, logging, and error handling in one place.
interface RateEntry {
from: string;
to: string;
rate: number;
timestamp: string;
source: string;
}
interface ValidationResult {
valid: boolean;
reason?: string;
}
function validateRate(
current: RateEntry,
previous: RateEntry | null,
maxChangePercent: number
): ValidationResult {
if (!previous) return { valid: true };
const changePercent =
Math.abs((current.rate - previous.rate) / previous.rate) * 100;
if (changePercent > maxChangePercent) {
return {
valid: false,
reason: 'Rate changed ' + changePercent.toFixed(2) + '% (' + previous.rate + ' to ' + current.rate + '), exceeds ' + maxChangePercent + '% threshold',
};
}
return { valid: true };
}
async function syncRates(
pairs: Array<{ from: string; to: string }>,
options: {
maxChangePercent: number;
onRateUpdate: (rate: RateEntry) => Promise<void>;
onValidationError: (
rate: RateEntry, result: ValidationResult
) => Promise<void>;
logger: {
info: (msg: string) => void;
warn: (msg: string) => void;
error: (msg: string) => void;
};
}
): Promise<void> {
const { maxChangePercent, onRateUpdate, onValidationError, logger } = options;
for (const pair of pairs) {
// Fetch current rate
const response = await fetch(
'https://currency-exchange.app/api/v1-rates',
{
headers: { 'x-api-key': process.env.CURRENCY_API_KEY! },
}
);
if (!response.ok) {
logger.error(`Fetch failed for ${pair.from}/${pair.to}`);
continue;
}
const data = await response.json();
const rate = data.rates?.[pair.to];
if (typeof rate !== 'number') {
logger.warn(`No rate data for ${pair.from}/${pair.to}`);
continue;
}
const entry: RateEntry = {
from: pair.from,
to: pair.to,
rate,
timestamp: new Date().toISOString(),
source: 'currency-exchange.app',
};
// Validate against previous rate (stored in DB)
const previous = await getPreviousRate(pair.from, pair.to);
const validation = validateRate(entry, previous, maxChangePercent);
if (!validation.valid) {
logger.warn(
`Validation failed for ${pair.from}/${pair.to}: ${validation.reason}`
);
await onValidationError(entry, validation);
continue; // Don't push unvalidated rates to ERP
}
// Push to ERP via configured adapter
await onRateUpdate(entry);
logger.info(
`Updated ${pair.from}/${pair.to}: ${rate} at ${entry.timestamp}`
);
// Store in rate history table
await storeRate(entry);
}
}This middleware pattern adapts to any ERP by swapping the onRateUpdate handler. The validation, logging, and error handling stay the same regardless of the target system.
Validation, Error Handling, and Audit Logging
Pushing rates into your ERP without validation is riskier than not having rates at all. A single bad rate can cascade through hundreds of transactions before anyone notices.
Rate change thresholding
Compare each incoming rate against the previous rate for the same pair. If the change exceeds a configurable threshold — 2% for stable pairs, 5% for emerging market currencies — flag it for review instead of pushing it directly to the ERP. This catches data errors and provider outages before they reach your financial statements.
Timestamp freshness check
The currency API returns a rateTime field. If this timestamp is older than your acceptable window (e.g., more than 24 hours for a daily update, more than 5 minutes for a real-time feed), reject the rate and alert the team. Stale rates are a common source of month-end discrepancies.
Full audit logging
Log every rate that enters your system: the currency pair, rate value, source API timestamp, your system's receive timestamp, the ERP write timestamp, and the outcome (success/failure/rejected). Store these logs in a queryable database, not just flat files. When auditors ask "what rate did we use for EUR balances in Q1?" you need to answer in seconds, not hours.
Graceful degradation
If the currency API is temporarily unavailable, fall back to the last known good rate — but log the fallback event and alert the team. Do not silently use stale data. When the API recovers, backfill the missing period automatically and flag any rates that changed significantly during the outage window.
What to Verify Before Integrating
Not every currency API works well for ERP integration. Here is a practical checklist based on what matters when you're pushing rates into a financial system of record.
| Requirement | Why It Matters for ERP |
|---|---|
| REST API with JSON response | Every modern ERP can call REST endpoints and parse JSON natively |
| Per-rate timestamp | Required for audit trail — proves when each rate was recorded |
| Sub-50ms response time | ERP integrations often pull multiple pairs in sequence; slow responses cascade |
| 99.9%+ uptime SLA | A rate fetch failure during close blocks the entire period-end process |
| 150+ currencies | Covers all active and dormant trading pairs without gaps |
| SOC 2 Type II, ISO 27001 | Standard security requirements for systems feeding financial data |
| Historical data endpoint | Needed for restating prior periods, audit support, and backfill operations |
For detailed evaluation criteria, see the currency API evaluation checklist.
Frequently Asked Questions
How do I push exchange rates into NetSuite?
NetSuite supports currency rate updates through SuiteScript (server-side JavaScript), CSV import via the Import Assistant, or REST API calls. The most reliable approach for automated daily updates is a SuiteScript scheduled script that calls the currency API, parses the response, and writes rates using the N/currency module. Set the script to run once per business day before the close window.
Can SAP pull exchange rates from a REST API automatically?
Yes. SAP supports custom exchange rate feeds through several mechanisms. The most common approach is an ABAP report or SAP CPI (Cloud Platform Integration) flow that calls an external REST API, transforms the JSON response, and writes rates to SAP table TCURR using BAPI_EXCHANGERATE_CREATE. This can be scheduled as a background job running daily or on demand.
How do I handle rate updates for QuickBooks Online?
QuickBooks Online has a built-in exchange rate service, but it updates daily and lacks historical depth. For more control, use the QuickBooks Online API to retrieve or set exchange rates via the ExchangeRate endpoint. Build a middleware service that fetches rates from your currency API provider and pushes them to QuickBooks using OAuth 2.0 authentication.
What happens if my ERP receives a stale or incorrect exchange rate?
A stale or incorrect rate can cause misstated financials, incorrect revenue recognition, and audit findings. Implement validation at the integration layer: compare incoming rates against previous values, reject rates with timestamps older than your threshold, and flag anomalous changes. Build a manual override process for edge cases, and log every rate update for the audit trail. Most finance teams set a 1-2% day-over-day change threshold for stable currency pairs.
Start Integrating Currency Data Into Your ERP
REST API, JSON responses, sub-50ms latency, 150+ currencies, and SOC 2 Type II security. Test the endpoints before writing a single line of integration code.
Related Resources
FX Data Automation Workflows
Google Sheets, Excel, n8n, and AI agent integration
Historical FX Data for Finance Teams
Reporting, auditing, and reconciliation with historical rates
Currency API Evaluation Checklist 2026
27-criteria procurement framework
Multi-Currency Quote-to-Cash Handbook
End-to-end FX operations for finance teams