Metadata-Version: 2.4
Name: foxbit-group-rest-api
Version: 0.2.0
Summary: Official Foxbit Python SDK for the Foxbit REST API v3 (trading, market data, wallets)
Home-page: https://docs.foxbit.com.br/rest/v3/
Author: Foxbit
Author-email: developers@foxbit.com.br
License: MIT
Project-URL: Documentation, https://docs.foxbit.com.br/rest/v3/
Project-URL: Changelog, https://docs.foxbit.com.br/rest/v3/changelog/
Keywords: foxbit,crypto,exchange,bitcoin,trading,api,sdk
Classifier: Intended Audience :: Developers
Classifier: License :: OSI Approved :: MIT License
Classifier: Operating System :: OS Independent
Classifier: Programming Language :: Python :: 3
Classifier: Programming Language :: Python :: 3.10
Classifier: Programming Language :: Python :: 3.11
Classifier: Programming Language :: Python :: 3.12
Classifier: Programming Language :: Python :: 3.13
Classifier: Programming Language :: Python :: 3.14
Classifier: Topic :: Office/Business :: Financial
Classifier: Typing :: Typed
Requires-Python: >=3.10
Description-Content-Type: text/markdown
License-File: LICENSE
Requires-Dist: urllib3<3,>=2.6.3
Requires-Dist: python-dateutil>=2.8.2
Requires-Dist: pydantic<3,>=2.4
Requires-Dist: typing-extensions>=4.7.1
Requires-Dist: cryptography>=42
Dynamic: author
Dynamic: author-email
Dynamic: classifier
Dynamic: description
Dynamic: description-content-type
Dynamic: home-page
Dynamic: keywords
Dynamic: license
Dynamic: license-file
Dynamic: project-url
Dynamic: requires-dist
Dynamic: requires-python
Dynamic: summary

# Foxbit Python SDK

The official Python client for the [Foxbit REST API v3](https://docs.foxbit.com.br/rest/v3/): trading, market data, wallets and account data.

- [Features](#features)
- [Installation](#installation)
- [Quick start](#quick-start)
- [Authentication](#authentication)
- [Receive window and clock](#receive-window-and-clock)
- [Timeouts and retries](#timeouts-and-retries)
- [Error handling](#error-handling)
- [Examples](#examples)
- [API reference](#api-reference)
- [Upgrading from an earlier version](#upgrading-from-an-earlier-version)
- [Disclaimer](#disclaimer)
- [License](#license)

## Features

- Every REST API v3 endpoint as a typed method, with pydantic v2 models for requests and responses.
- Request signing built in: **Ed25519** (recommended) or **HMAC-SHA256**, added to every authenticated call.
- Safe defaults: connect/read timeouts, a receive window on signed requests, no redirects, and no automatic retry of order-creating requests.
- Typed exceptions for HTTP errors, rate limits (with `retry_after`), network failures and unreadable responses.
- Forward compatible: an enum value added to the API later is still read by older SDK versions.
- Fully typed package (`py.typed`), Python 3.10 to 3.14.

## Installation

```bash
pip install foxbit-group-rest-api
```

Everything is included, Ed25519 signing too (through [`cryptography`](https://cryptography.io/)). Requires Python 3.10 or higher.

## Quick start

Public endpoints need no credentials:

```python
from foxbit_group.rest_api import ApiClient
from foxbit_group.rest_api.api import MarketDataApi

with ApiClient() as client:
    market_data = MarketDataApi(client)

    markets = market_data.list_markets()
    for market in markets.data or []:
        print(market.symbol, market.price_min, market.quantity_min)
```

## Authentication

Authenticated endpoints need an API key plus **one** of:

| Scheme | Configuration | Signature |
| --- | --- | --- |
| Ed25519 (recommended) | `api_key` + `private_key` | Ed25519 over the request, signed with your private key |
| HMAC-SHA256 | `api_key` + `api_secret` | HMAC-SHA256 over the request, keyed with your API secret |

Setting both `api_secret` and `private_key`, a secret or key without `api_key`, or an empty `api_secret` raises `ApiValueError` before any request. An `api_key` alone signs nothing: public endpoints work, and authenticated ones answer 401. Each API key belongs to one scheme, so nothing else changes between them. A few endpoints do not accept Ed25519 keys yet and answer error `2010`; use an HMAC key there.

### Ed25519 (recommended)

With Ed25519 the secret never leaves your machine: Foxbit only stores your **public** key, so a leak on the server side cannot be used to sign requests.

1. Generate a key pair:

   ```bash
   openssl genpkey -algorithm ed25519 -out foxbit-ed25519-private.pem
   openssl pkey -in foxbit-ed25519-private.pem -pubout -out foxbit-ed25519-public.pem
   ```

2. Open [app.foxbit.com.br/profile/api-key](https://app.foxbit.com.br/profile/api-key), create an API key under **Generated by you** and paste the contents of `foxbit-ed25519-public.pem`.
3. Keep `foxbit-ed25519-private.pem` private (for example `chmod 600`, a secrets manager or an environment variable) and pass it to `Configuration`:

```python
from pathlib import Path

from foxbit_group.rest_api import ApiClient, Configuration
from foxbit_group.rest_api.api import AccountApi

config = Configuration(
    api_key="YOUR_API_KEY",
    # A PEM (str or bytes) or a cryptography Ed25519PrivateKey
    private_key=Path("foxbit-ed25519-private.pem").read_bytes(),
)

with ApiClient(config) as client:
    accounts = AccountApi(client).list_accounts()
    print(accounts)
```

The key is checked when `Configuration` is created: a key that is not Ed25519, or a missing `cryptography` package, raises `ApiValueError` right away.

### HMAC-SHA256

```python
from foxbit_group.rest_api import ApiClient, Configuration
from foxbit_group.rest_api.api import AccountApi

config = Configuration(
    api_key="YOUR_API_KEY",
    api_secret="YOUR_API_SECRET",
)

with ApiClient(config) as client:
    accounts = AccountApi(client).list_accounts()
    print(accounts)
```

### What the SDK sends

On every authenticated request the SDK adds:

| Header | Value |
| --- | --- |
| `X-FB-ACCESS-KEY` | Your API key |
| `X-FB-ACCESS-TIMESTAMP` | The current time in milliseconds |
| `X-FB-ACCESS-SIGNATURE` | The signature, hex encoded (64 characters for HMAC, 128 for Ed25519) |
| `X-FB-RECEIVE-WINDOW` | The receive window in milliseconds (see below) |

Both schemes sign the same bytes: `timestamp + METHOD + path + query + body`. Your API secret and private key are only used locally and are never sent. `repr(config)` masks them, and debug logs redact the key and signature headers.

## Receive window and clock

The server rejects a signed request whose `X-FB-ACCESS-TIMESTAMP` is more than `receive_window` milliseconds away from its own clock, before or after, so a captured request cannot be replayed later. Outside the window the API answers 412 with code `4014`, an `ApiException` with `status` 412. The default is 10000 (10 s); any value from 1000 to 60000 is accepted. `None` or `0` leaves the header out, 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.

Signatures depend on your clock. If requests fail with an expired timestamp, sync the machine with NTP and compare it with the server time from `GET /rest/v3/system/time`:

```python
import time

from foxbit_group.rest_api import ApiClient, Configuration
from foxbit_group.rest_api.api import SystemApi

config = Configuration(
    api_key="YOUR_API_KEY",
    api_secret="YOUR_API_SECRET",
    receive_window=5000,  # ms, 1000 to 60000
)

with ApiClient(config) as client:
    server_time = SystemApi(client).get_current_time()
    if server_time.timestamp is not None:
        offset_ms = server_time.timestamp - time.time() * 1000
        print(f"local clock offset: {offset_ms:.0f} ms")
```

## Timeouts and retries

- **Timeouts.** Every request has a 10 s connect and 30 s read timeout by default. Change it with `Configuration(timeout=...)`, which takes a `urllib3.Timeout`, a total in seconds or a `(connect, read)` tuple (`None` waits forever). A per-call `_request_timeout` wins over it.
- **Retries.** Only connection errors (the request never reached the server) are retried, up to 3 times, and only for `GET`, `HEAD`, `OPTIONS` and `DELETE`. `POST`, `PUT` and `PATCH` are never retried, so an order is never sent twice. Responses such as 429 or 5xx are not retried; they raise an exception you can act on. Pass `Configuration(retries=...)` (an int or a `urllib3.Retry`) to change it for `GET`, `HEAD`, `OPTIONS` and `DELETE`; `POST`, `PUT` and `PATCH` stay without retries.
- **Redirects** are never followed, so credentials never reach another host; a 3xx raises `ApiException` with its status.

A retry resends the request as it was signed, so it must still fall inside the receive window. For anything else, call the method again: each call is signed afresh.

```python
import urllib3

from foxbit_group.rest_api import Configuration

config = Configuration(
    api_key="YOUR_API_KEY",
    api_secret="YOUR_API_SECRET",
    timeout=urllib3.Timeout(connect=5, read=15),
)
print(config.timeout)
```

## Error handling

HTTP, network and response-deserialization failures of an API call are `ApiException` instances, with `status`, `reason`, `body` and `headers`. Every class below lives in `foxbit_group.rest_api.exceptions` and is also importable from `foxbit_group.rest_api`. Arguments are checked before any request: a wrong type, a missing required argument or a value out of range raises `pydantic.ValidationError` (a `ValueError`), and an empty, `.` or `..` path parameter or an invalid `Configuration` raises `ApiValueError`:

| Exception | When |
| --- | --- |
| `BadRequestException` | 400: the request is invalid |
| `UnauthorizedException` | 401: missing or wrong credentials, a rejected signature, or a timestamp header the server refuses |
| `ForbiddenException` | 403: the API key lacks the permission |
| `NotFoundException` | 404 |
| `ConflictException` | 409 |
| `UnprocessableEntityException` | 422 |
| `TooManyRequestsException` | 429: rate limit hit; `retry_after` holds the seconds to wait (from `X-FB-RATE-LIMIT-RETRY-AFTER`, or a standard `Retry-After`), or `None` |
| `ServiceException` | 5xx |
| `NetworkException` | No HTTP response: connection refused, DNS, TLS, timeout. `status` is 0 and the urllib3 error is `__cause__` |
| `ResponseDeserializationException` | A 2xx whose body does not match the documented type. `body` keeps the response text (bytes that are not valid in its charset are replaced) and the parse error is `__cause__` |
| `ApiException` | Any other status, such as a 3xx |

Configuration mistakes (both `api_secret` and `private_key`, an out-of-range `receive_window`, a `..` path parameter) raise `ApiValueError` before anything is sent.

```python
from foxbit_group.rest_api import ApiClient, Configuration
from foxbit_group.rest_api.api import TradingApi
from foxbit_group.rest_api.exceptions import (
    ApiException,
    NetworkException,
    TooManyRequestsException,
    UnauthorizedException,
)

config = Configuration(api_key="YOUR_API_KEY", api_secret="YOUR_API_SECRET")

with ApiClient(config) as client:
    try:
        orders = TradingApi(client).list_orders()
        print(orders)
    except UnauthorizedException as e:
        print("Invalid credentials or rejected signature:", e.body)
    except TooManyRequestsException as e:
        print(f"Rate limited, retry in {e.retry_after} s")
    except NetworkException as e:
        print("Network problem:", e.reason)
    except ApiException as e:
        print(f"API error {e.status}: {e.body}")
```

## Examples

All amounts and prices are strings, to keep their exact decimal value.

### Create a LIMIT order

`CreateOrderRequest` wraps one of the order types: `OrderLimit`, `OrderMarket`, `OrderInstant`, `OrderStop` or `OrderStopLimit`.

```python
from foxbit_group.rest_api import ApiClient, Configuration
from foxbit_group.rest_api.api import TradingApi
from foxbit_group.rest_api.models import CreateOrderRequest, OrderLimit

config = Configuration(api_key="YOUR_API_KEY", api_secret="YOUR_API_SECRET")

with ApiClient(config) as client:
    created = TradingApi(client).create_order(
        CreateOrderRequest(
            OrderLimit(
                side="BUY",
                type="LIMIT",
                market_symbol="btcbrl",
                quantity="0.001",
                price="150000.00",
                time_in_force="GTC",
                post_only=True,
                client_order_id="1001",
            )
        )
    )
    print("order id:", created.id)
```

### Cancel an order by ID

```python
from foxbit_group.rest_api import ApiClient, Configuration
from foxbit_group.rest_api.api import TradingApi
from foxbit_group.rest_api.models import CancelOrdersRequest, OrdersCancelId

config = Configuration(api_key="YOUR_API_KEY", api_secret="YOUR_API_SECRET")

with ApiClient(config) as client:
    canceled = TradingApi(client).cancel_orders(
        CancelOrdersRequest(OrdersCancelId(type="ID", id="1234567890"))
    )
    for order in canceled.data or []:
        print("cancel requested:", order.id)
```

Cancellation is asynchronous: use `get_order_by_id` to confirm the final state. `CancelOrdersRequest` also takes `OrdersCancelClientOrderId`, `OrdersCancelMarket`, `OrdersCancelMarketSide` and `OrdersCancelAll`.

### List orders with a date filter and pagination

```python
from foxbit_group.rest_api import ApiClient, Configuration
from foxbit_group.rest_api.api import TradingApi

config = Configuration(api_key="YOUR_API_KEY", api_secret="YOUR_API_SECRET")
page_size = 100

with ApiClient(config) as client:
    trading = TradingApi(client)
    for page in range(1, 11):  # at most 10 pages here
        orders = trading.list_orders(
            start_time="2026-01-01T00:00:00.000Z",  # ISO-8601 UTC, at most 90 days apart
            end_time="2026-01-31T23:59:59.999Z",
            market_symbol="btcbrl",
            state="FILLED",
            page=page,
            page_size=page_size,
        )
        batch = orders.data or []
        for order in batch:
            print(order.id, order.side, order.price, order.quantity_executed)
        if len(batch) < page_size:
            break
```

### Balances

```python
from foxbit_group.rest_api import ApiClient, Configuration
from foxbit_group.rest_api.api import AccountApi

config = Configuration(api_key="YOUR_API_KEY", api_secret="YOUR_API_SECRET")

with ApiClient(config) as client:
    accounts = AccountApi(client).list_accounts()
    for account in accounts.data or []:
        print(account.currency_symbol, "available:", account.balance_available, "locked:", account.balance_locked)
```

### Market data

```python
from foxbit_group.rest_api import ApiClient
from foxbit_group.rest_api.api import MarketDataApi

with ApiClient() as client:
    market_data = MarketDataApi(client)

    book = market_data.get_orderbook("btcbrl", depth=5)
    if book.bids and book.asks:
        print("best bid:", book.bids[0], "best ask:", book.asks[0])

    for ticker in market_data.get_ticker("btcbrl").data:
        print(ticker.market_symbol, "last:", ticker.last_trade.price)
```

### Retry after a rate limit

```python
import time

from foxbit_group.rest_api import ApiClient, Configuration
from foxbit_group.rest_api.api import AccountApi
from foxbit_group.rest_api.exceptions import TooManyRequestsException

config = Configuration(api_key="YOUR_API_KEY", api_secret="YOUR_API_SECRET")

with ApiClient(config) as client:
    accounts = None
    for attempt in range(3):
        try:
            accounts = AccountApi(client).list_accounts()
            break
        except TooManyRequestsException as e:
            # Each new call is signed again, with a fresh timestamp.
            time.sleep(e.retry_after if e.retry_after is not None else 2 ** attempt)
    print(accounts)
```

### Custom timeout for one call

```python
from foxbit_group.rest_api import ApiClient
from foxbit_group.rest_api.api import MarketDataApi

with ApiClient() as client:
    # (connect, read) in seconds; wins over Configuration.timeout
    currencies = MarketDataApi(client).list_currencies(_request_timeout=(2.0, 5.0))
    print(currencies)
```

## API reference

The full reference, with every endpoint, field and rate limit, is at [docs.foxbit.com.br/rest/v3](https://docs.foxbit.com.br/rest/v3/). See the [changelog](https://docs.foxbit.com.br/rest/v3/changelog/) for API changes.

The SDK groups the endpoints in these classes (`foxbit_group.rest_api.api`); every method has its docstring, and the endpoints are described in the API reference above:


- `AccountApi`

- `BanksApi`

- `DepositApi`

- `MarketDataApi`

- `MemberInfoApi`

- `PrimeDeskApi`

- `SystemApi`

- `TradingApi`

- `TransactionalLimitsApi`

- `TravelRuleApi`

- `WithdrawalApi`


## Upgrading from an earlier version

- Python 3.10 or newer is required.
- Network failures (connection refused, timeouts, broken connections) raise `NetworkException`, a subclass of `ApiException`, instead of urllib3 exceptions such as `MaxRetryError`. The urllib3 error is kept as its `__cause__`.
- A 2xx response the SDK cannot read raises `ResponseDeserializationException` instead of a pydantic or JSON error, and a 429 raises `TooManyRequestsException`.
- Redirects are no longer followed: a 3xx raises `ApiException` with its status.
- Every request has a default timeout (connect 10 s, read 30 s), and signed requests send `X-FB-RECEIVE-WINDOW: 10000`. `receive_window=None` leaves the header out, which turns the replay check off for HMAC keys.
- `POST`, `PUT` and `PATCH` are never retried, even with a custom `retries`.
- `debug=True` logs through the package logger only; it no longer turns on `http.client` debugging for the whole process, and `Configuration.logger` no longer has an `urllib3_logger` entry.
- New minimum dependencies: `urllib3` 2.6.3, `pydantic` 2.4 (below 3) and `cryptography` 42, which is installed with the package.

## Disclaimer

This SDK is provided "as is", without warranty of any kind. Trading crypto assets involves risk, including the loss of the amounts invested; nothing here is investment advice. Test your integration with small amounts first, give each API key only the permissions it needs, and keep your API secret and private key out of source control.

## License

MIT; the license text ships with the package (`LICENSE`). Maintained by [Foxbit](https://foxbit.com.br).
