Python

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/False

Data / 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_positions and get_derivative_positions return None when 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:

PropertyTypeDescription
on_dataCallable[[message], Any]Receives every market data message (auto-parsed to typed message)
on_tradingCallable[[OrderStatusMessage | FCOOrderUpdateMessage | PortfolioMessage], Any]Receives every trading event
on_heartbeatCallable[[HeartbeatMessage], Any]Receives heartbeat
on_market_flagCallable[[MarketFlagMessage], Any]Trading session flag
on_index_summaryCallable[[IndexSummaryMessage], Any]Summary figures of an index
on_order, on_order_match, on_portfolioCallback per trading message typeNeeds OTP
on_connectionCallable[[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).

FunctionReturnsUse with
trading_ws(auth, **options)TradingWSServiceAuth
async_trading_ws(auth, **options)AsyncTradingWSServiceAsyncAuth

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 socket

The 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 use async with and await; callbacks of the async variants may be coroutines.
  • Sync variants (Auth, Data, Trading, Stream) use with — suitable for sequential flows.
  • All clients share the HTTP connection via Auth.
  • Common errors: AuthenticationError (auth, including SmartOTPPendingError, SmartOTPRejectedError, ReauthenticationRequired), WebSocketError (stream; TradingWSError for the Trading WebSocket), APIError (HTTP business errors; DuplicateRequestError for HTTP 409). See Exceptions.

On this page