API documentation

Start in 60 seconds

Three steps to your first classification

1
Create a free account get your API key
2
Install the SDK pip install finsignals-api
3
Classify your first post client.classify(ticker="AAPL", body="Beats earnings, up 12% AH")

Full reference below ↓

FinSignals API

REST API for classifying financial social posts (Reddit-style text, news reactions, titles, and bodies) in a single request. Responses are structured JSON—no prompt engineering or unreliable free-form output.

Overview

The FinSignals API exposes two classification endpoints that return seven signals per item: sentiment, directionality, content quality, post type, relevance to ticker, author confidence, and a sarcasm flag. All categorical heads include a winning label plus per-class probabilities. The sarcasm signal is still experimental—treat it as directional until you validate it on your own data.

  • Single request — one post per HTTP call; 1.00 credit per call.
  • Batch request — up to 256 items per call; 1.00 credit for the first item in that HTTP request plus 0.70 credits for each additional item (total 1.00 + 0.70 × (n - 1) for n items). A new request starts at the first-item rate again.
  • Usage endpoints — check remaining credits and plan rate limits.

Pricing detail: Batch credits are charged once per POST /v1/classify/batch call, not per item at a flat rate. See Credits & pricing for the table and a worked example. Sarcasm is called out again under Classification heads.

Authentication

Every request (except GET /v1/health) must include your API key in the header:

X-API-Key: fs_your_key_here

Create an account on finsignals.ai, then copy your key from the API Keys page. Keys can be revoked at any time; use one key per environment (development vs production).

Base URL

Send all requests to:

https://api.finsignals.ai

In local development your site may use a different API host (for example with a hosts file or tunnel). The path is always rooted at this base—there is no version prefix beyond /v1/….

Credits & pricing

Usage is metered in credits. Plans include a monthly credit pool; optional pay-as-you-go balance can cover overages when enabled on your account.

OperationCredits
POST /v1/classify (one item)1.00
POST /v1/classify/batch (n items, one request)1.00 for the first item + 0.70 × (n – 1) for the rest

Example: a batch of 100 items in one request costs 1.00 + 99 × 0.70 = 70.30 credits. If you exceed your balance, the API responds with 402 Payment Required (see Errors).

Plan limits and billing are managed in the web dashboard and pricing—not via the public API.

Rate limits

Limits are applied per API key using a 60-second sliding window. Separate counters apply to single and batch endpoints. When exceeded, the API returns 429 Too Many Requests with retry guidance in the body.

Typical per-plan limits (requests per minute):

Plan/v1/classify/v1/classify/batch
Free52
Starter3015
Pro12060
Scale / Enterprise600300

Exact values for your key are returned by GET /v1/plan.

POST /v1/classify

Classify a single post. At least one of ticker, company_name, title, or body must be non-empty.

Request body

FieldTypeRequiredDescription
tickerstringno*Symbol (e.g. NVDA).
company_namestringno*Company name if helpful for context.
titlestringno*Post or headline title.
bodystringno*Main text of the post or article body.

* At least one field must be non-empty.

Text construction

Non-empty fields are concatenated with the tokenizer separator token in order: ticker, company_name, title, body. Supplying ticker alongside body improves relevance scoring for ticker-specific questions.

Example

curl -X POST "https://api.finsignals.ai/v1/classify" \
  -H "X-API-Key: fs_your_key_here" \
  -H "Content-Type: application/json" \
  -d '{
    "ticker": "AAPL",
    "title": "Earnings beat",
    "body": "Apple crushes Q3, stock up 5% after hours."
  }'

POST /v1/classify/batch

Classify multiple posts in one request.

Billing for the whole request is 1.00 credit for the first item plus 0.70 credits for each additional item. Each new batch call pays the first-item rate again for its first item—combine items into fewer requests when you can.

  • Maximum items: 256 per request
  • Maximum payload size: 128,000 characters total across all ticker, company_name, title, and body fields combined
  • Minimum: at least one item; each item must satisfy the same non-empty rule as single classify

Request body

{
  "items": [
    { "ticker": "TSLA", "body": "Delivery miss, stock down premarket." },
    { "ticker": "NVDA", "title": "Blackwell demand", "body": "Hyperscaler capex still strong." }
  ]
}

The outputs array in the response is in the same order as items.

GET /v1/usage & GET /v1/plan

Both require X-API-Key.

GET /v1/usage

Returns credit balances and usage breakdown by endpoint name. Example shape:

{
  "plan": "pro",
  "monthly_credits_total": 1000000,
  "monthly_credits_used": 12500.5,
  "monthly_credits_remaining": 987499.5,
  "payg_balance": 0,
  "usage_by_endpoint": [ ... ]
}

GET /v1/plan

Returns your plan name, rate limits for this key, monthly credit allocation, and batch item cap:

{
  "plan": "pro",
  "rate_limits": { "single": 120, "batch": 60 },
  "monthly_credits": 1000000,
  "batch_max_items": 256
}

GET /v1/health

Liveness probe for the API service. Does not require authentication. Use it for connectivity checks only; it does not validate your API key or model availability for your account.

Classification heads

Each item in outputs[] contains the following. Categorical heads include a label and a probability for each class. The sarcasm head is experimental (see the sarcasm section).

sentiment

Overall tone toward the asset or discussion.

  • Labels: positive, negative, neutral
  • Fields: label plus positive, negative, neutral (probabilities summing to ~1)

directionality

Stated or implied market stance (bullish / bearish / neutral).

  • Labels: bullish, bearish, neutral_direction
  • Fields: label and one probability per class (the API uses the key neutral_direction for the neutral bucket)

quality

Whether the content looks worth treating as signal versus noise or spam.

  • Labels: relevant, noise, spam

post_type

High-level content category for routing or downstream rules.

  • Labels: dd, news_reaction, technical_analysis, fundamentals, question, general

relevance_score

Scalar in [0, 1]. Higher means the text is more on-topic for the supplied context (e.g. ticker + body).

author_confidence

Scalar in [0, 1]. Higher suggests the author sounds more confident or definitive (model-estimated; not financial advice).

sarcasm

Boolean. true when the sarcasm head crosses the service’s threshold—useful for filtering ironic social posts that invert literal sentiment.

The sarcasm head is still experimental; treat labels as directional, not ground truth, until you validate them on your data.

Response envelope

Successful classify responses share this top-level structure:

{
  "model_version": "2.0.0",
  "request_id": "ledger-event-id",
  "credits_charged": 1.0,
  "endpoint_type": "single",
  "endpoint_name": "reddit_single",
  "outputs": [
    {
      "sentiment": { "label": "positive", "positive": 0.89, "negative": 0.04, "neutral": 0.07 },
      "directionality": { "label": "bullish", "bullish": 0.85, "bearish": 0.08, "neutral_direction": 0.07 },
      "quality": { "label": "relevant", "relevant": 0.78, "noise": 0.15, "spam": 0.07 },
      "post_type": {
        "label": "news_reaction",
        "dd": 0.02, "news_reaction": 0.71, "technical_analysis": 0.05,
        "fundamentals": 0.08, "question": 0.04, "general": 0.10
      },
      "relevance_score": 0.9137,
      "author_confidence": 0.58,
      "sarcasm": false
    }
  ]
}

For batch requests, endpoint_type is batch, endpoint_name is reddit_batch, and outputs has one entry per input item. credits_charged is the total for the request (1.00 + 0.70 × (number of items – 1)), not a per-item figure. model_version reflects the deployed model and may change as the API is updated.

Errors

Errors use HTTP status codes. Details are JSON in the response body when available.

HTTPMeaningTypical body
401Missing or invalid API key{"detail":{"error":"invalid_api_key","message":"..."}}
402Insufficient creditsdetail includes quota_exceeded, remaining credits, and dashboard links
422Validation error (empty fields, batch too large, etc.)FastAPI validation payload describing the field errors
429Rate limit exceededdetail includes rate_limit_exceeded, limit, window_seconds, retry_after

Always inspect the JSON detail object (or validation array) for machine-readable codes.

Code examples

cURL (single)

curl -sS -X POST "https://api.finsignals.ai/v1/classify" \
  -H "X-API-Key: $FINSIGNALS_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{"ticker":"NVDA","body":"NVDA to $200 EOY 🚀 DD inside"}' | jq .

Python

import os, requests

url = "https://api.finsignals.ai/v1/classify"
r = requests.post(
    url,
    headers={"X-API-Key": os.environ["FINSIGNALS_API_KEY"]},
    json={"ticker": "AAPL", "body": "Beat on revenue, guidance raised."},
    timeout=30,
)
r.raise_for_status()
data = r.json()
out = data["outputs"][0]
print(out["sentiment"]["label"], out["relevance_score"])

Node.js (fetch)

const res = await fetch("https://api.finsignals.ai/v1/classify", {
  method: "POST",
  headers: {
    "X-API-Key": process.env.FINSIGNALS_API_KEY,
    "Content-Type": "application/json",
  },
  body: JSON.stringify({ ticker: "TSLA", body: "Production ramp on track." }),
});
const data = await res.json();
if (!res.ok) throw new Error(JSON.stringify(data));
console.log(data.outputs[0].directionality.label);

PHP

<?php
$apiKey  = getenv('FINSIGNALS_API_KEY');
$payload = json_encode([
    'ticker' => 'MSFT',
    'body'   => 'Azure revenue up 21% YoY, cloud momentum continues.',
]);

$ch = curl_init('https://api.finsignals.ai/v1/classify');
curl_setopt_array($ch, [
    CURLOPT_RETURNTRANSFER => true,
    CURLOPT_POST           => true,
    CURLOPT_POSTFIELDS     => $payload,
    CURLOPT_HTTPHEADER     => [
        'X-API-Key: ' . $apiKey,
        'Content-Type: application/json',
    ],
    CURLOPT_TIMEOUT        => 30,
]);

$body   = curl_exec($ch);
$status = curl_getinfo($ch, CURLINFO_HTTP_CODE);
curl_close($ch);

if ($status !== 200) {
    throw new RuntimeException("API error $status: $body");
}

$data = json_decode($body, true);
$out  = $data['outputs'][0];
echo $out['sentiment']['label'] . ' / ' . $out['directionality']['label'] . PHP_EOL;

Go

package main

import (
	"bytes"
	"encoding/json"
	"fmt"
	"net/http"
	"os"
)

func main() {
	payload, _ := json.Marshal(map[string]string{
		"ticker": "AMZN",
		"body":   "AWS margin expansion drives record operating income.",
	})

	req, _ := http.NewRequest("POST", "https://api.finsignals.ai/v1/classify", bytes.NewBuffer(payload))
	req.Header.Set("X-API-Key", os.Getenv("FINSIGNALS_API_KEY"))
	req.Header.Set("Content-Type", "application/json")

	resp, err := http.DefaultClient.Do(req)
	if err != nil {
		panic(err)
	}
	defer resp.Body.Close()

	var result map[string]interface{}
	json.NewDecoder(resp.Body).Decode(&result)

	outputs := result["outputs"].([]interface{})
	first   := outputs[0].(map[string]interface{})
	sentiment := first["sentiment"].(map[string]interface{})
	fmt.Println(sentiment["label"])
}

Ruby

require 'net/http'
require 'json'
require 'uri'

uri     = URI('https://api.finsignals.ai/v1/classify')
payload = { ticker: 'GOOGL', body: 'Search ad revenue rebounds, AI overviews expanding.' }

http          = Net::HTTP.new(uri.host, uri.port)
http.use_ssl  = uri.scheme == 'https'
http.open_timeout = 10
http.read_timeout = 30

request = Net::HTTP::Post.new(uri.path)
request['X-API-Key']     = ENV['FINSIGNALS_API_KEY']
request['Content-Type']  = 'application/json'
request.body             = payload.to_json

response = http.request(request)
raise "API error #{response.code}: #{response.body}" unless response.code == '200'

data = JSON.parse(response.body)
out  = data['outputs'][0]
puts "#{out['sentiment']['label']} | score: #{out['relevance_score']}"

For interactive reference and schemas, see the OpenAPI document at https://api.finsignals.ai/openapi.json.

Best practices

  • Prefer batching for backfills and crawlers—tiered batch credits (1.00 + 0.70 per additional item per request) and fewer HTTP round-trips than singles.
  • Include ticker when the text discusses a specific symbol so relevance_score is meaningful.
  • Separate title and body when your source provides both; the model sees structured context.
  • Handle 402 and 429 with exponential backoff; read detail.options on 402 for account actions.
  • Do not log full API keys; store them in secrets managers or environment variables.

Request payloads are processed to produce classifications; retain only what your own policies require. Refer to your agreement and privacy policy for data retention and compliance.

Getting Reddit data

FinSignals classifies any text you send it. For Reddit, you need a way to fetch posts first. Two standard options:

Option 1 — Reddit API (free)

The official API via PRAW is free and sufficient for most use cases. Setup takes 2 minutes at reddit.com/prefs/apps. Rate limit: 60 requests/minute — enough for 6,000 posts/minute.

import praw

reddit = praw.Reddit(
    client_id="YOUR_CLIENT_ID",
    client_secret="YOUR_CLIENT_SECRET",
    user_agent="my-scanner/1.0 by YOUR_USERNAME",
)
posts = list(reddit.subreddit("wallstreetbets").hot(limit=100))

Option 2 — Third-party scraper (high-volume)

For pipelines processing 100K+ posts/day, a provider like Bright Data’s Reddit Scraper API handles collection. Pipe results directly into /v1/classify/batch. When to use: production at scale, or if you need historical data the API doesn’t surface.

Full pipeline: PRAW → FinSignals

import praw
import finsignals

reddit = praw.Reddit(client_id="...", client_secret="...", user_agent="scanner/1.0")
client = finsignals.Client("fs_your_key_here")

posts = list(reddit.subreddit("wallstreetbets").hot(limit=100))
items = [{"ticker": "NVDA", "title": p.title, "body": p.selftext[:1500]} for p in posts]

# Single batch call: 100 posts = 70.3 credits
results = client.classify_batch(items)

signals = [
    (p, r) for p, r in zip(posts, results)
    if r.quality.label == "relevant"
    and r.directionality.label == "bullish"
    and r.relevance_score > 0.65
    and not r.sarcasm
]
print(f"{len(signals)} signals from {len(posts)} posts")

Credit cost: 100 posts in one batch = 70.3 credits (free tier: ~14 full scans/month).

Full pipeline tutorial →