Three steps to your first classification
pip install finsignals-apiclient.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.
| Operation | Credits |
|---|---|
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 |
|---|---|---|
| Free | 5 | 2 |
| Starter | 30 | 15 |
| Pro | 120 | 60 |
| Scale / Enterprise | 600 | 300 |
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
| Field | Type | Required | Description |
|---|---|---|---|
ticker | string | no* | Symbol (e.g. NVDA). |
company_name | string | no* | Company name if helpful for context. |
title | string | no* | Post or headline title. |
body | string | no* | 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, andbodyfields 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:
labelpluspositive,negative,neutral(probabilities summing to ~1)
directionality
Stated or implied market stance (bullish / bearish / neutral).
- Labels:
bullish,bearish,neutral_direction - Fields:
labeland one probability per class (the API uses the keyneutral_directionfor 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.
| HTTP | Meaning | Typical body |
|---|---|---|
| 401 | Missing or invalid API key | {"detail":{"error":"invalid_api_key","message":"..."}} |
| 402 | Insufficient credits | detail includes quota_exceeded, remaining credits, and dashboard links |
| 422 | Validation error (empty fields, batch too large, etc.) | FastAPI validation payload describing the field errors |
| 429 | Rate limit exceeded | detail 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
tickerwhen the text discusses a specific symbol sorelevance_scoreis meaningful. - Separate title and body when your source provides both; the model sees structured context.
- Handle 402 and 429 with exponential backoff; read
detail.optionson 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).