# FredAPIClient
[](https://hex.pm/packages/fred_api_client)
[](https://github.com/iamkanishka/fred_api_client/actions)
[](https://codecov.io/gh/iamkanishka/fred_api_client)
[](LICENSE.txt)
A fully-typed Elixir client for the [Federal Reserve Economic Data (FRED®) API](https://fred.stlouisfed.org/docs/api/fred/).
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](#installation)
- [Configuration](#configuration)
- [Minimal](#minimal)
- [Full reference](#full-reference)
- [Runtime secrets (production)](#runtime-secrets-production)
- [Quick Start](#quick-start)
- [Caching](#caching)
- [TTL strategy](#ttl-strategy)
- [Manual cache control](#manual-cache-control)
- [Disabling the cache](#disabling-the-cache)
- [Overriding TTLs](#overriding-ttls)
- [Rate Limiting](#rate-limiting)
- [Error Handling](#error-handling)
- [Multi-tenant / Explicit Config](#multi-tenant--explicit-config)
- [API Coverage](#api-coverage)
- [License](#license)
---
## Installation
Add `fred_api_client` to your `mix.exs` dependencies:
```elixir
def deps do
[
{:fred_api_client, "~> 0.1"}
]
end
```
Then fetch:
```bash
mix deps.get
```
Get a free FRED API key at <https://fred.stlouisfed.org/docs/api/api_key.html>.
---
## Configuration
### Minimal
```elixir
# 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
```elixir
# 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)
```elixir
# 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
```elixir
# 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](https://hex.pm/packages/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:
```elixir
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:
```elixir
config :fred_api_client, cache_enabled: false
```
**Per-config call** — pass an explicit config with `cache_enabled: false` (see
[Multi-tenant / Explicit Config](#multi-tenant--explicit-config) below):
```elixir
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:
```elixir
# 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:
```elixir
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{}}`:
```elixir
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 |
|---|---|---|
| `code` | `integer` | FRED API error code, or HTTP status code |
| `status` | `integer \| nil` | HTTP status (`nil` for timeout / network errors) |
| `message` | `string` | 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:
```elixir
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:
```elixir
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.Categories` | `get_category/2` | `GET /fred/category` | ✅ 24 h |
| | `get_children/2` | `GET /fred/category/children` | ✅ 24 h |
| | `get_related/2` | `GET /fred/category/related` | ✅ 24 h |
| | `get_series/2` | `GET /fred/category/series` | ✅ 24 h |
| | `get_tags/2` | `GET /fred/category/tags` | ✅ 24 h |
| | `get_related_tags/2` | `GET /fred/category/related_tags` | ✅ 24 h |
| `FredAPIClient.API.Releases` | `get_releases/2` | `GET /fred/releases` | ✅ 12 h |
| | `get_all_release_dates/2` | `GET /fred/releases/dates` | ✅ 1 h |
| | `get_release/2` | `GET /fred/release` | ✅ 12 h |
| | `get_release_dates/2` | `GET /fred/release/dates` | ✅ 1 h |
| | `get_release_series/2` | `GET /fred/release/series` | ✅ 12 h |
| | `get_release_sources/2` | `GET /fred/release/sources` | ✅ 24 h |
| | `get_release_tags/2` | `GET /fred/release/tags` | ✅ 12 h |
| | `get_release_related_tags/2` | `GET /fred/release/related_tags` | ✅ 12 h |
| | `get_release_tables/2` | `GET /fred/release/tables` | ✅ 12 h |
| `FredAPIClient.API.Series` | `get_series/2` | `GET /fred/series` | ✅ 24 h |
| | `get_categories/2` | `GET /fred/series/categories` | ✅ 24 h |
| | `get_observations/2` | `GET /fred/series/observations` | ⚠️ by freq |
| | `get_release/2` | `GET /fred/series/release` | ✅ 24 h |
| | `search/2` | `GET /fred/series/search` | ❌ |
| | `get_search_tags/2` | `GET /fred/series/search/tags` | ❌ |
| | `get_search_related_tags/2` | `GET /fred/series/search/related_tags` | ❌ |
| | `get_tags/2` | `GET /fred/series/tags` | ✅ 24 h |
| | `get_updates/2` | `GET /fred/series/updates` | ❌ |
| | `get_vintage_dates/2` | `GET /fred/series/vintagedates` | ✅ 6 h |
| `FredAPIClient.API.Sources` | `get_sources/2` | `GET /fred/sources` | ✅ 24 h |
| | `get_source/2` | `GET /fred/source` | ✅ 24 h |
| | `get_source_releases/2` | `GET /fred/source/releases` | ✅ 24 h |
| `FredAPIClient.API.Tags` | `get_tags/2` | `GET /fred/tags` | ✅ 12 h |
| | `get_related_tags/2` | `GET /fred/related_tags` | ✅ 12 h |
| | `get_series/2` | `GET /fred/tags/series` | ❌ |
| `FredAPIClient.API.Maps` | `get_shapes/2` | `GET /geofred/shapes/file` | ✅ 24 h |
| | `get_series_group/2` | `GET /geofred/series/group` | ✅ 24 h |
| | `get_series_data/2` | `GET /geofred/series/data` | ✅ 2 h |
| | `get_regional_data/2` | `GET /geofred/regional/data` | ✅ 2 h |
| `FredAPIClient.API.V2` | `get_release_observations/2` | `GET /fred/v2/release/observations` | ❌ |
⚠️ = frequency-aware: `m` → 1 h, `q`/`sa`/`a` → 6 h, `d`/`w`/`bw` → not cached
---
## License
MIT — see [LICENSE.txt](LICENSE.txt).