← All blueprints
Riksbanken

Riksbank Exchange Rates

Fetch daily exchange rates from the Swedish Riksbank API. No authentication required. Incremental loading by date.

financerest-api
Model path models/staging/exchange_rates.sql
Kind append
Vendor riksbank
Auth none
Source type rest-api

Fetch daily exchange rates from the Swedish Riksbank (Riksbanken) SWEA API. No authentication, no API key. Incremental loading by date with automatic pagination per series.

Quick Start

1. Add the blueprint

Copy lib/riksbank.star into your project.

2. Create models

Raw model — one row per series per date:

-- models/raw/exchange_rates.sql
-- @kind: append
-- @incremental: date
-- @incremental_initial: 2026-04-01

SELECT series, date, value
FROM riksbank('SEKEURPMI,SEKUSDPMI,SEKGBPPMI,SEKNOKPMI,SEKDKKPMI,SEKCHFPMI,SEKJPYPMI')

The SQL arg controls which series to fetch. One API call per series.

Staging model — pivots to one row per date:

-- models/staging/exchange_rates.sql
-- @kind: table
-- @constraint: not_null(date)

SELECT
    date::DATE AS date,
    MAX(CASE WHEN series = 'SEKEURPMI' THEN value END)::DECIMAL AS eur,
    MAX(CASE WHEN series = 'SEKUSDPMI' THEN value END)::DECIMAL AS usd,
    MAX(CASE WHEN series = 'SEKGBPPMI' THEN value END)::DECIMAL AS gbp,
    MAX(CASE WHEN series = 'SEKNOKPMI' THEN value END)::DECIMAL AS nok,
    MAX(CASE WHEN series = 'SEKDKKPMI' THEN value END)::DECIMAL AS dkk,
    MAX(CASE WHEN series = 'SEKCHFPMI' THEN value END)::DECIMAL AS chf,
    MAX(CASE WHEN series = 'SEKJPYPMI' THEN value END)::DECIMAL AS jpy
FROM raw.exchange_rates
GROUP BY date
ORDER BY date

3. Run

ondatrasql run
[OK] raw.exchange_rates (append, backfill, 91 rows, 1m1s — first run)
[OK] staging.exchange_rates (table, backfill, 13 rows, 269ms — first run)

Raw — one row per observation:

seriesdatevalue
SEKCHFPMI2026-04-0111.846
SEKDKKPMI2026-04-011.457
SEKEURPMI2026-04-0110.888
SEKGBPPMI2026-04-0112.499
SEKJPYPMI2026-04-010.059
SEKNOKPMI2026-04-010.971
SEKUSDPMI2026-04-019.382

Staging — pivoted, one row per date:

dateeurusdgbpnokdkkchfjpy
2026-04-0110.8889.38212.4990.9711.45711.8460.059
2026-04-0210.9489.49912.5470.9751.46511.8830.060
2026-04-0710.9909.50912.5950.9831.47111.8910.059

91 raw rows (7 series × 13 trading days) → 13 pivoted rows.

4. Run again — only new dates

ondatrasql run raw.exchange_rates
[OK] raw.exchange_rates (append, incremental, 0 rows, 214ms — incremental run)

No API calls when data is up to date.

Available Series

Series IDCurrency
SEKEURPMIEuro
SEKUSDPMIUS Dollar
SEKGBPPMIBritish Pound
SEKNOKPMINorwegian Krone
SEKDKKPMIDanish Krone
SEKCHFPMISwiss Franc
SEKJPYPMIJapanese Yen
SEKAUDPMIAustralian Dollar
SEKCADPMICanadian Dollar
SEKCNYPMIChinese Yuan

Values are SEK per 1 unit of foreign currency. Full list at the Riksbank Series API.

Customization

Fewer currencies

Only need EUR and USD?

SELECT series, date, value
FROM riksbank('SEKEURPMI,SEKUSDPMI')

Two series = two API calls instead of seven.

Different start date

-- @incremental_initial: 2020-01-01

Downstream model — join with revenue

-- models/mart/revenue_sek.sql
-- @kind: table

SELECT
    r.date,
    r.amount,
    r.currency,
    r.amount * CASE r.currency
        WHEN 'EUR' THEN fx.eur
        WHEN 'USD' THEN fx.usd
        WHEN 'GBP' THEN fx.gbp
    END AS amount_sek
FROM staging.revenue r
JOIN staging.exchange_rates fx ON r.date = fx.date

DuckDB PIVOT syntax

Instead of MAX(CASE WHEN ...), DuckDB supports native PIVOT:

-- models/staging/exchange_rates.sql
-- @kind: table

PIVOT raw.exchange_rates
ON series
USING MAX(value)
GROUP BY date::DATE AS date
ORDER BY date

How It Works

  1. Blueprint fetches one series at a time from GET /Observations/{seriesId}/{from}/{to}
  2. Returns raw rows: (series, date, value) — one per observation
  3. Pagination cursor tracks which series and which date range is next
  4. Each series fetches up to 365 days per API call, then advances
  5. Rate limited to 2 requests per second (declared in API dict)
  6. On incremental runs, only dates after MAX(date) are fetched
  7. Staging model pivots series into columns using SQL

Blueprint

lib/riksbank.star

API = {
    "base_url": "https://api.riksbank.se/swea/v1",
    "retry": 3,
    "backoff": 2,
    "rate_limit": {"requests": 2, "per": "1s"},
    "fetch": {
        "args": ["series"],
        "dynamic_columns": True,
    },
}

def fetch(series, page, columns=[], target=""):
    yesterday = _yesterday()

    if incremental.is_backfill:
        start_date = incremental.initial_value
    else:
        start_date = _next_day(incremental.last_value)

    if start_date > yesterday:
        abort()

    # Fetch one series at a time, then advance to next series
    series_list = [s.strip() for s in series.split(",")]
    series_idx = 0
    date_cursor = start_date

    if page.cursor != None and page.cursor != "":
        state = json.decode(page.cursor)
        series_idx = state["idx"]
        date_cursor = state["from"]

    if series_idx >= len(series_list):
        return {"rows": [], "next": None}

    current_series = series_list[series_idx]
    end_date = min(_add_days(date_cursor, 365), yesterday)

    resp = http.get("/Observations/" + current_series + "/" + date_cursor + "/" + end_date)
    if not resp.ok:
        fail(current_series + " (" + date_cursor + " to " + end_date + "): " + resp.text)

    rows = []
    for obs in resp.json:
        rows.append({
            "series": current_series,
            "date": obs["date"],
            "value": obs["value"],
        })

    # Determine next cursor
    if end_date < yesterday:
        # More dates for this series
        next_cursor = json.encode({"idx": series_idx, "from": _next_day(end_date)})
    elif series_idx + 1 < len(series_list):
        # Next series, reset date
        next_cursor = json.encode({"idx": series_idx + 1, "from": start_date})
    else:
        next_cursor = None

    return {"rows": rows, "next": next_cursor}


def _yesterday():
    return (time.now() - time.parse_duration("24h")).format("2006-01-02")


def _next_day(d):
    return (time.parse_time(d + "T00:00:00Z") + time.parse_duration("24h")).format("2006-01-02")


def _add_days(d, n):
    return (time.parse_time(d + "T00:00:00Z") + time.parse_duration(str(n * 24) + "h")).format("2006-01-02")


def min(a, b):
    if a < b:
        return a
    return b

Notes

  • No authentication — the Riksbank SWEA API is public
  • Rate limiting — 2 requests/second declared in API dict, runtime handles it
  • One request per series — the API only accepts one series ID per call (max 25 characters)
  • Pagination — JSON cursor tracks series index and date offset. Each call fetches up to 365 days for one series.
  • Values — API returns floats (e.g. 10.7685), cast to DECIMAL in staging
  • Weekends/holidays — no data points for non-trading days
  • Pivot in SQL — raw stores normalized data (series, date, value). Staging pivots to one row per date using MAX(CASE WHEN ...) or DuckDB’s native PIVOT

See API Dict Reference for all configuration options.