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
| Column | Why |
|---|---|
date | The calendar date of the row |
currency and base | The pair and direction |
rate | Units of currency per 1 base |
repeat | The value matched the previous calendar row |
run_id | Groups the rows from one export run |
fetched_at | When 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:
- Request
/v1/history?currency=EUR&base=USD&from=2026-01-01&to=2026-03-31&interval=daily. - Receive 90 calendar rows.
- Mark a row as
repeatwhen its rate equals the previous row. - Write the CSV with
run_idandfetched_at. - Load it into
fx_history. - Query
where not repeatfor observed fixings, or keep all rows for a dense chart that shows weekends.
Failure handling
| Status | What to do |
|---|---|
400 invalid_input | Check the currency code, date format and range |
401 unauthorized | Check the server-side key |
403 forbidden | A Free key asked for a from older than 30 days |
429 rate_limited | Honor Retry-After; chunk retries instead of hammering |
5xx | Retry 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.