LearnGuide

Historical FX rates and time-series data

Pull a multi-currency FX series from /v1/range and load the rows into pandas or another data tool. JSON responses are paged at 366 rows; CSV and Parquet exports cover the full supported window.

1 Oct 2026 · 6 min read

A request to /v1/range returns a data array of stored observations for the window. The default base is USD, symbols selects the quote currencies, and JSON pages include has_more and next_cursor when the window is longer than 366 rows. Each row carries its own date, rates, and source fields.

  • JSON responses contain up to 366 rows per page; follow next_cursor while has_more is true.
  • Params are start_date and end_date (snake_case). symbols is a comma-separated string.
  • Each row carries source and is_forward_filled. Reference sources publish on business days, so weekend dates are not returned as rows.
  • The supported daily window is up to 1,830 days per request; actual depth depends on the pair.
  • Rates are indicative, published for reference and analytics.

The range endpoint

GET /v1/range takes start_date, end_date, base (default USD), and comma-separated symbols. Free and keyless cover 30 days, Basic one year, Plus and Pro full history. This example needs Basic or above. JSON pages hold 366 rows; follow next_cursor while has_more is true. Use format=csv or format=parquet for one download.

If you need a browser-based lookup instead of an API response, use the historical exchange-rate lookup. It is designed for checking a date or downloading a small result; this guide is for code and repeatable data work.

curl · four-day USD series
$ curl -G "https://api.exchangerate.dev/v1/range" \    -H "Authorization: Bearer exr_live_..." \    --data-urlencode "base=USD" \    --data-urlencode "symbols=EUR,GBP" \    --data-urlencode "start_date=2026-06-10" \    --data-urlencode "end_date=2026-06-13"

Each element of data is a self-contained daily row with its own date, rates, source, and is_forward_filled:

json · response (abbreviated)
{  "result": "success",  "base": "USD",  "start_date": "2026-06-10",  "end_date": "2026-06-13",  "data": [    { "date": "2026-06-10", "rates": { "EUR": 0.86663, "GBP": 0.74727 },      "source": "ecb_daily", "is_forward_filled": false },    { "date": "2026-06-11", "rates": { "EUR": 0.86678, "GBP": 0.74829 },      "source": "ecb_daily", "is_forward_filled": false },    { "date": "2026-06-12", "rates": { "EUR": 0.86453, "GBP": 0.74613 },      "source": "ecb_daily", "is_forward_filled": false }  ],  "has_more": false,  "next_cursor": null}

Note that the Saturday (2026-06-13) is absent. Reference sources publish on business days only, so the range returns the published days in the window rather than one row per calendar day.

Python example

The same request in Python with requests. Iterate the data array, reading each row by key:

python · multi-currency time series
import requestsAPI = "https://api.exchangerate.dev/v1"KEY = "exr_live_..."   # Basic or higher: https://exchangerate.dev/pricingparams = {    "base": "USD",    "symbols": "EUR,GBP",    "start_date": "2026-06-10",    "end_date": "2026-06-13",}rows = []while True:    resp = requests.get(        f"{API}/range",        headers={"Authorization": f"Bearer {KEY}"},        params=params,        timeout=10,    )    resp.raise_for_status()    data = resp.json()    rows.extend(data["data"])    if not data["has_more"]:        break    cursor = data.get("next_cursor")    if not cursor or cursor <= params.get("cursor", ""):        raise RuntimeError("Incomplete historical response")    params["cursor"] = cursorfor row in rows:    filled = row["is_forward_filled"]    print(row["date"], row["rates"]["EUR"], row["rates"]["GBP"], "filled" if filled else "")

The is_forward_filled flag

Every range row carries is_forward_filled: false because it represents a stored observation. Missing weekend and holiday dates stay absent. A lookup for one date can return the earlier published value and flag it as carried forward; that is a different response from a range.

Need a specific weekend date?

The range returns published days, so a Saturday will not appear in data. To get the carried-forward value for one specific weekend or holiday date, request it directly with /v1/{date}/{base}, which returns the prior value and sets is_forward_filled: true.

/v1/range vs looping /v1/{date}/{base}

For a single day, use /v1/{date}/{base}. It carries is_forward_filled at the response root. For a series, use /v1/range and follow its cursors until every JSON page has arrived. CSV and Parquet exports return the full supported window in one download.

Approach/v1/{date}/{base} loop/v1/range
HTTP requests for a 30-day seriesUp to 30 requests1 request (plus paging)
Rate-limit costOne call per requested dateOne call per JSON page
Response shapeOne day per responseA data array of days
Forward-fill flagRoot-level fieldPer-row field in data
Best forOne-off date lookupsCharting, backtesting, bulk export

Window size and pair coverage

Daily requests support windows up to 1,830 days. That is a request limit, not a promise that every pair has data that far back. Coverage is pair-specific: check the returned rows and source values before building a long backfill. A pair with no observations returns an empty data array rather than a fabricated rate.

Loading into pandas

The data array loads into a DataFrame with pd.json_normalize, which flattens the nested rates object into rates.EUR, rates.GBP, and so on:

python · load into pandas
import pandas as pd# rows contains every page from the loop abovedf = pd.json_normalize(rows)df["date"] = pd.to_datetime(df["date"])df = df.set_index("date").sort_index()# keep only rows that were actually published that daydf = df[~df["is_forward_filled"]]print(df[["rates.EUR", "rates.GBP"]].tail())
pandas is not required

The code above is one convenience pattern. The API returns plain JSON, so any language or tool that can parse JSON works the same way.

Questions

What is the date format for start_date and end_date?

Use ISO 8601 format: YYYY-MM-DD, for example 2026-06-10. The date parameters define the inclusive window. `base` is optional and defaults to USD; `symbols` selects one or more currencies.

Can I request a single currency with /v1/range?

Yes. Set symbols to a single code, for example symbols=EUR. You can also pass a comma-separated list for multiple currencies in one call, such as symbols=EUR,GBP,JPY.

How do I read a weekend or holiday date from a range response?

A range contains stored observations and does not invent weekend or holiday rows. A direct dated request can return a carried-forward daily value when one exists; the range response keeps missing observations as gaps.

Are these rates suitable for financial settlement or trading?

No. The rates are indicative, published for reference, analytics, and display. They are not a dealing quote. Every API response carries this in its notice field.

Keep reading

Start with a free API key

5,000 requests a month on the free plan. No credit card.

Get a free key