Get an exchange rate on a past date

On 30 September 2026 one US dollar bought 17,891.1 Indonesian rupiah. Ask the observations endpoint for the base currency, the quote currency and the date, and read the rate from that day's row.

Get the dated rate with the API

  1. Choose the currencies and date Put the base currency in the observations path and the quote currency in currencies; for USD to IDR, use USD in the path and IDR in the parameter. Set both start_date and end_date to 2026-09-30 to ask for the observation on 30 September 2026, with the same date at each end of the range.
  2. Set your API key Get a free key and keep it in the environment variable NARWHAL_API_KEY. The script reads that variable for the Authorization: Bearer YOUR_KEY header, so put your key in the environment before making the request.
  3. Request the observation Use the observations endpoint with the date parameters and authorization header shown here. The script reads the response's observations array and prints each returned day's date alongside its rates array, where the quote currency and its rate appear together.
    Python, USD to IDR on a past date
    import os
    import requests
    
    r = requests.get("https://api.narwhalapi.com/v1/fx/rates/USD/observations",
                     params={"currencies": "IDR", "start_date": "2026-09-30", "end_date": "2026-09-30"},
                     headers={"Authorization": f"Bearer {os.environ['NARWHAL_API_KEY']}"}, timeout=10)
    r.raise_for_status()
    for day in r.json()["observations"]:
        print(day["date"], day["rates"])
  4. Read the returned date and rate For 30 September 2026, the output is 2026-09-30 [{'currency': 'IDR', 'rate': '17891.1'}]; use the date on the returned row when assigning the rate to a date in your own records. For a list of dates, request a start and an end, then use limit and next_cursor to paginate through the observations, since a page can hold many days.

Use GET /v1/fx/rates/{base}/observations for a past date. The endpoint for a current rate is GET /v1/fx/rates/{base}?currencies=...; the dated request belongs on the observations path with its start and end parameters.

The response contains base, observations and next_cursor. Inside observations, each row has a date and a rates array; each rate has currency and rate fields. When extracting the value for a sheet, take the date from the observation and the value from its IDR entry, preserving the connection between the requested quote currency and the returned rate.

If the request returns an error, its body uses application/problem+json with status, code, detail and request_id. Read the error's code and detail before treating the response as observations. An error body has these error fields; the dated rates belong in a successful response's observations array.

Find a historical rate without code

For a date you want to look up by hand, use the Narwhal currency pages, which show historical rates. Find the currencies and the date you need, and keep the date with the rate when you copy it into a sheet. A dated value lets you refer back to the observation you used for the conversion.

For a list, paste the dates and rates the script writes into a sheet, with dates in the lookup column and rates in the following column. Use date for the date you want and table for those columns in the formula. Replace those placeholders with your sheet's date reference and lookup range. The final FALSE selects an exact date match, while 2 selects the rate column. Keep the currency pair with the table so you know which base and quote the pasted values belong to, and use the returned observation dates as the dates in that lookup column.

Look up a date in a sheet
=VLOOKUP(date, table, 2, FALSE)

Which option to use when

SituationBest optionWhy
A single date by handThe currency pageThe currency pages show historical rates
A list of datesThe observations endpointRequest a start and an end, then look up each date
A single date outside your plan windowA Basic or Pro keyUse a plan whose history window includes the date

History access depends on your plan's window. An earlier start date is moved up to the window's first day without an error, so read the first returned date; 403 plan_upgrade_required applies when the whole range ends before the window, or when a single date or period is outside it.

What if the date was a weekend or a holiday?

If the requested date has no rate, take the last earlier date with a row. Rates are daily observations, and weekends or other days without a rate have no row, so a request for a Saturday returns no observation for that date.

Use the date on that earlier row when recording which observation supplied the rate. The invoice-date method explains the fallback for a date without an observation.

What does the exchange rate mean?

The rate is the number of quote-currency units per unit of the base currency. In the 30 September 2026 USD/IDR observation, USD is the base and IDR is the quote, so 17,891.1 means Indonesian rupiah per US dollar. Multiply the USD amount by that dated rate to get its IDR value.

How should I handle decimal rates?

Keep the returned rates as decimals or strings when you do money maths. The API returns decimal strings, as the quoted '17891.1' in the 30 September 2026 output illustrates. Use a decimal representation for the multiplication in your calculation rather than converting the rate to a binary float.

Worked example: USD/IDR on 30 September 2026

To convert 1,000 USD using the 30 September 2026 observation, first read the IDR rate from that date's USD row. Then multiply the dollar amount by 17,891.1 IDR per USD. Keep the date attached to the result so the amount refers to the historical observation used in the calculation. In the sheet, look up 30 September 2026 in the pasted dates, take its rate, and use that value as the multiplier for the USD amount.

StepValue
Observation date30 September 2026
Amount in the base currency1,000 USD
Rate on 30 September 202617,891.1 IDR per USD
Multiply1,000 USD × 17,891.1 IDR per USD
Amount in the quote currency17,891,100 IDR

Published .