*{margin:0;padding:0;box-sizing:border-box} :root{--bg:#0a0a0a;--surface:#141414;--card:#1a1a1a;--border:#262626;--text:#e2e8f0;--muted:#9ca3af;--green:#10b981;--red:#ef4444;--blue:#3b82f6;--yellow:#f59e0b;--orange:#e67e22;--purple:#8b5cf6} body{font-family:-apple-system,BlinkMacSystemFont,'Segoe UI',Roboto,sans-serif;background:var(--bg);color:var(--text);line-height:1.6;-webkit-font-smoothing:antialiased} a{color:var(--blue);text-decoration:none} a:hover{text-decoration:underline} code{font-family:'SF Mono',Monaco,Consolas,'Courier New',monospace} /* Layout */ .top-bar{background:var(--surface);border-bottom:1px solid var(--border);padding:16px 0;position:sticky;top:0;z-index:100;backdrop-filter:blur(12px);-webkit-backdrop-filter:blur(12px)} .top-bar-inner{max-width:1100px;margin:0 auto;padding:0 24px;display:flex;align-items:center;justify-content:space-between} .logo{display:flex;align-items:center;gap:10px;font-size:1.1rem;font-weight:700;color:var(--text);text-decoration:none} .logo:hover{text-decoration:none} .logo span{background:linear-gradient(135deg,var(--green),var(--blue));-webkit-background-clip:text;-webkit-text-fill-color:transparent} .back-link{font-size:0.82rem;color:var(--muted);display:flex;align-items:center;gap:6px} .back-link:hover{color:var(--text);text-decoration:none} .container{max-width:1100px;margin:0 auto;padding:0 24px} .hero{padding:60px 0 48px;text-align:center} .hero h1{font-size:2.2rem;font-weight:800;margin-bottom:12px;letter-spacing:-0.02em} .hero p{font-size:1.05rem;color:var(--muted);max-width:600px;margin:0 auto 32px} .hero-badges{display:flex;gap:12px;justify-content:center;flex-wrap:wrap} .badge{font-size:0.75rem;font-weight:600;padding:6px 14px;border-radius:20px;background:var(--card);border:1px solid var(--border)} .badge code{font-size:0.75rem} /* Sidebar nav */ .docs-layout{display:flex;gap:48px;padding:48px 0 80px} .docs-sidebar{width:220px;flex-shrink:0;position:sticky;top:80px;align-self:flex-start;max-height:calc(100vh - 100px);overflow-y:auto} .docs-sidebar::-webkit-scrollbar{width:4px} .docs-sidebar::-webkit-scrollbar-thumb{background:var(--border);border-radius:2px} .docs-nav{list-style:none} .docs-nav li{margin-bottom:2px} .docs-nav a{display:block;padding:6px 12px;border-radius:6px;font-size:0.82rem;color:var(--muted);transition:all .15s} .docs-nav a:hover{color:var(--text);background:rgba(255,255,255,0.04);text-decoration:none} .docs-nav a.active{color:var(--text);background:rgba(255,255,255,0.07)} .nav-section{font-size:0.68rem;font-weight:600;text-transform:uppercase;letter-spacing:0.08em;color:var(--muted);padding:16px 12px 6px;opacity:0.7} .nav-section:first-child{padding-top:0} .docs-main{flex:1;min-width:0} /* Cards */ .card{background:var(--card);border:1px solid var(--border);border-radius:12px;padding:28px;margin-bottom:24px} .card h2{font-size:1.2rem;font-weight:700;margin-bottom:16px;letter-spacing:-0.01em} .card h3{font-size:1rem;font-weight:700;margin-bottom:12px} .card p{font-size:0.85rem;color:var(--muted);margin-bottom:16px;line-height:1.7} /* Code blocks */ .code-block{background:var(--bg);border:1px solid var(--border);border-radius:8px;padding:14px 16px;margin-bottom:12px;overflow-x:auto} .code-block pre{font-size:0.78rem;margin:0;white-space:pre-wrap;color:var(--text);line-height:1.6} .code-block code{font-size:0.78rem} .code-label{font-size:0.68rem;color:var(--muted);text-transform:uppercase;font-weight:600;letter-spacing:0.04em;margin-bottom:6px} .inline-code{background:var(--bg);border:1px solid var(--border);padding:1px 6px;border-radius:4px;font-size:0.8rem} /* Endpoint */ .endpoint{margin-bottom:32px;padding-bottom:32px;border-bottom:1px solid var(--border)} .endpoint:last-child{border-bottom:none;margin-bottom:0;padding-bottom:0} .endpoint-header{display:flex;align-items:center;gap:10px;margin-bottom:10px} .method{font-size:0.68rem;font-weight:700;padding:4px 10px;border-radius:4px;color:#fff;flex-shrink:0} .method-get{background:var(--green)} .method-patch{background:var(--orange)} .method-post{background:var(--blue)} .method-delete{background:var(--red)} .endpoint-path{font-size:0.92rem;font-weight:600} .endpoint p{font-size:0.82rem;color:var(--muted);margin-bottom:12px;line-height:1.6} /* Table */ .params-table{width:100%;border-collapse:collapse;font-size:0.78rem;margin-bottom:12px} .params-table td{padding:8px 0;border-bottom:1px solid var(--border)} .params-table td:first-child{font-weight:600;width:130px;color:var(--text)} .params-table td:last-child{color:var(--muted)} /* Event types table */ .event-table{width:100%;border-collapse:collapse;font-size:0.8rem} .event-table th{text-align:left;padding:10px 12px;background:var(--bg);border-bottom:1px solid var(--border);font-size:0.72rem;text-transform:uppercase;letter-spacing:0.04em;color:var(--muted);font-weight:600} .event-table td{padding:10px 12px;border-bottom:1px solid var(--border);vertical-align:top} .event-table td:first-child{font-weight:600;white-space:nowrap;width:180px} .event-table td:last-child{color:var(--muted)} .event-table tr:last-child td{border-bottom:none} .event-group{background:var(--bg);font-size:0.72rem;font-weight:700;text-transform:uppercase;letter-spacing:0.06em;color:var(--muted)} .event-group td{padding:12px 12px 6px !important;border-bottom:none !important} /* Responsive */ @media(max-width:768px){ .docs-layout{flex-direction:column;gap:0;padding:24px 0 60px} .docs-sidebar{display:none} .hero{padding:40px 0 32px} .hero h1{font-size:1.6rem} .card{padding:20px} }
Back to app

API Documentation

Programmatic access to Jamaica Stock Exchange data — real-time prices, financials, directors, and live streaming.

Base URL: https://stacksja.com/api/v1/public
Auth: X-API-Key header
Format: JSON

Authentication

All API requests require an API key passed via the X-API-Key header. Keys are prefixed with pk_.

Request Header
X-API-Key: pk_your_key_here

Contact us to request an API key. Keep your key secret — it cannot be regenerated.

Example Request
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

GET /stock/{symbol}

Get the latest trading data for a single stock. Accepts a ticker symbol or company name (case-insensitive).

Example Requests
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
Response
{
  "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"
}
GET /quote?name={query}

Look up a stock by ticker or company name. Best for multi-word names like "First Rock" or "Caribbean Cement".

Examples
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"
GET /stocks

Get all stocks for the latest trading day.

marketOptional. Filter by main or junior
Example
curl -H "X-API-Key: pk_your_key" "https://stacksja.com/api/v1/public/stocks?market=main"
GET /stock/{symbol}/history

Get historical daily price data for a stock.

limitOptional. Number of trading days (1–365, default 30)
Example
curl -H "X-API-Key: pk_your_key" "https://stacksja.com/api/v1/public/stock/NCBFG/history?limit=90"
GET /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).

Example
curl -H "X-API-Key: pk_your_key" "https://stacksja.com/api/v1/public/pe-ratios"
Response
{
  "count": 35,
  "pe_ratios": [
    {
      "symbol": "JMMBGL",
      "company_name": "JMMB Group Limited",
      "pe_ratio": 5.2,
      "ttm_eps": 3.85,
      "shares_outstanding": 1920000000
    },
    ...
  ]
}

Financial Statements

GET /stock/{symbol}/financials

Get financial statements — income statement, balance sheet, and valuation data. Returns all confirmed periods grouped by fiscal year.

Example
curl -H "X-API-Key: pk_your_key" "https://stacksja.com/api/v1/public/stock/GK/financials"
Response
{
  "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.

GET /stock/{symbol}/documents

List available financial statement PDFs. Each includes a download URL. No API key required — this endpoint is freely accessible.

Example
curl "https://stacksja.com/api/v1/public/stock/GK/documents"
Response
{
  "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"
    }
  ]
}
GET /documents/{id}/pdf

Download the original PDF financial statement. Use the id from the /documents endpoint. No API key required.

Example
curl "https://stacksja.com/api/v1/public/documents/42/pdf" -o statement.pdf

Directors & Ownership

GET /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.

Example
curl -H "X-API-Key: pk_your_key" "https://stacksja.com/api/v1/public/stock/GK/directors"
Response
{
  "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": ""
      }
    ]
  }
}
PATCH /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.

bioDirector biography (string)
titleRole title, e.g. "CEO", "Chairman" (string)
director_typeOne of: chairman, executive, non-executive, independent
appointed_dateDate string, e.g. "2020-01-15"
Example
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"
Response
{
  "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 changesStreaming: 1 connection, instant updates
Most responses are "no change"Only fires when data actually changes
Eats your rate limitDoesn't count against rate limit
Delays up to your poll intervalSub-second delivery
GET /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.

JavaScript
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
curl -N "https://stacksja.com/api/v1/public/stream?api_key=pk_your_key"
Python
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)
Node.js
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}`);
});
GET /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.

Example
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:

EventDescription & Payload
connectedSent immediately when the stream opens. Payload: {"channel": "*", "message": "..."}
Market Data
price_updateStock prices updated. Payload: market, trade_date, count, stocks[] (each with symbol, last_traded_price, closing_price, price_change, change_percent, volume)
trade_updateIndividual trades executed. Payload: symbol, market, trade_date, trades[] (time, price, quantity), buy_orders[], sell_orders[]
index_updateMarket index changed. Payload: market, trade_date, indices[] (name, value, change, change_percent, volume)
stock_removedA stock record was deleted. Payload: id, symbol, market, trade_date
Financial Documents
document_addedNew 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_updatedPDF file replaced on existing statement. Payload: id, symbol, period_type, fiscal_year, pdf_filename
document_removedStatement deleted. Payload: id, symbol, period_type, fiscal_year, statement_type
financials_updatedLine items edited on a confirmed statement. Payload includes symbol, period_type, fiscal_year, statement_type, and the updated line_items[] array.
Ownership & Directors
ownership_updatedDirectors and shareholders updated for a company. Payload: symbol, directors[] (full array), shareholders[] (full array)
ownership_removedOwnership filing deleted. Payload: filing_id, symbol, fiscal_year, period_type
director_updatedDirector info changed via API (bio, title, etc). Payload: director object with id, symbol, name, title, director_type, appointed_date, bio.
News
news_updatedNew articles scraped. Payload: new_articles (count), articles[] (each with id, source, title, url, summary, image_url, published_at)
news_clearedNews 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.

price_update event
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"}]}
document_added event
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}]}
director_updated event
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 (every 30s)
: keepalive

The server sends a keepalive comment every 30 seconds to prevent proxy/client timeouts. These are ignored by EventSource automatically.

Error Codes

CodeMeaning
401Missing or invalid API key
400Bad request (e.g. invalid director_type value)
404Stock symbol or resource not found
429Rate limit exceeded — wait 60 seconds before retrying
Error Response Format
{"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")