infoway-sdk
v0.2.0
Published
Official Node.js/TypeScript SDK for Infoway real-time financial data API
Maintainers
Readme
Infoway SDK for Node.js / TypeScript
English | 中文
Official Node.js/TypeScript SDK for the Infoway real-time financial data API.
API Documentation | Get API Key
Get your free API key at infoway.io -- 7-day free trial
Installation
npm install infoway-sdk
# or
yarn add infoway-sdk
# or
pnpm add infoway-sdkRequirements: Node.js >= 18.0.0
Upgrading from 0.1.x?
basic.*changed shape in 0.2.0 — see Migrating to 0.2.0.
Quick Start
REST API
import { InfowayClient, KlineType, SymbolType } from "infoway-sdk";
const client = new InfowayClient({ apiKey: "YOUR_API_KEY" });
// Trades — [{ s, t, p, v, vw, td }]
const trades = await client.stock.getTrade("AAPL.US");
// Multiple symbols in one call
const multi = await client.stock.getTrade("AAPL.US,TSLA.US,GOOGL.US");
// Order book — a = asks, b = bids, both [[price…], [quantity…]]
const depth = await client.stock.getDepth("AAPL.US");
// Candles — [{ s, respList: [ … ] }]
const klines = await client.stock.getKline("AAPL.US", KlineType.DAY, 100);
// Crypto (trades 24/7)
const btc = await client.crypto.getTrade("BTCUSDT");
const ethKline = await client.crypto.getKline("ETHUSDT", KlineType.HOUR_1, 50);
// Market overview
const temp = await client.market.getTemperature("HK,US");
const breadth = await client.market.getBreadth("US");
// Fundamentals
const valuation = await client.stockInfo.getValuation("AAPL.US");
const ratings = await client.stockInfo.getRatings("AAPL.US");
// Sectors
const industries = await client.plate.getIndustry("HK");
const concepts = await client.plate.getConcept("HK");
// Reference data — note the instrument type and the YYYYMMDD ranges
const symbols = await client.basic.getSymbols(SymbolType.STOCK_US);
const info = await client.basic.getSymbolInfo("STOCK_US", "AAPL.US,TSLA.US");
const factors = await client.basic.getAdjustmentFactors("AAPL.US", "US", "20260801", "20260814");
const tradingDays = await client.basic.getTradingDays("US", "20260801", "20260814");
const schedule = await client.basic.getTradingSchedule();Symbol formats
| Market | Format | Example |
|--------|--------|---------|
| US | <TICKER>.US | AAPL.US |
| Hong Kong | 5 digits, zero-padded, .HK | 00700.HK (700.HK is rejected) |
| China A-shares | .SH / .SZ | 600519.SH |
| Japan | .JP | 7203.JP |
| India | .IN | RELIANCE.IN |
| Korea | .KS | 005930.KS |
| Crypto | pair | BTCUSDT |
| Forex / metals | pair | USDJPY, XAUUSD |
A missing or wrong suffix comes back as All product not exists, raised as an InfowayAPIError.
Environment Variable
You can set INFOWAY_API_KEY instead of passing it directly:
export INFOWAY_API_KEY=your_api_keyconst client = new InfowayClient(); // reads from INFOWAY_API_KEYResponse shapes (what the server really sends)
By default the SDK returns the payload untouched:
// getTrade — t is milliseconds; prices and volumes are strings
[{ "s": "AAPL.US", "t": 1786751999691, "p": "305.771", "v": "1", "vw": "305.771", "td": 0 }]
// getDepth — transposed columns: [[prices…], [quantities…]]
[{ "s": "AAPL.US", "t": 1786751397726, "a": [["305.800"], ["229"]], "b": [["305.770"], ["24"]] }]
// getKline — candles are nested under respList; t is SECONDS, as a string
[{ "s": "AAPL.US", "respList": [
{ "t": "1786737540", "o": "305.830", "h": "305.990", "l": "305.675", "c": "305.930",
"v": "331559", "vw": "101402874.954", "pc": "0.03%", "pca": "0.090" }
]}]Field notes:
vwis turnover (cash traded), not a VWAP.vw / vgives the price level. Older English API docs describe it as a weighted average price — that is wrong.tis milliseconds on trades and depth, seconds on candles.- The change percent is
pcover REST andpfrover WebSocket, formatted"0.03%". ais the sell side,bis the buy side.
Normalized output (parse: true)
Opt in per call to get numbers, Dates and a rebuilt order book:
const trades = await client.crypto.getTrade("BTCUSDT", { parse: true });
trades[0].price; // 63059.36 (number)
trades[0].time; // Date (from the millisecond field)
trades[0].turnover; // 665.9068416 (the `vw` field, renamed)
const book = await client.crypto.getDepth("BTCUSDT", { parse: true });
book[0].asks[0]; // { price: 63069.29, volume: 4.16811 }
const candles = await client.crypto.getKline("BTCUSDT", KlineType.MIN_1, 100, { parse: true });
candles[0].close; // 63069.28 (number)
candles[0].change_percent; // 0.0003 ("0.03%" as a fraction)
candles[0].time; // Date (from the second field)
candles[0].raw; // the untouched server objectThe same helpers are exported standalone: normalizeTrade, normalizeDepth, normalizeKline,
normalizeKlines, normalizeWsKline, parseChangePercent.
WebSocket Real-time Data
import { InfowayWebSocket, Business, InfowayAuthError } from "infoway-sdk";
const ws = new InfowayWebSocket({
apiKey: "YOUR_API_KEY",
business: Business.STOCK,
});
// Callbacks receive the tick itself: { s, p, t, v, vw, td }
ws.onTrade = (tick) => console.log(tick.s, tick.p, tick.t);
ws.onDepth = (book) => console.log(book.s, book.a, book.b);
ws.onKline = (candle) => console.log(candle.s, candle.ty, candle.c);
ws.onError = (err) => console.error(err);
ws.onDisconnect = () => console.log("Disconnected, reconnecting…");
ws.onReconnect = () => console.log("Reconnected — subscriptions replayed");
// Send every symbol of one channel in ONE call (see the rate limit below)
await ws.subscribeTrade("AAPL.US,TSLA.US");
await ws.subscribeDepth("AAPL.US");
try {
await ws.connect(); // resolves when you call close()
} catch (err) {
if (err instanceof InfowayAuthError) {
console.error("API key rejected at the handshake — reconnecting was stopped.");
}
}
// await ws.close();With parse: true the callbacks receive normalized objects instead — TypeScript picks up the change automatically:
const ws = new InfowayWebSocket({ apiKey: "YOUR_API_KEY", business: Business.CRYPTO, parse: true });
ws.onTrade = (t) => console.log(t.symbol, t.price, t.time.toISOString(), t.turnover);
await ws.subscribeTrade("BTCUSDT,ETHUSDT");
await ws.connect();Things the gateway does that will bite you
| Behaviour | What it means for your code |
|-----------|-----------------------------|
| A subscribe ack (10001/10004/10007) only means the frame was accepted | A wrong business, or a symbol that does not exist, is also acked and then stays silent forever. Check that data actually arrives. |
| business selects the market | stock = US/HK/CN, japan = .JP, india = .IN, korea = .KS, crypto = pairs, common = forex/metals. Subscribing to .JP on stock yields nothing. |
| 60 frames per minute per connection, heartbeats included | Batch symbols into one subscribeTrade("A,B,C") call; never loop one call per symbol. |
| Heartbeats are never answered | The SDK sends one every 30s and does not wait for a reply. Do not treat silence as a dead link. |
| A bad key fails the HTTP handshake with 401 | connect() rejects with InfowayAuthError and stops retrying — repeated reconnects with a bad key can get the key banned. |
| business=stock sends a plain-text first frame | Handled internally (skipped), never surfaced as an error. |
| Unsubscribing a candle period needs the period | unsubscribeKline("BTCUSDT", KlineType.MIN_1) sends klineTypes: "1"; without it the server drops every period of that symbol. |
Real-time News
News is a separate connection (wss://data.infoway.io/news) and needs its own entitlement on the key:
import { InfowayNewsWebSocket, InfowayAuthError } from "infoway-sdk";
const news = new InfowayNewsWebSocket({ apiKey: "YOUR_API_KEY" });
news.onNews = (item) => {
console.log(item.published, item.urgency, item.title, item.symbols);
};
await news.subscribe("en"); // en, zh-Hans, zh-Hant, ja, ko, de, fr, es, pt, ru, tr
try {
await news.connect();
} catch (err) {
if (err instanceof InfowayAuthError) {
console.error("This API key has no news entitlement:", err.message);
}
}Notes: one news connection per key; re-subscribing replaces the previous language;
published is a second timestamp; dk is a deduplication key; lower urgency is more urgent.
API Reference
REST Clients
| Client | Prefix | Description |
|--------|--------|-------------|
| client.stock | stock | HK, US, CN stock market data |
| client.crypto | crypto | Cryptocurrency data |
| client.japan | japan | Japan stock market data |
| client.india | india | India stock market data |
| client.common | common | Forex, metals and other common data |
| client.basic | -- | Symbols, adjustment factors, calendar |
| client.market | -- | Temperature, breadth, indexes |
| client.plate | -- | Industry/concept sectors |
| client.stockInfo | -- | Valuation, ratings, company info |
Market Data Methods (stock/crypto/japan/india/common)
| Method | HTTP | Endpoint |
|--------|------|----------|
| getTrade(codes, options?) | GET | /{prefix}/batch_trade/{codes} |
| getDepth(codes, options?) | GET | /{prefix}/batch_depth/{codes} |
| getKline(codes, klineType, count, options?) | POST | /{prefix}/v2/batch_kline |
options is { parse?: boolean }. Server limits: 500 candles per request, and only
2 candles per symbol when several symbols are requested at once.
Basic Info Methods
| Method | Endpoint | Parameters |
|--------|----------|------------|
| getSymbols(type, symbols?) | /common/basic/symbols | type is required: STOCK_US, STOCK_CN, STOCK_HK, STOCK_JP, STOCK_KS, STOCK_IN, CRYPTO, FOREX, FUTURES |
| getSymbolInfo(type, symbols) | /common/basic/symbols/info | up to 500 comma-separated symbols |
| getAdjustmentFactors(symbol, market, beginDay, endDay) | /common/basic/symbols/adjustment_factors | days are YYYYMMDD |
| getTradingDays(market, beginDay, endDay) | /common/basic/markets/trading_days | returns { trade_days, half_trade_days } |
| getTradingSchedule(market?) | /common/basic/markets/trading_schedule | sessions, holidays, break times |
| getTradingHours(market?) | — | deprecated alias of getTradingSchedule |
KlineType Enum
| Value | Interval |
|-------|----------|
| KlineType.MIN_1 (1) | 1 minute |
| KlineType.MIN_5 (2) | 5 minutes |
| KlineType.MIN_15 (3) | 15 minutes |
| KlineType.MIN_30 (4) | 30 minutes |
| KlineType.HOUR_1 (5) | 1 hour |
| KlineType.HOUR_2 (6) | 2 hours |
| KlineType.HOUR_4 (7) | 4 hours |
| KlineType.DAY (8) | 1 day |
| KlineType.WEEK (9) | 1 week |
| KlineType.MONTH (10) | 1 month |
| KlineType.QUARTER (11) | 1 quarter |
| KlineType.YEAR (12) | 1 year |
WebSocket Codes
Client → server:
| Code | Name | Description |
|------|------|-------------|
| 10000 | SUB_TRADE | Subscribe to trade data |
| 10003 | SUB_DEPTH | Subscribe to depth data |
| 10006 | SUB_KLINE | Subscribe to K-line data (payload data.arr=[{codes, type}]) |
| 10010 | HEARTBEAT | Heartbeat keepalive (never answered) |
| 10020 | SUB_NEWS | Subscribe to news (/news connection) |
| 11000 | UNSUB_TRADE | Unsubscribe trade data |
| 11001 | UNSUB_DEPTH | Unsubscribe depth data |
| 11002 | UNSUB_KLINE | Unsubscribe K-line data (send klineTypes) |
| 11020 | UNSUB_NEWS | Unsubscribe news |
Server → client:
| Code | Name | Description |
|------|------|-------------|
| 200 | — | Welcome frame {"code":200,"msg":"ws connect success"} |
| 10001 | SUB_TRADE_ACK | Trade subscribe acknowledgement |
| 10002 | PUSH_TRADE | Real-time trade push |
| 10004 | SUB_DEPTH_ACK | Depth subscribe acknowledgement |
| 10005 | PUSH_DEPTH | Real-time depth push |
| 10007 | SUB_KLINE_ACK | K-line subscribe acknowledgement |
| 10008 | PUSH_KLINE | Real-time K-line push |
| 10021 | SUB_NEWS_ACK | News subscribe acknowledgement |
| 10022 | PUSH_NEWS | Real-time news push |
| 11010 | UNSUB_ACK | Unsubscribe acknowledgement |
Error Handling
import {
InfowayAPIError,
InfowayAuthError,
InfowayRateLimitError,
InfowayTimeoutError,
} from "infoway-sdk";
try {
const data = await client.stock.getTrade("AAPL.US");
} catch (err) {
if (err instanceof InfowayAuthError) {
console.error("Authentication failed. Check your API key.");
} else if (err instanceof InfowayRateLimitError) {
console.error("Throttled — slow down."); // retried with backoff first
} else if (err instanceof InfowayTimeoutError) {
console.error("Request timed out.");
} else if (err instanceof InfowayAPIError) {
console.error(`API error [${err.ret}]: ${err.msg}`);
}
}InfowayAuthError and InfowayRateLimitError both extend InfowayAPIError, so a single
catch (err instanceof InfowayAPIError) still covers everything. Errors are raised for
HTTP 4xx/5xx, for RFC 7807 problem responses, for ret != 200, and for rate limiting —
which the gateway sometimes reports with HTTP 200 and a {"detail":"Rate limit exceeded"} body.
Client Options
const client = new InfowayClient({
apiKey: "YOUR_API_KEY",
baseUrl: "https://data.infoway.io", // default
timeout: 15_000, // ms, default
maxRetries: 3, // network errors and rate limits
retryBackoffMs: 1_000, // doubles per attempt, capped at 8s
});Migrating to 0.2.0
| 0.1.x | 0.2.0 |
|-------|-------|
| basic.getSymbols("US") → undefined | basic.getSymbols("STOCK_US") |
| basic.getSymbolInfo("AAPL.US") → undefined | basic.getSymbolInfo("STOCK_US", "AAPL.US") |
| basic.getAdjustmentFactors("AAPL.US") → undefined | basic.getAdjustmentFactors("AAPL.US", "US", "20260801", "20260814") |
| basic.getTradingDays("US") → undefined | basic.getTradingDays("US", "20260801", "20260814") |
| basic.getTradingHours() (404 path) | basic.getTradingSchedule() |
| HTTP 400/404/429 resolved to undefined | they raise InfowayAPIError / InfowayRateLimitError |
| Responses without a data key resolved to undefined | the whole body is returned |
| WS callbacks got { code, data } | WS callbacks get the tick (data) itself |
| unsubscribeKline dropped every period | it sends klineTypes and drops only the one you named |
| A bad key reconnected forever | connect() rejects with InfowayAuthError |
License
MIT
