How to Integrate RBA Exchange Rates into SAP, Xero, NetSuite and Dynamics 365 - Complete ERP Guide

Step-by-step integration guides for loading official Reserve Bank of Australia exchange rates into SAP S/4HANA, SAP Business One, Xero, Oracle NetSuite, Microsoft Dynamics 365 Business Central, Odoo and Excel. Includes working code, rate-direction cheat sheets and scheduling advice.

Most Australian finance teams don't need a currency converter. They need the RBA's daily rates inside the ERP, where invoices get posted, foreign bills get revalued and month-end gets closed. Usually that means someone copies numbers off the RBA website into SAP or NetSuite every afternoon, or the ERP quietly uses a commercial rate feed that doesn't match what the auditors expect.

This guide shows how to automate that step. We'll cover the common pattern first, then give working integration code for the systems Australian businesses use most:

  • SAP S/4HANA and SAP ECC: ABAP report + BAPI_EXCHANGERATE_CREATE
  • SAP Business One: Service Layer
  • Xero: per-transaction CurrencyRate
  • Oracle NetSuite: SuiteScript scheduled script
  • Microsoft Dynamics 365 Business Central: AL codeunit + Job Queue
  • Odoo: XML-RPC
  • Excel and Power BI: Power Query, no code outside the workbook

New to the API? Start with the Python or JavaScript tutorials for the basics, then come back here to wire it into your ERP.

The Integration Pattern

Every ERP integration in this guide follows the same three steps:

  1. Fetch the RBA rates once per business day, after they publish (around 4 PM AEST).
  2. Transform each rate into the direction and format the ERP expects.
  3. Load them into the ERP's exchange rate table with the RBA publication date as the effective date.
┌──────────────────────┐      ┌──────────────────────┐      ┌──────────────────────┐
│ Exchange Rates API   │      │ Your integration     │      │ ERP                  │
│ GET /latest          │ ───▶ │ • invert if needed   │ ───▶ │ Exchange rate table  │
│ (or webhook trigger) │      │ • map currency codes │      │ (rate type, date)    │
└──────────────────────┘      │ • skip TWI           │      └──────────────────────┘
                              └──────────────────────┘

Step 2 is where most integrations go wrong, so let's look at that before writing any code.

What the API returns

Every authenticated endpoint uses a Bearer token and returns rates as units of foreign currency per 1 AUD:

curl "https://api.exchangeratesapi.com.au/latest?symbols=USD,EUR,JPY" \
  -H "Authorization: Bearer your_api_key_here"
{
  "success": true,
  "timestamp": 1725080400,
  "base": "AUD",
  "date": "2025-08-31",
  "rates": {
    "USD": 0.643512,
    "EUR": 0.562934,
    "JPY": 96.8321
  }
}

So "USD": 0.643512 means 1 AUD = 0.643512 USD. That's how the RBA publishes it, but ERPs don't agree on a convention.

Rate direction cheat sheet

System How it stores the rate What to load from the API
Xero 1 AUD = x foreign rate as-is
Odoo Foreign units per 1 company currency rate as-is
Dynamics 365 Business Central Exchange Rate Amount (foreign) / Relational Amount (AUD) Amount = rate, Relational = 1
NetSuite Base currency per 1 foreign unit 1 / rate
SAP S/4HANA / ECC (direct quotation) AUD per n units of foreign, per TCURF ratios n / rate
SAP Business One (direct rate setting) Local currency per 1 foreign unit 1 / rate

These assume AUD is your local/base currency, which is the normal case for an Australian entity. If your company base is something else, you'll need to cross-rate through AUD (e.g. USD→EUR = rates.EUR / rates.USD).

Currency codes that need special handling

The API returns 21 series from the RBA feed. Two of them aren't normal currencies:

  • TWI (Trade-Weighted Index) is an index, not a currency. Always skip it when loading an ERP.
  • SDR (IMF Special Drawing Rights) has the ISO 4217 code XDR. Most ERPs don't have it set up. Map it to XDR if you need it, otherwise skip it.

Everything else (USD, EUR, JPY, CNY, GBP and so on) uses standard ISO codes. You can get the full list from the public /symbols endpoint.

Weekends and public holidays

The RBA doesn't publish on weekends or Australian public holidays. A request to GET /{date} for one of those days returns 404. Most ERPs deal with this for you by using the most recent rate on or before the posting date, so you only need to load the days the RBA actually publishes. If you need to look up a rate for a specific transaction date (the Xero example below does this), ask the /timeseries endpoint for a few days before it and take the latest one.

When to run the job

The RBA publishes around 4 PM AEST. Schedule your ERP job for 5 PM Sydney time or later on business days. Or, on Professional and above, use a rate-published webhook to trigger the load as soon as new rates arrive instead of guessing at the time.


SAP S/4HANA and ECC

In SAP, exchange rates live in table TCURR, keyed by exchange rate type (KURST), from-currency, to-currency and valid-from date. The standard rate type is M. People usually maintain it by hand in transaction OB08, which is exactly the job we want to automate.

The cleanest on-premise approach is a small ABAP report that calls the API and posts each rate through the standard BAPI_EXCHANGERATE_CREATE, scheduled daily as a background job in SM36.

1. Create an HTTP destination

In SM59, create a type G (HTTP to external server) destination called ZRBA_FX_API:

  • Host: api.exchangeratesapi.com.au
  • Port: 443, SSL active (import the certificate chain in STRUST if needed)
  • Path prefix: leave empty

Keep the API key out of source code. Store it in a secure store your Basis team already uses (SSF, a restricted customising table or the BTP credential store).

2. The import report

REPORT zfx_rba_rates_import.

PARAMETERS: p_kurst TYPE kurst_curr DEFAULT 'M',
            p_test  AS CHECKBOX DEFAULT abap_true.

TYPES: BEGIN OF ty_rates,
         usd TYPE decfloat34, cny TYPE decfloat34, jpy TYPE decfloat34,
         eur TYPE decfloat34, krw TYPE decfloat34, gbp TYPE decfloat34,
         sgd TYPE decfloat34, inr TYPE decfloat34, thb TYPE decfloat34,
         nzd TYPE decfloat34, twd TYPE decfloat34, myr TYPE decfloat34,
         idr TYPE decfloat34, vnd TYPE decfloat34, cad TYPE decfloat34,
         hkd TYPE decfloat34, chf TYPE decfloat34, php TYPE decfloat34,
       END OF ty_rates,
       BEGIN OF ty_response,
         success TYPE abap_bool,
         date    TYPE string,
         base    TYPE string,
         rates   TYPE ty_rates,
       END OF ty_response.

DATA: lo_client   TYPE REF TO if_http_client,
      ls_response TYPE ty_response,
      ls_return   TYPE bapiret2.

START-OF-SELECTION.

  " 1. Call GET /latest via the SM59 destination
  cl_http_client=>create_by_destination(
    EXPORTING  destination = 'ZRBA_FX_API'
    IMPORTING  client      = lo_client
    EXCEPTIONS OTHERS      = 1 ).
  IF sy-subrc <> 0.
    MESSAGE 'HTTP destination ZRBA_FX_API not available' TYPE 'E'.
  ENDIF.

  cl_http_utility=>set_request_uri( request = lo_client->request
                                    uri     = '/latest' ).
  lo_client->request->set_header_field(
    name  = 'Authorization'
    value = |Bearer { zcl_fx_secrets=>get_rba_api_key( ) }| ). " your secure store

  lo_client->send( EXCEPTIONS OTHERS = 1 ).
  lo_client->receive( EXCEPTIONS OTHERS = 2 ).
  IF sy-subrc <> 0.
    MESSAGE 'Exchange Rates API call failed' TYPE 'E'.
  ENDIF.

  lo_client->response->get_status( IMPORTING code = DATA(lv_status) ).
  IF lv_status <> 200.
    MESSAGE |Exchange Rates API returned HTTP { lv_status }| TYPE 'E'.
  ENDIF.

  " 2. Parse JSON into the typed structure
  /ui2/cl_json=>deserialize(
    EXPORTING json = lo_client->response->get_cdata( )
    CHANGING  data = ls_response ).

  " RBA publication date 'YYYY-MM-DD' -> SAP date
  DATA(lv_valid_from) = CONV datum(
    replace( val = ls_response-date sub = '-' with = `` occ = 0 ) ).

  " 3. Post one TCURR entry per currency (TWI and SDR deliberately excluded)
  DATA(lt_currencies) = VALUE string_table(
    ( `USD` ) ( `CNY` ) ( `JPY` ) ( `EUR` ) ( `KRW` ) ( `GBP` )
    ( `SGD` ) ( `INR` ) ( `THB` ) ( `NZD` ) ( `TWD` ) ( `MYR` )
    ( `IDR` ) ( `VND` ) ( `CAD` ) ( `HKD` ) ( `CHF` ) ( `PHP` ) ).

  LOOP AT lt_currencies INTO DATA(lv_currency).
    ASSIGN COMPONENT lv_currency OF STRUCTURE ls_response-rates
      TO FIELD-SYMBOL(<lv_rate>).
    IF sy-subrc <> 0 OR <lv_rate> IS INITIAL.
      CONTINUE.
    ENDIF.

    " Translation ratio: MUST match TCURF for this rate type / pair.
    " These are common examples - check yours in OBBS.
    DATA(lv_factor) = SWITCH i( lv_currency
                        WHEN `JPY` OR `KRW` OR `INR` OR `THB` OR `PHP` OR `TWD` THEN 100
                        WHEN `IDR` OR `VND` THEN 1000
                        ELSE 1 ).

    " Direct quotation: AUD per <factor> units of foreign currency
    DATA(ls_rate) = VALUE bapi1093_0(
      rate_type   = p_kurst
      from_curr   = lv_currency
      to_currncy  = 'AUD'
      valid_from  = lv_valid_from
      exch_rate   = lv_factor / <lv_rate>
      from_factor = lv_factor
      to_factor   = 1 ).

    IF p_test = abap_true.
      WRITE: / lv_currency, lv_valid_from, ls_rate-exch_rate, '(test run)'.
      CONTINUE.
    ENDIF.

    CALL FUNCTION 'BAPI_EXCHANGERATE_CREATE'
      EXPORTING
        exch_rate = ls_rate
        upd_allow = abap_true   " overwrite if a rate already exists for this date
      IMPORTING
        return    = ls_return.

    IF ls_return-type CA 'EA'.
      WRITE: / lv_currency, 'ERROR:', ls_return-message.
      CALL FUNCTION 'BAPI_TRANSACTION_ROLLBACK'.
    ELSE.
      CALL FUNCTION 'BAPI_TRANSACTION_COMMIT' EXPORTING wait = abap_true.
      WRITE: / lv_currency, lv_valid_from, ls_rate-exch_rate, 'posted'.
    ENDIF.
  ENDLOOP.

3. Schedule it

Create a background job in SM36 that runs ZFX_RBA_RATES_IMPORT with a variant where p_test is unticked. Schedule it daily at 17:00 Australia/Sydney. Check the job's spool output when you first go live, then compare a few rates in OB08 against the RBA's published table.

Things SAP teams ask about

  • Why invert? Most Australian SAP systems keep rate type M as a direct quotation (AUD per unit of foreign currency), and that's what the report posts. If your system uses indirect quotation for these pairs, post the API rate unchanged instead. Your FI consultant will know which convention you use.
  • Translation ratios (TCURF). The from_factor/to_factor you post must match the ratios in TCURF, or the BAPI rejects the entry. Ratios like 100 JPY or 1000 IDR also stop small rates losing precision in the 5-decimal EXCH_RATE field.
  • A separate rate type. Some companies keep RBA rates in their own rate type (e.g. ZRBA) so they can use them alongside a bank or treasury feed, then point the relevant company codes or valuation methods at it.
  • S/4HANA Cloud (public edition). You can't deploy classic ABAP reports there. Build the same flow in SAP Integration Suite instead: a timer-started iFlow that calls the API, maps the JSON and posts to the released exchange rate API for your release (look up "exchange rates" in the SAP Business Accelerator Hub). The transformation rules above still apply.

SAP Business One

SAP Business One stores daily rates in its Exchange Rates and Indexes table. Through the Service Layer you can set them with the SBOBobService_SetCurrencyRate action. Here's a Python job that runs on any server that can reach both the API and your B1 Service Layer:

import requests

RBA_API = "https://api.exchangeratesapi.com.au"
RBA_KEY = "your_api_key_here"

B1_URL = "https://your-b1-server:50000/b1s/v1"
B1_LOGIN = {"CompanyDB": "SBODEMOAU", "UserName": "manager", "Password": "..."}

# B1 currency codes are user-defined - map ISO codes to the codes in your company
CURRENCY_MAP = {"USD": "USD", "EUR": "EUR", "GBP": "GBP", "NZD": "NZD", "JPY": "JPY"}


def fetch_rba_rates():
    response = requests.get(
        f"{RBA_API}/latest",
        headers={"Authorization": f"Bearer {RBA_KEY}"},
        params={"symbols": ",".join(CURRENCY_MAP.keys())},
        timeout=30,
    )
    response.raise_for_status()
    return response.json()


def load_into_b1(data):
    session = requests.Session()
    session.verify = True  # point at your CA bundle if B1 uses an internal cert
    session.post(f"{B1_URL}/Login", json=B1_LOGIN).raise_for_status()

    rate_date = data["date"].replace("-", "")  # YYYYMMDD

    try:
        for iso_code, rate in data["rates"].items():
            b1_code = CURRENCY_MAP.get(iso_code)
            if not b1_code:
                continue

            # Direct rate setting: local currency (AUD) per 1 unit of foreign
            direct_rate = round(1 / rate, 6)

            session.post(
                f"{B1_URL}/SBOBobService_SetCurrencyRate",
                json={"Currency": b1_code, "Rate": str(direct_rate), "RateDate": rate_date},
            ).raise_for_status()
            print(f"{b1_code} {rate_date}: {direct_rate}")
    finally:
        session.post(f"{B1_URL}/Logout")


if __name__ == "__main__":
    load_into_b1(fetch_rba_rates())

If your company is set to indirect rates (Administration → System Initialization → Company Details → Basic Initialization), post rate without inverting it. Schedule the script with cron or Windows Task Scheduler for 5 PM Sydney time.


Xero

Xero behaves differently from the others. It doesn't let you overwrite its organisation-wide daily rate table (that comes from XE.com). What it does let you do is set the CurrencyRate on each multi-currency transaction (invoices, bills, bank transactions and so on) through the Xero Accounting API. If you leave CurrencyRate out, Xero uses its own rate.

So in Xero, the way to use RBA rates is to set the rate on each transaction as you create it. Handily, Xero expresses rates as 1 AUD = x foreign, the same direction as the API, so you can pass the value straight through.

Here's a Node.js example that creates a USD sales invoice at the RBA rate for the invoice date:

const RBA_API = 'https://api.exchangeratesapi.com.au';
const RBA_KEY = process.env.RBA_API_KEY;

// Most recent RBA rate on or before `date` (handles weekends & public holidays)
async function rbaRateOnOrBefore(currency, date) {
  const end = new Date(`${date}T00:00:00Z`);
  const start = new Date(end);
  start.setUTCDate(start.getUTCDate() - 6); // 7-day window covers any long weekend

  const params = new URLSearchParams({
    start_date: start.toISOString().slice(0, 10),
    end_date: date,
    symbols: currency,
  });

  const res = await fetch(`${RBA_API}/timeseries?${params}`, {
    headers: { Authorization: `Bearer ${RBA_KEY}` },
  });
  if (!res.ok) throw new Error(`Exchange Rates API returned ${res.status}`);

  const { rates } = await res.json();
  const latestDate = Object.keys(rates).sort().pop();
  if (!latestDate) throw new Error(`No RBA rate for ${currency} near ${date}`);

  return { rate: rates[latestDate][currency], rbaDate: latestDate };
}

async function createInvoiceAtRbaRate({ accessToken, tenantId, contactId, date, dueDate, lineItems }) {
  const { rate, rbaDate } = await rbaRateOnOrBefore('USD', date);

  const res = await fetch('https://api.xero.com/api.xro/2.0/Invoices', {
    method: 'POST',
    headers: {
      Authorization: `Bearer ${accessToken}`, // Xero OAuth 2.0 token
      'xero-tenant-id': tenantId,
      'Content-Type': 'application/json',
      Accept: 'application/json',
    },
    body: JSON.stringify({
      Invoices: [
        {
          Type: 'ACCREC',
          Contact: { ContactID: contactId },
          Date: date,
          DueDate: dueDate,
          CurrencyCode: 'USD',
          CurrencyRate: rate, // 1 AUD = rate USD, same direction as Xero
          Reference: `RBA rate ${rbaDate}`, // audit trail
          LineItems: lineItems,
          Status: 'DRAFT',
        },
      ],
    }),
  });

  if (!res.ok) throw new Error(`Xero returned ${res.status}: ${await res.text()}`);
  return res.json();
}

A few practical notes:

  • Leave an audit trail. Putting the RBA date in the reference (or a tracking field) lets your accountant tie each rate back to the RBA table.
  • Bills too. The same CurrencyRate field works on ACCPAY invoices, bank transactions and credit notes.
  • Plan limits. A 7-day /timeseries window works on Starter. If you're backfilling older transactions, you'll need Professional or above for history beyond the last 30 days.
  • Check it once. Create one test invoice in a demo organisation and confirm the rate in the Xero UI shows as 1 AUD = 0.6435 USD (or whatever the current rate is) before rolling out.

Oracle NetSuite

NetSuite stores rates as Currency Exchange Rate records and applies the most recent effective rate to each transaction. A SuiteScript 2.1 scheduled script can fetch the RBA rates and create those records directly.

NetSuite quotes rates as base currency per 1 unit of transaction currency, so with AUD as the base currency you load 1 / rate.

/**
 * @NApiVersion 2.1
 * @NScriptType ScheduledScript
 * @description Loads daily RBA exchange rates into NetSuite currency exchange rates.
 */
define(['N/https', 'N/record', 'N/search', 'N/runtime', 'N/log'], (https, record, search, runtime, log) => {
  const API_URL = 'https://api.exchangeratesapi.com.au/latest';
  const SKIP = new Set(['TWI', 'SDR']);

  // Map ISO code -> NetSuite currency internal ID
  const getCurrencyIds = () => {
    const ids = {};
    search
      .create({ type: search.Type.CURRENCY, columns: ['symbol'] })
      .run()
      .each((result) => {
        ids[result.getValue('symbol')] = result.id;
        return true;
      });
    return ids;
  };

  const execute = () => {
    const apiKey = runtime.getCurrentScript().getParameter({ name: 'custscript_rba_api_key' });
    // In production, prefer an API Secret: https.createSecureString({ input: '{custsecret_rba_api_key}' })

    const response = https.get({
      url: API_URL,
      headers: { Authorization: `Bearer ${apiKey}`, Accept: 'application/json' },
    });
    if (response.code !== 200) {
      throw new Error(`Exchange Rates API returned ${response.code}: ${response.body}`);
    }

    const data = JSON.parse(response.body);
    const [year, month, day] = data.date.split('-').map(Number);
    const effectiveDate = new Date(year, month - 1, day); // local date, no UTC shift

    const currencyIds = getCurrencyIds();
    const audId = currencyIds.AUD;

    Object.entries(data.rates).forEach(([code, rate]) => {
      if (SKIP.has(code) || !currencyIds[code] || !rate) return;

      const fxRecord = record.create({ type: record.Type.CURRENCY_RATE });
      fxRecord.setValue({ fieldId: 'basecurrency', value: audId });
      fxRecord.setValue({ fieldId: 'transactioncurrency', value: currencyIds[code] });
      fxRecord.setValue({ fieldId: 'exchangerate', value: Number((1 / rate).toFixed(8)) });
      fxRecord.setValue({ fieldId: 'effectivedate', value: effectiveDate });
      const id = fxRecord.save();

      log.audit('RBA rate loaded', `${code} ${data.date}: ${(1 / rate).toFixed(8)} (record ${id})`);
    });
  };

  return { execute };
});

Deployment:

  1. Upload the script, create a Script record and add a custscript_rba_api_key parameter (Password type), or use API Secrets.
  2. Create a deployment scheduled daily at 5:00 PM with the time zone set to Australia/Sydney.
  3. If Currency Exchange Rate Integration is enabled (Setup → Accounting → Accounting Preferences), make sure its automatic update won't overwrite your RBA rates. Either turn it off or run your script after it.
  4. For OneWorld accounts with more than one base currency, repeat the loop for each subsidiary base currency, using AUD cross-rates where needed.

Field IDs can vary slightly between accounts and features, so check them in the Records Browser for the Currency Exchange Rate record before you deploy.

If you'd rather not write SuiteScript, NetSuite's CSV Import also supports currency exchange rates. Generate a daily CSV from the API (see the Python pattern in the SAP Business One section) and import it with a saved import map.


Microsoft Dynamics 365 Business Central

Business Central stores rates in the Currency Exchange Rate table (one row per currency and starting date). It uses a pair of amounts rather than a single rate, which makes the RBA's format easy to load: Exchange Rate Amount in foreign currency equals Relational Exch. Rate Amount in local currency.

With AUD as local currency, 1 AUD = 0.643512 USD becomes Exchange Rate Amount 0.643512 and Relational 1. You don't have to invert anything.

Here's an AL codeunit you can add in a per-tenant extension and run from the Job Queue:

codeunit 50100 "RBA FX Rate Import"
{
    trigger OnRun()
    begin
        ImportLatestRates();
    end;

    procedure ImportLatestRates()
    var
        Client: HttpClient;
        Response: HttpResponseMessage;
        Body: Text;
        Root: JsonObject;
        Token: JsonToken;
        Rates: JsonObject;
        CurrencyCode: Text;
        StartingDate: Date;
    begin
        Client.DefaultRequestHeaders.Add('Authorization', 'Bearer ' + GetApiKey());
        if not Client.Get('https://api.exchangeratesapi.com.au/latest', Response) then
            Error('Could not reach the Exchange Rates API.');
        if not Response.IsSuccessStatusCode() then
            Error('Exchange Rates API returned status %1.', Response.HttpStatusCode());

        Response.Content().ReadAs(Body);
        Root.ReadFrom(Body);

        Root.Get('date', Token);
        Evaluate(StartingDate, Token.AsValue().AsText(), 9); // 9 = XML format, YYYY-MM-DD

        Root.Get('rates', Token);
        Rates := Token.AsObject();

        foreach CurrencyCode in Rates.Keys() do begin
            Rates.Get(CurrencyCode, Token);
            UpsertRate(CopyStr(CurrencyCode, 1, 10), StartingDate, Token.AsValue().AsDecimal());
        end;
    end;

    local procedure UpsertRate(CurrencyCode: Code[10]; StartingDate: Date; RateFromAud: Decimal)
    var
        Currency: Record Currency;
        ExchRate: Record "Currency Exchange Rate";
    begin
        // Skips TWI, SDR and any currency not set up in Business Central
        if not Currency.Get(CurrencyCode) then
            exit;

        if not ExchRate.Get(CurrencyCode, StartingDate) then begin
            ExchRate.Init();
            ExchRate."Currency Code" := CurrencyCode;
            ExchRate."Starting Date" := StartingDate;
            ExchRate.Insert(true);
        end;

        // 1 AUD (LCY) = RateFromAud units of foreign currency
        ExchRate.Validate("Exchange Rate Amount", RateFromAud);
        ExchRate.Validate("Relational Exch. Rate Amount", 1);
        ExchRate.Validate("Adjustment Exch. Rate Amount", RateFromAud);
        ExchRate.Validate("Relational Adjmt Exch Rate Amt", 1);
        ExchRate.Modify(true);
    end;

    local procedure GetApiKey(): Text
    var
        ApiKey: Text;
    begin
        if not IsolatedStorage.Get('RBA_API_KEY', DataScope::Company, ApiKey) then
            Error('Set the RBA API key in isolated storage first.');
        exit(ApiKey);
    end;
}

Scheduling: create a Job Queue Entry with Object Type to Run = Codeunit, Object ID = 50100, recurring on weekdays with a start time of 17:00. Set the adjustment amounts too (as above) so Adjust Exchange Rates revalues open balances with the same RBA rate at month-end.

Business Central also has a built-in Currency Exchange Rate Service page for simple feeds. The AL route gives you more control over authentication, error handling and which currencies get loaded.


Odoo

Odoo stores rates in the res.currency.rate model as units of currency per 1 company currency, the same direction as the API. This Python script uses Odoo's external XML-RPC API:

import xmlrpc.client
import requests

RBA_KEY = "your_api_key_here"
ODOO_URL, ODOO_DB = "https://yourcompany.odoo.com", "yourcompany"
ODOO_USER, ODOO_API_KEY = "finance@yourcompany.com.au", "odoo_api_key"

data = requests.get(
    "https://api.exchangeratesapi.com.au/latest",
    headers={"Authorization": f"Bearer {RBA_KEY}"},
    timeout=30,
).json()

common = xmlrpc.client.ServerProxy(f"{ODOO_URL}/xmlrpc/2/common")
uid = common.authenticate(ODOO_DB, ODOO_USER, ODOO_API_KEY, {})
models = xmlrpc.client.ServerProxy(f"{ODOO_URL}/xmlrpc/2/object")

def call(model, method, *args, **kwargs):
    return models.execute_kw(ODOO_DB, uid, ODOO_API_KEY, model, method, list(args), kwargs)

for code, rate in data["rates"].items():
    if code in ("TWI", "SDR"):
        continue
    currency_ids = call("res.currency", "search", [["name", "=", code]])
    if not currency_ids:
        continue

    existing = call("res.currency.rate", "search",
                    [["currency_id", "=", currency_ids[0]], ["name", "=", data["date"]]])
    values = {"currency_id": currency_ids[0], "name": data["date"], "rate": rate}

    if existing:
        call("res.currency.rate", "write", existing, values)
    else:
        call("res.currency.rate", "create", values)
    print(f"{code} {data['date']}: {rate}")

Turn off Odoo's Automatic Currency Rates setting (Accounting → Settings → Currencies) so it doesn't overwrite your RBA rates with another provider's. In multi-company databases, add company_id to the search and the values.


Excel and Power BI (Power Query)

Not every team needs a full ERP integration. For month-end workpapers, FX revaluation schedules or Power BI dashboards, Power Query can pull rates straight from the API with no code outside the workbook.

In Excel: Data → Get Data → From Other Sources → Blank Query, open the Advanced Editor, and paste:

let
    ApiKey    = "your_api_key_here",
    StartDate = "2025-08-01",
    EndDate   = "2025-08-31",
    Symbols   = "USD,EUR,GBP,NZD,JPY",

    Source = Json.Document(
        Web.Contents(
            "https://api.exchangeratesapi.com.au",
            [
                RelativePath = "timeseries",
                Query = [start_date = StartDate, end_date = EndDate, symbols = Symbols],
                Headers = [Authorization = "Bearer " & ApiKey]
            ]
        )
    ),

    AsTable  = Record.ToTable(Source[rates]),
    Expanded = Table.ExpandRecordColumn(AsTable, "Value", Text.Split(Symbols, ",")),
    Renamed  = Table.RenameColumns(Expanded, {{"Name", "Date"}}),
    Typed    = Table.TransformColumnTypes(
                   Renamed,
                   {{"Date", type date}} &
                   List.Transform(Text.Split(Symbols, ","), each {_, type number})
               )
in
    Typed

When Excel asks for credentials, choose Anonymous, because the API key goes in the header. You'll get one row per RBA business day and one column per currency, ready for XLOOKUP against your transaction dates. A 31-day range needs Professional or above; on Starter, /timeseries is limited to 7-day windows.

For a single month-end rate, swap the source for RelativePath = "latest" (or a specific date such as RelativePath = "2025-08-29") and expand Source[rates] the same way.


Production Checklist

Before you switch any of these on in production:

  • Idempotency. Every example above upserts by currency and date, so a job that runs twice won't create duplicates. Keep it that way.
  • Only publication dates. Load rates with the RBA's date from the response, not "today". On a public holiday the API still returns the last published rates, and stamping them with today's date would misstate your audit trail.
  • Alerting. If the job fails, someone should know before month-end. Send the job log to email or Teams, and check the API's /status endpoint when troubleshooting.
  • Secrets. Keep API keys in each platform's secret store (SSF, API Secrets, IsolatedStorage, environment variables), never in source code.
  • Backfill. When you first go live, load historical rates for open periods using /timeseries. Full history back to 2018 is on Professional and above.
  • Tell your auditors. Record in your accounting policy that FX rates are the RBA's 4 PM AEST rates, loaded automatically. The ATO accepts RBA rates for foreign currency translation, and a documented, automated source is easier to defend than hand-typed numbers.

Which Plan Do You Need?

Integration style Typical usage Suggested plan
Daily ERP rate load (SAP, NetSuite, BC, Odoo) ~22 requests/month Starter. Plenty of headroom
Per-transaction rates (Xero invoices, e-commerce) Hundreds to thousands/month Starter or Professional
Backfilling history, month-end revaluation of old periods Full history needed Professional (history back to 2018 + webhooks)
Multiple entities, many ERPs, CSV/Excel exports High volume, multiple API keys Business

A single daily ERP load uses about 22 requests a month, so most companies can automate their whole rate process on Starter. Upgrade to Professional when you need history beyond 30 days or want webhooks to trigger the load as soon as rates publish.

Wrapping Up

Getting RBA rates into your ERP comes down to fetching once a day, getting the direction right and loading with the RBA date. Once it's automated, month-end stops depending on someone remembering to update OB08 at 4:15 PM.

If you're still weighing whether to build a scraper instead, read about the hidden costs of scraping RBA data first. And if you're not sure whether RBA rates are the right source for your business, see RBA official rates vs forex market rates.

Ready to automate your FX rates? Get your free API key and try the integration in a sandbox company today.


Data sourced from the Reserve Bank of Australia. We are not affiliated with or endorsed by the Reserve Bank of Australia.

SAP, Xero, NetSuite, Microsoft Dynamics 365, Odoo and Excel are trademarks of their respective owners. Code samples are provided as starting points. Test them in a sandbox or non-production environment and review them with your ERP administrator before using them in production.

For API documentation, visit Exchange Rates API Docs