← All blueprints
Riksbanken

Riksbank SWEA

Fetch financial data from the Swedish Riksbank SWEA API — exchange rates, interest rates, bonds, STIBOR. No authentication required. Incremental loading by date.

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

Fetch financial data from the Swedish Riksbank SWEA API. Exchange rates, interest rates, government bonds, STIBOR — any series available at api.riksbank.se. No authentication, no API key. Incremental loading by date.

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, 105 rows, 1m1s — first run)
[OK] staging.exchange_rates (table, backfill, 15 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

105 raw rows (7 series × 15 trading days) → 15 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

The blueprint works with any series from the Riksbank SWEA API. Pass series IDs as a comma-separated string in the SQL arg.

Exchange rates (SEK per unit)

Series IDCurrency
SEKEURPMIEuro
SEKUSDPMIUS Dollar
SEKGBPPMIBritish Pound
SEKNOKPMINorwegian Krone
SEKDKKPMIDanish Krone
SEKCHFPMISwiss Franc
SEKJPYPMIJapanese Yen
SEKAUDPMIAustralian Dollar
SEKCADPMICanadian Dollar
SEKCNYPMIChinese Yuan
SEKBRLPMIBrazilian Real
SEKCZKPMICzech Koruna
SEKHKDPMIHong Kong Dollar
SEKHUFPMIHungarian Forint
SEKIDRPMIIndonesian Rupiah
SEKILSPMIIsraeli Shekel
SEKINRPMIIndian Rupee
SEKISKPMIIcelandic Krona
SEKKRWPMISouth Korean Won
SEKMXNPMIMexican Peso
SEKMYRPMIMalaysian Ringgit
SEKNZDPMINew Zealand Dollar
SEKPHPPMIPhilippine Peso
SEKPLNPMIPolish Zloty
SEKRONPMIRomanian Leu
SEKSGDPMISingapore Dollar
SEKTHBPMIThai Baht
SEKTRYPMITurkish Lira
SEKZARPMISouth African Rand

Interest rates

-- Riksbank policy rate
FROM riksbank('SECBREPOEFF')

-- STIBOR rates
FROM riksbank('SEDPT_NSTIBORDELAYC,SEDP3MSTIBORDELAYC,SEDP6MSTIBORDELAYC')

-- Reference and discount rates
FROM riksbank('SECBREFEFF,SECBDISCEFF')

Government bonds

-- Swedish government bonds (2, 5, 10 year)
FROM riksbank('SEGVB2YC,SEGVB5YC,SEGVB10YC')

-- Swedish treasury bills
FROM riksbank('SETB1MBENCHC,SETB3MBENCH,SETB6MBENCH')

-- International (US, Germany, Japan 10-year)
FROM riksbank('USGVB10Y,DEGVB10Y,JPGVB10Y')

Full list at api.riksbank.se/swea/v1/Series.

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 splits the series arg into individual series IDs
  2. Fetches one series at a time from GET /Observations/{seriesId}/{from}/{to}
  3. Returns raw rows: (series, date, value) — one per observation
  4. Pagination cursor tracks which series, date range, and end date (until) is next
  5. until is pinned at pipeline start — stable if the run spans midnight
  6. Each series fetches up to 365 days per API call, then advances
  7. On incremental runs, only dates after MAX(date) are fetched
  8. Staging model pivots series into columns using SQL

Blueprint

lib/riksbank.star

RESERVED_COLUMNS = ()

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

def fetch(series, page, is_backfill=True, last_value="", initial_value=""):
    if is_backfill:
        start_date = _to_iso_date(initial_value)
    else:
        start_date = _next_day(_to_iso_date(last_value))

    series_list = [s.strip() for s in series.split(",")]
    series_idx = 0
    date_cursor = start_date
    # Pin yesterday at pipeline start — stable across midnight
    yesterday = _yesterday()

    if page.cursor != None:
        series_idx = page.cursor["idx"]
        date_cursor = page.cursor["from"]
        yesterday = page.cursor["until"]

    if date_cursor > yesterday:
        abort()

    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)

    rows = []
    if not resp.ok:
        fail(current_series + " (" + date_cursor + " to " + end_date + "): " + resp.text)

    # 200 = data, 204 = no observations in range (weekends, holidays, future)
    if resp.status_code == 200:
        for obs in resp.json:
            rows.append({
                "series": current_series,
                "date": obs["date"],
                "value": obs["value"],
            })

    # Advance: more dates for this series, or next series
    if end_date < yesterday:
        next_cursor = {"idx": series_idx, "from": _next_day(end_date), "until": yesterday}
    elif series_idx + 1 < len(series_list):
        next_cursor = {"idx": series_idx + 1, "from": start_date, "until": yesterday}
    else:
        next_cursor = None

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


def _to_iso_date(d):
    """Normalize to YYYY-MM-DD. Handles YYYYMMDD, YYYY-MM-DD, and YYYY-MM-DDTHH:MM:SSZ."""
    if len(d) >= 8 and d[4] != "-":
        return d[:4] + "-" + d[4:6] + "-" + d[6:8]
    return d[:10]


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")

Notes

  • No authentication — the Riksbank SWEA API is public
  • Rate limiting — 1 request/second declared in API dict, runtime handles it. The API returns HTTP 429 with a cooldown period if exceeded.
  • One request per series — the API only accepts one series ID per call (max 25 characters)
  • Pagination — dict cursor tracks series index, date offset, and pinned end date. Each call fetches up to 365 days for one series.
  • 204 No Content — returned for ranges with no data (weekends, holidays, future dates). Blueprint handles this gracefully — no rows, no error.
  • 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
  • Runtime kwargs filtering — The blueprint only declares the parameters it uses. The runtime automatically filters kwargs to match.

See API Dict Reference for all configuration options.