libfmp 1.4.0

Async, typed Rust client for the Financial Modeling Prep API
Documentation

libfmp

libfmp is an async, typed Rust client for the Financial Modeling Prep (FMP) data API.

The crate implements all 276 endpoint entries in the repository's captured API documentation oracle through transport-independent descriptors, typed queries, response models, and 271 Client methods. This is coverage of that pinned oracle, not a claim that every endpoint currently or historically offered by the provider is covered. The separately distributed synchronous Python client (fmp-py-sdk, imported as fmp) exposes all 271 methods as client.<domain>.<method>(...) namespaces generated from these signatures; see crates/fmp-py/README.md.

use std::env;

use libfmp::{Client, config::Authentication, types::Ticker};

#[tokio::main]
async fn main() -> Result<(), Box<dyn std::error::Error>> {
    let client = Client::builder()
        .authentication(Authentication::fmp_header(env::var("FMP_API_KEY")?))
        .build()?;
    let rows = client.quote_short(Ticker::new("AAPL")?).await?;
    println!("{rows:#?}");
    Ok(())
}

The Rust builder never reads the environment on its own. When the key lives in FMP_API_KEY, Authentication::fmp_header_from_env() returns the same header authentication as Option<Authentication> (None when the variable is unset, empty, or whitespace-only), so the env::var line above is the explicit form.

The documented bare JSON array remains a Vec<QuoteShort>; empty and multi-row responses keep their original shape.

Using a proxy

A proxy that issues its own keys and forwards to FMP, such as valet, needs two environment variables and no code change:

export FMP_API_KEY=vk_...
export FMP_BASE_URL=https://valet.bitbrew.app/fmp
use libfmp::{Client, types::Ticker};

# async fn example() -> Result<(), Box<dyn std::error::Error>> {
let client = Client::from_env()?;
let rows = client.quote_short(Ticker::new("AAPL")?).await?;
# let _ = rows;
# Ok(())
# }

Client::from_env() (or ClientBuilder::from_env()? to keep configuring) sends the key in the apikey header and keeps the base URL's path, so the quote endpoint goes to https://valet.bitbrew.app/fmp/stable/quote. Without FMP_BASE_URL it targets https://financialmodelingprep.com, and without FMP_API_KEY it returns a MissingCredential configuration error. Both values are trimmed and a blank value counts as unset. A plaintext http:// base URL to a non-loopback host is still refused unless the builder opts in with danger_allow_insecure_authentication(true). Client::builder() itself never reads either variable.

To read the proxy's cache status and remaining daily budget on a success, run the endpoint spec through execute_with_metadata. It returns a Response with the decoded data, the HTTP status and the allowlisted, redacted headers (the same allowlist as status errors, below):

use libfmp::{Client, endpoints::quote::{QuoteShortQuery, quote_short}, types::Ticker};

# async fn example() -> Result<(), Box<dyn std::error::Error>> {
let client = Client::from_env()?;
let spec = quote_short(QuoteShortQuery::new(Ticker::new("AAPL")?));
let response = client.execute_with_metadata(&spec).await?;
let cache = response.headers.get("x-proxy-cache"); // Some("HIT") or Some("MISS")
let remaining = response.headers.get("x-proxy-daily-remaining");
# let _ = (response.data, cache, remaining);
# Ok(())
# }

Every endpoint method has a public spec function under libfmp::endpoints, and execute returns the same data without the metadata. On failure nothing changes: the error already carries its headers.

To watch every call, endpoint methods included, register ClientBuilder::on_response(|info| ...). After each decoded success it gets a ResponseInfo with the endpoint id, status and the same allowlisted headers. It never fires on errors, cannot fail the call, and runs inline on the request task, so keep it cheap and non-blocking.

Custom router or proxy

Transport choices do not change endpoint code. A caller can select a custom base URL and path prefix, no auth, FMP header or query auth, bearer auth, custom secret header or query auth, and arbitrary default headers.

use std::env;

use libfmp::{Client, config::Authentication, types::Ticker};

# async fn example() -> Result<(), Box<dyn std::error::Error>> {
let client = Client::builder()
    .base_url(env::var("FMP_PROXY_BASE_URL")?)
    .path_prefix("router/stable")
    .authentication(Authentication::custom_header(
        "X-Proxy-Token",
        Some("Bearer ".to_owned()),
        env::var("FMP_PROXY_TOKEN")?,
    ))
    .default_header("X-Tenant", env::var("FMP_TENANT")?)
    .build()?;
let rows = client.quote_short(Ticker::new("AAPL")?).await?;
# let _ = rows;
# Ok(())
# }

A status error keeps an allowlisted set of response headers: Retry-After, X-Proxy-* and X-RateLimit-*. err.retry_after() returns the delay as a Duration (delta-seconds or HTTP-date, a past date is zero), err.proxy_error() returns the proxy's X-Proxy-Error reason, and err.headers() lists every retained header. No other header is stored, and headers never appear in the error's Display text.

Rust 1.96 or newer is supported. The repository pins Rust 1.96.0 for development and release validation. This project is available under the MIT License.