finlight-client 0.1.1

Official Rust client for the finlight.me API — financial news with sentiment analysis, entity recognition, and real-time streaming
Documentation

finlight-client (Rust)

crates.io docs.rs CI

The official Rust client for the finlight.me API — financial news with sentiment analysis, entity recognition, and real-time streaming.

📚 Full API documentation: docs.finlight.me

Features

  • REST API: search articles, fetch single articles by link, list sources
  • Real-time streaming: enhanced and raw article streams over WebSocket, exposed as async Streams
  • Resilient by default: request retries with exponential backoff; WebSocket auto-reconnect, keepalive with pong watchdog, proactive connection rotation, and rate-limit handling
  • Webhook support: HMAC-SHA256 signature verification with replay protection
  • Async: built on tokio, reqwest, and tokio-tungstenite (rustls, no OpenSSL)

Requires Rust 1.85+.

Installation

cargo add finlight-client tokio futures-util

Quick Start

use finlight_client::{Client, Config, GetArticlesParams};

#[tokio::main]
async fn main() -> Result<(), Box<dyn std::error::Error>> {
    let client = Client::new(Config::new(std::env::var("FINLIGHT_API_KEY")?))?;

    let resp = client
        .articles
        .fetch_articles(&GetArticlesParams {
            query: Some("nvidia".into()),
            page_size: Some(10),
            ..Default::default()
        })
        .await?;
    for article in resp.articles {
        println!("[{}] {}", article.source, article.title);
    }
    Ok(())
}

REST API

Search articles

See the query language reference for the full query syntax.

use finlight_client::{Category, GetArticlesParams, OrderBy, SortOrder};

let resp = client
    .articles
    .fetch_articles(&GetArticlesParams {
        query: Some(r#"(ticker:AAPL OR ticker:NVDA) AND "Elon Musk""#.into()),
        from: Some("2024-01-01".into()),
        to: Some("2024-12-31".into()),
        language: Some("en".into()),
        categories: Some(vec![Category::Technology, Category::Markets]),
        include_content: Some(true),
        include_entities: Some(true),
        order_by: Some(OrderBy::PublishDate),
        order: Some(SortOrder::Desc),
        page_size: Some(50),
        ..Default::default()
    })
    .await?;

Fetch an article by link

use finlight_client::GetArticleByLinkParams;

let article = client
    .articles
    .fetch_article_by_link(&GetArticleByLinkParams {
        link: "https://example.com/some-article".into(),
        include_content: true,
        include_entities: false,
    })
    .await?;

List sources

let sources = client.sources.get_sources().await?;

WebSocket Streaming

Streams implement futures::Stream: consume them with StreamExt::next, drop them to disconnect. Reconnects, keepalive, and rate-limit waits are handled internally. An Err item is terminal — the stream ends after yielding it.

Enhanced stream (sentiment, entities, deduplicated)

use finlight_client::GetArticlesWebSocketParams;
use futures_util::StreamExt;

let mut stream = client.websocket.stream(GetArticlesWebSocketParams {
    tickers: Some(vec!["AAPL".into(), "NVDA".into()]),
    include_content: Some(true),
    ..Default::default()
});
while let Some(item) = stream.next().await {
    let article = item?; // e.g. Error::Blocked
    println!(
        "{}: {} (sentiment: {})",
        article.source,
        article.title,
        article.sentiment.as_deref().unwrap_or("-")
    );
}

Raw stream (lowest latency, no enrichment)

use finlight_client::GetRawArticlesWebSocketParams;

let mut stream = client.raw_websocket.stream(GetRawArticlesWebSocketParams {
    sources: Some(vec!["www.reuters.com".into()]),
    ..Default::default()
});
while let Some(item) = stream.next().await {
    println!("{}", item?.title);
}

Custom WebSocket options

use std::sync::Arc;
use std::time::Duration;
use finlight_client::{WebSocketClient, WebSocketOptions};

let ws = WebSocketClient::new(config, WebSocketOptions {
    ping_interval: Duration::from_secs(25),
    pong_timeout: Duration::from_secs(60),
    base_reconnect_delay: Duration::from_millis(500),
    max_reconnect_delay: Duration::from_secs(10),
    connection_lifetime: Duration::from_secs(115 * 60),
    takeover: true, // take over an existing connection for this key
    on_close: Some(Arc::new(|code, reason| {
        eprintln!("connection closed: {code} {reason}");
    })),
});

Webhooks

Verify incoming webhooks with your endpoint secret from the finlight dashboard:

use finlight_client::construct_webhook_event;

// raw_body: unmodified request body bytes
// signature: X-Webhook-Signature header (with or without "sha256=" prefix)
// timestamp: X-Webhook-Timestamp header, None if absent
let article = construct_webhook_event(
    &raw_body,
    &signature,
    &std::env::var("FINLIGHT_WEBHOOK_SECRET")?,
    timestamp.as_deref(),
)?;
println!("new article: {}", article.title);

Configuration

Field Default Description
api_key — (required) Your finlight API key
base_url https://api.finlight.me REST base URL
wss_url wss://wss.finlight.me WebSocket URL (/raw appended for raw)
timeout 5s Per-request timeout
retry_count 3 Total request attempts

WebSocket option defaults: ping every 25s, pong timeout 60s, reconnect backoff 500ms → 10s, proactive rotation after 115min.

Logging

The client logs through tracing and is silent unless you install a subscriber:

tracing_subscriber::fmt()
    .with_env_filter("finlight_client=debug")
    .init();

Error Handling

  • REST: retryable statuses (429, 500, 502, 503, 504) are retried with exponential backoff; other failures return Error::Api { status, body }.
  • Streaming: the stream ends silently when you drop it or the server preempts the connection. An Err item is terminal — Error::Blocked means the server permanently rejected the connection.
  • Webhooks: verification failures return WebhookVerificationError.

Testing

cargo test                                            # unit + protocol tests (offline)
FINLIGHT_API_KEY=sk_... cargo test -- --ignored       # against the live API
FINLIGHT_API_KEY=sk_... cargo run --example smoke     # one-shot smoke run

License

MIT — see LICENSE.

Support