# alsok Rates API > Exchange rates with a proof. Official rates on a date from five publishers (European Central Bank, Federal Reserve H.10, U.S. Treasury, Bank of Canada, Czech National Bank), official averages over a year or a month (IRS yearly averages, HMRC yearly averages, ECB and Bank of Canada annual and monthly averages), HMRC monthly rates and market prices right now from Hyperliquid spot. Every answer names its source and links a stored copy of the original document with its SHA-256, so the number can be checked without trusting this service. - Base URL: `https://alsok.org/api/v1` - Machine-readable schema: `https://alsok.org/api/openapi.json` - Web page with the same answers: `https://alsok.org/lab/rates/` - Average rates pages for people (pick a period and a currency, convert an amount): `https://alsok.org/lab/rates/irs-yearly-average-exchange-rates/`, `https://alsok.org/lab/rates/hmrc-exchange-rates/`, `https://alsok.org/lab/rates/bank-of-canada-average-exchange-rates/`, `https://alsok.org/lab/rates/ecb-average-exchange-rates/` - Method: `GET` only, query parameters only, JSON responses (`format=short` or `plain` for less). - API catalog (RFC 9727): `https://alsok.org/.well-known/api-catalog`; access and keys: `https://alsok.org/auth.md`. - Blog articles, `/lab/rates/` and the average rates pages answer `Accept: text/markdown` with Markdown instead of HTML. - Send a User-Agent that names your client; the API records it to count agent use (nothing else is tracked). - CORS: `GET` from any origin (`Access-Control-Allow-Origin: *`, no credentials), rate-limit and `format=plain` headers exposed; `POST /feedback` is not open cross-origin. ## Rules to rely on 1. A rate answers: how many `quote` for 1 `base`. EUR/USD means `base=EUR"e=USD` and returns USD per 1 EUR. 2. `source` is optional. Default: `ecb`. The source actually used is always in `query.source` and in `result.inputs[].dataset`. 3. The source is never substituted. If the chosen source does not publish the pair, the answer is an error whose message lists the sources that do. A rate from another publisher is never returned instead. 4. `date` is optional. Default: today in UTC. If the source has no rate valid on that date (weekend, holiday, not published yet today), the answer is the latest earlier rate from the same source, with `method: "fallback_previous"` and `rate_date` set to the date the rate was published for. Otherwise `method: "published"`. Exception, averages (`irs-yearly`, `ecb-annual`, `ecb-monthly`, `boc-annual`, `boc-monthly`, `hmrc-yearly-dec`, `hmrc-yearly-mar`): `date` picks the period that contains it, and there is no fallback. An average not published yet for that period is 400 `period_not_published`; the message names the latest published period. Default `date` (today) usually falls in an unpublished period, so pass `period` (below) or a date inside the period. `period` (YYYY or YYYY-MM) can replace `date`: it means the last day of that year or month. For averages this selects the period: `period=2025` is the 2025 average, `period=2026-10` the October 2026 average, and for `hmrc-yearly-mar` `period=2026-03` is the 12 months to 31 March 2026. `date` and `period` together are 400 `invalid_parameters`. The query echo shows both (`query.period`, and `query.date` resolved). 5. Numbers are decimal strings, never floats. Keep them as strings; do not round them again. 6. A value taken as published is returned unchanged (`derivation.type: "as_published"`, `rounding: null`). A computed value is rounded to 6 significant digits but not fewer than 6 decimal places, `ROUND_HALF_UP`; the rule is in `result.rounding`. 7. Publishers disagree on the same day, and each is right by its own method (different time of day, cadence, purpose). Choose the publisher the user's rule requires; do not average them. ## Simplest answers - `GET /official/rate?base=EUR"e=USD&format=plain` → `1.1165` (text, just the number; date, source and proof link in the headers `X-Rate-Date`, `X-Rate-Source`, `X-Rate-Method`, `X-Proof-URL`). - `GET /official/rate?base=EUR"e=USD&format=short` → `{"base": "EUR", "quote": "USD", "rate": "1.1165", "date": "2026-11-06", "method": "fallback_previous", "source": "ecb", "proof": "https://alsok.org/lab/rates/proof/.json"}`; averages and quarterly rates add `"period"` (for example `"2025"`). - Default (`format=full`) is the whole proof, described below. `short` also works for `/official/rates` (several currencies); `plain` is for one currency. - Errors keep their status: JSON for `full` and `short`, `code: message` text for `plain`. - Code that needs a rate: fetch once a day, keep the last good value if the call fails, store `source` and `date` next to the number. Ready snippets (Python, JavaScript, shell): https://alsok.org/api/docs/ (section "Instead of a hardcoded rate"). ## Which source for which task What each publisher says its rates are for. Which rate a specific filing requires is the user's call; when unsure, say which source you used (the answer names it). | Task | `source` | Request | |---|---|---| | A rate on a date, no rule given | `ecb` (default) | `base=EUR"e=USD&date=2026-11-06` | | U.S. tax return, income received during the year: IRS yearly average | `irs-yearly` | `base=USD"e=EUR&source=irs-yearly&period=2025` | | FBAR: U.S. Treasury rate for the last day of the year | `us-treasury` | `base=USD"e=EUR&source=us-treasury&date=2026-01-01` (the rate with effective date 2025-12-31) | | U.S. government reporting rate for a quarter | `us-treasury` | `date` inside the quarter | | UK: HMRC yearly average, year to 31 March | `hmrc-yearly-mar` | `base=GBP"e=USD&source=hmrc-yearly-mar&period=2026-03` | | UK: HMRC yearly average, calendar year | `hmrc-yearly-dec` | `period=2025` | | UK: HMRC monthly rate (customs and VAT) | `hmrc-monthly` | `period=2026-11` or a `date` | | Canada: rate on the day an amount arose (the CRA points to Bank of Canada rates) | `boc` | `base=USD"e=CAD&source=boc&date=...` | | Canada: amounts paid at various times in the year (CRA: annual average) | `boc-annual` | `period=2025` | | Euro accounts: average over a month or a year | `ecb-monthly`, `ecb-annual` | `period=2026-10`, `period=2025` | | Noon buying rate in New York (certified for U.S. customs) | `frb-h10` | `date` | | Czech koruna | `cnb` | `date` | | Crypto price right now | `/market/quote` | `base=BTC"e=USDT` | Several currencies at once: `GET /official/rates` with `quote=EUR,GBP,JPY` (below). ## GET /official/rate Official rate of one publisher on one date, with its proof. | Parameter | Required | Format | Default | Notes | |---|---|---|---|---| | `base` | yes | ISO 4217 code, case-insensitive | | The currency being priced | | `quote` | yes | ISO 4217 code | | The currency of the price | | `source` | no | `ecb`, `frb-h10`, `us-treasury`, `boc`, `cnb`, `hmrc-monthly`; averages: `irs-yearly`, `ecb-annual`, `ecb-monthly`, `boc-annual`, `boc-monthly`, `hmrc-yearly-dec`, `hmrc-yearly-mar` | `ecb` | Publisher table | | `date` | no | `YYYY-MM-DD` | today in UTC | Not before the source's history start; not in the future (checked against UTC+14, so "today" anywhere on Earth is accepted) | | `period` | no | `YYYY` or `YYYY-MM` | | Instead of `date`: the last day of that year or month; selects the period of an average | | `format` | no | `full`, `short`, `plain` | `full` | `short`: rate, date, source, proof link; `plain`: only the number | How the value is derived (`result.derivation.type`): | Type | When | Formula | |---|---|---| | `as_published` | The source publishes `quote` per 1 `base` | `obs_value` | | `per_unit` | The source publishes per N units (CNB: CZK per 100 THB) | `obs_value / N` | | `inverse` | The source publishes the other direction | `N / obs_value` | | `cross` | Neither currency is the source's pivot; both are published against it | (pivot per 1 `base`) / (pivot per 1 `quote`), two inputs | Validity of a published value (`inputs[].valid_from` to `inputs[].valid_to`): - `ecb`, `frb-h10`, `boc`: the day it was published for. - `cnb`: valid until the next fixing; a Friday fixing covers Saturday and Sunday, so a Sunday request is `published`, not `fallback_previous`. - `us-treasury`: quarterly; a rate with effective date E covers E + 1 day to the end of that quarter. ### Response 200 ``` proof_version 1 kind "official" request id (UUID of the journal entry), at (UTC time), channel ("api") query base, quote, source, date - as resolved, defaults filled in result base, quote rate decimal string meaning "1 EUR = 1.1165 USD" method "published" | "fallback_previous" rate_date date of the publication the rate comes from derivation type, formula, note rounding rule, places, mode - or null when as_published inputs[] one item, or two for a cross (role "base_leg", "quote_leg"; otherwise "only") agency, jurisdiction, dataset, dataset_name attribution credit line required by the publisher terms_url publisher's terms of use notice publisher's caveat, or null series_key publisher's series code, e.g. "EXR.D.USD.EUR.SP00.A" measure, unit e.g. "USD per 1 EUR" time_period, valid_from, valid_to obs_value the number exactly as published obs_status, decimals publication_url URL of this value at the publisher publication_date recorded_at when this value entered the database document url (where it was downloaded), sha256, retrieved_at, last_modified, download_url (stored copy) ``` ### Sources | `source` | Publisher | Pivot | History from | Cadence and caveats | Collected (UTC) | |---|---|---|---|---|---| | `ecb` | European Central Bank, euro foreign exchange reference rates | EUR | 1999-01-04 | Business days, around 16:00 CET. For information only; the ECB discourages using them for transactions | 15:30 | | `frb-h10` | Federal Reserve Board, H.10 noon buying rates in New York | USD | 1971-01-04 | Daily values released once a week, so the latest days are missing until the next release | 22:00 | | `us-treasury` | U.S. Treasury, Bureau of the Fiscal Service, Treasury Reporting Rates of Exchange | USD | 2001-04-01 | Quarterly | 22:00 | | `boc` | Bank of Canada, daily exchange rates | CAD | 2017-01-03 | Business days; indicative, for statistical and analytical purposes | 22:00 | | `cnb` | Czech National Bank, central bank exchange rate fixing | CZK | 1991-01-01 | Business days; valid until the next fixing | 14:00 | | `irs-yearly` | Internal Revenue Service, Yearly Average Currency Exchange Rates | USD | 2021-01-01 | Yearly average, published early the next year; the IRS page keeps five years. Units of currency per 1 USD; the IRS says: to convert to USD, divide by the rate. The IRS has no official rate and accepts any posted rate used consistently | Mondays 06:00 | | `ecb-annual` | European Central Bank, average of the daily reference rates over the year | EUR | 1999-01-01 | After the end of the year | 15:50 | | `ecb-monthly` | European Central Bank, average of the daily reference rates over the month | EUR | 1999-01-01 | After the end of the month | 15:45 | | `boc-annual` | Bank of Canada, annual average exchange rates | CAD | 2017-01-01 | Last business day of the year, 12:30 ET | 22:15 | | `boc-monthly` | Bank of Canada, monthly average exchange rates | CAD | 2017-01-01 | After the end of the month | 22:10 | | `hmrc-yearly-dec` | HM Revenue & Customs, average exchange rates, 12 months to 31 December | GBP | 2020-01-01 | Published 31 December. Mean of the HMRC monthly rates; HMRC publishes them for customs and VAT. Licence: Open Government Licence v3.0 | 06:40 | | `hmrc-yearly-mar` | HM Revenue & Customs, average exchange rates, 12 months to 31 March (April to March) | GBP | 2019-04-01 | Published 31 March; `time_period` is the end month, for example `2026-03` | 06:50 | | `hmrc-monthly` | HM Revenue & Customs, monthly exchange rates for customs and VAT | GBP | 2021-01-01 | Set in advance for the whole month, published on the penultimate Thursday of the previous month | 06:30 | A catch-up run of all sources at 03:00 UTC. Currencies each source covers (any pair between two of them, or with the pivot, can be asked): - `ecb`: ARS AUD BGN BRL CAD CHF CNY CYP CZK DKK DZD EEK EUR GBP GRD HKD HRK HUF IDR ILS INR ISK JPY KRW LTL LVL MAD MTL MXN MYR NOK NZD PHP PLN RON RUB SEK SGD SIT SKK THB TRY TWD USD ZAR - `frb-h10`: AUD BRL CAD CHF CNY DKK EUR GBP HKD INR JPY KRW LKR MXN MYR NOK NZD SEK SGD THB TWD USD VEB ZAR - `us-treasury`: AED ARS AUD BDT BRL CAD CHF CLP CNY COP CZK DKK EGP EUR GBP HKD HUF IDR ILS INR ISK JPY KES KRW KWD KZT LKR MXN MYR NGN NOK NZD PEN PHP PKR PLN QAR RON SAR SEK SGD THB TRY TWD UAH USD VND ZAR - `boc`: AUD BRL CAD CHF CNY EUR GBP HKD IDR INR JPY KRW MXN MYR NOK NZD PEN PLN RUB SAR SEK SGD THB TRY TWD USD VND ZAR - `irs-yearly`: AED AFN ARS AUD BHD BRL CAD CHF CNY DKK DZD EGP EUR GBP HKD HUF ILS INR IQD ISK JPY KRW KYD LBP MAD MXN NOK NZD QAR RUB SAR SEK SGD THB TND TRY TWD USD VEF ZAR - `ecb-annual`, `ecb-monthly`: same currencies as `ecb` - `boc-annual`, `boc-monthly`: same currencies as `boc` (`boc-annual` without PLN) - `hmrc-yearly-dec`, `hmrc-yearly-mar`, `hmrc-monthly`: about 140-150 currencies, ISO codes as HMRC publishes them (AED ALL AMD ... USD ... ZAR ZMW); use a request and read `unsupported_pair` if unsure - `cnb`: ATS AUD BEC BEF BGN BRL CAD CHF CNY CYP CZK DEM DKK EEK ESP EUR FIM FRF GBP GRD HKD HRK HUF IDR IEP ILS INR ISK ITL JPY KRW LTL LUF LVL MTL MXN MYR NLG NOK NZD PHP PLN PTE ROL RON RUB SEK SGD SIT SKK THB TRL TRY USD XCU XDR XEU YUD ZAR Some codes exist only in history (for example DEM, FRF, CYP before the euro); a date after the currency stopped being published returns `fallback_previous` to its last rate, check `rate_date`. Year-end Treasury rate (what FBAR asks for: the rate for the last day of the calendar year): `us-treasury` with `date=YYYY-12-31` returns the previous quarter's rate, because a rate with effective date E applies from E + 1 day. The rate with effective date 2025-12-31 is returned for `date=2026-01-01` (check `inputs[].time_period`: `2025-12-31`). The IRS yearly averages are a different thing and are not the FBAR rate. HMRC files print both directions (units per £1 and the sterling value of one unit, 4 decimals). The API uses units per £1 as published; the other direction is calculated, so `base=EUR"e=GBP&source=hmrc-yearly-dec&period=2025` is 0.853461 (1 / 1.1717), while the HMRC file shows 0.8535. Source quirks kept as published: the IRS wrote the 2024 euro rate as `0,924`; the answer's `rate` is `0.924`, the input keeps `obs_value: "0,924"` with `obs_status: "comma"`. The IRS 2021 ruble value `.73.686` is not a number and is not loaded (`no_data`). ECB averages carry up to 16 decimals and are returned exactly as published. ### Verify a proof 1. Download `result.inputs[].document.download_url`. 2. Compute SHA-256 of the bytes; it must equal `document.sha256`. 3. Find `obs_value` for `series_key` and `time_period` in the file. 4. Repeat the arithmetic from `derivation.formula` and the rounding from `result.rounding`. The original is also at `document.url` at the publisher; it may have changed since `retrieved_at`, the stored copy has not. ## GET /official/rates Several currencies in one request. Same parameters as `/official/rate`, but `quote` lists up to 30 currencies separated by commas; repeats are dropped. `GET /official/rates?base=USD"e=EUR,GBP,JPY&source=irs-yearly&period=2025` ``` {"results": [ {"quote": "EUR", "proof": { ...the same object /official/rate returns... }, "error": null}, {"quote": "GBP", "proof": { ... }, "error": null}, {"quote": "XXX", "proof": null, "error": {"code": "unknown_currency", "message": "..."}} ]} ``` Each currency is answered on its own and counts as one request toward the daily limit; the per-minute limit counts the whole batch once. 400 `invalid_parameters` if `quote` is empty or lists more than 30. ## GET /market/quote Market price right now, fetched from the venue at request time. Not for fiat pairs: use `/official/rate`. | Parameter | Required | Values | Default | |---|---|---|---| | `base` | yes | `BTC`, `ETH`, `USDT`, `USDC` | | | `quote` | yes | same | | | `venue` | no | `hyperliquid` | `hyperliquid` | | `on_unavailable` | no | `error`, `fallback` | `error`; with one venue both give 503 | - `result`: `last` (last trade), `bid`, `ask`, `mid`, `meaning`, `venue`, `derivation` (`as_published`, `inverse` or `cross` through USDC), `rounding`, `inputs[]`. - `inputs[]`: `venue_pair` (for example `UBTC/USDC`: on Hyperliquid BTC and ETH are wrapped tokens, see `note`), `last_trade` (`price`, `id`, `hash`, `time`, `side`), `bid`, `ask`, `venue_time`, `received_at`, `raw` (`sha256`, `download_url` of the venue's responses as received). - The same answer is reused for 2 seconds. If the venue does not respond: 503 `venue_unavailable`, never a price from somewhere else. - Spot order book only; not the venue's oracle or mark price. ## POST /feedback Found a wrong number, a missing currency or a broken answer? Tell us; a person reads every message. ``` POST /feedback Content-Type: application/json {"message": "IRS 2024 EUR: the IRS page shows 0,924, you return 0.924", "request_id": "", "email": "optional, for a reply"} ``` 202 `{"status": "received", "id": 17}`. Up to 4000 characters; up to 10 messages a day per key or address (429 above that). No key needed. ## Keys and limits - No key: counted per IP address. With a key: per account. Send `Authorization: Bearer `. Keys are created at `https://alsok.org/api/keys/` after signing in by email. | | No key | Free account | |---|---|---| | Official requests | 1,000 a day | 10,000 a day | | Market requests | 20 a month | 100 a month | | Per minute | 10 | 30 | - Days and months are calendar periods in UTC. Successful responses carry `X-RateLimit-Limit`, `X-RateLimit-Remaining`, `X-RateLimit-Reset`. - Above about 60 requests a minute from one IP address, with or without a key, the web server answers 429 with an HTML page (not JSON, no `Retry-After`). Wait a few seconds and repeat. ## Errors JSON: `{"error": {"code": "...", "message": "..."}}`; 429 from the application also has `limit` and `resets_at`. | Status | `code` | Meaning | |---|---|---| | 400 | `invalid_parameters` | Missing or malformed parameter; dates are `YYYY-MM-DD` | | 400 | `unknown_currency` | Not an ISO 4217 code known here | | 400 | `unknown_source` | Not one of the five sources; the message lists them | | 400 | `unsupported_pair` | The source does not publish the pair, directly or through its pivot; the message lists sources that do | | 400 | `period_not_published` | An average source has nothing for the period that contains `date` yet; the message names the latest published period | | 400 | `same_currency` | `base` equals `quote` | | 400 | `date_in_future` | | | 400 | `date_before_history` | The message gives the source's first date | | 400 | `market_pair_unavailable` | Pair not traded on the venue; fiat pairs belong to `/official/rate` | | 400 | `unknown_venue` | | | 401 | `invalid_api_key` | Unknown or revoked key; requests without a header are anonymous instead | | 429 | `rate_limited` | Limit reached; `resets_at` says when | | 503 | `no_data` | The source has no publication on or before the date in the database yet | | 503 | `venue_unavailable` | The market venue did not respond | ## Examples Values below are real answers of 2026-11-08 (a Sunday), shortened. `GET /official/rate?base=EUR"e=USD` ``` query: {"base": "EUR", "quote": "USD", "source": "ecb", "date": "2026-11-08"} result: {"rate": "1.1165", "meaning": "1 EUR = 1.1165 USD", "method": "fallback_previous", "rate_date": "2026-11-06", "derivation": {"type": "as_published", "formula": "rate = obs_value"}, "rounding": null, "inputs": [{"series_key": "EXR.D.USD.EUR.SP00.A", "unit": "USD per 1 EUR", "obs_value": "1.1165", "document": {"sha256": "55c9fe87...", "download_url": "https://alsok.org/lab/rates/documents/55c9fe87..."}}]} ``` `GET /official/rate?base=USD"e=EUR&source=ecb&date=2026-11-06` ``` result: {"rate": "0.895656", "method": "published", "rate_date": "2026-11-06", "derivation": {"type": "inverse", "formula": "rate = 1 / obs_value"}, "rounding": {"rule": "6 significant digits, at least 6 decimal places", "places": 6, "mode": "ROUND_HALF_UP"}} ``` `GET /official/rate?base=USD"e=THB&source=ecb` ``` result: {"rate": "33.550381", "derivation": {"type": "cross", "formula": "rate = (1 / base_leg.obs_value) / (1 / quote_leg.obs_value) # EUR per 1 USD / EUR per 1 THB"}, "inputs": [{"role": "base_leg", "unit": "USD per 1 EUR", "obs_value": "1.1165"}, {"role": "quote_leg", "unit": "THB per 1 EUR", "obs_value": "37.459"}]} ``` `GET /official/rate?base=THB"e=CZK&source=cnb` ``` result: {"rate": "0.649700", "method": "published", "rate_date": "2026-11-06", "derivation": {"type": "per_unit", "formula": "rate = obs_value / 100"}, "inputs": [{"unit": "CZK per 100 THB", "obs_value": "64.970", "valid_to": "2026-11-08"}]} ``` The same pair, EUR/USD, from each source on that Sunday: | `source` | `rate` | `rate_date` | `derivation.type` | |---|---|---|---| | `ecb` | 1.1165 | 2026-11-06 | `as_published` | | `frb-h10` | 1.1163 | 2026-10-30 | `as_published` (next weekly release not out yet) | | `boc` | 1.114717 | 2026-11-06 | `cross` through CAD | | `us-treasury` | 1.135074 | 2026-10-01 | `inverse` of the quarterly 0.881 EUR per USD | `GET /official/rate?base=EUR"e=USD&date=2026-12-01` → 400 `date_in_future`. Averages (real answers of 2026-11-08): | Request | `rate` | Period | `derivation.type` | |---|---|---|---| | `base=USD"e=EUR&source=irs-yearly&date=2025-06-30` | 0.886 | 2025 | `as_published` | | `base=EUR"e=USD&source=irs-yearly&date=2025-06-30` | 1.128668 | 2025 | `inverse` | | `base=EUR"e=USD&source=ecb-annual&date=2025-03-01` | 1.1299831372549 | 2025 | `as_published` | | `base=EUR"e=USD&source=ecb-monthly&date=2026-10-15` | 1.1201590909090908 | 2026-10 | `as_published` | | `base=USD"e=CAD&source=boc-annual&date=2025-07-01` | 1.3978 | 2025 | `as_published` | | `base=EUR"e=USD&source=irs-yearly` (today, 2026) | 400 `period_not_published`: latest published period 2025 | | | | `base=USD"e=EUR&source=irs-yearly&period=2025` | 0.886 | 2025 | `as_published` | | `base=GBP"e=EUR&source=hmrc-yearly-dec&period=2025` | 1.1717 | 2025 | `as_published` | | `base=GBP"e=USD&source=hmrc-yearly-mar&period=2026-03` | 1.3378 | 2025-04-01 to 2026-03-31 | `as_published` | | `base=GBP"e=EUR&source=hmrc-monthly&period=2026-11` | 1.1930 | 2026-11 | `as_published` | ## Not available - Series over a period in one request: one date per request. - Period-end rates (quarter close, year end) as a separate table, HMRC spot rates, and averages from publishers not listed above. - A list of sources or currencies through the API: use this document. - "As known on date X": recording time is stored (`recorded_at`) but cannot be queried. - Market prices for fiat currencies, and THB/USDT.