1.Overview
Syenu exposes HTTP endpoints for catalog, candles, orders, trades and withdrawals, plus two WebSocket streams. Public market data needs no key. Private HTTP and the account stream use an HMAC API key created under Account → API Keys. Monetary values are JSON strings.
| Service | URL |
|---|---|
| HTTP | https://api.syenu.com |
| Public WebSocket | wss://ws.syenu.com/ws/v1/public |
| Private WebSocket | wss://ws.syenu.com/ws/v1/private |
2.Conventions
Every error body uses the same shape. HTTP 204 responses have no body.
{
"error": {
"code": "VALIDATION_ERROR",
"message": "Invalid request.",
"fields": { "email": "Invalid email" }
}
}- Common codes: VALIDATION_ERROR, UNAUTHENTICATED, FORBIDDEN, NOT_FOUND, RATE_LIMITED, OPEN_ORDER_LIMIT, CONFLICT, INTERNAL_ERROR.
- Prices, amounts, fees, balances and ticks are strings.
- Candle intervals: 1m, 5m, 15m, 1H, 4H, 1D, 1W. time is the bar open in Unix milliseconds.
- List endpoints paginate with limit (1–100, default 50) and cursor. Another page exists when nextCursor is not null.
3.Authentication
Create a key in the account UI. The secret is shown once. Permissions:
| Permission | Allows |
|---|---|
| read | GET orders, trades, transfers, deposit address |
| spot | place and cancel spot orders |
| withdraw | create and cancel withdrawals |
Sign every private HTTP request. Headers:
- syenu-api-key — key id (syk_…)
- syenu-timestamp — Unix time in milliseconds
- syenu-signature — hex HMAC-SHA256
Signature payload is the concatenation timestamp + METHOD + path + body, with no separators. METHOD is uppercase. path is the request path including the query string (Express originalUrl). body is the raw request body, or an empty string for GET.
signature = hex(HMAC-SHA256(secret, timestamp + METHOD + path + body))The timestamp must be within 30 seconds of the server clock. Optional IP allowlists are enforced. Rate limit is 1000 requests per 2 seconds per key.
JavaScript (Node)
import { createHmac } from 'node:crypto'
function sign(secret, timestamp, method, path, body = '') {
return createHmac('sha256', secret)
.update(`${timestamp}${method.toUpperCase()}${path}${body}`)
.digest('hex')
}
export function signedHeaders({ apiKey, secret, method, path, body = '' }) {
const timestamp = String(Date.now())
return {
'syenu-api-key': apiKey,
'syenu-timestamp': timestamp,
'syenu-signature': sign(secret, timestamp, method, path, body),
}
}Java
import javax.crypto.Mac;
import javax.crypto.spec.SecretKeySpec;
import java.nio.charset.StandardCharsets;
import java.util.LinkedHashMap;
import java.util.Map;
public final class SyenuAuth {
public static String sign(String secret, String timestamp, String method, String path, String body) {
try {
Mac mac = Mac.getInstance("HmacSHA256");
mac.init(new SecretKeySpec(secret.getBytes(StandardCharsets.UTF_8), "HmacSHA256"));
byte[] raw = mac.doFinal((timestamp + method.toUpperCase() + path + body)
.getBytes(StandardCharsets.UTF_8));
StringBuilder hex = new StringBuilder(raw.length * 2);
for (byte b : raw) hex.append(String.format("%02x", b));
return hex.toString();
} catch (Exception e) {
throw new IllegalStateException(e);
}
}
public static Map<String, String> headers(String apiKey, String secret, String method, String path, String body) {
String timestamp = Long.toString(System.currentTimeMillis());
Map<String, String> headers = new LinkedHashMap<>();
headers.put("syenu-api-key", apiKey);
headers.put("syenu-timestamp", timestamp);
headers.put("syenu-signature", sign(secret, timestamp, method, path, body));
return headers;
}
}The private WebSocket uses the same HMAC, but METHOD is always GET, path is the pathname only (/ws/v1/private), and body is empty. Send the three values as headers or as query parameters apiKey, timestamp and signature.
4.HTTP — market, asset, candle
These endpoints are public. No API key.
| Method | Path | Notes |
|---|---|---|
| GET | /api/v1/spot/market | Ticks, min/max cost, fees, maxOpenOrders, quoteAssetIds |
| GET | /api/v1/spot/asset | Assets and deposit/withdrawal networks |
| GET | /api/v1/spot/candle | Query marketId, interval, limit (1–1000, default 160), to |
JavaScript — list markets and candles
const API = 'http://localhost:4000'
const markets = await fetch(`${API}/api/v1/spot/market`).then((r) => r.json())
// { quoteAssetIds: ['USDT', 'USDC', 'ZAR'], markets: [{ id, symbol, baseAssetId, quoteAssetId, priceTick, amountTick, minCost, maxCost, makerFeeRate, takerFeeRate, maxOpenOrders }] }
const assets = await fetch(`${API}/api/v1/spot/asset`).then((r) => r.json())
// { assets: [{ id, symbol, name, precision, type, networks: [...] }] }
const qs = new URLSearchParams({
marketId: 'BTC-USDT',
interval: '15m',
limit: '160',
})
const candles = await fetch(`${API}/api/v1/spot/candle?${qs}`).then((r) => r.json())
// { candles: [{ time, open, high, low, close, volume }] }Java — list markets and candles
HttpClient http = HttpClient.newHttpClient();
String api = "http://localhost:4000";
HttpResponse<String> markets = http.send(
HttpRequest.newBuilder().uri(URI.create(api + "/api/v1/spot/market")).GET().build(),
HttpResponse.BodyHandlers.ofString());
HttpResponse<String> candles = http.send(
HttpRequest.newBuilder()
.uri(URI.create(api + "/api/v1/spot/candle?marketId=BTC-USDT&interval=15m&limit=160"))
.GET()
.build(),
HttpResponse.BodyHandlers.ofString());5.HTTP — order, trade, withdrawal
These endpoints require an API key. Place and cancel order return 202 — the engine accepts the request asynchronously.
| Method | Path | Permission |
|---|---|---|
| GET | /api/v1/spot/order | read or spot |
| POST | /api/v1/spot/order | spot |
| DELETE | /api/v1/spot/order | spot |
| GET | /api/v1/spot/trade | read or spot |
| GET | /api/v1/account/transfer | read, spot or withdraw |
| GET | /api/v1/account/deposit-address | read |
| POST | /api/v1/account/withdrawal | withdraw |
| DELETE | /api/v1/account/withdrawal | withdraw |
List query for orders and trades: side, marketId, from, to, limit, cursor. Orders also accept status (open, filled, cancelled). Place-order body:
{
"marketId": "BTC-USDT",
"type": "limit",
"side": "buy",
"price": "100000",
"amount": "0.01",
"timeInForce": "gtc",
"memo": "bot-1"
}type is limit or market. Market orders are ioc: sell sends amount, buy sends cost. Limit requires price and amount. memo is optional text, max 32 characters. Open orders per account are capped at maxOpenOrders from GET /api/v1/spot/market; exceeding it returns OPEN_ORDER_LIMIT. Cancel body: { orderId, marketId }. Deposit address query: assetId, networkId. Response: { depositAddress: { networkId, address, tag } }. tag is null when the network does not use one. An empty address pool returns DEPOSIT_ADDRESS_UNAVAILABLE. Withdrawal body: { assetId, networkId, address, amount, tag?, memo? }. Cancel a pending withdrawal with { id }.
JavaScript — signed place order
const API = 'http://localhost:4000'
const path = '/api/v1/spot/order'
const body = JSON.stringify({
marketId: 'BTC-USDT',
type: 'limit',
side: 'buy',
price: '100000',
amount: '0.01',
timeInForce: 'gtc',
memo: 'bot-1',
})
const headers = signedHeaders({
apiKey: process.env.SYENU_API_KEY,
secret: process.env.SYENU_API_SECRET,
method: 'POST',
path,
body,
})
const res = await fetch(`${API}${path}`, {
method: 'POST',
headers: { 'content-type': 'application/json', ...headers },
body,
})
// 202 { id }JavaScript — list trades and request a withdrawal
const listPath = '/api/v1/spot/trade?marketId=BTC-USDT&limit=50'
const listHeaders = signedHeaders({
apiKey, secret, method: 'GET', path: listPath, body: '',
})
await fetch(`${API}${listPath}`, { headers: listHeaders })
const depositPath = '/api/v1/account/deposit-address?assetId=USDT&networkId=TRC20'
const depositHeaders = signedHeaders({
apiKey, secret, method: 'GET', path: depositPath, body: '',
})
await fetch(`${API}${depositPath}`, { headers: depositHeaders })
// { depositAddress: { networkId, address, tag } }
const withdrawPath = '/api/v1/account/withdrawal'
const withdrawBody = JSON.stringify({
assetId: 'USDT',
networkId: 'TRC20',
address: 'T…',
amount: '10',
})
const withdrawHeaders = signedHeaders({
apiKey, secret, method: 'POST', path: withdrawPath, body: withdrawBody,
})
await fetch(`${API}${withdrawPath}`, {
method: 'POST',
headers: { 'content-type': 'application/json', ...withdrawHeaders },
body: withdrawBody,
})Java — signed place order
String api = "http://localhost:4000";
String path = "/api/v1/spot/order";
String body = """
{"marketId":"BTC-USDT","type":"limit","side":"buy","price":"100000","amount":"0.01","timeInForce":"gtc","memo":"bot-1"}
""";
Map<String, String> signed = SyenuAuth.headers(apiKey, secret, "POST", path, body);
HttpRequest.Builder req = HttpRequest.newBuilder()
.uri(URI.create(api + path))
.header("content-type", "application/json")
.POST(HttpRequest.BodyPublishers.ofString(body));
signed.forEach(req::header);
HttpResponse<String> res = HttpClient.newHttpClient()
.send(req.build(), HttpResponse.BodyHandlers.ofString());
// 202 { "id": "…" }6.WebSocket protocol
Both streams speak JSON. The server pings every 30 seconds; browsers reply automatically. Client methods are subscribe and unsubscribe.
{ "method": "subscribe", "params": { "channel": "ticker" } }Errors look like { type: "error", message: "…" }. Subscribe is acknowledged with { type: "subscribe", params, result: true }, then a { type: "snapshot", channel, … } message. Live updates use { type: "delta", channel, … }.
7.Public WebSocket
Connect to wss://ws.syenu.com/ws/v1/public (locally ws://localhost:40000/ws/v1/public). No API key. One connection can hold ticker, marketdata, and candle subscriptions.
| Channel | Subscribe params | Snapshot | Deltas |
|---|---|---|---|
| ticker | { channel: "ticker" } | All-market 24h tickers | Changed tickers every 0.5s |
| marketdata | { channel: "marketdata", marketId } | Ticker, book, recent trades | Partial book / trades / ticker |
| candle | { channel: "candle", marketId, interval } | Recent interval bars; last is forming | Upserted interval bars (forming, plus dirty if any) |
marketdata trades are [price, amount, side, time]. Book levels are [price, amount]. In a delta, amount "0" removes that price level. ticker is [time, open, last, high, low, change, baseVolume, quoteVolume, dir] (time is Unix seconds; dir is 1 / -1 / 0 vs the previous last). Snapshot is { tickers: { [marketId]: ticker } }. Deltas use the same tickers map and flush at most every 0.5s when something changed. trade and candle times are Unix ms. candle is [time, open, close, high, low, volume]. Snapshot and delta both send { candles }. Closed REST bars win on overlap; the newest snapshot bar (forming) and every delta bar upsert.
JavaScript — subscribe marketdata and candle
const ws = new WebSocket('wss://ws.syenu.com/ws/v1/public')
ws.addEventListener('open', () => {
ws.send(JSON.stringify({
method: 'subscribe',
params: { channel: 'ticker' },
}))
ws.send(JSON.stringify({
method: 'subscribe',
params: { channel: 'marketdata', marketId: 'BTC-USDT' },
}))
ws.send(JSON.stringify({
method: 'subscribe',
params: { channel: 'candle', marketId: 'BTC-USDT', interval: '15m' },
}))
})
ws.addEventListener('message', (event) => {
const msg = JSON.parse(event.data)
if (msg.type === 'snapshot' && msg.channel === 'marketdata') {
console.log(msg.sequence, msg.orderbook, msg.trades)
}
if (msg.type === 'delta' && msg.channel === 'marketdata') {
console.log(msg.sequence, msg.orderbook, msg.trades)
}
})Java — subscribe marketdata
HttpClient.newHttpClient()
.newWebSocketBuilder()
.buildAsync(URI.create("wss://ws.syenu.com/ws/v1/public"), new WebSocket.Listener() {
@Override
public void onOpen(WebSocket ws) {
ws.sendText("""
{"method":"subscribe","params":{"channel":"marketdata","marketId":"BTC-USDT"}}
""", true);
WebSocket.Listener.super.onOpen(ws);
}
@Override
public CompletionStage<?> onText(WebSocket ws, CharSequence data, boolean last) {
System.out.println(data);
return WebSocket.Listener.super.onText(ws, data, last);
}
})
.join();Example marketdata delta
{
"type": "delta",
"channel": "marketdata",
"marketId": "BTC-USDT",
"sequence": "12346",
"orderbook": {
"asks": [["96850", "0"]],
"bids": [["96840", "1.8"]]
}
}8.Private WebSocket
Connect to wss://ws.syenu.com/ws/v1/private (locally ws://localhost:40001/ws/v1/private). Authenticate on the HTTP upgrade with an API key that has read, spot or withdraw. Sign GET + pathname + empty body. pathname excludes the query string and is /ws/v1/private. The account is inferred from the key — do not pass an account id.
There are two channels: balance and openOrder. Subscribe to each separately. openOrder accepts optional marketId and then only that market's resting orders. Deltas merge partial { available?, locked? } per asset. An open order with openAmount "0" is deleted. A sequence gap triggers a full snapshot on the subscribed channels.
| Channel | Subscribe params | Snapshot | Deltas |
|---|---|---|---|
| balance | { channel: "balance" } | sequence, balances | Partial balances |
| openOrder | { channel: "openOrder", marketId? } | sequence, openOrders | openOrders upsert; openAmount "0" deletes |
JavaScript — signed account stream
const timestamp = String(Date.now())
const path = '/ws/v1/private'
const signature = sign(secret, timestamp, 'GET', path, '')
const url = new URL('wss://ws.syenu.com/ws/v1/private')
url.searchParams.set('apiKey', apiKey)
url.searchParams.set('timestamp', timestamp)
url.searchParams.set('signature', signature)
const ws = new WebSocket(url)
ws.addEventListener('open', () => {
ws.send(JSON.stringify({ method: 'subscribe', params: { channel: 'balance' } }))
ws.send(JSON.stringify({ method: 'subscribe', params: { channel: 'openOrder', marketId: 'BTC-USDT' } }))
})
ws.addEventListener('message', (event) => {
const msg = JSON.parse(event.data)
if (msg.type === 'snapshot' && msg.channel === 'balance') {
console.log(msg.balances)
}
if (msg.type === 'snapshot' && msg.channel === 'openOrder') {
console.log(msg.openOrders)
}
if (msg.type === 'delta' && msg.channel === 'balance') {
console.log(msg.balances)
}
if (msg.type === 'delta' && msg.channel === 'openOrder') {
console.log(msg.openOrders)
}
})Java — signed account stream
String timestamp = Long.toString(System.currentTimeMillis());
String signature = SyenuAuth.sign(secret, timestamp, "GET", "/ws/v1/private", "");
URI uri = URI.create("wss://ws.syenu.com/ws/v1/private?apiKey=" + apiKey
+ "×tamp=" + timestamp
+ "&signature=" + signature);
HttpClient.newHttpClient()
.newWebSocketBuilder()
.buildAsync(uri, new WebSocket.Listener() {
@Override
public void onOpen(WebSocket ws) {
ws.sendText("""
{"method":"subscribe","params":{"channel":"balance"}}
""", true);
ws.sendText("""
{"method":"subscribe","params":{"channel":"openOrder","marketId":"BTC-USDT"}}
""", true);
WebSocket.Listener.super.onOpen(ws);
}
})
.join();Example subscribe ack
{
"type": "subscribe",
"params": { "channel": "openOrder", "marketId": "BTC-USDT" },
"result": true
}Example balance snapshot
{
"type": "snapshot",
"channel": "balance",
"sequence": "42",
"balances": {
"BTC": { "available": "1.5", "locked": "0.1" }
}
}Example openOrder snapshot
{
"type": "snapshot",
"channel": "openOrder",
"sequence": "42",
"openOrders": [
{
"type": "limit",
"side": "buy",
"cost": null,
"id": "…",
"quoteAssetId": "USDT",
"memo": null,
"marketId": "BTC-USDT",
"timeInForce": "gtc",
"time": "2026-09-08T00:00:00.000Z",
"amount": "0.01",
"openAmount": "0.01",
"filledAmount": "0",
"cancelledAmount": "0",
"baseAssetId": "BTC",
"price": "95000"
}
]
}