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 — Authentication

The SDK supports three authentication strategies. Pick the one that matches your application:

StrategyImportUse when
HMAC@gemini-markets/sdk/serverServer-side apps with API key + secret
OAuth (confidential)@gemini-markets/sdk/serverServer-side apps acting on behalf of users
OAuth (public/PKCE)@gemini-markets/sdk/browserBrowser and mobile apps — no client secret

HMAC authentication

HMAC is the simplest path for server-to-server integration. Every request is signed with your API secret using HMAC-SHA384.

Code
import { createClient, HmacAuth } from "@gemini-markets/sdk/server"; const client = await createClient({ env: "sandbox", auth: new HmacAuth({ apiKey: process.env.GEMINI_API_KEY, apiSecret: process.env.GEMINI_API_SECRET, }), }); const balances = await client.account.getAvailableBalances({ account: "primary" }); client.close();

The SDK handles payload encoding, nonce generation, and signature computation automatically. You never construct headers manually.

Nonce modes

Every authenticated request includes a nonce to prevent replay attacks. The SDK supports two modes:

Code
// Monotonic (default) — strictly increasing counter based on Date.now() new HmacAuth({ apiKey, apiSecret }); // Time-based — Unix epoch seconds. Required by some sandbox configurations. new HmacAuth({ apiKey, apiSecret, nonceMode: "time-based" });

Use "time-based" if the sandbox returns InvalidNonce errors with the default mode.

How HMAC signing works

For reference, this is what the SDK does on every authenticated request:

  1. Build a JSON payload with the request path and a nonce
  2. Base64-encode the payload → X-GEMINI-PAYLOAD header
  3. HMAC-SHA384 sign the base64 string with your secret → X-GEMINI-SIGNATURE header
  4. Send the API key in X-GEMINI-APIKEY

See the API key authentication docs for the full protocol specification.

OAuth — confidential server client

Use OAuthAuth when your server acts on behalf of a user who has authorized your application. Confidential clients have a client_secret.

Code
import { createClient, OAuthAuth } from "@gemini-markets/sdk/server"; const auth = new OAuthAuth({ client: { type: "confidential", clientId: process.env.OAUTH_CLIENT_ID, clientSecret: process.env.OAUTH_CLIENT_SECRET, redirectUri: "https://yourapp.com/callback", }, env: "sandbox", tokenStore: myTokenStore, // you implement this });

Authorization flow

OAuth requires a multi-step flow: redirect the user, receive a callback, exchange the code for tokens.

Code
// Step 1: Generate the authorization URL const { url, transaction } = await auth.beginAuthorization([ "orders:create", "orders:read", "balances:read", ]); // Step 2: Redirect the user to `url` // They log in at Gemini and authorize your app // Step 3: Handle the callback const tokens = await auth.completeAuthorization(callbackUrl, transaction); // Step 4: Use the authenticated client const client = await createClient({ env: "sandbox", auth }); const positions = await client.predictions.getPositions({ limit: 10 });

Implementing a token store

The SDK does not persist tokens — you provide a tokenStore that handles storage. The store must implement load, save, clear, consumeAuthorizationState, and runExclusive for shared locking and one-shot authorization-code exchanges:

Code
interface OAuthTokenStore { load(): Promise<OAuthTokens | undefined>; save(tokens: OAuthTokens): Promise<void>; clear(): Promise<void>; /** Atomically claim a callback state; return false when it was claimed before. */ consumeAuthorizationState(state: string): Promise<boolean>; runExclusive<T>(operation: () => Promise<T>): Promise<T>; }

The lock must cover every OAuthAuth instance and process sharing the store. Implement it with a distributed lock (e.g., Redis or a database row lock) when the store is shared across processes, so concurrent single-use refresh token rotations and authorization-code exchanges cannot race. Implement consumeAuthorizationState as a durable atomic claim with a short expiry (for example, a database insert with a unique state key and a ten-minute TTL) whenever authorization transactions can cross page or process boundaries. The method must return false for a state that has already been claimed.

A minimal in-memory implementation for development:

Code
class MemoryTokenStore { private tokens?: OAuthTokens; private authorizationStates = new Set<string>(); async load() { return this.tokens; } async save(tokens: OAuthTokens) { this.tokens = tokens; } async clear() { this.tokens = undefined; } async consumeAuthorizationState(state: string) { if (this.authorizationStates.has(state)) return false; this.authorizationStates.add(state); return true; } async runExclusive<T>(operation: () => Promise<T>) { const result = this.lock.then(operation, operation); this.lock = result.then(() => undefined, () => undefined); return result; } private lock = Promise.resolve(); }

Token refresh

Access tokens expire after 24 hours. The SDK refreshes them automatically when credentialHeaders() detects an expired token. Refresh happens inside the store's runExclusive lock to prevent concurrent rotation of single-use refresh tokens.

You can tune refresh timing:

Code
new OAuthAuth({ // ... refreshSkewMs: 60_000, // refresh 60s before expiry (default) });

Revocation

Revoke tokens explicitly when a user disconnects your app:

Code
await auth.revoke(); // tokens are cleared from the store after successful server-side revocation

Browser OAuth (PKCE)

Browser apps cannot hold a client secret. Use BrowserOAuthAuth which enforces public-client PKCE at both the type and runtime levels — you cannot accidentally pass a confidential client.

Code
// login.ts — the page that starts the OAuth flow import { createClient, BrowserOAuthAuth } from "@gemini-markets/sdk/browser"; const auth = new BrowserOAuthAuth({ client: { type: "public", clientId: "your-client-id", redirectUri: "http://localhost:3000/callback", }, env: "sandbox", tokenStore: myBrowserTokenStore, }); // Step 1: Start authorization (generates PKCE challenge automatically) const { url, transaction } = await auth.beginAuthorization(["orders:read"]); // Step 2: Persist the transaction — the redirect will lose in-memory state sessionStorage.setItem("gemini_oauth_tx", JSON.stringify(transaction)); // Step 3: Redirect the user window.location.href = url;
Code
// callback.ts — the page Gemini redirects back to const raw = sessionStorage.getItem("gemini_oauth_tx"); if (!raw) throw new Error("OAuth transaction not found — was the flow started?"); const transaction = JSON.parse(raw); sessionStorage.removeItem("gemini_oauth_tx"); // Step 4: Complete the exchange with the saved transaction await auth.completeAuthorization(window.location.href, transaction); // Step 5: Use the client const client = createClient({ env: "sandbox", auth }); const events = await client.predictions.listEvents({ status: ["active"] });

The PKCE code challenge and verifier are generated automatically using Web Crypto (crypto.subtle). The verifier is sent during code exchange — no secret ever leaves the browser.

Browser OAuth authenticates REST requests only. It does not make private WebSocket streams or WebSocket order methods available in the browser: native browser WebSockets cannot send the required upgrade Authorization header, and the SDK rejects private WebSocket operations from the browser entry point. Use @gemini-markets/sdk/server or a server-side relay for authenticated WebSockets.

Scopes

Scopes control what the OAuth token can access. Request only what your app needs:

ScopeGrants
orders:createPlace and cancel orders
orders:readView orders and trade history
balances:readView account balances
addresses:readView approved withdrawal addresses
history:readView transaction history

The full scope list is in the OAuth documentation.

Browser token persistence

In the browser, persist tokens to localStorage or sessionStorage:

Code
const stateKey = "gemini_oauth_used_states"; const browserTokenStore = { async load() { const raw = localStorage.getItem("gemini_tokens"); return raw ? JSON.parse(raw) : undefined; }, async save(tokens) { localStorage.setItem("gemini_tokens", JSON.stringify(tokens)); }, async clear() { localStorage.removeItem("gemini_tokens"); }, async consumeAuthorizationState(state: string) { const now = Date.now(); const raw = localStorage.getItem(stateKey); const states = raw ? JSON.parse(raw) as Record<string, number> : {}; for (const [value, expiresAt] of Object.entries(states)) { if (expiresAt <= now) delete states[value]; } if (states[state] !== undefined) return false; states[state] = now + 10 * 60_000; localStorage.setItem(stateKey, JSON.stringify(states)); return true; }, async runExclusive<T>(operation: () => Promise<T>): Promise<T> { if (!("locks" in navigator)) { throw new Error("The Web Locks API is required for cross-tab OAuth token refresh locking"); } return (navigator as Navigator & { locks: LockManager }).locks.request( "gemini-oauth-token-refresh", { mode: "exclusive" }, operation, ); }, };

navigator.locks provides an origin-wide exclusive lock, so multiple tabs do not rotate the same single-use refresh token or exchange the same authorization code concurrently. The state claim is short-lived and is only for replay protection; persist the authorization transaction itself (including the PKCE verifier) in sessionStorage as shown above. If the target browser does not support the Web Locks API, use a compatible Web Locks polyfill or move token refresh into a service with a shared lock.

For production apps, consider encrypting tokens at rest and using sessionStorage for shorter-lived sessions.

What's next

  • WebSocket — real-time streams with authenticated access
  • Error handling — OAuth-specific error types and recovery
On this page
  • HMAC authentication
    • Nonce modes
    • How HMAC signing works
  • OAuth — confidential server client
    • Authorization flow
    • Implementing a token store
    • Token refresh
    • Revocation
  • Browser OAuth (PKCE)
    • Scopes
    • Browser token persistence
  • What's next
TypeScript
TypeScript
TypeScript
TypeScript
TypeScript
TypeScript
TypeScript
TypeScript
TypeScript
TypeScript
TypeScript