GeminiGemini
Demo environmentGet API key
  • Overview
  • Crypto Trading
  • Prediction Markets
  • Perpetuals
  • Stocks
  • API Reference
  • SDKs & Tools
Changelog
Gemini logoGemini logo

© 2026 Gemini Space Station, Inc.

TypeScript SDK — WebSocket

The SDK provides real-time market data and account updates over WebSocket. Public streams require no authentication. Private streams and request methods (orders, balances, positions, RFQ deliveries, and order actions) require server-side WebSocket authentication and run only from the server entry point.

Public and private surfaces

The client makes the authentication boundary explicit in its API. A single server client can expose both surfaces, but they use separate WebSocket connections and never share frames or connection state.

SurfaceAvailable fromAuthenticationConnectionIncludes
client.websocket.publicBrowser and serverNoneShared public sessionTrades, book tickers, depth updates, contract status, public RFQ discovery, and public controls
client.websocket.privateServer onlyHMAC or confidential OAuthSeparate authenticated sessionAccount/session orders, balances, positions (including terminal settlement details), RFQ deliveries, order actions, and RFQ quote mutations
client.websocket.public.orderBook()Browser and serverNoneSeparate shared order-book sessionSelf-healing order books and resync events
client.websocket.public.depth()Browser and serverNoneOne isolated session per snapshot streamPartial-depth snapshots

Use the public surface for information Gemini makes available to everyone. Use the private surface for account-specific data or any operation that can change state. The browser entry point intentionally does not expose .private; OAuth in a browser authenticates REST only. See Authentication for why.

Public streams

Subscribe to market data without authentication:

Code
import { createClient } from "@gemini-markets/sdk/browser"; const client = createClient({ env: "sandbox" }); // Real-time trades const trades = client.websocket.public.trades("BTCUSD"); trades.on("message", (trade) => { console.log(trade.p, trade.q, trade.m); // price, quantity, is-maker }); await trades.ready; // wait for subscription confirmation // Book ticker (best bid/ask) const ticker = client.websocket.public.bookTicker("BTCUSD"); ticker.on("message", (tick) => console.log(tick)); // Depth updates (order book diffs) const depthUpdates = client.websocket.public.depthUpdates("BTCUSD"); depthUpdates.on("message", (update) => console.log(update)); // Depth snapshots (top N levels) const depthSnapshot = client.websocket.public.depth("BTCUSD", { levels: 10 }); depthSnapshot.on("message", (snapshot) => console.log(snapshot));

Public streams share a single underlying WebSocket connection. Opening multiple streams to different symbols reuses the same session.

Closing a stream

Code
await trades.close(); // sends unsubscribe, waits for acknowledgement

Closing one stream does not affect others on the shared session. Call client.close() to shut down all streams and connections.

Live order book

The SDK maintains a self-healing L2 order book from WebSocket depth data:

Code
const book = client.orderBook("BTCUSD"); book.on("update", (lob, delta) => { console.log("Best bid:", lob.bestBid()); console.log("Best ask:", lob.bestAsk()); console.log("Spread:", lob.spread()); console.log("Mid:", lob.mid()); console.log("Top 5 bids:", lob.topN("bids", 5)); console.log("Changed levels:", delta); }); book.on("resync", () => { // A gap was detected in the update stream. // The book is rebuilding from a fresh snapshot. // Treat current state as stale until the next "update" event. console.warn("Order book resyncing — data may be stale"); }); book.on("error", (err) => console.error(err));

The first "update" event after subscribing (or after a "resync") carries the full book — treat it as a replacement, not an incremental diff. Subsequent updates carry only changed levels; a quantity of "0" means the level was removed.

For exact decimal arithmetic, use spreadDecimal() and midDecimal() which return string decimals ("0.01"). spread() and mid() return floating-point numbers for display only.

Code
book.close(); // stop updates and release the stream

Authenticated streams

Authenticated streams require the server entry point and an auth strategy. They use the ws package to set custom headers on the WebSocket upgrade request.

Code
import { createClient, HmacAuth } from "@gemini-markets/sdk/server"; const client = await createClient({ env: "sandbox", auth: new HmacAuth({ apiKey, apiSecret }), }); // Order updates for the current session const orders = client.websocket.private.orders({ scope: "session" }); orders.on("message", (order) => { console.log(order.i, order.X, order.z); // orderId, status, remainingQuantity }); // Account-wide balance updates const balances = client.websocket.private.balances(); balances.on("message", (update) => { for (const b of update.B) { console.log(b.a, b.f, b.c); // asset, free, locked } }); // Prediction-market positions and terminal settlement details const positions = client.websocket.private.positions(); positions.on("message", (report) => { for (const row of report.P) { const payout = row.a.find((amount) => amount.t === "settlement_payout"); if (payout) console.log(row.s, payout.o, payout.v, payout.c); } }); // Use intervalMs: 1000 for periodic open-position snapshots. That stream does // not include terminal settlement rows. const openPositions = client.websocket.private.positions({ intervalMs: 1000 }); // Private RFQ deliveries for this account (maker acceptances and outcomes) const rfqDeliveries = client.websocket.private.rfqDeliveries({ scope: "account" }); rfqDeliveries.on("message", (delivery) => { console.log(delivery.i, delivery.r, delivery.x, delivery.q); }); await orders.ready;

The public RFQ discovery stream is deliberately separate from these private deliveries: subscribe with client.websocket.public.rfqs() to discover open auctions, then use client.websocket.private.rfq.submitQuote() or confirmQuote() to perform authenticated maker actions. See the RFQ deep dive for the complete flow and application decision hooks.

Browser limitation

Browser WebSocket cannot set custom HTTP headers on the upgrade request. BrowserOAuthAuth authenticates REST only; it does not authenticate private RIO WebSocket streams or request methods. The browser entry point exposes only client.websocket.public, so private operations are neither available in its API nor included in its WebSocket bundle. Browser apps can use:

  • Public streams (trades, depth, book tickers) — no auth needed
  • REST endpoints via OAuth for authenticated operations

If a first-party Gemini web application has a cookie-authenticated WebSocket endpoint, that is an application-specific integration and is not the SDK's browser authentication mechanism. Use the server entry point or a trusted server-side relay when an SDK integration needs private WebSocket access.

WebSocket order operations

Place and cancel orders over WebSocket for lower latency:

Code
// Place an order const result = await client.websocket.private.placeOrder({ symbol: "BTCUSD", side: "BUY", type: "LIMIT", price: "50000.00", quantity: "0.001", timeInForce: "GTC", }); // response: { id (request correlation ID), status, result? } // The exchange order ID is inside the result object: const orderResult = result.result as Record<string, unknown> | undefined; if (!orderResult?.orderId) throw new Error("Place response missing orderId"); // Cancel an order using the exchange order ID await client.websocket.private.cancelOrder({ orderId: String(orderResult.orderId) });

Cancellation methods that affect multiple orders require explicit confirmation:

Code
await client.websocket.private.cancelAllOrders({ confirm: true }); await client.websocket.private.cancelSessionOrders({ confirm: true });

Reconnection

WebSocket connections reconnect automatically on disconnection with exponential backoff:

  • Public stream subscriptions are replayed after reconnect
  • Authenticated streams re-authenticate with fresh credentials (nonces and tokens are regenerated)
  • Mutating requests (order placement/cancellation) are never replayed — they reject with an error if the connection drops mid-request

Monitor reconnection:

Code
const stream = client.websocket.public.trades("BTCUSD"); stream.on("resubscribed", () => { console.log("Stream reconnected and resubscribed"); }); stream.on("subscriptionError", (err) => { console.error("Resubscription failed:", err); });

Stream state

Code
stream.state; // "active" | "reconnecting" | "failed" | "closed" stream.lastError; // the last error, if any stream.malformedFrameCount; // count of frames that couldn't be parsed

WebSocket architecture

The transport and session implementation is intentionally internal. Applications should use the typed client.websocket.public or client.websocket.private facade returned by createClient(); this keeps connection lifecycle, request correlation, reconnection, and stream cleanup in one place. Custom socket behavior can be supplied with the public webSocketFactory option on the server entry point.

What's next

  • WebSocket Reference — every stream, method, and wire-format field
  • Order Book Reconstruction — snapshot + diff internals and gap recovery
  • Error handling — WebSocket-specific errors and connection failures
  • Patterns & recipes — heartbeat, liveness checks, and advanced configuration
On this page
  • Public and private surfaces
  • Public streams
    • Closing a stream
  • Live order book
  • Authenticated streams
    • Browser limitation
    • WebSocket order operations
  • Reconnection
    • Stream state
  • WebSocket architecture
  • What's next
TypeScript
TypeScript
TypeScript
TypeScript
TypeScript
TypeScript
TypeScript
TypeScript
TypeScript