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.
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.
https://api.tasi-api.com/api/v1| Item | Value |
|---|---|
| Authentication | X-API-Key: tasi_live_… |
| Content type | application/json |
| Market timezone | Asia/Riyadh (timestamps are RFC 3339) |
| Trial | 7 days of full access, no card required |
| Portal | app.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.
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.
# 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 -s "https://api.tasi-api.com/api/v1/market/status" \
-H "X-API-Key: $TASI_API_KEY"{
"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.
Streaming
wss://api.tasi-api.com/api/v1/ws — the server pushes a frame whenever a quote changes, instead of you polling. Pro plan.
Paper trading
Simulated MARKET and LIMIT orders against the same quotes, with a cash ledger and performance reports. No exchange, no real money.
03 Authentication
Every market-data request carries your key in the X-API-Key header. Authorization: Bearer <key> is also accepted.
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 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
| Topic | Behaviour |
|---|---|
| Symbols | Upper-case codes, e.g. 2222, 1120, TASI. Path and query values are upper-cased for you. |
| Prices | JSON numbers in market responses; money and quantities in Paper Trading are decimal strings to keep precision. |
| Timestamps | RFC 3339 with offset. Session boundaries are Riyadh time and returned as absolute instants. |
| Errors | {"error":{"code":"…","message":"…"}} with a matching HTTP status. |
| Lists | No cursors; each list has a fixed cap. /quotes is bounded by your symbol entitlement. |
| Freshness | realtime_connected is true when a quote snapshot was written in the last 15 seconds. |
| Change | Derived 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.
| Method | Path | Returns |
|---|---|---|
GET | /market/status | phase, execution flag, freshness, resolved session |
GET | /market/calendar | effective calendar for a date range |
GET | /instruments | active 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
/market/statusno parameterscurl -s "https://api.tasi-api.com/api/v1/market/status" \
-H "X-API-Key: $TASI_API_KEY"{
"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
/market/calendar?from=YYYY-MM-DD&to=YYYY-MM-DDrange ≤ 366 dayscurl -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
/instrumentsactive instrumentscurl -s "https://api.tasi-api.com/api/v1/instruments" \
-H "X-API-Key: $TASI_API_KEY" | jq '.data[:2]'{
"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
/quotes?symbols=2222,1120,TASIbounded by symbol entitlement (20)curl -s "https://api.tasi-api.com/api/v1/quotes?symbols=2222,1120,TASI" \
-H "X-API-Key: $TASI_API_KEY"{
"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
}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
/history/{symbol}?interval=1m|5m|15m|30m|1h|1d&from=&to=&limit=newest first, returned ascendingcurl -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'{
"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(finalorprovisional). - 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-21includes 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
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
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.
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
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.
- In the portal open Paper Trading → Portfolios and create a portfolio (name + opening SAR balance).
- Open API Access for that portfolio and copy its key (
tasi_paper_…). It is shown once. - Use it against
/paper/portfolio…; it cannot reach your account, billing or another portfolio.
Read the portfolio
curl -s "https://api.tasi-api.com/api/v1/paper/portfolio" \
-H "X-API-Key: $PAPER_KEY"{
"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 -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 -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 -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"snapshot-v1 and snapshot-v1-limit). They are never exchange executions, there are no fees, no partial fills and no short selling.08 Errors
| Status | Code | What to do |
|---|---|---|
| 400 | invalid_request, invalid_symbol, invalid_order | Fix the body or parameter; unknown JSON fields are rejected. |
| 401 | api_key_required, invalid_api_key | Send X-API-Key; check the key was not rotated or revoked. |
| 403 | subscription_required, insufficient_scope, symbol_not_entitled | Renew the subscription, use a key with the required scope, or request a symbol your plan covers. symbol_not_entitled carries upgrade_url. |
| 404 | quote_not_found, instrument_not_found, portfolio_not_found | Check the symbol is active and has been observed, and that the resource belongs to you. |
| 409 | stale_quote, market_execution_unavailable, insufficient_cash, insufficient_position, portfolio_limit_reached | Retry during continuous trading, adjust quantity to available cash/position, or reuse the portfolio your plan allows. |
| 422 | symbol_limit_exceeded | Split the request; limit tells you the maximum. |
| 429 | rate_limited | Daily allowance exhausted. Retry-After carries the seconds to wait; the counter resets at 00:00 Asia/Riyadh. |
| 500 | server_error | Retry with backoff; if it persists, contact support with the timestamp. |
{ "error": { "code": "stale_quote",
"message": "The latest market snapshot is too old for a simulated market order." } }09 Limits
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.| Item | Free | Pro |
|---|---|---|
| Market Data keys | 1 active key | 1 active key |
Symbols per /quotes request | 3 (2222, 1120, 7010) | 20 (per-account override possible) |
| Paper portfolios | 1 active, with its own key | 3 active, each with its own key |
| Requests per day | 500, resetting at 00:00 Asia/Riyadh | Not metered — poll no faster than the 1-second snapshot cadence |
| Calendar range | 366 days per request | 366 days per request |
| Request body | 1 MiB, unknown fields rejected | 1 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.
| Capability | Status |
|---|---|
| Live quotes, instruments, market status and calendar | Available |
Historical bars — /history/{symbol} at 1m, 5m, 15m, 30m, 1h, 1d | Available |
| 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, 1mo | Not available yet |
/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.
plan_required and close code 4403 — REST polling stays available on Free.Connect
wss://api.tasi-api.com/api/v1/wsThree ways to authenticate, none of which puts the key in a URL:
| Mechanism | Use it when |
|---|---|
X-API-Key header | server-side client (Python, Node, Go) |
Authorization: Bearer <key> | scripts and CLIs |
second Sec-WebSocket-Protocol value | browsers — the only option, since a browser cannot set headers on a WebSocket |
first frame {"type":"auth","key":"…"} within 5s | fallback when neither of the above is possible |
Referer headers; the endpoint does not accept it.Frames
| Direction | type | Payload |
|---|---|---|
| server → client | hello | plan, max_symbols, tick_ms, server_time |
| client → server | subscribe | symbols array |
| client → server | unsubscribe | symbols array |
| client → server | ping | id echoed in pong |
| server → client | subscribed / unsubscribed | resulting symbol set and count |
| server → client | quote | data: the REST quote object plus sequence |
| server → client | error | code, message, docs_url |
A quote frame
{
"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
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
| Item | Value |
|---|---|
| Symbols per connection | 20 |
| Connections per API key | 5 |
| Frame push cadence | ~1s, only when the snapshot advances |
4401 | missing, invalid or lapsed key |
4403 | plan does not include streaming |
4429 | connection or message rate over the limit |
1001 | server restarting — reconnect with jitter |
/market/status if your client needs to know whether the market is open.