Enterprise IntegrationMarch 22, 20269 min read

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.

PatternHow It WorksBest ForRefresh Frequency
CSV / Flat File ImportExport rates to CSV, import via ERP file loaderLegacy ERPs, manual processes, low frequencyDaily or weekly
REST API PullERP script calls currency API on schedule, writes ratesNetSuite SuiteScript, SAP CPI, custom integrationsHourly or daily
Webhook / PushCurrency API pushes rate updates via webhook to middlewareReal-time pricing, transactional systemsReal-time or near-real-time
Decision guide

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.

JavaScript — NetSuite SuiteScript scheduled rate update
/**
 * @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.

ABAP — Conceptual rate import via BAPI
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.

TypeScript — Middleware: fetch rates and push to QuickBooks
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.

TypeScript — Generic rate sync middleware
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.

RequirementWhy It Matters for ERP
REST API with JSON responseEvery modern ERP can call REST endpoints and parse JSON natively
Per-rate timestampRequired for audit trail — proves when each rate was recorded
Sub-50ms response timeERP integrations often pull multiple pairs in sequence; slow responses cascade
99.9%+ uptime SLAA rate fetch failure during close blocks the entire period-end process
150+ currenciesCovers all active and dormant trading pairs without gaps
SOC 2 Type II, ISO 27001Standard security requirements for systems feeding financial data
Historical data endpointNeeded 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

150+ currencies · <50ms response · 99.9% uptime · SOC 2 Type II · REST API · JSON/XML