Integrations

Exporting Rate Data

Pull a daily series, mark repeated values, write a reproducible CSV, and load it into a warehouse without overcounting carry-forward days.


The job

Move rates into a spreadsheet, a warehouse, or an archive with enough metadata to rebuild the file later. Use /v1/history for a dated series, /v1/rates or /v1/rates/{base} for a snapshot, and /v1/currencies for the reference list.

The API returns JSON. There is no CSV API endpoint. The CSV links on the website history pages are browser downloads, not an API route. If you need CSV in a pipeline, build it from the JSON response.

What the history response contains

{
  "currency": "EUR",
  "base": "USD",
  "interval": "daily",
  "from": "2026-01-01",
  "to": "2026-03-31",
  "rates": {
    "2026-01-01": 1.0352,
    "2026-01-02": 1.0321,
    "2026-01-03": 1.0321
  }
}

The map is dense: every calendar day in the range is present, including weekends and holidays. A repeated value means the previous trading day was carried forward, but it can also be a genuinely unchanged fixing. The data does not distinguish the two. Label repeats as repeats, not as confirmed carry-forwards.

A complete export script

This script fetches one currency pair, marks repeated values, writes a CSV with run metadata, and prints the row counts. It runs server-side with a secret key.

import { writeFileSync } from "node:fs";
import { randomUUID } from "node:crypto";

type HistoryResponse = {
  currency: string;
  base: string;
  interval: string;
  from: string;
  to: string;
  rates: Record<string, number>;
};

type Row = {
  date: string;
  currency: string;
  base: string;
  rate: number;
  repeat: boolean;
  runId: string;
  fetchedAt: string;
};

async function fetchHistory(
  currency: string,
  base: string,
  from: string,
  to: string,
): Promise<HistoryResponse> {
  const url = new URL("https://api.currency-core.com/v1/history");
  url.searchParams.set("currency", currency);
  url.searchParams.set("base", base);
  url.searchParams.set("from", from);
  url.searchParams.set("to", to);
  url.searchParams.set("interval", "daily");

  const res = await fetch(url, {
    headers: { Authorization: `Bearer ${process.env.CURRENCECORE_API_KEY}` },
    signal: AbortSignal.timeout(30_000),
  });
  const body = await res.json();

  if (!res.ok) {
    throw Object.assign(new Error(body.error?.message ?? `HTTP ${res.status}`), {
      status: res.status,
      code: body.error?.code,
    });
  }
  return body as HistoryResponse;
}

export async function exportSeries(input: {
  currency: string;
  base: string;
  from: string;
  to: string;
  outFile: string;
}) {
  const runId = randomUUID();
  const fetchedAt = new Date().toISOString();
  const data = await fetchHistory(input.currency, input.base, input.from, input.to);

  const rows: Row[] = [];
  let previous: number | undefined;

  for (const [date, rate] of Object.entries(data.rates)) {
    rows.push({
      date,
      currency: data.currency,
      base: data.base,
      rate,
      repeat: previous !== undefined && rate === previous,
      runId,
      fetchedAt,
    });
    previous = rate;
  }

  const header = "date,currency,base,rate,repeat,run_id,fetched_at";
  const body = rows
    .map((row) =>
      [
        row.date,
        row.currency,
        row.base,
        row.rate,
        row.repeat ? "true" : "false",
        row.runId,
        row.fetchedAt,
      ].join(","),
    )
    .join("\n");

  writeFileSync(input.outFile, `${header}\n${body}\n`, "utf8");

  return {
    runId,
    rows: rows.length,
    repeats: rows.filter((row) => row.repeat).length,
    firstDate: rows[0]?.date,
    lastDate: rows.at(-1)?.date,
  };
}

Call it once per series:

const result = await exportSeries({
  currency: "EUR",
  base: "USD",
  from: "2026-01-01",
  to: "2026-03-31",
  outFile: "eur-usd-2026-q1.csv",
});

console.log(result);
// { runId: "...", rows: 90, repeats: 26, firstDate: "2026-01-01", lastDate: "2026-03-31" }

The repeat count is illustrative. It depends on weekends, holidays and genuine unchanged fixings in the window.

Chunking large exports

/v1/history has no maximum span. A single request can return a multi-decade daily series. Chunking is still useful for three engineering reasons:

  • a failed request does not lose the whole export
  • memory stays bounded for very large ranges
  • each chunk can be retried independently

Fetch in yearly or five-year windows and write each chunk to its own file. Do not describe chunking as an API limit, because it is not one.

Free is limited to the last 30 days. Older from dates return 403 forbidden. Use a test key on Free or a live key on Starter and above for full history.

What to store

ColumnWhy
dateThe calendar date of the row
currency and baseThe pair and direction
rateUnits of currency per 1 base
repeatThe value matched the previous calendar row
run_idGroups the rows from one export run
fetched_atWhen your job pulled the response

For a snapshot export, also keep date, requestedDate and asOf. A weekend snapshot can carry Friday’s values forward, and asOf tells you which currencies were carried.

Loading into a warehouse

A minimal table:

create table fx_history (
  date        date not null,
  currency    char(3) not null,
  base        char(3) not null,
  rate        numeric(18,8) not null,
  repeat      boolean not null,
  run_id      uuid not null,
  fetched_at  timestamptz not null,
  primary key (date, currency, base, run_id)
);

Load the CSV with the warehouse’s bulk loader. For PostgreSQL:

copy fx_history (date, currency, base, rate, repeat, run_id, fetched_at)
from '/path/eur-usd-2026-q1.csv'
with (format csv, header true);

For object storage, upload the file and keep the run_id in the object key:

aws s3 cp eur-usd-2026-q1.csv \
  s3://your-bucket/fx/eur-usd/2026-q1.csv

Do not deduplicate by dropping repeated rows unless your analysis explicitly wants observed fixings only. The repeat flag lets a downstream query choose.

Worked example

Export EUR/USD for 2026-01-01 to 2026-03-31:

  1. Request /v1/history?currency=EUR&base=USD&from=2026-01-01&to=2026-03-31&interval=daily.
  2. Receive 90 calendar rows.
  3. Mark a row as repeat when its rate equals the previous row.
  4. Write the CSV with run_id and fetched_at.
  5. Load it into fx_history.
  6. Query where not repeat for observed fixings, or keep all rows for a dense chart that shows weekends.

Failure handling

StatusWhat to do
400 invalid_inputCheck the currency code, date format and range
401 unauthorizedCheck the server-side key
403 forbiddenA Free key asked for a from older than 30 days
429 rate_limitedHonor Retry-After; chunk retries instead of hammering
5xxRetry the chunk with backoff and keep the completed chunks

The per-minute limit counts every request, including retries. The monthly quota counts successful billable requests. See rate limits.

Next steps