# 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.

Canonical: https://currency-core.com/docs/guides/exports/
Updated: 2026-11-12
Published: 2026-11-11

---
## The job

Move rates into a spreadsheet, a warehouse, or an archive with enough metadata to
rebuild the file later. Use [`/v1/history`](/docs/history-endpoint/) for a dated
series, [`/v1/rates`](/docs/rates-endpoint/) or
[`/v1/rates/{base}`](/docs/rates-endpoint/) for a snapshot, and
[`/v1/currencies`](/docs/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

```json
{
  "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.

```ts

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:

```ts
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:

```sql
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:

```sql
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:

```bash
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

| 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](/docs/rate-limits/).

## Next steps

- [History endpoint](/docs/history-endpoint/)
- [Rates endpoint](/docs/rates-endpoint/)
- [Data quality](/docs/guides/data-quality/)
- [Spreadsheets](/docs/spreadsheets/)
- [All developer guides](/docs/guides/)
