Authentication
All API requests require an API key passed via the X-API-Key header. Keys are prefixed with pk_.
X-API-Key: pk_your_key_here
Contact us to request an API key. Keep your key secret — it cannot be regenerated.
curl -H "X-API-Key: pk_your_key" https://stacksja.com/api/v1/public/stock/GK
Rate Limits
Each API key has a rate limit (default: 60 requests per minute). If exceeded, you'll receive a 429 response. Wait 60 seconds before retrying.
For real-time data, use the SSE streaming endpoints instead of polling — a single connection replaces all periodic requests.
Market Data
/stock/{symbol}
Get the latest trading data for a single stock. Accepts a ticker symbol or company name (case-insensitive).
curl -H "X-API-Key: pk_your_key" https://stacksja.com/api/v1/public/stock/GK curl -H "X-API-Key: pk_your_key" https://stacksja.com/api/v1/public/stock/GraceKennedy curl -H "X-API-Key: pk_your_key" https://stacksja.com/api/v1/public/stock/Wisynco
{
"symbol": "GK",
"company_name": "GraceKennedy Limited",
"market": "main",
"trade_date": "2026-03-06",
"closing_price": "72.20",
"last_traded_price": "72.17",
"price_change": "0.03",
"change_percent": 0.04,
"volume": "6,749",
"closing_bid": "72.17",
"closing_ask": "72.23",
"todays_range": "72.17 - 72.24",
"week_range_52": "67.01 - 75.00"
}
/quote?name={query}
Look up a stock by ticker or company name. Best for multi-word names like "First Rock" or "Caribbean Cement".
curl -H "X-API-Key: pk_your_key" "https://stacksja.com/api/v1/public/quote?name=First+Rock" curl -H "X-API-Key: pk_your_key" "https://stacksja.com/api/v1/public/quote?name=Caribbean+Cement"
/stocks
Get all stocks for the latest trading day.
| market | Optional. Filter by main or junior |
curl -H "X-API-Key: pk_your_key" "https://stacksja.com/api/v1/public/stocks?market=main"
/stock/{symbol}/history
Get historical daily price data for a stock.
| limit | Optional. Number of trading days (1–365, default 30) |
curl -H "X-API-Key: pk_your_key" "https://stacksja.com/api/v1/public/stock/NCBFG/history?limit=90"
/search?q={query}
Search for stocks by ticker or company name. Returns all matches.
curl -H "X-API-Key: pk_your_key" "https://stacksja.com/api/v1/public/search?q=jamaica"
/pe-ratios
Get P/E ratios, trailing twelve month EPS, and shares outstanding for all stocks with financial data. Sorted by P/E ratio (lowest first).
curl -H "X-API-Key: pk_your_key" "https://stacksja.com/api/v1/public/pe-ratios"
{
"count": 35,
"pe_ratios": [
{
"symbol": "JMMBGL",
"company_name": "JMMB Group Limited",
"pe_ratio": 5.2,
"ttm_eps": 3.85,
"shares_outstanding": 1920000000
},
...
]
}
Financial Statements
/stock/{symbol}/financials
Get financial statements — income statement, balance sheet, and valuation data. Returns all confirmed periods grouped by fiscal year.
curl -H "X-API-Key: pk_your_key" "https://stacksja.com/api/v1/public/stock/GK/financials"
{
"symbol": "GK",
"company_name": "GraceKennedy Limited",
"count": 3,
"valuation": {
"pe_ratio": 12.5,
"ttm_eps": 5.78,
"shares_outstanding": 995040000
},
"financials": [
{
"fiscal_year": 2025,
"period_type": "Q3",
"statement_type": "unaudited",
"currency": "JMD",
"periods": {
"current": {
"revenue": 120500000000,
"gross_profit": 42000000000,
"net_income": 8500000000,
"total_assets": 350000000000,
"total_equity": 95000000000,
"eps_basic": 8.54
},
"comparative": {
"revenue": 110200000000,
"net_income": 7200000000
}
}
}
]
}
Financial values are in absolute currency units (not thousands/millions). Available line items: revenue, cost_of_sales, gross_profit, operating_expenses, operating_profit, finance_costs, profit_before_tax, tax_expense, net_income, total_assets, total_liabilities, total_equity, cash_and_equivalents, eps_basic, eps_diluted, dividends_per_share.
/stock/{symbol}/documents
List available financial statement PDFs. Each includes a download URL. No API key required — this endpoint is freely accessible.
curl "https://stacksja.com/api/v1/public/stock/GK/documents"
{
"symbol": "GK",
"company_name": "GraceKennedy Limited",
"count": 4,
"documents": [
{
"id": 42,
"fiscal_year": 2025,
"period_type": "Q3",
"statement_type": "unaudited",
"currency": "JMD",
"filename": "GK-Q3-2025.pdf",
"confirmed_at": "2026-03-01T12:00:00",
"pdf_url": "/api/v1/public/documents/42/pdf"
}
]
}
/documents/{id}/pdf
Download the original PDF financial statement. Use the id from the /documents endpoint. No API key required.
curl "https://stacksja.com/api/v1/public/documents/42/pdf" -o statement.pdf
Directors & Ownership
/stock/{symbol}/directors
Get board of directors, top shareholders, and cross-board connections for a stock. Director IDs can be used with the PATCH endpoint to update info.
curl -H "X-API-Key: pk_your_key" "https://stacksja.com/api/v1/public/stock/GK/directors"
{
"symbol": "GK",
"company_name": "GraceKennedy Limited",
"directors": [
{
"id": 1,
"name": "Prof. Gordon V. Shirley",
"title": "Chairman",
"director_type": "chairman",
"appointed_date": "",
"bio": ""
}
],
"shareholders": [
{
"shareholder_name": "GraceKennedy (Pension) Ltd",
"shares_held": 109000000,
"ownership_percent": 10.9,
"shareholder_type": "institutional",
"as_of_date": "2024-12-31"
}
],
"cross_board_connections": {
"Prof. Gordon V. Shirley": [
{
"company_name": "Bank of Jamaica",
"role": "Governor",
"is_jse_listed": 0,
"jse_symbol": ""
}
]
}
}
/directors/{id}
Update director information — add bios, correct titles, or update types. All fields are optional; only include what you want to change. Changes are instantly pushed to all SSE subscribers.
| bio | Director biography (string) |
| title | Role title, e.g. "CEO", "Chairman" (string) |
| director_type | One of: chairman, executive, non-executive, independent |
| appointed_date | Date string, e.g. "2020-01-15" |
curl -X PATCH \
-H "X-API-Key: pk_your_key" \
-H "Content-Type: application/json" \
-d '{"bio": "20+ years in financial services...", "appointed_date": "2019-03-01"}' \
"https://stacksja.com/api/v1/public/directors/1"
{
"status": "updated",
"director": {
"id": 1,
"symbol": "GK",
"name": "Prof. Gordon V. Shirley",
"title": "Chairman",
"director_type": "chairman",
"appointed_date": "2019-03-01",
"bio": "20+ years in financial services..."
}
}
Live Streaming (Server-Sent Events)
Open a single persistent connection and receive every data change the moment it happens. No polling. No wasted requests. The server pushes updates to you.
SSE (Server-Sent Events) is a standard HTTP protocol supported by every browser and every language. The connection auto-reconnects if dropped.
Why streaming instead of polling?
| Polling: 60 requests/min to check for changes | Streaming: 1 connection, instant updates |
| Most responses are "no change" | Only fires when data actually changes |
| Eats your rate limit | Doesn't count against rate limit |
| Delays up to your poll interval | Sub-second delivery |
/stream?api_key=pk_your_key
Stream all market updates — every price change, every new financial statement, every director update, every news article. API key is passed as a query parameter (not header) because the browser's EventSource API doesn't support custom headers.
const source = new EventSource(
"https://stacksja.com/api/v1/public/stream?api_key=pk_your_key"
);
source.addEventListener("connected", (e) => {
console.log("Stream active:", JSON.parse(e.data).message);
});
source.addEventListener("price_update", (e) => {
const data = JSON.parse(e.data);
for (const stock of data.stocks) {
console.log(`${stock.symbol}: $${stock.last_traded_price} (${stock.change_percent}%)`);
}
});
source.addEventListener("document_added", (e) => {
const data = JSON.parse(e.data);
console.log(`New statement: ${data.symbol} ${data.period_type} ${data.fiscal_year}`);
console.log(`${data.line_items_count} line items:`, data.line_items);
});
source.addEventListener("director_updated", (e) => {
const { director } = JSON.parse(e.data);
console.log(`Director updated: ${director.name} @ ${director.symbol}`);
});
source.addEventListener("ownership_updated", (e) => {
const data = JSON.parse(e.data);
console.log(`${data.symbol}: ${data.directors.length} directors, ${data.shareholders.length} shareholders`);
});
source.addEventListener("news_updated", (e) => {
const data = JSON.parse(e.data);
console.log(`${data.new_articles} new articles`);
});
// Handle errors / reconnect
source.onerror = () => {
console.log("Connection lost, auto-reconnecting...");
};
curl -N "https://stacksja.com/api/v1/public/stream?api_key=pk_your_key"
import requests
import json
resp = requests.get(
"https://stacksja.com/api/v1/public/stream",
params={"api_key": "pk_your_key"},
stream=True,
timeout=None,
)
event_type = None
for line in resp.iter_lines(decode_unicode=True):
if not line:
event_type = None
continue
if line.startswith("event: "):
event_type = line[7:]
elif line.startswith("data: "):
payload = json.loads(line[6:])
print(f"[{event_type}]", payload)
import EventSource from "eventsource"; // npm install eventsource
const source = new EventSource(
"https://stacksja.com/api/v1/public/stream?api_key=pk_your_key"
);
source.addEventListener("price_update", (e) => {
const data = JSON.parse(e.data);
console.log(`${data.count} stocks updated`);
});
source.addEventListener("document_added", (e) => {
const data = JSON.parse(e.data);
console.log(`New: ${data.symbol} ${data.period_type} ${data.fiscal_year}`);
});
/stream/{symbol}?api_key=pk_your_key
Stream updates for a single stock only. Same event types, filtered to that symbol. Use this when you only care about one company.
const source = new EventSource(
"https://stacksja.com/api/v1/public/stream/GK?api_key=pk_your_key"
);
source.addEventListener("price_update", (e) => {
const data = JSON.parse(e.data);
// Only GK updates arrive here
document.getElementById("price").textContent = data.stocks[0].last_traded_price;
});
source.addEventListener("document_added", (e) => {
// New GK financial statement just published
const data = JSON.parse(e.data);
showNotification(`New ${data.statement_type} statement: ${data.period_type} ${data.fiscal_year}`);
});
source.addEventListener("director_updated", (e) => {
// A GK director's info was updated
const { director } = JSON.parse(e.data);
updateDirectorCard(director);
});
All Event Types
Every mutation in the system broadcasts an event. Here is the complete list:
| Event | Description & Payload |
|---|---|
connected | Sent immediately when the stream opens. Payload: {"channel": "*", "message": "..."} |
| Market Data | |
price_update | Stock prices updated. Payload: market, trade_date, count, stocks[] (each with symbol, last_traded_price, closing_price, price_change, change_percent, volume) |
trade_update | Individual trades executed. Payload: symbol, market, trade_date, trades[] (time, price, quantity), buy_orders[], sell_orders[] |
index_update | Market index changed. Payload: market, trade_date, indices[] (name, value, change, change_percent, volume) |
stock_removed | A stock record was deleted. Payload: id, symbol, market, trade_date |
| Financial Documents | |
document_added | New financial statement confirmed and published. Payload includes symbol, period_type, fiscal_year, statement_type, pdf_filename, confirmed_at, and the complete line_items[] array so you don't need a follow-up API call. |
document_updated | PDF file replaced on existing statement. Payload: id, symbol, period_type, fiscal_year, pdf_filename |
document_removed | Statement deleted. Payload: id, symbol, period_type, fiscal_year, statement_type |
financials_updated | Line items edited on a confirmed statement. Payload includes symbol, period_type, fiscal_year, statement_type, and the updated line_items[] array. |
| Ownership & Directors | |
ownership_updated | Directors and shareholders updated for a company. Payload: symbol, directors[] (full array), shareholders[] (full array) |
ownership_removed | Ownership filing deleted. Payload: filing_id, symbol, fiscal_year, period_type |
director_updated | Director info changed via API (bio, title, etc). Payload: director object with id, symbol, name, title, director_type, appointed_date, bio. |
| News | |
news_updated | New articles scraped. Payload: new_articles (count), articles[] (each with id, source, title, url, summary, image_url, published_at) |
news_cleared | News articles purged. Payload: deleted (count) |
Example SSE Event (Raw)
This is what the raw HTTP response looks like. Each event has an event: line and a data: line with JSON.
event: price_update
data: {"market":"main","trade_date":"2026-03-08","count":42,"stocks":[{"symbol":"GK","market":"main","trade_date":"2026-03-08","last_traded_price":"125.00","closing_price":"123.50","price_change":"1.50","change_percent":"1.21","volume":"45000"}]}
event: document_added
data: {"id":42,"symbol":"GK","period_type":"Q3","fiscal_year":2025,"statement_type":"unaudited","pdf_filename":"GK-Q3-2025.pdf","confirmed_at":"2026-03-08T12:00:00","line_items_count":12,"line_items":[{"line_item":"revenue","value":120500000000,"period_label":"Q3 2025","period_year":2025}]}
event: director_updated
data: {"director":{"id":1,"symbol":"GK","name":"Prof. Gordon V. Shirley","title":"Chairman","director_type":"chairman","appointed_date":"2019-03-01","bio":"Distinguished academic and corporate leader..."}}
: keepalive
The server sends a keepalive comment every 30 seconds to prevent proxy/client timeouts. These are ignored by EventSource automatically.
Error Codes
| Code | Meaning |
|---|---|
| 401 | Missing or invalid API key |
| 400 | Bad request (e.g. invalid director_type value) |
| 404 | Stock symbol or resource not found |
| 429 | Rate limit exceeded — wait 60 seconds before retrying |
{"detail": "Invalid or revoked API key"}
Quick Start Examples
Python
import requests
API_KEY = "pk_your_key_here"
BASE = "https://stacksja.com/api/v1/public"
headers = {"X-API-Key": API_KEY}
# Get latest price for a stock
stock = requests.get(f"{BASE}/stock/GK", headers=headers).json()
print(f"{stock['symbol']}: ${stock['closing_price']}")
# Get all main market stocks
all_stocks = requests.get(f"{BASE}/stocks?market=main", headers=headers).json()
for s in all_stocks["stocks"]:
print(f" {s['symbol']}: ${s['closing_price']}")
# Get financial statements
financials = requests.get(f"{BASE}/stock/GK/financials", headers=headers).json()
for f in financials["financials"]:
revenue = f["periods"]["current"].get("revenue", "N/A")
print(f" {f['period_type']} {f['fiscal_year']}: Revenue {revenue}")
# Update a director's bio
import json
resp = requests.patch(
f"{BASE}/directors/1",
headers={**headers, "Content-Type": "application/json"},
data=json.dumps({"bio": "20+ years in financial services..."})
)
print(resp.json())
JavaScript (Browser)
const API_KEY = "pk_your_key_here";
const BASE = "https://stacksja.com/api/v1/public";
const headers = { "X-API-Key": API_KEY };
// Fetch a stock
const res = await fetch(`${BASE}/stock/GK`, { headers });
const stock = await res.json();
console.log(`${stock.symbol}: $${stock.closing_price}`);
// Stream live updates (no polling!)
const stream = new EventSource(`${BASE}/stream?api_key=${API_KEY}`);
stream.addEventListener("price_update", (e) => {
const data = JSON.parse(e.data);
data.stocks.forEach(s => updatePrice(s.symbol, s.last_traded_price));
});
Google Sheets (Apps Script)
function getJSEPrice(symbol) {
var url = "https://stacksja.com/api/v1/public/stock/" + symbol;
var options = { headers: { "X-API-Key": "pk_your_key_here" } };
var response = UrlFetchApp.fetch(url, options);
var data = JSON.parse(response.getContentText());
return parseFloat(data.closing_price);
}
// Use in a cell: =getJSEPrice("GK")
Python SSE Streaming (Real-time)
import requests
import json
# Connect once, receive all updates forever
resp = requests.get(
"https://stacksja.com/api/v1/public/stream",
params={"api_key": "pk_your_key"},
stream=True,
timeout=None,
)
event_type = None
for line in resp.iter_lines(decode_unicode=True):
if not line:
event_type = None
continue
if line.startswith(":"):
continue # keepalive comment
if line.startswith("event: "):
event_type = line[7:]
elif line.startswith("data: "):
payload = json.loads(line[6:])
if event_type == "price_update":
for s in payload["stocks"]:
print(f" {s['symbol']}: ${s['last_traded_price']}")
elif event_type == "document_added":
print(f" New statement: {payload['symbol']} {payload['period_type']} {payload['fiscal_year']}")
elif event_type == "director_updated":
d = payload["director"]
print(f" Director: {d['name']} - {d.get('bio', '')[:50]}...")
elif event_type == "news_updated":
print(f" {payload['new_articles']} new articles")