# SAHMK API Documentation

> Machine-readable API documentation for developers and AI agents.
> Base URL: `https://app.sahmk.sa/api/v1`
> Portal: https://sahmk.sa/developers

## Recommended Flow

- Start here: make your first REST request with `GET /quote/{symbol}/`
- Build faster: use the official Python SDK or CLI
- Add AI workflows: connect the MCP server or read this file directly
- Scale up: move to WebSocket and Realtime Event Engine rules when polling is not enough

---

## Start Here

Create an account, get your API key from the dashboard, and make your first request:

```bash
curl "https://app.sahmk.sa/api/v1/quote/2222/" \
  -H "X-API-Key: YOUR_API_KEY"
```

Expected response:

```json
{
  "symbol": "2222",
  "name_en": "Saudi Arabian Oil Co",
  "price": 25.86,
  "change_percent": 0.7,
  "volume": 9803705,
  "updated_at": "2026-02-10T12:19:22+00:00",
  "is_delayed": false
}
```

You just fetched live Saudi market data from SAHMK.

Use exchange symbols in the path (for example `2222`).
If you need name/alias resolution, pass `identifier` as a query param:
`GET /quote/{symbol}/?identifier=أرامكو`.

---

## SDK & Tools

```bash
pip install -U sahmk
export SAHMK_API_KEY="your_api_key"
sahmk quote "Saudi Aramco"
```

Python SDK:

```python
from sahmk import SahmkClient

client = SahmkClient(api_key="YOUR_API_KEY")

# Discover symbols before quote/company calls
directory = client.companies(search="aramco", market="NOMUC", limit=20, offset=0)
symbol = directory["results"][0]["symbol"]

print(client.quote("أرامكو السعودية"))
print(client.company(symbol))
```

Full examples: https://github.com/sahmk-sa/sahmk-python
PyPI: https://pypi.org/project/sahmk/

---

## AI & Agents

Use SAHMK inside Claude Desktop, Cursor, and other MCP-compatible clients.

```bash
pip install sahmk-mcp
```

Configuration (Claude Desktop and Cursor):

```json
{
  "mcpServers": {
    "sahmk": {
      "command": "sahmk-mcp",
      "env": {
        "SAHMK_API_KEY": "your_api_key_here"
      }
    }
  }
}
```

Available MCP tools:
- `get_quote` — Real-time price, change, volume, liquidity for one company
- `get_quotes` — Batch quotes for multiple companies (up to 50)
- `get_market_summary` — Market summary by `index` (default `TASI`) with index level, breadth, mood, and `is_delayed`
- `companies_list` — Company directory lookup for symbol discovery with `search`, `market`, `limit`, and `offset`
- `get_company` — Company profile, sector, fundamentals
- `get_historical` — Historical OHLCV data (daily/weekly/monthly)

Source: https://github.com/sahmk-sa/sahmk-mcp
PyPI: https://pypi.org/project/sahmk-mcp/
Tutorial: https://sahmk.sa/developers/tutorials/sahmk-mcp-ai-agents

---

## Authentication

All requests require the `X-API-Key` header:

```
X-API-Key: YOUR_API_KEY
```

API key formats:
- `shmk_live_*` — Production keys
- `shmk_test_*` — Test keys (same data, separate quota)

---

## Subscription Plans

| Feature | Free | Starter (149 SAR/mo) | Pro (499 SAR/mo) | Business | Enterprise (Custom) |
|---------|------|----------------------|------------------|----------|---------------------|
| Requests/day | 100 | 5,000 | 50,000 | 150,000 | Custom |
| Requests/min | 10 | 100 | 500 | 1,000 | Custom |
| API Keys | 1 | 3 | 10 | 30 | Custom |
| Real-time data | No (15-min delay) | No (15-min delay) | Yes | Yes | Yes |
| Historical data | No | Yes | Yes | Yes | Yes |
| Financials | No | Yes | Yes | Yes | Yes |
| Dividends | No | Yes | Yes | Yes | Yes |
| Bulk quotes (/quotes/) | No | Yes | Yes | Yes | Yes |
| Company fundamentals | Basic | Full | Full | Full | Full |
| Technical indicators | No | No | Yes | Yes | Yes |
| Fair price (proprietary) | No | No | Yes | Yes | Yes |
| Stock events | No | No | Yes | Yes | Yes |
| Event webhooks | 0 | 0 | 3 | 10 | Custom |
| Event rules | 0 | 0 | 10 | 50 | Custom |

*Prices exclude 15% VAT*

**Enterprise (Custom):** Limits designed for your workload + dedicated infra. Limits are finalized after a quick usage review (symbols, traffic, concurrency).

**Enterprise options:**
- **Option A — Shared high-volume:** higher daily quota + higher burst + prioritized processing + custom limits for webhooks/alerts.
- **Option B — Dedicated:** dedicated VM / dedicated cache (optional) + cloud Postgres + SLA; scalable solutions for your use case.

---

## Endpoints

### Path vs Query Parameters

- Use path parameters for single-resource endpoints (example: `GET /quote/{symbol}/`).
- Use query parameters to filter collection endpoints (example: `GET /events/?symbol=2222&limit=20`).
- Error handling convention: common HTTP/API errors are centralized in [`Error Codes`](#error-codes). Endpoint sections only show endpoint-specific errors when needed.

### Stocks

#### GET /quote/{symbol}/
Get current price for a single stock.

**Plan:** Free

**Parameters:**
- `symbol` (path, required): Exchange symbol (example: `2222`)
- `identifier` (query, optional): Arabic/English name or alias for resolution (example: `أرامكو`)

**Example with identifier resolution:** `GET /quote/2222/?identifier=أرامكو`

> **Need valid symbols first?** Use `GET /companies/` to discover symbols by symbol, Arabic name, or English name before calling quote endpoints.

**Response:**
```json
{
  "symbol": "2222",
  "name": "أرامكو السعودية",
  "name_en": "Saudi Arabian Oil Co",
  "price": 25.86,
  "change": 0.18,
  "change_percent": 0.7,
  "open": 25.6,
  "high": 25.86,
  "low": 25.6,
  "previous_close": 25.68,
  "volume": 9803705,
  "value": 252308343.0,
  "bid": 25.82,
  "ask": 25.86,
  "liquidity": {
    "inflow_value": 184950463.03,
    "inflow_volume": 7182468,
    "inflow_trades": 7261,
    "outflow_value": 67357881.91,
    "outflow_volume": 2621237,
    "outflow_trades": 5028,
    "net_value": 117592581.12
  },
  "updated_at": "2026-02-10T12:19:22+00:00",
  "is_delayed": false
}
```

**Liquidity Fields:**
- `inflow_value` — Total SAR value of buy orders
- `inflow_volume` — Total shares bought
- `inflow_trades` — Number of buy trades
- `outflow_value` — Total SAR value of sell orders
- `outflow_volume` — Total shares sold
- `outflow_trades` — Number of sell trades
- `net_value` — Net liquidity (inflow - outflow)

---

#### GET /quotes/
Get quotes for multiple stocks.

**Plan:** Starter+

> **Note:** This bulk endpoint requires a Starter plan or higher. Free-tier users can use `GET /quote/{symbol}/` for individual quotes.

**Parameters:**
- `symbols` (query, optional): Comma-separated exchange symbols (max 50)
- `identifiers` (query, optional): Comma-separated Arabic/English names or aliases

Use exactly one of `symbols` or `identifiers` per request.

**Examples:** `/quotes/?symbols=2222,1120,2010` or `/quotes/?identifiers=Aramco,الراجحي`

> **Need valid symbols first?** Use `GET /companies/` to search and validate symbols before batch quote requests.

> **Pro Tip:** Use this batch endpoint instead of polling individual `/quote/{symbol}/` calls — it's more efficient and counts as a single API request.

**Response:**
```json
{
  "quotes": [
    {
      "symbol": "2222",
      "name": "أرامكو السعودية",
      "name_en": "Saudi Arabian Oil Co",
      "price": 25.86,
      "change": 0.18,
      "change_percent": 0.7,
      "high": 25.90,
      "low": 25.60,
      "volume": 9803705,
      "net_liquidity": 117592581.12,
      "updated_at": "2026-02-10T12:19:22+00:00",
      "is_delayed": false
    }
  ],
  "count": 1
}
```

- `net_liquidity` — Net money flow (buy value - sell value) in SAR

---

### Market

#### GET /market/summary/?index=TASI
Get market index and market overview.

**Plan:** Free

**Parameters:**
- `index` (query, optional): Market index (`TASI` or `NOMU`). Default: `TASI`
- Backward compatibility: Existing URLs without `index` still work and default to `TASI`

**Response:**
```json
{
  "index": "TASI",
  "timestamp": "2026-01-28T12:20:00+00:00",
  "index_value": 11458.11,
  "index_change": 76.28,
  "index_change_percent": 0.67,
  "is_delayed": true,
  "total_volume": 279874553,
  "advancing": 117,
  "declining": 139,
  "unchanged": 14,
  "market_mood": "Bullish"
}
```

---

#### GET /market/gainers/?limit=10&index=TASI
Get top gaining stocks.

**Plan:** Free

**Parameters:**
- `index` (query, optional): Market index (`TASI` or `NOMU`). Default: `TASI`
- `limit` (query, optional): Number of results (default: 10, max: 50)

**Response:**
```json
{
  "index": "TASI",
  "is_delayed": true,
  "gainers": [
    {
      "symbol": "4194",
      "name": "مجموعة منزل التسويق للتجارة",
      "name_en": "Maison Marketing Trade Group",
      "price": 59.5,
      "change": 4.9,
      "change_percent": 8.97,
      "volume": 611349,
      "updated_at": "2026-01-28T12:19:50+00:00"
    }
  ],
  "count": 10
}
```

---

#### GET /market/losers/?limit=10&index=TASI
Get top losing stocks.

**Plan:** Free

**Parameters:**
- `index` (query, optional): Market index (`TASI` or `NOMU`). Default: `TASI`
- `limit` (query, optional): Number of results (default: 10, max: 50)

**Response:**
```json
{
  "index": "TASI",
  "is_delayed": true,
  "losers": [
    {
      "symbol": "9639",
      "name": "شركة أنماط التقنية للتجارة",
      "name_en": "Anmat Technology Trading Co",
      "price": 8.2,
      "change": -0.8,
      "change_percent": -8.89,
      "volume": 9206,
      "updated_at": "2026-01-28T12:10:18+00:00"
    }
  ],
  "count": 10
}
```

---

#### GET /market/volume/?limit=10&index=TASI
Get top stocks by trading volume.

**Plan:** Free

**Parameters:**
- `index` (query, optional): Market index (`TASI` or `NOMU`). Default: `TASI`
- `limit` (query, optional): Number of results (default: 10, max: 50)

**Response:**
```json
{
  "index": "TASI",
  "is_delayed": true,
  "stocks": [
    {
      "symbol": "2222",
      "name": "أرامكو السعودية",
      "name_en": "Saudi Arabian Oil Co",
      "price": 25.64,
      "change": 0.38,
      "change_percent": 1.5,
      "volume": 15738067,
      "updated_at": "2026-01-28T12:19:48+00:00"
    }
  ],
  "count": 10
}
```

---

#### GET /market/value/?limit=10&index=TASI
Get top stocks by trading value (SAR).

**Plan:** Free

**Parameters:**
- `index` (query, optional): Market index (`TASI` or `NOMU`). Default: `TASI`
- `limit` (query, optional): Number of results (default: 10, max: 50)

**Response:**
```json
{
  "index": "TASI",
  "is_delayed": true,
  "stocks": [
    {
      "symbol": "2222",
      "name": "أرامكو السعودية",
      "name_en": "Saudi Arabian Oil Co",
      "price": 25.64,
      "change": 0.38,
      "change_percent": 1.5,
      "volume": 15738067,
      "value": 402108076.72,
      "updated_at": "2026-01-28T12:19:48+00:00"
    }
  ],
  "count": 10
}
```

---

#### GET /market/sectors/?index=TASI
Get sector performance.

**Plan:** Free

**Parameters:**
- `index` (query, optional): Market index (`TASI` or `NOMU`). Default: `TASI`

**Response:**
```json
{
  "index": "TASI",
  "is_delayed": true,
  "sectors": [
    {
      "id": "TBNI",
      "name": "Banks",
      "change_percent": 0.45,
      "avg_change_percent": 0.38,
      "volume": 45027873,
      "num_stocks": 10
    }
  ],
  "count": 20
}
```

**Endpoint-specific error (invalid `index`):**
```json
{
  "error": {
    "code": "INVALID_INDEX",
    "message": "Invalid index 'XYZ'. Supported values: TASI, NOMU."
  }
}
```

---

### Company / Symbol APIs

#### GET /companies/
Lightweight company directory for symbol discovery before quote and company calls.

**Plan:** Free+

**Auth:** Requires `X-API-Key` (same as other private `/api/v1` endpoints).

**Parameters:**
- `search` (query, optional): Matches symbol, Arabic name, or English name
- `market` (query, optional): `TASI` or `NOMU` (`NOMUC` alias accepted)
- `limit` (query, optional): Number of results (default: 100, max: 500)
- `offset` (query, optional): Result offset (default: 0)

**Response:**
```json
{
  "results": [
    {
      "symbol": "2222",
      "name_ar": "أرامكو السعودية",
      "name_en": "Saudi Arabian Oil Co",
      "market": "TASI",
      "status": "active"
    }
  ],
  "count": 1,
  "total": 590,
  "limit": 100,
  "offset": 0
}
```

**Error (invalid `market`):**
```json
{
  "error": {
    "code": "INVALID_MARKET",
    "message": "market must be one of: TASI, NOMU."
  }
}
```

**Error (invalid pagination params):**
```json
{
  "error": {
    "code": "INVALID_PARAM",
    "message": "limit and offset must be valid integers."
  }
}
```

---

#### GET /company/{symbol}/
Get company information. Response varies by plan.

**Plan:** Free (basic), Starter (full fundamentals), Pro+ (+ technicals, valuation, analysts)

**Parameters:**
- `symbol` (path, required): Stock ticker

**Data by Plan:**
- Free: name, sector, industry, description, website
- Starter: + full fundamentals (PE, EPS, book value, beta, week/month/52w ranges)
- Pro+: + technicals (RSI, MACD), valuation (fair price), analysts (targets, consensus)
- `is_delayed` indicates pricing freshness: `true` for delayed prices (Free/Starter) and `false` for real-time prices (Pro/Business/Enterprise).

**Fundamentals Fields (Starter+):**
- `float_shares` — Free float shares (tradeable)
- `week_high`, `week_low` — Highest/lowest price in last 7 days
- `month_high`, `month_low` — Highest/lowest price in last 30 days

**Response (Pro+):**
```json
{
  "symbol": "2222",
  "name": "أرامكو السعودية",
  "name_en": "Saudi Arabian Oil Co",
  "current_price": 25.64,
  "is_delayed": false,
  "sector": "Energy",
  "industry": "Oil & Gas",
  "description": "Saudi Aramco is the world's largest oil producer...",
  "website": "https://www.aramco.com",
  "country": "Saudi Arabia",
  "currency": "SAR",
  
  "fundamentals": {
    "market_cap": 6258120000000,
    "pe_ratio": 16.77,
    "forward_pe": 15.48,
    "eps": 1.54,
    "book_value": 6.16,
    "price_to_book": 4.19,
    "beta": 0.104,
    "shares_outstanding": 242000000000,
    "float_shares": 5969578000,
    "week_high": 26.10,
    "week_low": 25.40,
    "month_high": 27.20,
    "month_low": 24.80,
    "fifty_two_week_high": 27.85,
    "fifty_two_week_low": 23.04
  },
  
  "technicals": {
    "rsi_14": 55.3,
    "macd_line": 0.12,
    "macd_signal": 0.08,
    "macd_histogram": 0.04,
    "fifty_day_average": 26.1,
    "technical_strength": 0.65,
    "price_direction": "bullish",
    "updated_at": "2026-01-28T10:00:00+03:00"
  },
  
  "valuation": {
    "fair_price": 28.50,
    "fair_price_confidence": 0.85,
    "calculated_at": "2026-01-28T10:00:00+03:00"
  },
  
  "analysts": {
    "target_mean": 29.5,
    "target_median": 29.0,
    "target_high": 35.0,
    "target_low": 24.0,
    "consensus": "buy",
    "consensus_score": 2.1,
    "num_analysts": 15
  }
}
```

---

### Historical Data

#### GET /historical/{symbol}/
Get historical OHLCV data.

**Plan:** Starter+

**Parameters:**
- `symbol` (path, required): Stock ticker
- `from` (query, optional): Start date YYYY-MM-DD (default: 30 days ago)
- `to` (query, optional): End date YYYY-MM-DD (default: today)
- `interval` (query, optional): accepted values `1d`, `1w`, `1m` (default: `1d`; `1w` and `1m` return weekly/monthly candles aggregated from daily rows)

**Example:** `/historical/2222/?from=2026-01-01&to=2026-01-28`

**Response:**
```json
{
  "symbol": "2222",
  "interval": "1d",
  "from": "2026-01-01",
  "to": "2026-01-28",
  "count": 20,
  "data": [
    {
      "date": "2026-01-28",
      "open": 25.3,
      "high": 25.68,
      "low": 25.3,
      "close": 25.64,
      "volume": 15738067,
      "adjusted_close": 25.64,
      "turnover": 402108076.72
    }
  ]
}
```

---

### Financials

#### GET /financials/{symbol}/
Access structured income statement, balance sheet, and cash flow data for Saudi listed companies.

#### Request Examples

```bash
# Starter
GET /api/v1/financials/1120/

# Pro/Business/Enterprise
GET /api/v1/financials/1120/?period=quarterly&history=5y&metrics=extended
```

#### Key Parameters

- `type` — `income`, `balance`, `cashflow`, `all`
- `period` — `annual`, `quarterly`, `auto`
- `history` — `1y`, `3y`, `5y`, `10y`, `max`
- `metrics` — `core`, `extended`
- `result` — `series`, `latest`
- `include_quality=1` — include metadata and coverage info
- `include_future_placeholders=1` — include future-dated placeholder rows (default is hidden)

#### Auto Period Behavior

- `period=auto` resolves to `annual` when the latest fiscal year is full-year for the requested statement scope.
- `period=auto` resolves to `quarterly` when the latest fiscal year is not full-year.
- `result=latest` and history windows follow the resolved granularity.
- API default remains `period=annual` for backward compatibility.

#### Plan Access

**Starter**  
Annual financials, core metrics, up to 3 years history, latest snapshot.

**Pro / Business / Enterprise**  
Quarterly financials, extended metrics, 5Y / 10Y / max history, full views.

#### Notes

- Financial statements are returned directly in statement arrays (for example `income_statements`, `balance_sheets`, `cash_flows`).
- Responses include fields like `symbol`, `statement_period`, and optional `quality` when requested.

#### Response Example
```json
{
  "symbol": "1120",
  "statement_period": "annual",
  "quality": {
    "coverage": "high",
    "warnings": []
  },
  "income_statements": [
    {
      "report_date": "2025-09-30",
      "total_revenue": 123456789.0,
      "net_income": 9876543.0
    }
  ]
}
```

---

### Analytics

Compare and analyze company ratios with compact analytics endpoints.

- Minimal meta only: `period`, `metrics`, `warnings`.
- Coverage and metric availability vary by company sector and data completeness.
- Ratio keys are dynamic by symbol/profile/availability; frontend should render available keys dynamically and not assume all ratio keys are always present.

#### GET /analytics/ratios/{symbol}/

Financial ratios for one symbol.

**Query Parameters**
- `history` — `latest`, `3y`, `5y`, `10y`, `max` (default: `latest`)
- `period` — `annual`, `quarterly` (default: `annual`)
- `metrics` — `core`, `extended` (default: `core`)

**Plan Note**
- Starter supports `latest + annual + core` only.
- Pro/Business/Enterprise unlock all ratios options.

#### Request Examples

```bash
# Starter
GET /api/v1/analytics/ratios/1120/

# Pro/Business/Enterprise
GET /api/v1/analytics/ratios/1120/?history=5y&period=quarterly&metrics=extended
```

#### Ratios Response Example
```json
{
  "symbol": "1120",
  "ratios": [
    {
      "report_date": "2025-12-31",
      "statement_period": "annual",
      "fiscal_year": 2025,
      "fiscal_quarter": null,
      "ratios": {
        "roe": 9.77,
        "roa": 3.89,
        "net_margin": 19.09,
        "debt_to_equity": 0.6,
        "revenue_growth_yoy": 10.0,
        "net_income_growth_yoy": 16.67
      },
      "key_metrics": {
        "total_revenue": 1100.0,
        "operating_income": 250.0,
        "net_income": 210.0,
        "operating_cash_flow": 280.0,
        "total_assets": 5400.0,
        "stockholders_equity": 2150.0,
        "total_debt": 1300.0
      }
    }
  ],
  "meta": {
    "period": "annual",
    "metrics": "core",
    "warnings": []
  }
}
```

#### GET /analytics/compare/

Compare ratio snapshots across multiple symbols.

**Query Parameters**
- `symbols` — required comma-separated symbols (example: `1120,1180,1010`)
- `metrics` — `core`, `extended` (default: `core`)

**Plan Note**
- Starter supports up to 3 symbols and `core` metrics only.
- Pro supports up to 10 symbols and `extended`.
- Business and Enterprise support up to 20 symbols and `extended`.

#### Request Examples
```bash
# Starter
GET /api/v1/analytics/compare/?symbols=1120,1180,1010

# Pro
GET /api/v1/analytics/compare/?symbols=1120,1180,1010,2222&metrics=extended
```

#### Compare Response Example
```json
{
  "results": [
    {
      "symbol": "1120",
      "company_name": "الراجحي",
      "sector": "Financial Services",
      "market_cap": 1000001120.0,
      "current_price": 89.4,
      "coverage": "high",
      "ratios": {
        "roe": 9.77,
        "roa": 3.89,
        "net_margin": 19.09,
        "debt_to_equity": 0.6,
        "revenue_growth_yoy": 10.0,
        "net_income_growth_yoy": 16.67,
        "asset_turnover": 0.2037,
        "debt_ratio": 0.2407
      },
      "key_metrics": {
        "total_revenue": 1100.0,
        "operating_income": 250.0,
        "net_income": 210.0,
        "operating_cash_flow": 280.0,
        "total_assets": 5400.0,
        "stockholders_equity": 2150.0,
        "total_debt": 1300.0
      }
    }
  ],
  "count": 1,
  "meta": {
    "period": "annual",
    "metrics": "extended",
    "warnings": []
  }
}
```

---

### Dividends

#### GET /dividends/{symbol}/
Get dividend history and yield.

**Plan:** Starter+

**Parameters:**
- `symbol` (path, required): Stock ticker
- `limit` (query, optional): Number of records (default: 10, max: 50)

**Response:**
```json
{
  "symbol": "2222",
  "current_price": 25.64,
  "trailing_12m_yield": 4.2,
  "trailing_12m_dividends": 1.60,
  "payments_last_year": 4,
  "upcoming": [
    {
      "value": 0.40,
      "period": "Q4",
      "eligibility_date": "2026-03-15",
      "distribution_date": "2026-04-01"
    }
  ],
  "history": [
    {
      "value": 0.40,
      "value_percent": 1.5,
      "period": "Q3",
      "fiscal_year": 2025,
      "announcement_date": "2025-09-01",
      "eligibility_date": "2025-09-15",
      "distribution_date": "2025-10-01"
    }
  ]
}
```

---

### Stock Events

#### GET /events/
Get AI-generated stock event summaries.

**Plan:** Pro+

**Parameters:**
- `symbol` (query, optional): Filter by stock ticker
- `type` (query, optional): Filter by event type (UPPERCASE)
- `importance` (query, optional): Filter by importance (`CRITICAL`, `IMPORTANT`, `REGULAR`)
- `limit` (query, optional): Number of results (default: 20, max: 100)

**Response:**
```json
{
  "events": [
    {
      "symbol": "4190",
      "stock_name": "جرير للتسويق",
      "event_type": "FINANCIAL_REPORT",
      "importance": "important",
      "sentiment": "positive",
      "description": "شركة جرير للتسويق تعلن عن نتائج مالية قياسية للربع الرابع 2025 مع نمو أرباح بنسبة 13% وأرباح سنوية بلغت 1049.2 مليون ريال بزيادة 8%.",
      "event_date": "2026-01-29",
      "article_date": "2026-01-29T17:10:06+00:00",
      "created_at": "2026-01-29T17:10:12+00:00"
    }
  ],
  "count": 1,
  "available_types": [
    "FINANCIAL_REPORT", "DIVIDEND_ANNOUNCEMENT", "STOCK_SPLIT",
    "MERGER_ACQUISITION", "MANAGEMENT_CHANGE", "NEW_LISTING",
    "REGULATORY_ACTION", "PARTNERSHIP", "MARKET_EXPANSION",
    "RESTRUCTURING", "EARNINGS_SURPRISE", "OTHER"
  ]
}
```

**Notes:**
- `stock_name` can be `null` for some events
- `event_type` values are UPPERCASE
- `available_types` is server-defined and may expand in future releases
- `sentiment` values: `very_positive`, `positive`, `slightly_positive`, `neutral`, `slightly_negative`, `negative`, `very_negative`
- `importance` values: `critical`, `important`, `regular`

---

## WebSocket Streaming

Real-time stock price streaming via WebSocket. Pro, Business, and Enterprise plans only.

### Connection

```
wss://app.sahmk.sa/ws/v1/stocks/?api_key=YOUR_API_KEY
```

**Plan:** Pro, Business, or Enterprise

### Subscription Limits

| Plan | Max symbols/connection | Max symbols/call | Subscribe all (*) |
|------|------------------------|------------------|-------------------|
| Pro | 60 | 20 | ❌ |
| Business | 120 | 40 | ❌ |
| Enterprise | 200 | 20 (or 100 on enterprise realtime profile) | ✅ |

**Notes:**
- Use multiple connections to exceed your plan's per-connection cap.
- Limits are also returned in the initial `connected` message under `limits`.

### Client → Server Messages

| Action | Message | Description |
|--------|---------|-------------|
| Subscribe | `{"action": "subscribe", "symbols": ["2222", "1120"]}` | Subscribe to specific stocks |
| Subscribe All | `{"action": "subscribe", "symbols": ["*"]}` | Subscribe to all stocks (Enterprise only) |
| Unsubscribe | `{"action": "unsubscribe", "symbols": ["2222"]}` | Stop receiving updates for symbols |
| Ping | `{"action": "ping"}` | Keep-alive |

### Server → Client Messages

| Type | Description |
|------|-------------|
| `connected` | Connection confirmed with plan and limits |
| `subscribed` | Subscription confirmed |
| `unsubscribed` | Unsubscribe confirmed |
| `quote` | Real-time price update |
| `pong` | Ping response |
| `error` | Error message |

### Connected Message Format

```json
{
  "type": "connected",
  "plan": "business",
  "delivery_profile": "business_standard",
  "limits": {
    "max_symbols_per_connection": 120,
    "max_symbols_per_call": 40,
    "stream_modes": ["standard"]
  },
  "message": "Connected to SAHMK real-time stock stream",
  "timestamp": "2026-02-10T10:00:00.000Z"
}
```

### Quote Message Format

```json
{
  "type": "quote",
  "symbol": "2222",
  "mode": "standard",
  "timestamp": "2026-02-10T10:30:15.123Z",
  "latency_ms": 42,
  "data": {
    "price": 25.86,
    "open": 25.60,
    "high": 25.86,
    "low": 25.60,
    "close": 25.86,
    "change": 0.18,
    "change_percent": 0.7,
    "previous_close": 25.68,
    "volume": 9803705,
    "value": 252308343.0,
    "bid": 25.82,
    "ask": 25.86,
    "market_session": "REGULAR",
    "liquidity": {
      "inflow_value": 184950463.03,
      "inflow_volume": 7182468,
      "outflow_value": 67357881.91,
      "outflow_volume": 2621237,
      "net_value": 117592581.12
    },
    "trade_time": "2026-02-10T10:30:12+00:00"
  }
}
```

**WebSocket Quote Fields:**
| Field | Description |
|-------|-------------|
| `price` | Current price |
| `change` / `change_percent` | Price change |
| `previous_close` | Yesterday's close |
| `volume` / `value` | Trading volume & turnover (SAR) |
| `bid` / `ask` | Best bid/ask prices |
| `liquidity.*` | Money flow data (same as REST) |

### JavaScript Example

```javascript
const API_KEY = "shmk_live_xxxxxxxxxxxxxxxx";
const ws = new WebSocket(`wss://app.sahmk.sa/ws/v1/stocks/?api_key=${API_KEY}`);

ws.onopen = () => {
  ws.send(JSON.stringify({
    action: "subscribe",
    symbols: ["2222", "1120", "4191"]
  }));
};

ws.onmessage = (event) => {
  const msg = JSON.parse(event.data);

  if (msg.type === "connected") {
    console.log("Plan:", msg.plan, "Limits:", msg.limits);
  }

  if (msg.type === 'quote') {
    console.log(`${msg.symbol}: ${msg.data.price} (${msg.data.change_percent}%)`);
  }
};

// Keep-alive ping every 30 seconds
setInterval(() => {
  if (ws.readyState === WebSocket.OPEN) {
    ws.send(JSON.stringify({ action: "ping" }));
  }
}, 30000);
```

### Python Example

```python
import asyncio
import websockets
import json

API_KEY = "shmk_live_xxxxxxxxxxxxxxxx"

async def stream_stocks():
    uri = f"wss://app.sahmk.sa/ws/v1/stocks/?api_key={API_KEY}"
    
    async with websockets.connect(uri) as ws:
        await ws.send(json.dumps({
            "action": "subscribe",
            "symbols": ["2222", "1120", "4191"]
        }))
        
        async for message in ws:
            data = json.loads(message)
            if data["type"] == "quote":
                print(f"{data['symbol']}: {data['data']['price']}")

asyncio.run(stream_stocks())
```

### WebSocket Error Codes

| Code | Meaning |
|------|---------|
| 4000 | Generic connection or server failure |
| 4001 | Invalid or missing API key |

### Update Frequency

- Updates are pushed as data changes during active trading sessions
- Only changed symbols are pushed (no redundant data)

---

## Realtime Event Engine v1

Realtime Event Engine v1 is the next step of the existing webhook + price alert workflow.
Use it to define event rules, deliver webhook notifications, and inspect delivery history.

### Supported Event Types

- `price_alert`
- `large_move`
- `abnormal_volume`
- `unusual_value_traded`

### Endpoint Summary

| Method | Endpoint | Purpose |
|--------|----------|---------|
| POST | `/api/v1/webhooks/` | Register webhook destination |
| POST | `/api/v1/alerts/` | Create event rule |
| GET | `/api/v1/alerts/` | List event rules |
| GET | `/api/developers/events/` | Query event history and delivery status (Developer JWT required) |

---

### POST /api/v1/webhooks/

Register a webhook destination for Realtime Event Engine callbacks.

**Headers**
- `X-API-Key: YOUR_API_KEY`
- `Content-Type: application/json`

**Request Body**
- `url` (string, required): HTTPS callback URL
- `name` (string, optional): Display name

#### Example Request

```bash
curl -X POST "https://app.sahmk.sa/api/v1/webhooks/" \
  -H "X-API-Key: YOUR_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "url": "https://example.com/hooks/sahmk",
    "name": "Trading Prod"
  }'
```

```python
import requests

response = requests.post(
    "https://app.sahmk.sa/api/v1/webhooks/",
    headers={
        "X-API-Key": "YOUR_API_KEY",
        "Content-Type": "application/json"
    },
    json={
        "url": "https://example.com/hooks/sahmk",
        "name": "Trading Prod"
    }
)
print(response.json())
```

```javascript
const response = await fetch("https://app.sahmk.sa/api/v1/webhooks/", {
  method: "POST",
  headers: {
    "X-API-Key": "YOUR_API_KEY",
    "Content-Type": "application/json"
  },
  body: JSON.stringify({
    url: "https://example.com/hooks/sahmk",
    name: "Trading Prod"
  })
});

console.log(await response.json());
```

#### Example Response

```json
{
  "id": 19,
  "url": "https://example.com/hooks/sahmk",
  "name": "Trading Prod",
  "signing_secret": "xxxxxxxxxxxxxxxx",
  "is_verified": false,
  "is_active": true,
  "created_at": "2026-05-08T13:30:12+03:00"
}
```

---

### POST /api/v1/alerts/

Create a Realtime Event Engine rule (the evolved version of a price alert).

**Headers**
- `X-API-Key: YOUR_API_KEY`
- `Content-Type: application/json`

**Request Body**
- `symbol` (string, required): Stock symbol (example: `2222`)
- `event_type` (string, required): `price_alert`, `large_move`, `abnormal_volume`, `unusual_value_traded`
- `condition` (string, required in practice; type-specific): condition selector used by rule matching
- `value` (number, required): numeric threshold
- `webhook_id` (integer, required): Destination id from `POST /api/v1/webhooks/`
- `once` (boolean, optional): When `true`, rule auto-deactivates after first trigger
- `config` (object, optional): deterministic options (for example `window` for `large_move`)

**Condition rules**
- `price_alert`: `price_above`, `price_below`, `pct_change`
- `large_move`: `pct_change_abs_gte` (normalized by API)
- `abnormal_volume`: `ratio_gte`
- `unusual_value_traded`: `ratio_gte`

#### Example Request

```bash
curl -X POST "https://app.sahmk.sa/api/v1/alerts/" \
  -H "X-API-Key: YOUR_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "symbol": "2222",
    "event_type": "large_move",
    "condition": "pct_change_abs_gte",
    "value": 3.0,
    "webhook_id": 19,
    "once": false,
    "config": { "window": "15m" }
  }'
```

```python
import requests

response = requests.post(
    "https://app.sahmk.sa/api/v1/alerts/",
    headers={"X-API-Key": "YOUR_API_KEY"},
    json={
        "symbol": "2222",
        "event_type": "large_move",
        "condition": "pct_change_abs_gte",
        "value": 3.0,
        "webhook_id": 19,
        "once": False,
        "config": {"window": "15m"}
    }
)
print(response.json())
```

```javascript
const response = await fetch("https://app.sahmk.sa/api/v1/alerts/", {
  method: "POST",
  headers: {
    "X-API-Key": "YOUR_API_KEY",
    "Content-Type": "application/json"
  },
  body: JSON.stringify({
    symbol: "2222",
    event_type: "large_move",
    condition: "pct_change_abs_gte",
    value: 3.0,
    webhook_id: 19,
    once: false,
    config: { window: "15m" }
  })
});

console.log(await response.json());
```

#### Practical Rule Examples

```bash
# 1) Large move rule (>= 3% absolute move in 15m)
curl -X POST "https://app.sahmk.sa/api/v1/alerts/" \
  -H "X-API-Key: YOUR_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "symbol": "2222",
    "event_type": "large_move",
    "condition": "pct_change_abs_gte",
    "value": 3.0,
    "webhook_id": 19,
    "once": false,
    "config": { "window": "15m" }
  }'

# 2) Abnormal volume rule (>= 2.5x baseline volume)
curl -X POST "https://app.sahmk.sa/api/v1/alerts/" \
  -H "X-API-Key: YOUR_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "symbol": "2010",
    "event_type": "abnormal_volume",
    "condition": "ratio_gte",
    "value": 2.5,
    "webhook_id": 19,
    "once": false
  }'

# 3) Unusual traded value rule (>= 3.0x traded value baseline)
curl -X POST "https://app.sahmk.sa/api/v1/alerts/" \
  -H "X-API-Key: YOUR_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "symbol": "1180",
    "event_type": "unusual_value_traded",
    "condition": "ratio_gte",
    "value": 3.0,
    "webhook_id": 19,
    "once": false
  }'
```

#### Example Response

```json
{
  "id": 42,
  "symbol": "2222",
  "event_type": "large_move",
  "condition": "pct_change_abs_gte",
  "value": "3.0",
  "webhook_id": 19,
  "status": "active",
  "fire_once": false,
  "created_at": "2026-05-08T13:36:45+03:00"
}
```

---

### GET /api/v1/alerts/

List event rules for your account.

**Query Parameters**
- `status` (optional): `active`, `paused`, `all` (default: `active`)

#### Example Request

```bash
curl "https://app.sahmk.sa/api/v1/alerts/?status=active&limit=20" \
  -H "X-API-Key: YOUR_API_KEY"
```

```python
import requests

response = requests.get(
    "https://app.sahmk.sa/api/v1/alerts/",
    headers={"X-API-Key": "YOUR_API_KEY"},
    params={"status": "active", "limit": 20}
)
print(response.json())
```

```javascript
const query = new URLSearchParams({
  status: "active",
  limit: "20"
});

const response = await fetch(
  `https://app.sahmk.sa/api/v1/alerts/?${query}`,
  { headers: { "X-API-Key": "YOUR_API_KEY" } }
);

console.log(await response.json());
```

#### Example Response

```json
{
  "alerts": [
    {
      "id": 42,
      "symbol": "2222",
      "event_type": "large_move",
      "condition": "pct_change_abs_gte",
      "value": "3.0",
      "status": "active",
      "fire_once": false,
      "webhook_id": 19,
      "created_at": "2026-05-08T13:36:45+03:00"
    }
  ]
}
```

---

### GET /api/developers/events/

Read event history and webhook delivery outcomes.

**Authentication:** Developer JWT (`Authorization: Bearer <token>`)

**Query Parameters**
- `status` (optional): `pending`, `delivered`, `retrying`, `dead_letter`
- `event_type` (optional): filter by event type
- `workspace_id` (optional): filter by workspace
- `limit` (optional): default `50`, max `100`
- `offset` (optional): default `0`

#### Example Request

```bash
curl "https://app.sahmk.sa/api/developers/events/?status=dead_letter&limit=20" \
  -H "Authorization: Bearer YOUR_DEVELOPER_JWT"
```

```python
import requests

response = requests.get(
    "https://app.sahmk.sa/api/developers/events/",
    headers={"Authorization": "Bearer YOUR_DEVELOPER_JWT"},
    params={"status": "dead_letter", "event_type": "abnormal_volume", "limit": 20}
)
print(response.json())
```

```javascript
const response = await fetch(
  "https://app.sahmk.sa/api/developers/events/?status=dead_letter&limit=20",
  { headers: { "Authorization": "Bearer YOUR_DEVELOPER_JWT" } }
);

console.log(await response.json());
```

#### Example Response

```json
{
  "events": [
    {
      "event_id": "be03f577-c9d0-4d8f-a7ef-5353f11e59a7",
      "event_type": "abnormal_volume",
      "symbol": "2222",
      "detected_at": "2026-05-08T13:48:22+03:00",
      "delivery_status": "retrying",
      "delivery_attempts": 2,
      "last_attempt_number": 2,
      "webhook_id": 19
    }
  ],
  "total": 1,
  "has_more": false
}
```

---

### Webhook Payload Schema

Realtime Event Engine webhook payloads use one canonical envelope across event types:

```json
{
  "event_id": "9a6b8f22-2d1d-4b3f-b75f-7f9d5f101234",
  "event_type": "large_move",
  "symbol": "2222",
  "detected_at": "2026-05-08T13:48:22+03:00",
  "severity": "important",
  "title": "Large move detected",
  "summary": "Price moved more than configured threshold in the selected window.",
  "metrics": {
    "price": 25.86,
    "pct_change": 3.4,
    "volume": 9803705,
    "value": 252308343.0,
    "avg_volume": 8243000,
    "reference_value": 245000000.0,
    "rolling_volume_baseline": 4100000,
    "rolling_value_baseline": 120000000.0,
    "baseline_source": "rolling_window",
    "baseline_sample_count": 30,
    "volume_baseline_sample_count": 30,
    "value_baseline_sample_count": 30,
    "high": 25.86,
    "low": 25.60,
    "change": 0.85,
    "window": "15m",
    "threshold": 3.0
  },
  "conditions_matched": {
    "window": "15m",
    "operator": "abs>=",
    "observed_pct_change": 3.4,
    "threshold_pct": 3.0,
    "baseline_price": 25.01,
    "current_price": 25.86
  },
  "correlation_id": "webhook-evt-01HXY...",
  "version": "v1"
}
```

**Notes**
- Top-level envelope keys are always included.
- `metrics` is always present; many metric values may be `null` depending on event context.
- `conditions_matched` is always present as an object but may be empty in some events.
- Ignore unknown keys for forward compatibility.

### Event-specific `conditions_matched` shape

```json
{
  "price_alert": {
    "price_above": { "operator": ">", "observed": 26.1, "threshold": 26.0 },
    "price_below": { "operator": "<", "observed": 24.8, "threshold": 25.0 },
    "pct_change": { "operator": "abs>=", "observed": 3.2, "threshold": 3.0 }
  },
  "large_move": {
    "window": "15m",
    "operator": "abs>=",
    "observed_pct_change": 3.4,
    "threshold_pct": 3.0,
    "baseline_price": 25.01,
    "current_price": 25.86
  },
  "abnormal_volume": {
    "event_type": "abnormal_volume",
    "operator": ">=",
    "metric": "volume_ratio",
    "current_value": 9803705,
    "baseline_value": 4100000,
    "ratio": 2.39,
    "threshold": 2.0
  },
  "unusual_value_traded": {
    "event_type": "unusual_value_traded",
    "operator": ">=",
    "metric": "value_ratio",
    "current_value": 252308343.0,
    "baseline_value": 120000000.0,
    "ratio": 2.1,
    "threshold": 2.0
  }
}
```

Numeric encoding is JSON number (not string). `ratio` and `observed_pct_change` are rounded to 4 decimals.

Important: `unusual_value_traded` matching uses `value / rolling_value_baseline` (not `reference_value`).

### Webhook Signing (`X-SAHMK-Signature`)

Each delivery includes:

- `X-SAHMK-Signature`: `t=<unix_ts>,v1=<hex_hmac>`
- `X-SAHMK-Event`
- `X-SAHMK-Event-Id`

Compute expected signature:
- Serialize JSON with sorted keys and no extra whitespace.
- Build signed payload as: `<timestamp>.<canonical_json>`
- Compute `v1` as lowercase hex HMAC-SHA256 over UTF-8 bytes.

#### Python Verification Example

```python
import hmac
import hashlib
import json

def verify_signature(secret, payload_dict, header_value):
    # header format: t=1715523245,v1=<hex>
    parts = dict(part.split("=", 1) for part in header_value.split(","))
    timestamp = parts["t"]
    incoming_signature = parts["v1"]

    canonical_json = json.dumps(payload_dict, sort_keys=True, separators=(",", ":"))
    signed_payload = f"{timestamp}.{canonical_json}".encode("utf-8")
    expected = hmac.new(secret.encode("utf-8"), signed_payload, hashlib.sha256).hexdigest()
    return hmac.compare_digest(expected, incoming_signature)
```

#### JavaScript Verification Example

```javascript
import crypto from "crypto";

function verifySignature(secret, payload, signatureHeader) {
  // signatureHeader format: "t=1715523245,v1=<hex>"
  const parts = Object.fromEntries(
    signatureHeader.split(",").map((part) => part.split("=", 2))
  );
  const timestamp = parts.t;
  const incomingSignature = parts.v1;

  // Use a canonical JSON serializer that sorts keys and removes extra whitespace.
  const canonicalJson = canonicalStringify(payload);
  const signedPayload = `${timestamp}.${canonicalJson}`;
  const expected = crypto
    .createHmac("sha256", secret)
    .update(signedPayload, "utf8")
    .digest("hex");

  return crypto.timingSafeEqual(
    Buffer.from(expected, "utf8"),
    Buffer.from(incomingSignature, "utf8")
  );
}
```

### Delivery & Retry Behavior

- State flow: `pending -> retrying -> delivered | dead_letter`
- Retry delays: `2s`, `10s`, `60s`
- Attempts: `4` total (initial + 3 retries)
- Retry trigger: all non-2xx outcomes are retried (4xx, 5xx, timeout, connection errors)
- No jitter is currently applied
- No public replay endpoint is currently available for dead-letter events

---

## Rate Limits

| Plan | Daily Limit | Burst Limit | API Keys | WebSocket | Event Webhooks | Event Rules |
|---|---:|---:|---:|:---:|---:|---:|
| Free | 100/day | 10/min | 1 | ✗ | 0 | 0 |
| Starter | 5,000/day | 100/min | 3 | ✗ | 0 | 0 |
| Pro | 50,000/day | 500/min | 10 | ✓ | 3 | 10 |
| Business | 150,000/day | 1,000/min | 30 | ✓ | 10 | 50 |
| Enterprise | Custom | Custom | Custom | ✓ | Custom | Custom |

**Burst Protection:** To prevent abuse and protect stability, per-minute throttling is applied at both API-key and account levels. Requests exceeding these limits return HTTP 429. Daily limits reset at midnight (UTC+3). Enterprise limits are contract-based and may be monthly quotas or resource-based.

### Rate Limit Headers

```
X-RateLimit-Limit: 5000
X-RateLimit-Remaining: 4987
X-RateLimit-Reset: 1769816400
```

`X-RateLimit-Reset` is a Unix timestamp (seconds).

---

## Error Codes

| HTTP Code | Error Code | Description |
|-----------|------------|-------------|
| 400 | INVALID_ROUTE | Wrong endpoint path; response includes suggested correct route |
| 401 | INVALID_API_KEY | API key missing, invalid, or revoked |
| 403 | PLAN_LIMIT | Endpoint requires higher plan |
| 404 | INVALID_SYMBOL | Company identifier not resolved |
| 429 | RATE_LIMIT | Daily or burst limit exceeded |
| 429 | TEMP_SECURITY_LIMIT | Temporary security limit for some new free accounts |
| 500 | SERVER_ERROR | Internal server error |

Some new free accounts may temporarily hit a security limit.  
If you receive `TEMP_SECURITY_LIMIT` (`HTTP 429`), try again later or upgrade for higher limits.

### Common Integration Error: Wrong Batch Quote Route

If a client calls `GET /api/v1/quote/batch/` by mistake, the API returns `400 INVALID_ROUTE` with route guidance instead of an `INVALID_SYMBOL` for `"BATCH"`.

```json
{
  "error": {
    "code": "INVALID_ROUTE",
    "message": "Did you mean /api/v1/quotes/?symbols=2222,1120 ?"
  }
}
```

### Error Response Format

```json
{
  "error": {
    "code": "RATE_LIMIT",
    "message": "Daily request limit exceeded."
  }
}
```

---

## Quick Start

```bash
# Get Aramco quote by symbol
curl "https://app.sahmk.sa/api/v1/quote/2222/" \
  -H "X-API-Key: YOUR_API_KEY"

# Resolve by name/alias using optional identifier query param
curl "https://app.sahmk.sa/api/v1/quote/2222/?identifier=%D8%A3%D8%B1%D8%A7%D9%85%D9%83%D9%88" \
  -H "X-API-Key: YOUR_API_KEY"

# Get multiple quotes
curl "https://app.sahmk.sa/api/v1/quotes/?symbols=2222,1120,2010" \
  -H "X-API-Key: YOUR_API_KEY"

# Batch by names/aliases
curl "https://app.sahmk.sa/api/v1/quotes/?identifiers=Aramco,%D8%A7%D9%84%D8%B1%D8%A7%D8%AC%D8%AD%D9%8A" \
  -H "X-API-Key: YOUR_API_KEY"

# Discover companies/symbols
curl "https://app.sahmk.sa/api/v1/companies/?search=aramco&market=TASI&limit=20" \
  -H "X-API-Key: YOUR_API_KEY"

# Get market summary (defaults to TASI if index is omitted)
curl "https://app.sahmk.sa/api/v1/market/summary/?index=TASI" \
  -H "X-API-Key: YOUR_API_KEY"

# Get top gainers
curl "https://app.sahmk.sa/api/v1/market/gainers/?limit=5&index=TASI" \
  -H "X-API-Key: YOUR_API_KEY"

# Get top by volume
curl "https://app.sahmk.sa/api/v1/market/volume/?limit=10&index=TASI" \
  -H "X-API-Key: YOUR_API_KEY"

# Get company info (tiered by plan)
curl "https://app.sahmk.sa/api/v1/company/2222/" \
  -H "X-API-Key: YOUR_API_KEY"

# Get historical data (Starter+ plan)
curl "https://app.sahmk.sa/api/v1/historical/2222/?from=2026-01-01" \
  -H "X-API-Key: YOUR_API_KEY"

# Get financials (Starter+ plan)
curl "https://app.sahmk.sa/api/v1/financials/2222/" \
  -H "X-API-Key: YOUR_API_KEY"

# Get dividends (Starter+ plan)
curl "https://app.sahmk.sa/api/v1/dividends/2222/" \
  -H "X-API-Key: YOUR_API_KEY"

# Get stock events (Pro+ plan)
curl "https://app.sahmk.sa/api/v1/events/?symbol=2222" \
  -H "X-API-Key: YOUR_API_KEY"
```

---

## Support

- Documentation: https://sahmk.sa/developers/docs
- Dashboard: https://sahmk.sa/developers/dashboard
- MCP Server: https://pypi.org/project/sahmk-mcp/
- Python SDK: https://pypi.org/project/sahmk/
- Contact: https://sahmk.sa/contactus?type=api-support
