FredAPIClient

Hex.pmCICoverageLicense: MIT

A fully-typed Elixir client for the Federal Reserve Economic Data (FRED®) API.

Covers all 36 endpoints across 7 groups — Categories, Releases, Series, Sources, Tags, GeoFRED Maps, and bulk API v2 — with built-in Cachex caching, frequency-aware TTLs, and automatic retry on rate-limit errors.


Table of Contents


Installation

Add fred_api_client to your mix.exs dependencies:

def deps do
  [
    {:fred_api_client, "~> 0.1"}
  ]
end

Then fetch:

mix deps.get

Get a free FRED API key at https://fred.stlouisfed.org/docs/api/api_key.html.


Configuration

Minimal

# config/runtime.exs  ← recommended: keeps secrets out of compiled code
import Config

config :fred_api_client,
  api_key: System.fetch_env!("FRED_API_KEY")

Full reference

# config/config.exs
import Config

config :fred_api_client,
  # ── Required ─────────────────────────────────────────────────────
  api_key:   System.get_env("FRED_API_KEY"),

  # ── HTTP ─────────────────────────────────────────────────────────
  base_url:  "https://api.stlouisfed.org",  # default
  file_type: "json",                         # default — "json" | "xml"
  timeout:   30_000,                         # default — milliseconds

  # ── Caching (Cachex) ─────────────────────────────────────────────
  cache_enabled: true,           # default — set false to disable globally
  cache_name:    :fred_api_cache, # default — Cachex process name

  # Optional: override individual TTL buckets (values in milliseconds).
  # Only set the buckets you want to change — others keep their defaults.
  ttl_overrides: %{
    ttl_24h: :timer.hours(48),      # categories, series metadata, sources, shapes
    ttl_12h: :timer.hours(6),       # release metadata, tags
    ttl_6h:  :timer.hours(3),       # quarterly/annual observations, vintage dates
    ttl_2h:  :timer.hours(1),       # GeoFRED series/regional data
    ttl_1h:  :timer.minutes(30)     # monthly observations, release dates
  },

  # ── Rate Limiting ────────────────────────────────────────────────
  # FRED enforces 120 requests/minute per API key.
  # On HTTP 429 the client retries with exponential backoff:
  #   attempt 1 → wait base_delay × 1  (default 20 s)
  #   attempt 2 → wait base_delay × 2  (default 40 s)
  #   attempt 3 → wait base_delay × 3  (default 60 s) → give up
  rate_limit_max_retries:    3,       # default
  rate_limit_base_delay_ms:  20_000   # default

Runtime secrets (production)

# config/runtime.exs
import Config

if config_env() == :prod do
  config :fred_api_client,
    api_key: System.fetch_env!("FRED_API_KEY")
end

Quick Start

# GDP quarterly observations — automatically cached for 6 h
{:ok, data} = FredAPIClient.get_series_observations(%{
  series_id:         "GDP",
  observation_start: "2010-01-01",
  units:             "pc1",
  frequency:         "q"
})

IO.inspect(data["observations"])
# [%{"date" => "2010-01-01", "value" => "3.7"}, ...]

# Search for series (not cached — free-text results vary)
{:ok, results} = FredAPIClient.search_series(%{
  search_text: "unemployment rate",
  limit:       5,
  order_by:    "popularity",
  sort_order:  "desc"
})

# All releases (cached 12 h)
{:ok, releases} = FredAPIClient.get_releases(%{limit: 10})

# Category tree (cached 24 h)
{:ok, children} = FredAPIClient.get_category_children(%{category_id: 0})

# GeoFRED regional data (cached 2 h)
{:ok, geo} = FredAPIClient.get_regional_data(%{
  series_group: "882",
  region_type:  "state",
  date:         "2023-01-01",
  season:       "NSA",
  units:        "Dollars"
})

Caching

Caching is enabled by default via Cachex. The Cachex process is started automatically by the library’s OTP application — no setup required.

TTL strategy

TTLs are tuned to match FRED’s actual publication cadence, not arbitrary round numbers:

Data type TTL Reason
Category tree 24 h Essentially static — structure never changes
Series metadata 24 h Title, units, frequency never change
Series categories / release / tags 24 h Static mappings
Release metadata, series, tables, tags 12 h Rarely changes
Release sources 24 h Source organisations never change
Tags vocabulary 12 h New tags are rare
GeoFRED shapes / series group 24 h Static geographic metadata
Observations — quarterly / semi-annual / annual 6 h Published ~4× per year
Series vintage dates 6 h List grows slowly
GeoFRED series / regional data 2 h Updated on release schedule
Observations — monthly 1 h Published monthly
Release dates 1 h Changes on publish schedule

Not cached (volatile):

Endpoint Reason
Series.search/2 Free-text — results differ per query
Series.get_updates/2 Volatile by design
Series.get_search_tags/2 / get_search_related_tags/2 Query-dependent
Tags.get_series/2 Tag combination results vary
V2.get_release_observations/2 Large bulk payload
Observations — d / w / bw and all weekly variants Updated too frequently
Observations — unspecified frequency Cannot determine volatility safely

Manual cache control

The FredAPIClient.Cache module exposes the full cache API:

alias FredAPIClient.Cache

# Invalidate a single key
Cache.invalidate("fred:series:get_series:abc123")

# Invalidate an entire group by prefix
{:ok, deleted_count} = Cache.invalidate_prefix("fred:categories:")
{:ok, deleted_count} = Cache.invalidate_prefix("fred:series:")

# Clear everything
Cache.clear()

# Inspect cache size and status
{:ok, stats} = Cache.stats()
# %{hits: 142, misses: 38, evictions: 0, ...}

# Manually build a key (useful for targeted invalidation)
key = Cache.build_key("series", "get_series", %{series_id: "GDP"})
Cache.invalidate(key)

Disabling the cache

Globally — in config/test.exs or wherever you don’t want caching:

config :fred_api_client, cache_enabled: false

Per-config call — pass an explicit config with cache_enabled: false (see Multi-tenant / Explicit Config below):

config = %{api_key: "...", cache_enabled: false}
FredAPIClient.get_series_observations(%{series_id: "GDP"}, config)

Overriding TTLs

You can tune any TTL bucket without changing code:

# config/config.exs
config :fred_api_client,
  ttl_overrides: %{
    ttl_1h:  :timer.minutes(30),   # halve the monthly-observations TTL
    ttl_24h: :timer.hours(48)      # cache static data for 2 days instead of 1
  }

Only the keys you specify are overridden — others stay at their defaults.


Rate Limiting

The FRED API enforces 120 requests per minute per API key. Exceeding this returns HTTP 429 Too Many Requests.

The client handles 429 automatically with exponential backoff. The default of 3 retries with a 20 s base delay recovers safely within the 60 s rate-limit window:

Attempt Wait
1st retry 20 s
2nd retry 40 s
3rd retry 60 s
Give up {:error, %FredAPIClient.HTTP.Error{code: 429}}

503 Service Unavailable is also retried automatically (5 s base delay, shorter backoff).

Practical tip: For bulk data collection, enable caching (default) and batch your calls. A warm cache means most calls never hit the network, making the rate limit a non-issue in practice.

To tune retry behaviour:

config :fred_api_client,
  rate_limit_max_retries:   5,       # more retries for unreliable networks
  rate_limit_base_delay_ms: 10_000   # shorter delay if you have burst headroom

Error Handling

All functions return {:ok, map()} or {:error, %FredAPIClient.HTTP.Error{}}:

case FredAPIClient.get_series_observations(%{series_id: "INVALID"}) do
  {:ok, data} ->
    IO.inspect(data["observations"])

  {:error, %FredAPIClient.HTTP.Error{code: 400, message: message}} ->
    Logger.warning("Bad request: #{message}")

  {:error, %FredAPIClient.HTTP.Error{code: 429, message: message}} ->
    Logger.error("Rate limit hit after all retries: #{message}")

  {:error, %FredAPIClient.HTTP.Error{code: 408}} ->
    Logger.error("Request timed out")
end

FredAPIClient.HTTP.Error fields:

Field Type Description
codeinteger FRED API error code, or HTTP status code
statusinteger | nil HTTP status (nil for timeout / network errors)
messagestring Human-readable description

Multi-tenant / Explicit Config

Pass a config map as the second argument to use a different API key per call. This bypasses application config entirely:

config = %{
  api_key:                  "tenant_specific_key",
  timeout:                  10_000,
  cache_enabled:            true,
  rate_limit_max_retries:   2,
  rate_limit_base_delay_ms: 5_000
}

FredAPIClient.get_series_observations(%{series_id: "GDP"}, config)

All API modules also accept an explicit config directly:

FredAPIClient.API.Series.get_observations(%{series_id: "GDP"}, config)
FredAPIClient.API.Categories.get_category(%{category_id: 125}, config)

API Coverage

All 36 endpoints, grouped by module:

Module Function Endpoint Cached
FredAPIClient.API.Categoriesget_category/2GET /fred/category ✅ 24 h
get_children/2GET /fred/category/children ✅ 24 h
get_related/2GET /fred/category/related ✅ 24 h
get_series/2GET /fred/category/series ✅ 24 h
get_tags/2GET /fred/category/tags ✅ 24 h
get_related_tags/2GET /fred/category/related_tags ✅ 24 h
FredAPIClient.API.Releasesget_releases/2GET /fred/releases ✅ 12 h
get_all_release_dates/2GET /fred/releases/dates ✅ 1 h
get_release/2GET /fred/release ✅ 12 h
get_release_dates/2GET /fred/release/dates ✅ 1 h
get_release_series/2GET /fred/release/series ✅ 12 h
get_release_sources/2GET /fred/release/sources ✅ 24 h
get_release_tags/2GET /fred/release/tags ✅ 12 h
get_release_related_tags/2GET /fred/release/related_tags ✅ 12 h
get_release_tables/2GET /fred/release/tables ✅ 12 h
FredAPIClient.API.Seriesget_series/2GET /fred/series ✅ 24 h
get_categories/2GET /fred/series/categories ✅ 24 h
get_observations/2GET /fred/series/observations ⚠️ by freq
get_release/2GET /fred/series/release ✅ 24 h
search/2GET /fred/series/search
get_search_tags/2GET /fred/series/search/tags
get_search_related_tags/2GET /fred/series/search/related_tags
get_tags/2GET /fred/series/tags ✅ 24 h
get_updates/2GET /fred/series/updates
get_vintage_dates/2GET /fred/series/vintagedates ✅ 6 h
FredAPIClient.API.Sourcesget_sources/2GET /fred/sources ✅ 24 h
get_source/2GET /fred/source ✅ 24 h
get_source_releases/2GET /fred/source/releases ✅ 24 h
FredAPIClient.API.Tagsget_tags/2GET /fred/tags ✅ 12 h
get_related_tags/2GET /fred/related_tags ✅ 12 h
get_series/2GET /fred/tags/series
FredAPIClient.API.Mapsget_shapes/2GET /geofred/shapes/file ✅ 24 h
get_series_group/2GET /geofred/series/group ✅ 24 h
get_series_data/2GET /geofred/series/data ✅ 2 h
get_regional_data/2GET /geofred/regional/data ✅ 2 h
FredAPIClient.API.V2get_release_observations/2GET /fred/v2/release/observations

⚠️ = frequency-aware: m → 1 h, q/sa/a → 6 h, d/w/bw → not cached


License

MIT — see LICENSE.txt.