finlight

package module
v1.0.1 Latest Latest
Warning

This package is not in the latest version of its module.

Go to latest
Published: Aug 10, 2026 License: MIT Imports: 21 Imported by: 0

README

finlight-client-go

The official Go 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 Go 1.23 range-over-func iterators
  • 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
  • Minimal dependencies: only github.com/coder/websocket (itself dependency-free); logging via stdlib log/slog

Installation

go get github.com/callbk/finlight-client-go

Requires Go 1.23+.

Quick Start

package main

import (
	"context"
	"fmt"
	"log"
	"os"

	finlight "github.com/callbk/finlight-client-go"
)

func main() {
	client, err := finlight.New(finlight.Config{APIKey: os.Getenv("FINLIGHT_API_KEY")})
	if err != nil {
		log.Fatal(err)
	}

	resp, err := client.Articles.FetchArticles(context.Background(), finlight.GetArticlesParams{
		Query:    "nvidia",
		PageSize: 10,
	})
	if err != nil {
		log.Fatal(err)
	}
	for _, article := range resp.Articles {
		fmt.Printf("[%s] %s\n", article.Source, article.Title)
	}
}

REST API

Search articles

See the query language reference for the full Query syntax.

resp, err := client.Articles.FetchArticles(ctx, finlight.GetArticlesParams{
	Query:           `(ticker:AAPL OR ticker:NVDA) AND "Elon Musk"`,
	From:            "2024-01-01",
	To:              "2024-12-31",
	Language:        "en",
	Categories:      []finlight.Category{finlight.CategoryTechnology, finlight.CategoryMarkets},
	IncludeContent:  true,
	IncludeEntities: true,
	OrderBy:         finlight.OrderByPublishDate,
	Order:           finlight.SortOrderDesc,
	PageSize:        50,
})
Fetch an article by link
article, err := client.Articles.FetchArticleByLink(ctx, finlight.GetArticleByLinkParams{
	Link:           "https://example.com/some-article",
	IncludeContent: true,
})
List sources
sources, err := client.Sources.GetSources(ctx)

WebSocket Streaming

Streams are Go iterators: range over them, break to stop, cancel the context to shut down. Reconnects, keepalive, and rate-limit waits are handled internally.

Enhanced stream (sentiment, entities, deduplicated)
ctx, stop := signal.NotifyContext(context.Background(), os.Interrupt)
defer stop()

for article := range client.Websocket.Stream(ctx, finlight.GetArticlesWebSocketParams{
	Tickers:        []string{"AAPL", "NVDA"},
	IncludeContent: true,
}) {
	fmt.Printf("%s: %s (sentiment: %s)\n", article.Source, article.Title, article.Sentiment)
}
if err := client.Websocket.Err(); err != nil {
	log.Fatal(err) // e.g. finlight.ErrBlocked
}
Raw stream (lowest latency, no enrichment)
for article := range client.RawWebsocket.Stream(ctx, finlight.GetRawArticlesWebSocketParams{
	Sources: []string{"www.reuters.com"},
}) {
	fmt.Println(article.Title)
}
Custom WebSocket options
ws := finlight.NewWebSocketClient(cfg, finlight.WebSocketOptions{
	PingInterval:       25 * time.Second,
	PongTimeout:        60 * time.Second,
	BaseReconnectDelay: 500 * time.Millisecond,
	MaxReconnectDelay:  10 * time.Second,
	ConnectionLifetime: 115 * time.Minute,
	Takeover:           true, // take over an existing connection for this key
	OnClose: func(code int, reason string) {
		log.Printf("connection closed: %d %s", code, reason)
	},
})

Webhooks

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

http.HandleFunc("/webhook", func(w http.ResponseWriter, r *http.Request) {
	body, err := io.ReadAll(r.Body)
	if err != nil {
		w.WriteHeader(http.StatusBadRequest)
		return
	}
	article, err := finlight.ConstructWebhookEvent(
		body,
		r.Header.Get("X-Webhook-Signature"),
		os.Getenv("FINLIGHT_WEBHOOK_SECRET"),
		r.Header.Get("X-Webhook-Timestamp"), // "" if absent
	)
	if err != nil {
		w.WriteHeader(http.StatusBadRequest)
		return
	}
	fmt.Println("new article:", article.Title)
	w.WriteHeader(http.StatusOK)
})

Configuration

Field Default Description
APIKey — (required) Your finlight API key
BaseURL https://api.finlight.me REST base URL
WssURL wss://wss.finlight.me WebSocket URL (/raw appended for raw)
Timeout 5s Per-request timeout
RetryCount 3 Total request attempts
Logger silent *slog.Logger; pass slog.Default() to enable logs
HTTPClient derived from Timeout Optional *http.Client override

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

Logging

The client is silent by default. Pass any *slog.Logger to see what it does:

client, _ := finlight.New(finlight.Config{
	APIKey: apiKey,
	Logger: slog.New(slog.NewTextHandler(os.Stderr, &slog.HandlerOptions{Level: slog.LevelDebug})),
})

Error Handling

  • REST: retryable statuses (429, 500, 502, 503, 504) are retried with exponential backoff; other failures return *finlight.APIError (inspect with errors.As).
  • Streaming: Stream ends silently on context cancellation or consumer break. Check Err() afterwards — finlight.ErrBlocked means the server permanently rejected the connection.
  • Webhooks: verification failures return *finlight.WebhookVerificationError.

Testing

go test ./...                                     # unit tests (offline)
FINLIGHT_API_KEY=sk_... go test -tags integration ./...  # against the live API

License

MIT — see LICENSE.

Support

Documentation

Overview

Package finlight is the official Go client for the finlight.me API.

It provides access to the REST API (article search, article lookup by link, source listing), real-time article streaming over WebSocket, and webhook signature verification.

Quick start

client, err := finlight.New(finlight.Config{APIKey: os.Getenv("FINLIGHT_API_KEY")})
if err != nil {
	log.Fatal(err)
}
resp, err := client.Articles.FetchArticles(ctx, finlight.GetArticlesParams{Query: "nvidia"})

Streaming

WebSocket streaming uses a range-over-func iterator. The internal reconnect loop (exponential backoff, proactive connection rotation, rate-limit handling) is transparent to the consumer:

for article := range client.Websocket.Stream(ctx, finlight.GetArticlesWebSocketParams{}) {
	fmt.Println(article.Title)
}
if err := client.Websocket.Err(); err != nil {
	log.Fatal(err)
}

Stop streaming by breaking out of the loop or cancelling the context.

Index

Examples

Constants

This section is empty.

Variables

View Source
var ErrBlocked = errors.New("finlight: connection rejected by server (blocked)")

ErrBlocked is returned by the WebSocket clients' Err method when the server permanently rejected the connection (close code 1008). Reconnecting will not help; contact finlight support.

Functions

This section is empty.

Types

type APIError

type APIError struct {
	StatusCode int
	Status     string
	Body       []byte
}

APIError is returned for non-2xx REST responses (after retries).

func (*APIError) Error

func (e *APIError) Error() string

type Article

type Article struct {
	Link        string     `json:"link"`
	Title       string     `json:"title"`
	PublishDate FlexTime   `json:"publishDate"`
	Source      string     `json:"source"`
	Language    string     `json:"language"`
	Summary     string     `json:"summary,omitempty"`
	Images      []string   `json:"images,omitempty"`
	CreatedAt   *FlexTime  `json:"createdAt,omitempty"`
	RevisedDate *FlexTime  `json:"revisedDate,omitempty"`
	IsUpdate    *bool      `json:"isUpdate,omitempty"`
	Categories  []string   `json:"categories,omitempty"`
	Sentiment   string     `json:"sentiment,omitempty"`
	Confidence  *FlexFloat `json:"confidence,omitempty"`
	Content     string     `json:"content,omitempty"`
	Companies   []Company  `json:"companies,omitempty"`
	Countries   []string   `json:"countries,omitempty"`
}

Article is an enriched news article as returned by the REST API, the enhanced WebSocket stream, and webhooks.

func ConstructWebhookEvent

func ConstructWebhookEvent(rawBody []byte, signature, endpointSecret, timestamp string) (*Article, error)

ConstructWebhookEvent verifies a finlight webhook and returns the contained article.

rawBody must be the unmodified request body. signature is the value of the X-Webhook-Signature header (with or without the "sha256=" prefix). endpointSecret is your webhook secret from the finlight dashboard. timestamp is the X-Webhook-Timestamp header; pass "" if the webhook has none, otherwise it is included in the signed message and checked against a 5-minute replay tolerance.

Example
package main

import (
	"fmt"
	"io"
	"net/http"
	"os"

	finlight "github.com/callbk/finlight-client-go"
)

func main() {
	http.HandleFunc("/webhook", func(w http.ResponseWriter, r *http.Request) {
		body, err := io.ReadAll(r.Body)
		if err != nil {
			w.WriteHeader(http.StatusBadRequest)
			return
		}
		article, err := finlight.ConstructWebhookEvent(
			body,
			r.Header.Get("X-Webhook-Signature"),
			os.Getenv("FINLIGHT_WEBHOOK_SECRET"),
			r.Header.Get("X-Webhook-Timestamp"),
		)
		if err != nil {
			w.WriteHeader(http.StatusBadRequest)
			return
		}
		fmt.Println("new article:", article.Title)
		w.WriteHeader(http.StatusOK)
	})
}

type ArticleResponse

type ArticleResponse struct {
	Status   string    `json:"status"`
	Page     int       `json:"page"`
	PageSize int       `json:"pageSize"`
	Articles []Article `json:"articles"`
}

ArticleResponse is the paginated result of ArticleService.FetchArticles.

type ArticleService

type ArticleService struct {
	// contains filtered or unexported fields
}

ArticleService fetches financial news articles.

func (s *ArticleService) FetchArticleByLink(ctx context.Context, params GetArticleByLinkParams) (*Article, error)

FetchArticleByLink fetches a single article by its URL.

func (*ArticleService) FetchArticles

func (s *ArticleService) FetchArticles(ctx context.Context, params GetArticlesParams) (*ArticleResponse, error)

FetchArticles searches articles matching params and returns one result page.

type Category

type Category string

Category is an article category assigned by finlight's classification.

const (
	CategoryMarkets     Category = "markets"
	CategoryEconomy     Category = "economy"
	CategoryBusiness    Category = "business"
	CategoryPolitics    Category = "politics"
	CategoryGeopolitics Category = "geopolitics"
	CategoryRegulation  Category = "regulation"
	CategoryTechnology  Category = "technology"
	CategoryEnergy      Category = "energy"
	CategoryCommodities Category = "commodities"
	CategoryCrypto      Category = "crypto"
	CategoryHealth      Category = "health"
	CategoryClimate     Category = "climate"
	CategorySecurity    Category = "security"
)

All categories known to the API.

type Client

type Client struct {
	Articles     *ArticleService
	Sources      *SourceService
	Websocket    *WebSocketClient    // enhanced article stream, default options
	RawWebsocket *RawWebSocketClient // raw article stream, default options
}

Client is the entry point to the finlight API.

func New

func New(cfg Config) (*Client, error)

New validates cfg, applies defaults, and returns a ready-to-use client. For WebSocket clients with custom options use NewWebSocketClient or NewRawWebSocketClient.

Example
package main

import (
	"context"
	"fmt"
	"log"
	"os"

	finlight "github.com/callbk/finlight-client-go"
)

func main() {
	client, err := finlight.New(finlight.Config{APIKey: os.Getenv("FINLIGHT_API_KEY")})
	if err != nil {
		log.Fatal(err)
	}

	resp, err := client.Articles.FetchArticles(context.Background(), finlight.GetArticlesParams{
		Query:      "nvidia",
		PageSize:   10,
		Categories: []finlight.Category{finlight.CategoryTechnology},
	})
	if err != nil {
		log.Fatal(err)
	}
	for _, article := range resp.Articles {
		fmt.Println(article.Title)
	}
}

type Company

type Company struct {
	CompanyID      int        `json:"companyId"`
	Confidence     *FlexFloat `json:"confidence,omitempty"`
	Country        string     `json:"country,omitempty"`
	Exchange       string     `json:"exchange,omitempty"`
	Industry       string     `json:"industry,omitempty"`
	Sector         string     `json:"sector,omitempty"`
	Name           string     `json:"name"`
	Ticker         string     `json:"ticker"`
	ISIN           string     `json:"isin,omitempty"`
	OpenFIGI       string     `json:"openfigi,omitempty"`
	PrimaryListing *Listing   `json:"primaryListing,omitempty"`
	ISINs          []string   `json:"isins,omitempty"`
	OtherListings  []Listing  `json:"otherListings,omitempty"`
}

Company is an entity recognized in an article.

type Config

type Config struct {
	APIKey     string        // required
	BaseURL    string        // default "https://api.finlight.me"
	WssURL     string        // default "wss://wss.finlight.me"
	Timeout    time.Duration // per-request timeout, default 5s
	RetryCount int           // total request attempts, default 3
	Logger     *slog.Logger  // default: silent
	HTTPClient *http.Client  // optional override; default uses Timeout
}

Config configures the finlight client. Only APIKey is required; zero-valued fields fall back to the documented defaults.

type FlexFloat

type FlexFloat float64

FlexFloat is a float64 that also unmarshals from JSON strings ("0.95"). The API and webhooks deliver confidence values in both representations.

func (FlexFloat) Float64

func (f FlexFloat) Float64() float64

Float64 returns the value as a plain float64.

func (FlexFloat) MarshalJSON

func (f FlexFloat) MarshalJSON() ([]byte, error)

func (*FlexFloat) UnmarshalJSON

func (f *FlexFloat) UnmarshalJSON(data []byte) error

type FlexTime

type FlexTime struct {
	time.Time
}

FlexTime is a time.Time that unmarshals from the timestamp formats used by the finlight API (RFC 3339 with or without zone, space-separated, date-only).

func (FlexTime) MarshalJSON

func (t FlexTime) MarshalJSON() ([]byte, error)

func (*FlexTime) UnmarshalJSON

func (t *FlexTime) UnmarshalJSON(data []byte) error

type GetArticleByLinkParams

type GetArticleByLinkParams struct {
	Link            string // required
	IncludeContent  bool
	IncludeEntities bool
}

GetArticleByLinkParams are the parameters for ArticleService.FetchArticleByLink.

type GetArticlesParams

type GetArticlesParams struct {
	// Query supports the finlight query language, e.g.
	// `(ticker:AAPL OR ticker:NVDA) AND NOT source:www.reuters.com AND "Elon Musk"`.
	Query string `json:"query,omitempty"`
	// Deprecated: use Sources.
	Source              string     `json:"source,omitempty"`
	Sources             []string   `json:"sources,omitempty"`
	ExcludeSources      []string   `json:"excludeSources,omitempty"`
	OptInSources        []string   `json:"optInSources,omitempty"`
	From                string     `json:"from,omitempty"` // YYYY-MM-DD or ISO 8601
	To                  string     `json:"to,omitempty"`
	Language            string     `json:"language,omitempty"`
	Tickers             []string   `json:"tickers,omitempty"`
	IncludeEntities     bool       `json:"includeEntities,omitempty"`
	ExcludeEmptyContent bool       `json:"excludeEmptyContent,omitempty"`
	IncludeContent      bool       `json:"includeContent,omitempty"`
	OrderBy             OrderBy    `json:"orderBy,omitempty"`
	Order               SortOrder  `json:"order,omitempty"`
	PageSize            int        `json:"pageSize,omitempty"` // 1–1000
	Page                int        `json:"page,omitempty"`
	Countries           []string   `json:"countries,omitempty"`
	Categories          []Category `json:"categories,omitempty"`
}

GetArticlesParams are the search parameters for ArticleService.FetchArticles. Zero-valued fields are omitted from the request; the server applies its documented defaults.

type GetArticlesWebSocketParams

type GetArticlesWebSocketParams struct {
	Query          string   `json:"query,omitempty"`
	Sources        []string `json:"sources,omitempty"`
	ExcludeSources []string `json:"excludeSources,omitempty"`
	OptInSources   []string `json:"optInSources,omitempty"`
	Language       string   `json:"language,omitempty"`
	// Deprecated: use IncludeContent.
	Extended            bool       `json:"extended,omitempty"`
	Tickers             []string   `json:"tickers,omitempty"`
	IncludeEntities     bool       `json:"includeEntities,omitempty"`
	ExcludeEmptyContent bool       `json:"excludeEmptyContent,omitempty"`
	IncludeContent      bool       `json:"includeContent,omitempty"`
	Countries           []string   `json:"countries,omitempty"`
	Categories          []Category `json:"categories,omitempty"`
	IncludeUpdates      bool       `json:"includeUpdates,omitempty"`
}

GetArticlesWebSocketParams filter the enhanced WebSocket stream.

type GetRawArticlesWebSocketParams

type GetRawArticlesWebSocketParams struct {
	Query          string   `json:"query,omitempty"`
	Sources        []string `json:"sources,omitempty"`
	ExcludeSources []string `json:"excludeSources,omitempty"`
	OptInSources   []string `json:"optInSources,omitempty"`
	Language       string   `json:"language,omitempty"`
	IncludeUpdates bool     `json:"includeUpdates,omitempty"`
}

GetRawArticlesWebSocketParams filter the raw WebSocket stream.

type Listing

type Listing struct {
	Ticker          string `json:"ticker"`
	ExchangeCode    string `json:"exchangeCode"`
	ExchangeCountry string `json:"exchangeCountry"`
}

Listing describes one exchange listing of a company.

type OrderBy

type OrderBy string

OrderBy selects the sort field for article queries.

const (
	OrderByPublishDate OrderBy = "publishDate"
	OrderByCreatedAt   OrderBy = "createdAt"
	OrderByRevisedDate OrderBy = "revisedDate"
)

type RawArticle

type RawArticle struct {
	Link        string    `json:"link"`
	Title       string    `json:"title"`
	PublishDate FlexTime  `json:"publishDate"`
	Source      string    `json:"source"`
	Language    string    `json:"language"`
	Summary     string    `json:"summary,omitempty"`
	Images      []string  `json:"images,omitempty"`
	CreatedAt   *FlexTime `json:"createdAt,omitempty"`
	RevisedDate *FlexTime `json:"revisedDate,omitempty"`
	IsUpdate    *bool     `json:"isUpdate,omitempty"`
	Categories  []string  `json:"categories,omitempty"`
}

RawArticle is an unenriched article as delivered by the raw WebSocket stream (no sentiment, entities, or content).

type RawWebSocketClient

type RawWebSocketClient struct {
	// contains filtered or unexported fields
}

RawWebSocketClient streams unenriched articles in real time (no sentiment, entities, or content — lower latency). No duplicate suppression.

A client instance supports one active stream at a time.

func NewRawWebSocketClient

func NewRawWebSocketClient(cfg Config, opts WebSocketOptions) *RawWebSocketClient

NewRawWebSocketClient returns a raw streaming client with custom options. Client instances created by New use default options.

func (*RawWebSocketClient) Err

func (w *RawWebSocketClient) Err() error

Err reports why the last stream ended: nil after a normal shutdown, or a terminal error such as ErrBlocked.

func (*RawWebSocketClient) Stream

Stream connects to the raw finlight WebSocket and yields articles matching params. See WebSocketClient.Stream for the streaming semantics.

type SortOrder

type SortOrder string

SortOrder selects the sort direction for article queries.

const (
	SortOrderAsc  SortOrder = "ASC"
	SortOrderDesc SortOrder = "DESC"
)

type Source

type Source struct {
	Domain             string `json:"domain"`
	IsContentAvailable bool   `json:"isContentAvailable"`
	IsDefaultSource    bool   `json:"isDefaultSource"`
}

Source is a news source available through the API.

type SourceService

type SourceService struct {
	// contains filtered or unexported fields
}

SourceService lists the news sources available through the API.

func (*SourceService) GetSources

func (s *SourceService) GetSources(ctx context.Context) ([]Source, error)

GetSources returns all sources with their availability flags.

type WebSocketClient

type WebSocketClient struct {
	// contains filtered or unexported fields
}

WebSocketClient streams enriched articles in real time. Duplicate articles (same link within the last 10 deliveries) are suppressed.

A client instance supports one active stream at a time.

func NewWebSocketClient

func NewWebSocketClient(cfg Config, opts WebSocketOptions) *WebSocketClient

NewWebSocketClient returns a streaming client with custom options. Client instances created by New use default options.

func (*WebSocketClient) Err

func (w *WebSocketClient) Err() error

Err reports why the last stream ended: nil after a normal shutdown (context cancelled or consumer break), or a terminal error such as ErrBlocked.

func (*WebSocketClient) Stream

Stream connects to the finlight WebSocket and yields articles matching params. Reconnects (exponential backoff, proactive rotation, rate-limit waits) are handled internally. End the stream by breaking out of the range loop or cancelling ctx; afterwards check Err for a terminal failure.

Example
package main

import (
	"context"
	"fmt"
	"log"
	"os"
	"os/signal"

	finlight "github.com/callbk/finlight-client-go"
)

func main() {
	client, err := finlight.New(finlight.Config{APIKey: os.Getenv("FINLIGHT_API_KEY")})
	if err != nil {
		log.Fatal(err)
	}

	// Stop streaming on Ctrl+C; breaking out of the loop works too.
	ctx, stop := signal.NotifyContext(context.Background(), os.Interrupt)
	defer stop()

	for article := range client.Websocket.Stream(ctx, finlight.GetArticlesWebSocketParams{
		Tickers: []string{"AAPL", "NVDA"},
	}) {
		fmt.Println(article.Title)
	}
	if err := client.Websocket.Err(); err != nil {
		log.Fatal(err)
	}
}

type WebSocketOptions

type WebSocketOptions struct {
	PingInterval       time.Duration // application-level ping cadence, default 25s
	PongTimeout        time.Duration // force reconnect when no pong arrives, default 60s
	BaseReconnectDelay time.Duration // first reconnect backoff, default 500ms
	MaxReconnectDelay  time.Duration // backoff cap, default 10s
	ConnectionLifetime time.Duration // proactive rotation, default 115min (under the 2h server cap)
	Takeover           bool          // take over an existing connection for the same key
	OnClose            func(code int, reason string)
}

WebSocketOptions tune the streaming clients. Zero values fall back to the documented defaults, which match the TypeScript and Python clients.

type WebhookVerificationError

type WebhookVerificationError struct {
	// contains filtered or unexported fields
}

WebhookVerificationError is returned by ConstructWebhookEvent when a webhook fails signature, timestamp, or payload validation.

func (*WebhookVerificationError) Error

func (e *WebhookVerificationError) Error() string

Jump to

Keyboard shortcuts

? : This menu
/ : Search site
f or F : Jump to
y or Y : Canonical URL