Client Classes
Auth, Data, Trading, Stream, Trading WebSocket — initialization and usage
SDK 3.3.2 provides 4 specialized clients, each with sync and async variants. There is also a Trading WebSocket, see Trading WebSocket.
Auth / AsyncAuth
Root client — manages REST client, authentication, and tokens. All other clients receive Auth as a parameter.
from ssi_sdk import Auth, Config
config = Config(
client_id="<CLIENT_ID>",
api_key="<API_KEY>",
api_secret="<API_SECRET>",
private_key="<PRIVATE_KEY_CONTENT>",
)
with Auth(config) as auth:
# Authenticate with OTP
token = auth.authenticate(otp="<OTP>")
print(f"Expires at: {token.expires_at}")Do not print or log token.access_token. Every duration in Config is in milliseconds (_ms suffix), see Configuration.
Authentication methods
# Request OTP (returns the raw dict)
result = auth.request_otp()
# Same, but returns OTPResponse(message, transaction_id)
otp = auth.request_otp_typed()
# Authenticate with OTP
token = auth.authenticate(otp="<OTP>")
# Refresh token
token = auth.refresh()
# Auto-refresh if expired, or authenticate if needed
access_token = auth.ensure_authenticated(otp="<OTP>")otp and transaction_id are mutually exclusive: pass only one. A token counts as expired 30 seconds early. Each apiKey has only 1 active session.
Smart OTP (approve in the app)
Accounts with Smart OTP enabled approve directly in the app instead of entering a code. Call request_otp_typed() to send the request and get a transaction_id, then pass it to ensure_authenticated(). The SDK polls automatically until the user approves.
from ssi_sdk.exceptions import (
ReauthenticationRequired,
SmartOTPPendingError,
SmartOTPRejectedError,
)
# Step 1: send the approval request, get transaction_id
otp = auth.request_otp_typed()
# Step 2: the SDK polls automatically until the user approves in the Smart OTP app
try:
access_token = auth.ensure_authenticated(
transaction_id=otp.transaction_id,
poll_interval_ms=5000, # milliseconds between polls (default Config.otp_poll_interval_ms = 5000)
poll_max_retries=12, # max poll attempts (default derived from Config.otp_poll_max_wait_ms = 60000)
)
except SmartOTPPendingError:
# Not approved in time: call again with the same transaction_id
...
except SmartOTPRejectedError:
# Rejected / expired: request a new OTP
...
except ReauthenticationRequired:
# Cannot refresh: authenticate again with a new OTP
...While waiting, the server returns HTTP 202 + code 401114 ("Push-approval is pending"); the SDK handles this automatically. If the poll attempts run out without approval, ensure_authenticated raises SmartOTPPendingError. poll_interval_ms and poll_max_retries are optional; when omitted the SDK uses Config.otp_poll_interval_ms and Config.otp_poll_max_wait_ms. Error details: Exceptions. The OTPResponse model: Models.
Token status
print(auth.token) # Token object or None
# auth.access_token # Access token string or None (do not print/log)
print(auth.is_token_expired) # True/False
print(auth.has_refresh_token) # True/FalseData / AsyncData
Market data client. No OTP needed — just auth.authenticate().
from ssi_sdk import Auth, Data, Config
with Auth(config) as auth:
auth.authenticate() # No OTP needed for market data
with Data(auth) as data:
# Access via service
ohlc = data.market_data.get_ohlc_1minute("SSI")
indices = data.market_data.get_indexes()
info = data.market_data.get_securities_info("SSI")Service: data.market_data (MarketDataService) — OHLC, indexes, securities, securities summary, master data, corporate actions. Details: Services, section 3.
Trading / AsyncTrading
Trading, account, and portfolio client. OTP required.
from ssi_sdk import Auth, Trading, Config
from ssi_sdk.enums import OrderSide
with Auth(config) as auth:
auth.authenticate(otp="<OTP>")
with Trading(auth) as trading:
# Account
accounts = trading.account.get_account_info()
# Portfolio (may return None when the account has no data yet)
balance = trading.portfolio.get_equity_balance("<EQUITY_ACCOUNT_NO>")
positions = trading.portfolio.get_equity_positions("<EQUITY_ACCOUNT_NO>")
orders = trading.portfolio.get_today_orders("<EQUITY_ACCOUNT_NO>")
# Trading
result = trading.trading.place_limit_order(
account_no="<EQUITY_ACCOUNT_NO>",
symbol="SSI",
side=OrderSide.BUY,
quantity=100,
price=66000,
)Services:
trading.account(AccountService) — account information.trading.portfolio(PortfolioService) — balances, positions, order book, PPMMR.get_equity_positionsandget_derivative_positionsreturnNonewhen there are no positions (or the server returns 204).trading.trading(TradingService) — place/modify/cancel orders, batch orders, max buy/sell, conditional orders (FCO).
Batch Order (place/cancel several orders in one request) is a method of trading.trading, see Batch Order. The per-batch limit comes from Config.max_batch_orders (default 20).
Stream / AsyncStream
Realtime streaming client via WebSocket. The market data channel needs no separate OTP; the trading channel (on_order, on_order_match, on_portfolio) needs OTP.
from ssi_sdk import Auth, Stream, Config
with Auth(config) as auth:
auth.authenticate(otp="<OTP>")
with Stream(auth) as stream:
# Register callbacks BEFORE connect
stream.streaming.on_data = lambda msg: print(f"Data: {msg}")
stream.streaming.on_trading = lambda msg: print(f"Trading: {msg}")
stream.streaming.on_connection = lambda ev: print(f"Conn: {ev.state}")
stream.streaming.connect()
# Subscribe
stream.streaming.subscribe_symbol(["SSI", "HPG"])
stream.streaming.subscribe_order_status()
# Wait for data (milliseconds; leave empty to wait until closed)
stream.streaming.wait(timeout_ms=30_000)Service: stream.streaming (StreamingService) — subscribe/unsubscribe realtime data. The connection reconnects automatically when Config.auto_reconnect is on (default True).
Callbacks:
| Property | Type | Description |
|---|---|---|
on_data | Callable[[message], Any] | Receives every market data message (auto-parsed to typed message) |
on_trading | Callable[[OrderStatusMessage | FCOOrderUpdateMessage | PortfolioMessage], Any] | Receives every trading event |
on_heartbeat | Callable[[HeartbeatMessage], Any] | Receives heartbeat |
on_market_flag | Callable[[MarketFlagMessage], Any] | Trading session flag |
on_index_summary | Callable[[IndexSummaryMessage], Any] | Summary figures of an index |
on_order, on_order_match, on_portfolio | Callback per trading message type | Needs OTP |
on_connection | Callable[[ConnectionEvent], Any] | Connection state: connected, reconnected, disconnected, failed |
Typed callbacks do not replace on_data / on_trading, and setting a callback does not subscribe. For real-time index data (index tick, 1m/5m candles, index summary, market flag) see Services, section 6.7; for the message models see Models: Streaming Messages; for connection state and reconnect see section 6.9.
Trading WebSocket
Import from ssi_sdk. In 3.3.1 and earlier these names lived in ssi_sdk.experimental; that module was removed in 3.3.2 (importing it raises ModuleNotFoundError).
Unlike Trading / Stream, the Trading WebSocket has no client class in the facade: you create it with a factory function, passing an authenticated Auth / AsyncAuth (the token comes from auth.token_manager).
| Function | Returns | Use with |
|---|---|---|
trading_ws(auth, **options) | TradingWSService | Auth |
async_trading_ws(auth, **options) | AsyncTradingWSService | AsyncAuth |
The URL comes from Config.trading_ws_domain (default wss://api.ssi.com.vn/ws/v3/trading). The options (request_timeout_ms, signature_encoding, handshake_grace_ms, respect_rate_limit, max_rate_limit_wait_ms, ping_interval_ms, user_agent) are not in Config; pass them straight to the factory function. State-changing commands need Config.private_key.
from ssi_sdk import Auth, Config, TradingWSError, trading_ws
with Auth(config) as auth:
auth.ensure_authenticated(otp="<OTP>")
try:
ws = trading_ws(auth)
except TradingWSError as e:
raise SystemExit(f"Server rejected the connection: {e}")
try:
book = ws.query_order_book("<EQUITY_ACCOUNT_NO>", page_size=10)
print(book.status)
finally:
ws.close() # always close the socketThe Trading WS does not reconnect on its own, and a command that times out is never resent by the SDK. Full method list, scopes, rate limit, idempotency and examples: Services, section 7.
Full async example
import asyncio
from ssi_sdk import AsyncAuth, AsyncData, AsyncTrading, AsyncStream, Config
async def main():
config = Config(
client_id="<CLIENT_ID>",
api_key="<API_KEY>",
api_secret="<API_SECRET>",
private_key="<PRIVATE_KEY_CONTENT>",
)
async with AsyncAuth(config) as auth:
await auth.authenticate(otp="<OTP>")
async with AsyncData(auth) as data:
ohlc = await data.market_data.get_ohlc_1minute("SSI")
print(ohlc)
async with AsyncTrading(auth) as trading:
accounts = await trading.account.get_account_info()
print(accounts)
async with AsyncStream(auth) as stream:
stream.streaming.on_data = lambda msg: print(msg)
await stream.streaming.connect()
await stream.streaming.subscribe_symbol_trade(["SSI"])
await stream.streaming.wait(timeout_ms=30_000)
asyncio.run(main())Notes
Async*variants useasync withandawait; callbacks of the async variants may be coroutines.- Sync variants (
Auth,Data,Trading,Stream) usewith— suitable for sequential flows. - All clients share the HTTP connection via
Auth. - Common errors:
AuthenticationError(auth, includingSmartOTPPendingError,SmartOTPRejectedError,ReauthenticationRequired),WebSocketError(stream;TradingWSErrorfor the Trading WebSocket),APIError(HTTP business errors;DuplicateRequestErrorfor HTTP 409). See Exceptions.