@foxbit-group/rest-api
v0.4.0
Published
Foxbit's API client
Readme
Foxbit REST API SDK for JavaScript and TypeScript
Official Foxbit client for the REST API v3: market data, trading, accounts, deposits and withdrawals from Node.js.
Features
- Typed requests and responses for every REST v3 endpoint, generated from the official OpenAPI specification.
- Request signing built in: HMAC-SHA256 with your API secret, or Ed25519 with your own private key.
- Receive window on every signed request, a 30 second default timeout and no redirects, so credentials never follow a redirect to another host.
- A private axios instance: your application's global axios interceptors, headers and credentials do not reach the SDK, and a
baseURLset on the global axios never redirects its requests. - Helpers for rate limits (
isTooManyRequests,getRetryAfterSeconds) and typed API errors. - CommonJS and ES module builds, with TypeScript declarations.
Installation
Requires Node.js 18 or later.
npm install @foxbit-group/rest-apiQuick start
Public endpoints need no credentials:
import { MarketDataApi } from '@foxbit-group/rest-api';
const marketData = new MarketDataApi();
const { data } = await marketData.listMarkets();
console.log(data.data);Authentication
Private endpoints are signed. Pass a Configuration with your API key and exactly one of:
apiSecret: the secret shown when you created the key. Signs with HMAC-SHA256.privateKey: an Ed25519 private key you generated yourself (PEM PKCS#8 string orcrypto.KeyObject). Only the public key is ever registered with Foxbit.
Both use the same headers and the same signed payload; each API key belongs to one scheme. A few endpoints do not accept Ed25519 keys yet and answer error 2010; use an HMAC key there. Setting both, or a key that is not Ed25519, throws when the Configuration is created.
We recommend Ed25519: the private key never leaves your machine, so Foxbit never stores a secret that could sign on your behalf.
Ed25519 (recommended)
Generate a key pair:
openssl genpkey -algorithm ed25519 -out foxbit-ed25519.pem openssl pkey -in foxbit-ed25519.pem -pubout -out foxbit-ed25519.pub.pem cat foxbit-ed25519.pub.pemOpen https://app.foxbit.com.br/profile/api-key, create an API key under Generated by you and paste the public key (
foxbit-ed25519.pub.pem).Keep
foxbit-ed25519.pemprivate and pass it to the SDK:
import { readFileSync } from 'fs';
import { AccountApi, Configuration } from '@foxbit-group/rest-api';
const config = new Configuration({
apiKey: 'YOUR_API_KEY',
privateKey: readFileSync('foxbit-ed25519.pem', 'utf8'),
});
const account = new AccountApi(config);
const { data } = await account.listAccounts();
console.log(data.data);HMAC-SHA256
import { AccountApi, Configuration } from '@foxbit-group/rest-api';
const config = new Configuration({
apiKey: 'YOUR_API_KEY',
apiSecret: 'YOUR_API_SECRET',
});
const account = new AccountApi(config);
const { data } = await account.listAccounts();
console.log(data.data);On every signed request the SDK sends:
X-FB-ACCESS-KEY: your API key.X-FB-ACCESS-TIMESTAMP: the current time in milliseconds.X-FB-ACCESS-SIGNATURE: the signature of timestamp, method, path, query and body, in hex.X-FB-RECEIVE-WINDOW: how long the request stays valid, in milliseconds.
apiSecret and privateKey are not enumerable, so they do not show up in JSON.stringify(config) or console.log(config). For the same reason { ...config } drops them: build another Configuration instead of copying one. Load them from a secret store or environment variables, never from source code.
Receive window and clock
A signed request is only accepted while its timestamp is within the receive window of the server's clock, before or after; outside it the API answers 412 with code 4014. The SDK sends X-FB-RECEIVE-WINDOW: 10000 (10 seconds) by default. Set receiveWindow from 1000 to 60000 ms. null or 0 does not send the header, and then HMAC requests get no receive-window check at all (Ed25519 requests keep the server's fixed 5-minute window, answered with 401 and code 2006), so keep the header on.
The timestamp comes from your machine's clock, so keep it in sync (NTP). To check the drift, compare it with GET /rest/v3/system/time:
import { Configuration, SystemApi } from '@foxbit-group/rest-api';
const config = new Configuration({
apiKey: 'YOUR_API_KEY',
apiSecret: 'YOUR_API_SECRET',
receiveWindow: 5000,
});
const { data } = await new SystemApi(config).getCurrentTime();
const drift = Date.now() - (data.timestamp ?? Date.now());
console.log(`Local clock is ${drift} ms off; receive window is ${config.receiveWindow} ms`);Timeouts
Every request times out after 30 seconds by default. Change it for all calls with timeout in the Configuration, or for one call with the last options argument, which always wins:
import { Configuration, MarketDataApi } from '@foxbit-group/rest-api';
const marketData = new MarketDataApi(new Configuration({ timeout: 10000 }));
// This call gives up after 2 seconds; the others after 10.
const { data } = await marketData.getOrderbook({ marketSymbol: 'btcbrl', depth: 20 }, { timeout: 2000 });
console.log(data.bids.length, data.asks.length);Error handling
Every method returns the axios response. A non-2xx status rejects with an AxiosError (re-exported by this package with isAxiosError), whose error.response.data.error holds the API error:
code: the numeric Foxbit error code.message: a readable description.details: a list with more context, such as which field failed validation.
When you hit a rate limit the API answers HTTP 429. isTooManyRequests(error) detects it and getRetryAfterSeconds(error) returns the seconds to wait from the X-FB-RATE-LIMIT-RETRY-AFTER header the API sends (or a standard Retry-After, the longer one if both come), or undefined without one. Invalid arguments, such as a missing required parameter or an empty path parameter, throw a RequiredError before any request is sent.
import {
ErrorResponse,
MarketDataApi,
RequiredError,
getRetryAfterSeconds,
isAxiosError,
isTooManyRequests,
} from '@foxbit-group/rest-api';
const marketData = new MarketDataApi();
try {
const { data } = await marketData.getTicker({ marketSymbol: 'btcbrl' });
console.log(data.data);
} catch (error) {
if (isTooManyRequests(error)) {
const seconds = getRetryAfterSeconds(error) ?? 1;
console.warn(`Rate limited, retry in ${seconds} s`);
} else if (isAxiosError<ErrorResponse>(error) && error.response?.data?.error) {
const { code, message, details } = error.response.data.error;
console.error(error.response.status, code, message, details);
} else if (isAxiosError(error) && error.response) {
// Not a Foxbit error body, e.g. a redirect or a proxy page.
console.error(error.response.status, error.response.data);
} else if (error instanceof RequiredError) {
console.error(`Invalid argument ${error.field}: ${error.message}`);
} else {
throw error;
}
}Configuration
| Option | Default | Description |
| --- | --- | --- |
| apiKey | none | Your API key. Required for signed endpoints. |
| apiSecret | none | API secret for HMAC-SHA256 signing. |
| privateKey | none | Ed25519 private key (PEM PKCS#8 or crypto.KeyObject) for Ed25519 signing. |
| receiveWindow | 10000 | Receive window in ms, from 1000 to 60000. null or 0 does not send it. |
| timeout | 30000 | Timeout of every request in ms. A per-call options.timeout wins. |
| basePath | https://api.foxbit.com.br | API host, without a trailing slash. Always wins over the axios instance's baseURL. |
| baseOptions | none | Extra axios request options for every call. |
Each API class also takes an axios instance as its third argument, for example to add your own interceptors: new MarketDataApi(config, undefined, myAxios). Any axios 1.x instance works. Without one, the SDK uses its own private instance.
Examples
The examples below assume Node.js 18+ with ES modules (top-level await). Replace YOUR_API_KEY and YOUR_API_SECRET with your credentials, or use privateKey as shown in Authentication.
Create a LIMIT order
import { Configuration, TradingApi } from '@foxbit-group/rest-api';
const trading = new TradingApi(new Configuration({ apiKey: 'YOUR_API_KEY', apiSecret: 'YOUR_API_SECRET' }));
const { data } = await trading.createOrder({
createOrderRequest: {
market_symbol: 'btcbrl',
side: 'BUY',
type: 'LIMIT',
quantity: '0.001',
price: '300000',
time_in_force: 'GTC',
post_only: true,
client_order_id: '1001',
},
});
console.log(`Order ${data.id} created (SN ${data.sn})`);Cancel an order by ID
Cancellation is asynchronous: check the final state with getOrderById.
import { Configuration, TradingApi } from '@foxbit-group/rest-api';
const trading = new TradingApi(new Configuration({ apiKey: 'YOUR_API_KEY', apiSecret: 'YOUR_API_SECRET' }));
const { data } = await trading.cancelOrders({
cancelOrdersRequest: { type: 'ID', id: '1234567890' },
});
console.log(data.data);
const { data: order } = await trading.getOrderById({ id: '1234567890' });
console.log(order.state);List orders with a date filter and pagination
startTime and endTime are ISO-8601 UTC and at most 90 days apart; pageSize goes up to 100.
import { Configuration, OrderWithoutRemark, TradingApi } from '@foxbit-group/rest-api';
const trading = new TradingApi(new Configuration({ apiKey: 'YOUR_API_KEY', apiSecret: 'YOUR_API_SECRET' }));
const pageSize = 100;
const orders: OrderWithoutRemark[] = [];
for (let page = 1; ; page++) {
const { data } = await trading.listOrders({
marketSymbol: 'btcbrl',
state: 'FILLED',
startTime: '2026-01-01T00:00:00.000Z',
endTime: '2026-01-31T23:59:59.999Z',
page,
pageSize,
});
const batch = data.data ?? [];
orders.push(...batch);
if (batch.length < pageSize) break;
}
console.log(`${orders.length} filled orders in January`);Get balances
import { AccountApi, Configuration } from '@foxbit-group/rest-api';
const account = new AccountApi(new Configuration({ apiKey: 'YOUR_API_KEY', apiSecret: 'YOUR_API_SECRET' }));
const { data } = await account.listAccounts();
for (const balance of data.data ?? []) {
console.log(balance.currency_symbol, 'available', balance.balance_available, 'locked', balance.balance_locked);
}Market data: ticker and order book
import { MarketDataApi } from '@foxbit-group/rest-api';
const marketData = new MarketDataApi();
const { data: ticker } = await marketData.getTicker({ marketSymbol: 'btcbrl' });
for (const market of ticker.data) {
console.log(market.market_symbol, 'last', market.last_trade.price, 'bid', market.best.bid.price, 'ask', market.best.ask.price);
}
const { data: book } = await marketData.getOrderbook({ marketSymbol: 'btcbrl', depth: 10 });
const [bestBid] = book.bids;
const [bestAsk] = book.asks;
console.log('best bid', bestBid, 'best ask', bestAsk);Retry after a rate limit
import { MarketDataApi, getRetryAfterSeconds, isTooManyRequests } from '@foxbit-group/rest-api';
const marketData = new MarketDataApi();
const sleep = (ms: number) => new Promise((resolve) => setTimeout(resolve, ms));
for (let attempt = 1; attempt <= 3; attempt++) {
try {
const { data } = await marketData.getTicker24h();
console.log(`${data.data.length} markets`);
break;
} catch (error) {
if (!isTooManyRequests(error) || attempt === 3) throw error;
await sleep((getRetryAfterSeconds(error) ?? attempt) * 1000);
}
}Set a custom timeout
import { Configuration, TradingApi } from '@foxbit-group/rest-api';
const trading = new TradingApi(new Configuration({
apiKey: 'YOUR_API_KEY',
apiSecret: 'YOUR_API_SECRET',
timeout: 5000,
}));
// Fail fast on this one call.
const { data } = await trading.getOrderById({ id: '1234567890' }, { timeout: 1500 });
console.log(data.state);API reference
The full reference of every endpoint, parameter and error code is in the Foxbit API documentation. API changes are listed in the changelog.
The SDK groups the endpoints in these classes:
AccountApiBanksApiDepositApiMarketDataApiMemberInfoApiPrimeDeskApiSystemApiTradingApiTransactionalLimitsApiTravelRuleApiWithdrawalApi
Upgrading from an earlier version
- The SDK uses its own axios instance, so interceptors, headers,
auth,paramsandbaseURLset on the globalaxiosno longer apply to it. To keep them, pass your instance as the third argument of the API class. On signed requests the query and body must stay as signed: an instance or call that setsparamsorparamsSerializeris refused withRequiredError, andtransformRequestis not applied. A request interceptor of your instance must not change the method, URL, query or body of a signed request either, or the API answers 401. - axios 1.20 or newer is required. If your app stays on an older axios, npm installs a separate copy for the SDK, and
error instanceof AxiosErrorwith your ownAxiosErrorbecomes false; useisAxiosError(error), exported by this package. - Redirects are never followed, even with
maxRedirectsinbaseOptionsor in the call options. - Every request has a default timeout of 30 s, and signed requests send
X-FB-RECEIVE-WINDOW: 10000.receiveWindow: nullleaves the header out, which turns the replay check off for HMAC keys. apiSecretandprivateKeyare not enumerable: a{ ...config }copy has no credentials. Build a newConfigurationinstead.Configurationthrows right away for areceiveWindowoutside 1000 to 60000, forapiSecrettogether withprivateKey, and for a secret or key withoutapiKey.- An explicit
basePath(inConfigurationor as the second argument) now wins over thebaseURLof your own axios instance. - The
Configurationtype hasreceiveWindowandtimeoutmembers, so an object literal typed asConfigurationor a class thatimplementsit must declare them; build one withnew Configuration(...)instead. - Subclasses of
BaseAPIseebasePathasstring | undefinedandaxiosasAxiosLike. - Node 18 or newer is required, and the package ships only the compiled
dist/(no.tssources): import from@foxbit-group/rest-api, not from a source path.
Disclaimer
Trading crypto assets involves significant risk, including the loss of your funds. This SDK is provided "as is", without warranty of any kind; you are responsible for your orders, credentials and use of the API. Test your integration with small amounts first. Use at your own risk.
License
MIT © Foxbit Group; the license text ships with the package (LICENSE).
