npm package: @plutarc/bitmex
Description: Fully-typed BitMEX REST and WebSocket client for TypeScript/JavaScript
Author: sayedhfatimi
Latest version: 0.2.0

# @plutarc/bitmex

A fully-typed BitMEX REST and WebSocket client for TypeScript/JavaScript. Zero runtime dependencies — uses only Node.js/Bun built-ins.

## Table of Contents

- [Features](#features)
- [Installation](#installation)
- [Quick Start](#quick-start)
  - [REST Client](#rest-client)
  - [Public WebSocket](#public-websocket)
  - [Private WebSocket](#private-websocket)
- [REST API Reference](#rest-api-reference)
  - [Client Options](#client-options)
  - [Order](#order)
  - [Position](#position)
  - [Instrument](#instrument)
  - [Trade](#trade)
  - [Order Book](#order-book)
  - [Execution](#execution)
  - [User / Account](#user--account)
  - [Funding](#funding)
  - [Insurance](#insurance)
  - [Liquidation](#liquidation)
  - [Settlement](#settlement)
  - [Stats](#stats)
  - [Quote](#quote)
  - [Announcement](#announcement)
  - [API Key](#api-key)
  - [Leaderboard](#leaderboard)
- [WebSocket API Reference](#websocket-api-reference)
  - [WebSocket Options](#websocket-options)
  - [Public Topics](#public-topics)
  - [Private Topics](#private-topics)
  - [Lifecycle Events](#lifecycle-events)
  - [Connection Management](#connection-management)
- [Utilities](#utilities)
  - [Currency Conversion](#currency-conversion)
  - [Delta Parser](#delta-parser)
  - [OrderBook Store](#orderbook-store)
  - [Authentication Helpers](#authentication-helpers)
- [Error Handling](#error-handling)
- [Rate Limiting](#rate-limiting)
- [Testnet](#testnet)
- [Custom Logger](#custom-logger)
- [Development](#development)
- [Resources](#resources)
- [License](#license)

## Features

- **Complete REST API** — All BitMEX REST endpoints with full TypeScript types
- **Real-time WebSocket** — Public and private WebSocket clients with typed event emitters
- **Zero dependencies** — Uses only `node:crypto` and `node:events` built-ins
- **Fully typed** — Every request parameter and response field has TypeScript interfaces
- **Rate limiting** — Built-in rolling window rate limiter that tracks `x-ratelimit-remaining`
- **Auto-retry** — Exponential backoff with jitter for retryable errors (429, 5xx)
- **Auto-reconnect** — WebSocket reconnection with exponential backoff
- **Ping/pong** — Automatic heartbeat to keep WebSocket connections alive
- **Delta parser** — Generic WebSocket state management with orderbook convenience store
- **Authentication** — HMAC-SHA256 signing for both REST and WebSocket
- **Testnet support** — Single flag to switch between production and testnet
- **Dual format** — Ships ESM and CJS builds with full declaration files

## Installation

```bash
# bun
bun add @plutarc/bitmex

# npm
npm install @plutarc/bitmex

# pnpm
pnpm add @plutarc/bitmex
```

## Quick Start

### REST Client

```typescript
import { BitMEXClient } from "@plutarc/bitmex";

const client = new BitMEXClient({
  apiKey: "your-api-key",
  apiSecret: "your-api-secret",
  testnet: true, // use testnet for development
});

// Get active instruments
const instruments = await client.instrument.getActive();

// Place a limit order
const order = await client.order.place({
  symbol: "XBTUSD",
  side: "Buy",
  ordType: "Limit",
  orderQty: 100,
  price: 50000,
});

// Get open positions
const positions = await client.position.getOpen();

// Get wallet balance
const wallets = await client.user.getWallet();
```

### Public WebSocket

```typescript
import { BitMEXPublicWs } from "@plutarc/bitmex";

const ws = new BitMEXPublicWs({ testnet: true });

// Subscribe to real-time trades — fully typed callback
ws.on("trade", (action, data) => {
  for (const trade of data) {
    console.log(`${trade.side} ${trade.size} @ ${trade.price}`);
  }
});

// Subscribe to orderbook updates
ws.on("orderBookL2_25", (action, data) => {
  console.log(`Orderbook ${action}: ${data.length} entries`);
});

// Subscribe to 1-minute candles
ws.on("tradeBin1m", (action, data) => {
  for (const candle of data) {
    console.log(`${candle.symbol} O:${candle.open} H:${candle.high} L:${candle.low} C:${candle.close}`);
  }
});

// Start subscriptions and connect
ws.subscribe(["trade:XBTUSD", "orderBookL2_25:XBTUSD", "tradeBin1m:XBTUSD"]);
ws.connect();
```

### Private WebSocket

```typescript
import { BitMEXPrivateWs } from "@plutarc/bitmex";

const ws = new BitMEXPrivateWs({
  apiKey: "your-api-key",
  apiSecret: "your-api-secret",
  testnet: true,
});

// Listen for authentication success
ws.on("authenticated", () => {
  console.log("Authenticated — subscribing to private topics");
  ws.subscribe(["order", "position", "wallet", "execution"]);
});

// Order updates
ws.on("order", (action, data) => {
  for (const order of data) {
    console.log(`Order ${order.orderID}: ${order.ordStatus}`);
  }
});

// Position updates
ws.on("position", (action, data) => {
  for (const pos of data) {
    console.log(`${pos.symbol}: qty=${pos.currentQty} pnl=${pos.unrealisedPnl}`);
  }
});

// Execution (fill) updates
ws.on("execution", (action, data) => {
  for (const exec of data) {
    console.log(`Fill: ${exec.lastQty} @ ${exec.lastPx}`);
  }
});

ws.connect();
```

## REST API Reference

### Client Options

```typescript
const client = new BitMEXClient({
  apiKey: "...",               // Optional — required for authenticated endpoints
  apiSecret: "...",            // Optional — required for authenticated endpoints
  testnet: false,              // Use testnet (default: false)
  baseUrl: "https://...",      // Custom base URL (overrides testnet flag)
  rateLimitPerMinute: 100,     // Self-throttle limit (default: 100, BitMEX limit: 120)
  maxRetries: 3,               // Max retries for retryable errors (default: 3)
  baseRetryDelayMs: 500,       // Base delay for exponential backoff (default: 500)
  requestTimeoutMs: 15000,     // Request timeout (default: 15000)
  authExpirySec: 5,            // Signature expiry window (default: 5)
  logger: console,             // Custom logger (default: silent)
});
```

### Order

```typescript
// Get open orders
const open = await client.order.getOpen({ symbol: "XBTUSD" });

// Get order history
const history = await client.order.getHistory({ symbol: "XBTUSD", count: 100 });

// Query with filters
const orders = await client.order.get({
  symbol: "XBTUSD",
  filter: { ordStatus: "Filled" },
  count: 50,
  reverse: true,
});

// Place an order
const order = await client.order.place({
  symbol: "XBTUSD",
  side: "Buy",
  ordType: "Limit",
  orderQty: 100,
  price: 50000,
  timeInForce: "GoodTillCancel",
});

// Place bulk orders
const orders = await client.order.placeBulk([
  { symbol: "XBTUSD", side: "Buy", ordType: "Limit", orderQty: 100, price: 49000 },
  { symbol: "XBTUSD", side: "Buy", ordType: "Limit", orderQty: 100, price: 48000 },
]);

// Amend an order
const amended = await client.order.amend({
  orderID: "order-id",
  price: 51000,
});

// Amend bulk orders
const amended = await client.order.amendBulk([
  { orderID: "id-1", price: 51000 },
  { orderID: "id-2", price: 47000 },
]);

// Cancel an order
const cancelled = await client.order.cancel({ orderID: "order-id" });
// or by client order ID
const cancelled = await client.order.cancel({ clOrdID: "my-order-1" });

// Cancel all orders
await client.order.cancelAll({ symbol: "XBTUSD" });

// Dead man's switch — cancel all after timeout (ms)
await client.order.cancelAllAfter(60000); // cancel all in 60s
await client.order.cancelAllAfter(0);     // disable timer

// Close position at market
const closed = await client.order.closePosition({ symbol: "XBTUSD" });
```

### Position

```typescript
// Get open positions
const positions = await client.position.getOpen("XBTUSD");

// Get all positions (including closed)
const all = await client.position.get();

// Set leverage
await client.position.setLeverage("XBTUSD", 10);

// Set cross margin
await client.position.setIsolateMargin("XBTUSD", false);

// Set isolated margin
await client.position.setIsolateMargin("XBTUSD", true);

// Set risk limit
await client.position.setRiskLimit("XBTUSD", 5000000000);

// Transfer margin to position
await client.position.transferMargin("XBTUSD", 100000);
```

### Instrument

```typescript
// Get specific instrument
const instruments = await client.instrument.get({ symbol: "XBTUSD" });

// Get all active instruments
const active = await client.instrument.getActive();

// Get active instruments and indices
const all = await client.instrument.getActiveAndIndices();

// Get active intervals
const intervals = await client.instrument.getActiveIntervals();

// Get composite index data
const index = await client.instrument.getCompositeIndex({ symbol: ".BXBT" });

// Get all indices
const indices = await client.instrument.getIndices();

// Get USD volume summary
const volume = await client.instrument.getUsdVolume();
```

### Trade

```typescript
// Get recent trades
const trades = await client.trade.get({
  symbol: "XBTUSD",
  count: 100,
  reverse: true,
});

// Get OHLCV candles
const candles = await client.trade.getBucketed({
  binSize: "1h",
  symbol: "XBTUSD",
  count: 100,
  reverse: true,
  partial: false,
});
```

### Order Book

```typescript
// Get L2 orderbook snapshot (default 25 levels)
const book = await client.orderBook.getL2({ symbol: "XBTUSD" });

// Custom depth
const deepBook = await client.orderBook.getL2({ symbol: "XBTUSD", depth: 50 });
```

### Execution

```typescript
// Get raw executions
const execs = await client.execution.get({
  symbol: "XBTUSD",
  count: 50,
  reverse: true,
});

// Get trade history (fills only)
const fills = await client.execution.getTradeHistory({
  symbol: "XBTUSD",
  startTime: "2025-01-01T00:00:00.000Z",
});
```

### User / Account

```typescript
// Get current user
const user = await client.user.get();

// Get wallet balance
const wallets = await client.user.getWallet("USDt");

// Get wallet history
const history = await client.user.getWalletHistory({ currency: "USDt", count: 50 });

// Get wallet summary
const summary = await client.user.getWalletSummary("USDt");

// Get commission rates
const fees = await client.user.getCommission();

// Get margin status
const margin = await client.user.getMargin("USDt");

// Update preferences
await client.user.updatePreferences({ locale: "en-US" });
```

### Funding

```typescript
const funding = await client.funding.get({
  symbol: "XBTUSD",
  count: 100,
  reverse: true,
});
```

### Insurance

```typescript
const insurance = await client.insurance.get({ count: 50, reverse: true });
```

### Liquidation

```typescript
const liquidations = await client.liquidation.get({
  symbol: "XBTUSD",
  count: 100,
  reverse: true,
});
```

### Settlement

```typescript
const settlements = await client.settlement.get({ symbol: "XBTUSD" });
```

### Stats

```typescript
const stats = await client.stats.get();
const history = await client.stats.getHistory();
const usd = await client.stats.getHistoryUSD();
```

### Quote

```typescript
// Get quotes
const quotes = await client.quote.get({ symbol: "XBTUSD", count: 10 });

// Get bucketed quotes
const bucketed = await client.quote.getBucketed({
  binSize: "1h",
  symbol: "XBTUSD",
  count: 100,
});
```

### Announcement

```typescript
const announcements = await client.announcement.get();
const urgent = await client.announcement.getUrgent();
```

### API Key

```typescript
const keys = await client.apiKey.get();
const newKey = await client.apiKey.create({ name: "trading-bot" });
await client.apiKey.remove("key-id");
```

### Leaderboard

```typescript
const board = await client.leaderboard.get({ method: "notional" });
const myName = await client.leaderboard.getName();
```

## WebSocket API Reference

### WebSocket Options

```typescript
// Public WebSocket — no authentication required
const pub = new BitMEXPublicWs({
  testnet: false,              // Use testnet (default: false)
  wsUrl: "wss://...",          // Custom WebSocket URL (overrides testnet flag)
  pingIntervalMs: 15000,       // Heartbeat interval (default: 15000)
  maxReconnectAttempts: 10,    // Max reconnect attempts, 0 = infinite (default: 10)
  baseReconnectDelayMs: 1000,  // Base delay for reconnect backoff (default: 1000)
  logger: console,             // Custom logger (default: silent)
});

// Private WebSocket — authentication required
const priv = new BitMEXPrivateWs({
  apiKey: "...",               // Required
  apiSecret: "...",            // Required
  testnet: false,
  authExpirySec: 5,            // Auth signature expiry (default: 5)
  // ...same options as public
});
```

### Public Topics

Subscribe using BitMEX topic format: `"table:symbol"` for symbol-specific topics.

| Event | Data Type | Description |
|-------|-----------|-------------|
| `trade` | `WsTrade[]` | Real-time trades |
| `tradeBin1m` | `WsTradeBin[]` | 1-minute OHLCV candles |
| `tradeBin5m` | `WsTradeBin[]` | 5-minute OHLCV candles |
| `tradeBin1h` | `WsTradeBin[]` | 1-hour OHLCV candles |
| `tradeBin1d` | `WsTradeBin[]` | Daily OHLCV candles |
| `orderBookL2_25` | `WsOrderBookL2[]` | Orderbook L2 (25 levels) |
| `orderBookL2` | `WsOrderBookL2[]` | Orderbook L2 (full) |
| `orderBook10` | `WsOrderBook10[]` | Top 10 orderbook |
| `instrument` | `WsInstrument[]` | Instrument updates |
| `funding` | `WsFunding[]` | Funding rate updates |
| `liquidation` | `WsLiquidation[]` | Liquidation orders |
| `settlement` | `WsSettlement[]` | Settlement data |
| `insurance` | `WsInsurance[]` | Insurance fund |
| `quote` | `WsQuote[]` | Top-of-book quotes |
| `quoteBin1m` | `WsQuoteBin[]` | 1-minute bucketed quotes |
| `quoteBin5m` | `WsQuoteBin[]` | 5-minute bucketed quotes |
| `quoteBin1h` | `WsQuoteBin[]` | 1-hour bucketed quotes |
| `quoteBin1d` | `WsQuoteBin[]` | Daily bucketed quotes |

All data events receive `(action: WsAction, data: T[])` where `WsAction` is `"partial" | "insert" | "update" | "delete"`.

- `partial` — Full snapshot (initial data load)
- `insert` — New entries added
- `update` — Existing entries modified
- `delete` — Entries removed

```typescript
ws.subscribe([
  "trade:XBTUSD",
  "orderBookL2_25:XBTUSD",
  "tradeBin1m:XBTUSD",
  "instrument:XBTUSD",
]);
```

### Private Topics

Private topics do not require a symbol suffix — they stream all data for your account.

| Event | Data Type | Description |
|-------|-----------|-------------|
| `order` | `WsOrder[]` | Order status updates |
| `execution` | `WsExecution[]` | Trade executions / fills |
| `position` | `WsPosition[]` | Position updates |
| `wallet` | `WsWallet[]` | Wallet balance changes |
| `margin` | `WsMargin[]` | Margin / available balance |
| `transact` | `WsTransaction[]` | Deposit/withdrawal transactions |
| `affiliate` | `WsAffiliate[]` | Affiliate status |

```typescript
ws.on("authenticated", () => {
  ws.subscribe(["order", "position", "execution", "wallet", "margin"]);
});
```

### Lifecycle Events

Both public and private WebSocket clients emit lifecycle events:

| Event | Args | Description |
|-------|------|-------------|
| `open` | — | Connection established (and authenticated for private) |
| `close` | `(code, reason)` | Connection closed |
| `reconnecting` | `(attempt)` | Reconnection attempt |
| `error` | `(error)` | Connection or auth error |
| `authenticated` | — | *(Private only)* Authentication successful |

```typescript
ws.on("open", () => console.log("Connected"));
ws.on("close", (code, reason) => console.log(`Disconnected: ${code}`));
ws.on("reconnecting", (attempt) => console.log(`Reconnecting (attempt ${attempt})`));
ws.on("error", (err) => console.error("WS error:", err));
```

### Connection Management

```typescript
ws.connect();      // Establish connection
ws.disconnect();   // Gracefully disconnect (no auto-reconnect)

ws.subscribe(["trade:XBTUSD"]);    // Subscribe to topics
ws.unsubscribe(["trade:XBTUSD"]);  // Unsubscribe from topics

ws.state;               // "connecting" | "connected" | "reconnecting" | "closed"
ws.activeSubscriptions; // string[] of current subscriptions
```

## Utilities

### Currency Conversion

Helpers for BitMEX's internal representations:

```typescript
import { satoshiToBtc, microUsdtToUsdt, convertWalletAmount } from "@plutarc/bitmex";

// BitMEX stores BTC as satoshi (XBt): 1 BTC = 100,000,000 satoshi
satoshiToBtc(100_000_000); // 1.0

// BitMEX stores USDT as micro-USDT (USDt): 1 USDT = 1,000,000 micro
microUsdtToUsdt(1_000_000); // 1.0

// Auto-detect by currency code
convertWalletAmount(100_000_000, "XBt"); // 1.0 BTC
convertWalletAmount(1_000_000, "USDt");  // 1.0 USDT
```

### Delta Parser

A generic, zero-dependency delta parser that maintains local state from BitMEX WebSocket delta messages (`partial`/`insert`/`update`/`delete`). Works with any WebSocket table type.

```typescript
import { DeltaParser } from "@plutarc/bitmex";
import type { WsOrderBookL2, WsOrder } from "@plutarc/bitmex";

// Create a parser for orderbook data — keys identify unique rows
const bookParser = new DeltaParser<WsOrderBookL2>({ keys: ["id"] });

ws.on("orderBookL2_25", (action, data) => {
  const snapshot = bookParser.update(action, data);
  // snapshot is the full current orderbook state
  console.log(`Book has ${snapshot.length} levels`);
});

// Works with any table — orders, positions, etc.
const orderParser = new DeltaParser<WsOrder>({ keys: ["orderID"] });

ws.on("order", (action, data) => {
  const orders = orderParser.update(action, data);
  console.log(`${orders.length} active orders`);
});
```

For insert-only tables like `trade` that grow unboundedly, use `maxItems` to cap the store:

```typescript
import type { WsTrade } from "@plutarc/bitmex";

const tradeParser = new DeltaParser<WsTrade>({ keys: ["trdMatchID"], maxItems: 1000 });

ws.on("trade", (action, data) => {
  const trades = tradeParser.update(action, data);
  // keeps only the most recent 1000 trades
});
```

**API:**

| Method / Property | Returns | Description |
|---|---|---|
| `update(action, data)` | `readonly T[]` | Process a delta message, returns full current state |
| `snapshot` | `readonly T[]` | Current state without processing a new message |
| `length` | `number` | Number of items in the store |
| `clear()` | `void` | Reset the store to empty |

### OrderBook Store

A higher-level convenience store for L2 orderbook data. Maintains sorted bids and asks with common orderbook queries. Optionally wraps a `DeltaParser` — you can bring your own or use the built-in one.

```typescript
import { OrderBookStore } from "@plutarc/bitmex";

const book = new OrderBookStore();

// Plug directly into a WebSocket event handler (update is auto-bound)
ws.on("orderBookL2_25", book.update);

// Query the book
console.log(book.bestBid());   // 50000
console.log(book.bestAsk());   // 50001
console.log(book.spread());    // 1
console.log(book.midPrice());  // 50000.5

// Get top 5 levels from each side
const { bids, asks } = book.depth(5);

// Volume at top of book
console.log(book.bidVolume(10)); // total bid size in top 10 levels
console.log(book.askVolume());   // total ask size across all levels

// Sorted level arrays
book.bids; // OrderBookLevel[] sorted desc by price (best bid first)
book.asks; // OrderBookLevel[] sorted asc by price (best ask first)
```

**Composability — use as much or as little as you need:**

```typescript
import { DeltaParser, OrderBookStore } from "@plutarc/bitmex";
import type { WsOrderBookL2 } from "@plutarc/bitmex";

// 1. Batteries included — OrderBookStore manages everything
const book = new OrderBookStore();
ws.on("orderBookL2_25", book.update);

// 2. DeltaParser only — feed into your own store (zustand, redux, etc.)
const parser = new DeltaParser<WsOrderBookL2>({ keys: ["id"] });
ws.on("orderBookL2_25", (action, data) => {
  const snapshot = parser.update(action, data);
  useMyStore.setState({ orderbook: snapshot });
});

// 3. Shared DeltaParser + OrderBookStore
const parser = new DeltaParser<WsOrderBookL2>({ keys: ["id"] });
const book = new OrderBookStore({ parser });
ws.on("orderBookL2_25", book.update);
// Access parser.snapshot for raw data, book.bids/asks for sorted view

// 4. Neither — handle raw deltas yourself
ws.on("orderBookL2_25", (action, data) => { /* your own logic */ });
```

**API:**

| Method / Property | Returns | Description |
|---|---|---|
| `update(action, data)` | `void` | Process a delta message (auto-bound arrow function) |
| `bids` | `readonly OrderBookLevel[]` | All bids sorted descending by price |
| `asks` | `readonly OrderBookLevel[]` | All asks sorted ascending by price |
| `bestBid()` | `number \| undefined` | Highest bid price |
| `bestAsk()` | `number \| undefined` | Lowest ask price |
| `spread()` | `number \| undefined` | Best ask minus best bid |
| `midPrice()` | `number \| undefined` | Average of best bid and best ask |
| `depth(n)` | `{ bids, asks }` | Top N levels from each side |
| `bidVolume(n?)` | `number` | Total bid size (optionally top N levels only) |
| `askVolume(n?)` | `number` | Total ask size (optionally top N levels only) |
| `clear()` | `void` | Reset the book to empty |

### Authentication Helpers

For advanced usage:

```typescript
import { sign, getSignedHeaders, signWs } from "@plutarc/bitmex";

// REST API signature
const sig = sign(apiSecret, "GET", "/api/v1/order", expires, "");

// Full signed headers
const headers = getSignedHeaders(apiKey, apiSecret, "GET", "/api/v1/order", "");

// WebSocket auth signature
const wsSig = signWs(apiSecret, expires);
```

## Error Handling

All REST errors throw typed error classes:

```typescript
import { BitMEXApiError, BitMEXRateLimitError } from "@plutarc/bitmex";

try {
  await client.order.place({ /* ... */ });
} catch (err) {
  if (err instanceof BitMEXRateLimitError) {
    console.log(`Rate limited — retry after ${err.retryAfterMs}ms`);
  } else if (err instanceof BitMEXApiError) {
    console.log(`API error ${err.status}: ${err.errorMessage}`);
    console.log(`Retryable: ${err.retryable}`);
  }
}
```

**Retryable errors** (automatically retried up to `maxRetries` times):
- `429` Too Many Requests
- `5xx` Server errors
- Network/timeout errors

**Non-retryable errors** (thrown immediately):
- `400` Bad Request
- `401` Unauthorized
- `403` Forbidden
- `404` Not Found

## Rate Limiting

The client self-throttles to stay within BitMEX rate limits:

- Default: 100 requests/minute (BitMEX limit is 120)
- Tracks the `x-ratelimit-remaining` response header from BitMEX
- Pauses requests when approaching the limit (5-request buffer)
- Configurable via `rateLimitPerMinute` option

## Testnet

All clients support testnet with a single flag:

```typescript
// REST
const client = new BitMEXClient({ testnet: true });
// → https://testnet.bitmex.com

// WebSocket
const ws = new BitMEXPublicWs({ testnet: true });
// → wss://ws.testnet.bitmex.com/realtime
```

Get testnet API keys at [testnet.bitmex.com](https://testnet.bitmex.com).

## Custom Logger

Pass any object implementing `{ debug, info, warn, error }` methods. By default the client is silent.

```typescript
// Use console
const client = new BitMEXClient({ logger: console });

// Use pino
import pino from "pino";
const client = new BitMEXClient({ logger: pino({ name: "bitmex" }) });

// Use winston
import winston from "winston";
const client = new BitMEXClient({ logger: winston.createLogger({ /* ... */ }) });
```

## Development

```bash
# Install dependencies
bun install

# Type check
bun run typecheck

# Run tests
bun test

# Build
bun run build

# Lint & format
bun run lint
bun run format
```

## Resources

- [BitMEX API Explorer](https://www.bitmex.com/api/explorer/) — Interactive API documentation
- [BitMEX API Reference](https://www.bitmex.com/app/apiOverview) — API overview and guides
- [BitMEX WebSocket API](https://www.bitmex.com/app/wsAPI) — WebSocket documentation
- [BitMEX Testnet](https://testnet.bitmex.com) — Testnet for development
- [BitMEX API Changelog](https://www.bitmex.com/app/apiChangelog) — API changes and updates

## License

[MIT](LICENSE) — see [LICENSE](LICENSE) for details.