DEVELOPER DOCUMENTATION

Everything you need to call the TASI API.

One key, one base URL and a small set of JSON endpoints for Saudi market data and Paper Trading. This page takes you from sign-up to a working request, with copy‑paste examples.

REST · v1X-API-KeyJSON in / JSON outAsia/Riyadh7 days full access · free

01 Overview

The TASI API exposes the Saudi market through a single REST surface. You authenticate with an API key, request symbols by code, and receive JSON with explicit timestamps.

  • Market data — latest quote per symbol, derived change and change percent, instrument reference data, trading-session state and calendar.
  • Paper Trading — simulated portfolios with their own scoped keys, MARKET and LIMIT orders, positions, cash ledger and performance.
BASE URLhttps://api.tasi-api.com/api/v1
ItemValue
AuthenticationX-API-Key: tasi_live_…
Content typeapplication/json
Market timezoneAsia/Riyadh (timestamps are RFC 3339)
Trial7 days of full access, no card required
Portalapp.tasi-api.com — keys, usage, paper portfolios, billing

02 Quick start

Four steps, about five minutes. No SDK is required; any HTTP client works.

Create your account

Sign up with an email and a password of at least 12 characters. The trial starts when the email is verified.

Create account

Verify your email

Open the link we send you. It is valid for 24 hours; verifying activates 7 days of Developer access.

Create an API key

In the portal open API Keys → Create API key. The secret is shown once, in the form tasi_live_<id>_<secret>. Store it in your server environment; rotating or revoking it takes effect immediately.

shell
# keep the key out of source control
export TASI_API_KEY="tasi_live_xxxxxxxxxxxxxxxx_yyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyy"

Make your first request

Ask for the market state. A 200 with "phase" means your key and subscription are working.

curl
curl -s "https://api.tasi-api.com/api/v1/market/status" \
  -H "X-API-Key: $TASI_API_KEY"
200 OK
{
  "status": "open",
  "phase": "continuous",
  "execution_open": true,
  "realtime_connected": true,
  "last_snapshot_at": "2026-10-18T10:31:07.412Z",
  "timezone": "Asia/Riyadh"
}

What else you can call

History is one of four surfaces. Most integrations that start with /history add one of these the same week:

Live quotes

GET /quotes?symbols=2222,1120 — latest price, bid, ask and the session's cumulative volume, in one call.

Quotes →

Streaming

wss://api.tasi-api.com/api/v1/ws — the server pushes a frame whenever a quote changes, instead of you polling. Pro plan.

Streaming →

Paper trading

Simulated MARKET and LIMIT orders against the same quotes, with a cash ledger and performance reports. No exchange, no real money.

Paper trading →

03 Authentication

Every market-data request carries your key in the X-API-Key header. Authorization: Bearer <key> is also accepted.

headers
X-API-Key: tasi_live_xxxxxxxxxxxxxxxx_yyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyy
Content-Type: application/json
  • One active Market Data key per account. Rotating replaces it; the old key stops working at once.
  • The key is checked on every request together with the account status and the subscription or trial.
  • Call the API from your server. A key in browser JavaScript is visible to every visitor; the portal's own browser calls use a session instead.
401 vs 403. 401 invalid_api_key means the key is unknown or revoked. 403 subscription_required means the key is valid but the trial expired or the subscription is inactive.

04 Conventions

TopicBehaviour
SymbolsUpper-case codes, e.g. 2222, 1120, TASI. Path and query values are upper-cased for you.
PricesJSON numbers in market responses; money and quantities in Paper Trading are decimal strings to keep precision.
TimestampsRFC 3339 with offset. Session boundaries are Riyadh time and returned as absolute instants.
Errors{"error":{"code":"…","message":"…"}} with a matching HTTP status.
ListsNo cursors; each list has a fixed cap. /quotes is bounded by your symbol entitlement.
Freshnessrealtime_connected is true when a quote snapshot was written in the last 15 seconds.
ChangeDerived server-side: source change → previous close → previous daily bar. Do not recompute it client-side.

05 Endpoints

Six market-data endpoints cover the current REST surface. All require your API key.

MethodPathReturns
GET/market/statusphase, execution flag, freshness, resolved session
GET/market/calendareffective calendar for a date range
GET/instrumentsactive instruments with sector and ISIN
GET/instruments/{symbol}one instrument
GET/quotes?symbols=…one or more quotes, entitlement enforced
GET/quotes/{symbol}one quote with derived change

Market status

GET/market/statusno parameters
curl
curl -s "https://api.tasi-api.com/api/v1/market/status" \
  -H "X-API-Key: $TASI_API_KEY"
200 OK
{
  "status": "open",
  "phase": "continuous",
  "realtime_connected": true,
  "execution_open": true,
  "last_snapshot_at": "2026-10-18T10:31:07.412Z",
  "timezone": "Asia/Riyadh",
  "session": {
    "date": "2026-10-18",
    "status": "open",
    "opens_at": "2026-10-18T07:00:00Z",
    "continuous_closes_at": "2026-10-18T12:00:00Z",
    "trade_at_last_ends_at": "2026-10-18T12:20:00Z",
    "calendar_override": false
  }
}

Trading calendar

GET/market/calendar?from=YYYY-MM-DD&to=YYYY-MM-DDrange ≤ 366 days
curl
curl -s "https://api.tasi-api.com/api/v1/market/calendar?from=2026-10-18&to=2026-10-22" \
  -H "X-API-Key: $TASI_API_KEY"

Each day returns its effective status and phase boundaries. Sundays to Thursdays are regular sessions unless a holiday or special session overrides them; a missing override is normal.

Instruments

GET/instrumentsactive instruments
curl
curl -s "https://api.tasi-api.com/api/v1/instruments" \
  -H "X-API-Key: $TASI_API_KEY" | jq '.data[:2]'
200 OK
{
  "data": [
    { "symbol": "1120", "name_en": "Al Rajhi Bank", "kind": "stock",
      "active": true, "sector_code": "BAN" }
  ],
  "count": 1
}

Use /instruments/{symbol} for a single record. Instruments are reference data: names and sector come from the curated catalogue, not from the live feed.

Quotes

GET/quotes?symbols=2222,1120,TASIbounded by symbol entitlement (20)
curl
curl -s "https://api.tasi-api.com/api/v1/quotes?symbols=2222,1120,TASI" \
  -H "X-API-Key: $TASI_API_KEY"
200 OK
{
  "data": [
    {
      "symbol": "2222",
      "captured_at": "2026-10-18T10:31:07.412Z",
      "last": 26.90, "bid": 26.88, "ask": 26.92,
      "volume_day": 12345678, "value_day": 332000000,
      "open": 26.55, "change": 0.35, "change_percent": 1.32,
      "name_en": "Saudi Arabian Oil Co."
    }
  ],
  "count": 1, "limit": 20, "requested": 3
}
Requesting more symbols than your entitlement returns 422 symbol_limit_exceeded with limit and requested — the response never silently drops symbols.

Single symbol: GET /quotes/2222 returns {"data": { … }}. A symbol that has never been observed returns 404 quote_not_found.

Historical bars

GET/history/{symbol}?interval=1m|5m|15m|30m|1h|1d&from=&to=&limit=newest first, returned ascending
curl
curl -s "https://api.tasi-api.com/api/v1/history/2222?interval=15m&from=2026-10-20&to=2026-10-21&limit=32" \
  -H "X-API-Key: $TASI_API_KEY" | jq '.data | length'
200 OK
{
  "symbol": "2222", "interval": "15m",
  "from": "2026-10-20T00:00:00Z", "to": "2026-10-21T23:59:59Z",
  "limit": 32, "count": 32,
  "data": [
    { "t": "2026-10-21T09:30:00Z", "open": 26.55, "high": 26.71,
      "low": 26.48, "close": 26.66, "volume": 410233,
      "value": 10903842.1, "quality": "final" }
  ]
}
  • 1d reads the daily series directly and carries quality (final or provisional).
  • 5m to 1h are aggregated from the 1-minute series on the server: open is the first minute, close the last, high and low the extremes, and volume and value are summed.
  • limit defaults to 500 and caps at 5000; the window defaults to 7 days for intraday and 30 days for daily.
  • Dates are read in Riyadh time, so to=2026-10-21 includes that whole session.

See it rendered: candlestick examples at every interval →

06 Working examples

Small, complete scripts you can run after exporting TASI_API_KEY.

Python — quotes for a watchlist

python
import os, requests

API = "https://api.tasi-api.com/api/v1"
KEY = os.environ["TASI_API_KEY"]

r = requests.get(
    f"{API}/quotes",
    params={"symbols": "2222,1120,2010,TASI"},
    headers={"X-API-Key": KEY},
    timeout=10,
)
r.raise_for_status()

for q in r.json()["data"]:
    print(q["symbol"], q["last"], q.get("change_percent"))

Node.js 18+ — same request with fetch

quotes.mjs
const API = "https://api.tasi-api.com/api/v1";

const res = await fetch(`${API}/quotes?symbols=2222,1120`, {
  headers: { "X-API-Key": process.env.TASI_API_KEY },
});

if (!res.ok) {
  const body = await res.json();
  throw new Error(body.error.message);
}

const { data } = await res.json();
console.table(data.map(({ symbol, last, change_percent }) => ({ symbol, last, change_percent })));

Python — poll only while the market is live

The API reports the phase, so a poller can idle outside trading hours instead of hammering a closed market.

python
import os, time, requests

API = "https://api.tasi-api.com/api/v1"
H = {"X-API-Key": os.environ["TASI_API_KEY"]}

while True:
    s = requests.get(f"{API}/market/status", headers=H, timeout=10).json()
    if s["execution_open"]:
        quotes = requests.get(
            f"{API}/quotes", params={"symbols": "2222,1120"}, headers=H, timeout=10
        ).json()["data"]
        for q in quotes:
            print(q["symbol"], q["last"], q["captured_at"])
        time.sleep(10)
    else:
        print("market closed:", s["phase"])
        time.sleep(60)

cURL + jq — find instruments by name

shell
curl -s "https://api.tasi-api.com/api/v1/instruments" \
  -H "X-API-Key: $TASI_API_KEY" \
  | jq -r '.data[] | select(.name_en | test("bank";"i")) | "\(.symbol)\t\(.name_en)"'

07 Paper Trading API

Paper Trading uses a separate key that is bound to one portfolio and carries three scopes: market:read, portfolio:read, orders:write.

  1. In the portal open Paper Trading → Portfolios and create a portfolio (name + opening SAR balance).
  2. Open API Access for that portfolio and copy its key (tasi_paper_…). It is shown once.
  3. Use it against /paper/portfolio…; it cannot reach your account, billing or another portfolio.

Read the portfolio

curl
curl -s "https://api.tasi-api.com/api/v1/paper/portfolio" \
  -H "X-API-Key: $PAPER_KEY"
200 OK
{
  "portfolio": { "id": "…", "name": "Alpha", "currency": "SAR",
                 "opening_balance": "100000.0000", "cash_balance": "100000.0000" },
  "summary": { "equity": "100000.0000", "market_value": "0.0000",
               "total_pnl": "0.0000", "return_pct": "0.0000" }
}

Place a MARKET order

curl
curl -s -X POST "https://api.tasi-api.com/api/v1/paper/portfolio/orders" \
  -H "X-API-Key: $PAPER_KEY" \
  -H "Content-Type: application/json" \
  -d '{"symbol":"2222","side":"buy","order_type":"market","quantity":"10"}'

A market order fills immediately at the latest snapshot when the phase is continuous and the snapshot is at most 15 seconds old. Outside that window it returns 409 market_execution_unavailable or 409 stale_quote.

Place a LIMIT order

curl
curl -s -X POST "https://api.tasi-api.com/api/v1/paper/portfolio/orders" \
  -H "X-API-Key: $PAPER_KEY" \
  -H "Content-Type: application/json" \
  -d '{"symbol":"1120","side":"buy","order_type":"limit","quantity":"20","limit_price":"64.00"}'

A limit order stays open and reserves cash (BUY) or quantity (SELL). It fills on the first newer snapshot whose ask (BUY) or bid (SELL) crosses your price.

Positions and orders

curl
curl -s "https://api.tasi-api.com/api/v1/paper/portfolio/positions" -H "X-API-Key: $PAPER_KEY"
curl -s "https://api.tasi-api.com/api/v1/paper/portfolio/orders"    -H "X-API-Key: $PAPER_KEY"
Simulated only. Paper fills are derived from market snapshots (models snapshot-v1 and snapshot-v1-limit). They are never exchange executions, there are no fees, no partial fills and no short selling.

08 Errors

StatusCodeWhat to do
400invalid_request, invalid_symbol, invalid_orderFix the body or parameter; unknown JSON fields are rejected.
401api_key_required, invalid_api_keySend X-API-Key; check the key was not rotated or revoked.
403subscription_required, insufficient_scope, symbol_not_entitledRenew the subscription, use a key with the required scope, or request a symbol your plan covers. symbol_not_entitled carries upgrade_url.
404quote_not_found, instrument_not_found, portfolio_not_foundCheck the symbol is active and has been observed, and that the resource belongs to you.
409stale_quote, market_execution_unavailable, insufficient_cash, insufficient_position, portfolio_limit_reachedRetry during continuous trading, adjust quantity to available cash/position, or reuse the portfolio your plan allows.
422symbol_limit_exceededSplit the request; limit tells you the maximum.
429rate_limitedDaily allowance exhausted. Retry-After carries the seconds to wait; the counter resets at 00:00 Asia/Riyadh.
500server_errorRetry with backoff; if it persists, contact support with the timestamp.
error envelope
{ "error": { "code": "stale_quote",
             "message": "The latest market snapshot is too old for a simulated market order." } }

09 Limits

Free plan: 2222, 1120, 7010 and TASI, 500 requests per day, one paper portfolio. Anything outside that set answers 403 symbol_not_entitled. Pro removes the symbol restriction and the daily cap.
ItemFreePro
Market Data keys1 active key1 active key
Symbols per /quotes request3 (2222, 1120, 7010)20 (per-account override possible)
Paper portfolios1 active, with its own key3 active, each with its own key
Requests per day500, resetting at 00:00 Asia/RiyadhNot metered — poll no faster than the 1-second snapshot cadence
Calendar range366 days per request366 days per request
Request body1 MiB, unknown fields rejected1 MiB, unknown fields rejected

Free responses carry X-RateLimit-Limit, X-RateLimit-Remaining and X-RateLimit-Reset; Pro is not metered and sends none. Quotes are refreshed roughly once per second during a session, so 5–15 seconds is enough for a dashboard.

10 Availability

This page documents only what the API serves today. Anything else is not a feature yet.

CapabilityStatus
Live quotes, instruments, market status and calendarAvailable
Historical bars — /history/{symbol} at 1m, 5m, 15m, 30m, 1h, 1dAvailable
Paper Trading (portfolios, MARKET/LIMIT orders, positions, reports)Available
WebSocket streaming — wss://api.tasi-api.com/api/v1/ws, 20 symbols (Pro)Available
Intervals 10m, 60m, 1w, 1moNot available yet
REST polling or streaming, your choice. /history serves the six intervals above — anything else answers 400 invalid_interval. Streaming is documented in section 11.

Questions: [email protected] · account, keys and usage: app.tasi-api.com

11 Streaming (WebSocket)

One connection, a fixed symbol set, and a frame whenever the market snapshot for a subscribed symbol advances. This replaces polling /quotes in a loop.

Pro plan only. A Free account receives plan_required and close code 4403 — REST polling stays available on Free.

Connect

endpoint
wss://api.tasi-api.com/api/v1/ws

Three ways to authenticate, none of which puts the key in a URL:

MechanismUse it when
X-API-Key headerserver-side client (Python, Node, Go)
Authorization: Bearer <key>scripts and CLIs
second Sec-WebSocket-Protocol valuebrowsers — the only option, since a browser cannot set headers on a WebSocket
first frame {"type":"auth","key":"…"} within 5sfallback when neither of the above is possible
Never pass the key as a query parameter. It would land in proxy logs and Referer headers; the endpoint does not accept it.

Frames

DirectiontypePayload
server → clienthelloplan, max_symbols, tick_ms, server_time
client → serversubscribesymbols array
client → serverunsubscribesymbols array
client → serverpingid echoed in pong
server → clientsubscribed / unsubscribedresulting symbol set and count
server → clientquotedata: the REST quote object plus sequence
server → clienterrorcode, message, docs_url

A quote frame

200 · quote
{
  "type": "quote",
  "data": {
    "symbol": "2222",
    "sequence": "1792917214818277600",
    "captured_at": "2026-10-25T08:33:34.818277Z",
    "last": 25.18, "bid": 25.16, "ask": 25.20,
    "volume_day": 2426281, "value_day": 61296937,
    "change": -0.26, "change_percent": -1.02,
    "name_en": "Saudi Arabian Oil Co."
  }
}

sequence is a string. It is an int64 larger than 2^53, so a client that parses it as a JSON number would round it and could not tell two frames apart. Compare it as an integer or a decimal string, and drop any frame whose sequence is not greater than the last one you saw for that symbol.

Client

python
import asyncio, json, os, websockets

async def main():
    headers = {"X-API-Key": os.environ["TASI_API_KEY"]}
    async with websockets.connect("wss://api.tasi-api.com/api/v1/ws",
                                  additional_headers=headers) as ws:
        print(json.loads(await ws.recv()))            # hello
        await ws.send(json.dumps({"type": "subscribe",
                                  "symbols": ["2222", "1120", "TASI"]}))
        async for raw in ws:
            msg = json.loads(raw)
            if msg["type"] == "quote":
                d = msg["data"]
                print(d["symbol"], d["last"], d["sequence"])
            else:
                print(msg)

asyncio.run(main())

Limits and close codes

ItemValue
Symbols per connection20
Connections per API key5
Frame push cadence~1s, only when the snapshot advances
4401missing, invalid or lapsed key
4403plan does not include streaming
4429connection or message rate over the limit
1001server restarting — reconnect with jitter
Quotes push only during a live session. Outside trading hours the connection stays open and silent; call /market/status if your client needs to know whether the market is open.