Client Classes
AuthClient, DataClient, TradingClient, StreamClient — initialization and usage
The SDK provides 4 specialized clients. All API methods are async and return Task/Task<T>.
AuthClient
The root client — manages the REST client, authentication, and tokens. Implements IDisposable. All other clients receive AuthClient as a parameter.
using SsiSdk;
var config = new Config("YOUR_CLIENT_ID")
{
ApiKey = "YOUR_API_KEY",
ApiSecret = "YOUR_API_SECRET",
PrivateKey = "YOUR_PRIVATE_KEY",
};
using var auth = new AuthClient(config);
var token = await auth.AuthenticateAsync("YOUR_OTP");
Console.WriteLine($"Authenticated, token expires at (epoch seconds): {token.ExpiresAt}");Do not print or log
AccessToken: it is a credential.
Authentication methods
// Request OTP (returns OtpResponse { Message, TransactionId? })
OtpResponse otp = await auth.RequestOtpTypedAsync();
// RequestOtpAsync() returns the raw JsonElement
// Authenticate with OTP
var token = await auth.AuthenticateAsync("YOUR_OTP");
// Authenticate without OTP (market data only)
var token = await auth.AuthenticateAsync();
// Refresh token
var token = await auth.RefreshAsync();
// Ensure authenticated (use the current token, refresh if expired, or authenticate again)
var accessToken = await auth.EnsureAuthenticatedAsync("YOUR_OTP");AuthenticateAsync(otp?, transactionId?) is a single call. If the server reports that the Smart OTP is not approved yet (HTTP 202, code 401114), the method throws SmartOtpPendingException. Passing both otp and transactionId throws AuthenticationException. On Token, ExpiresAt and RefreshTokenExpiresAt are epoch seconds; 0 means unknown or expired.
Approve OTP (Smart OTP push-approval)
Accounts with Smart OTP enabled can approve directly in the app instead of entering a code. Call RequestOtpTypedAsync() to send the request and get a TransactionId, then pass it to EnsureAuthenticatedAsync() — the SDK polls until the user approves or the wait time runs out.
// Step 1: send the approval request, get the TransactionId
var otp = await auth.RequestOtpTypedAsync();
// Step 2: the SDK polls until the user approves in the Smart OTP app
var accessToken = await auth.EnsureAuthenticatedAsync(
transactionId: otp.TransactionId,
pollIntervalMs: 5000, // default Config.OtpPollIntervalMs
pollMaxRetries: 12); // default derived from Config.OtpPollMaxWaitMsOrder of EnsureAuthenticatedAsync: use a valid token, otherwise refresh, otherwise authenticate with the OTP, and with a transactionId poll the Smart OTP. While waiting, the server returns HTTP 202 + code 401114 ("Push-approval is pending") — the SDK keeps polling. Failure cases:
| Situation | Exception |
|---|---|
| Poll attempts run out and the user has not approved | SmartOtpPendingException |
The user rejects or the request expires (401113, 401115, 401116) | SmartOtpRejectedException (terminal, no more polling) |
The token is expired, cannot be refreshed, and no new otp/transactionId is given | ReauthenticationRequiredException |
No token yet and no otp/transactionId | AuthenticationException |
pollMaxRetries defaults to Config.OtpPollMaxWaitMs / Config.OtpPollIntervalMs (60000 / 5000 by default, so 12 attempts). See Error Handling.
Token state
auth.AccessToken; // current access token (string, empty if none)
auth.TokenManager.Token; // full Token object
auth.TokenManager.IsTokenExpired; // true/false
auth.SetToken(token); // set token (cache); auth.TokenManager.SetToken also exists
auth.RateLimit; // RateLimitInfo from the latest responseDataClient
Market data client. No OTP needed — just call auth.AuthenticateAsync().
using SsiSdk;
using var auth = new AuthClient(config);
await auth.AuthenticateAsync();
var data = new DataClient(auth);
var ohlc = await data.MarketData.GetOhlc1MinuteAsync("SSI");
var indexes = await data.MarketData.GetIndexesAsync();
var info = await data.MarketData.GetSecuritiesInfoAsync("SSI");Service: data.MarketData (MarketDataService) — OHLC, indexes, securities.
TradingClient
Client for orders, account, and portfolio. OTP required.
using SsiSdk;
using var auth = new AuthClient(config);
await auth.AuthenticateAsync("YOUR_OTP");
var trading = new TradingClient(auth);
// Account
var accounts = await trading.Account.GetAccountInfoAsync();
// Portfolio
var balance = await trading.Portfolio.GetEquityBalanceAsync("YOUR_ACCOUNT_NO");
var positions = await trading.Portfolio.GetEquityPositionsAsync("YOUR_ACCOUNT_NO");
var orders = await trading.Portfolio.GetTodayOrdersAsync("YOUR_ACCOUNT_NO");
// Trading
var result = await trading.Trading.PlaceLimitOrderAsync(
"YOUR_ACCOUNT_NO", "SSI", OrderSide.Buy, 100, 66000m);Services:
trading.Account(AccountService) — account information.trading.Portfolio(PortfolioService) — balances, positions, order book, PPMMR.trading.Trading(TradingService) — place/modify/cancel orders, batch orders, max buy/sell, conditional orders (FCO).
Trading commands over the separate WebSocket do not go through TradingClient: see Trading WebSocket.
StreamClient
Realtime WebSocket streaming client. Implements IDisposable. OTP required.
using SsiSdk;
using SsiSdk.Models;
using var auth = new AuthClient(config);
await auth.AuthenticateAsync("YOUR_OTP");
using var stream = new StreamClient(auth);
// Register callbacks
stream.Streaming.SetOnData(msg =>
{
switch (msg)
{
case TradeMessage trade:
Console.WriteLine($"[TRADE] {trade.Symbol} | {trade.Price} | {trade.Quantity}");
break;
case QuoteMessage quote:
Console.WriteLine($"[QUOTE] {quote.Symbol}");
break;
}
});
stream.Streaming.SetOnTrading(msg =>
{
if (msg is OrderStatusMessage order)
Console.WriteLine($"[ORDER] {order.Symbol} {order.Side} | {order.Status}");
});
// Connect and subscribe
await stream.ConnectAsync();
await stream.Streaming.SubscribeSymbolAsync(["SSI", "HPG"]);
await stream.Streaming.SubscribeOrderStatusAsync();
await stream.WaitAsync();Service: stream.Streaming (StreamingService) — subscribe/unsubscribe realtime data.
Callbacks:
| Method | Callback | Description |
|---|---|---|
SetOnData | Action<object>? | Receives market data (use pattern matching to handle) |
SetOnTrading | Action<object>? | Receives trading events (order status, portfolio) |
SetOnHeartbeat | Action<HeartbeatMessage>? | Receives heartbeat |
Connection state & cleanup:
stream.IsConnected; // true/false
stream.LastError; // latest Exception?
stream.ReconnectCount; // number of reconnects so far
stream.OnConnection = e => Console.WriteLine($"{e.State} (reconnect #{e.ReconnectCount})");
stream.Streaming.StopPingLoop();
await stream.DisconnectAsync(); // call when not using `using`; a synchronous Disconnect() also existsThe socket reconnects automatically after a drop (Config.AutoReconnect, on by default) and re-sends the recorded subscriptions. See Streaming for details.
Full Example
using SsiSdk;
using SsiSdk.Models;
var config = new Config("YOUR_CLIENT_ID")
{
ApiKey = "YOUR_API_KEY",
ApiSecret = "YOUR_API_SECRET",
PrivateKey = "YOUR_PRIVATE_KEY",
};
using var auth = new AuthClient(config);
await auth.AuthenticateAsync("YOUR_OTP");
// Market data
var data = new DataClient(auth);
var ohlc = await data.MarketData.GetOhlc1MinuteAsync("SSI");
Console.WriteLine(ohlc);
// Trading
var trading = new TradingClient(auth);
var accounts = await trading.Account.GetAccountInfoAsync();
Console.WriteLine(accounts);
// Streaming
using var stream = new StreamClient(auth);
stream.Streaming.SetOnData(msg =>
{
if (msg is TradeMessage trade)
Console.WriteLine($"[TRADE] {trade.Symbol} | {trade.Price} | {trade.Quantity}");
});
await stream.ConnectAsync();
await stream.Streaming.SubscribeSymbolAsync(["SSI", "HPG"]);
await stream.Streaming.SubscribeOrderStatusAsync();
await stream.WaitAsync(timeoutMs: 30000);Notes
- All API methods are
async— useawait. AuthClientandStreamClientimplementIDisposable— always useusing/using var.- All clients share the same HTTP connection via
AuthClient. - Common errors:
AuthenticationException(authentication; includesSmartOtpPendingException,SmartOtpRejectedException,ReauthenticationRequiredException),WebSocketException(streaming),ApiException(HTTP errors),RateLimitException(429),ValidationException(bad input, no request sent). See Error Handling. auth.RateLimitholds the rate-limit info of the latest response.