> ## Documentation Index
> Fetch the complete documentation index at: https://docs.qfex.com/llms.txt
> Use this file to discover all available pages before exploring further.

# Programmatic deposits and withdrawals

> Fund a QFEX account and request crypto or USD withdrawals with the REST API.

QFEX exposes funding endpoints on a separate REST service:

| Environment | Base URL                  |
| ----------- | ------------------------- |
| Production  | `https://banker.qfex.com` |
| UAT         | `https://banker.qfex.io`  |

The API supports:

* USDC deposits and withdrawals on Arbitrum One (`ARBITRUM_ONE`)
* USDT deposits and withdrawals on TRON (`TRON`)
* USD deposits by ACH push, wire, or FedNow
* USD withdrawals by ACH or wire

<Warning>
  Always send the asset returned by the address endpoint on the exact network
  returned by that endpoint. Funds sent with a different asset or network may
  be lost.
</Warning>

## Before you start

Complete account verification and accept the QFEX terms before using the funding API. Then [create an API key](/api-reference/api-keys) with:

* **All accounts** access
* **Deposit and withdraw** permission

Funding applies to the primary account. To fund a subaccount, fund the primary account first and then [transfer funds to the subaccount](/api-reference/subaccounts-api#transfer-between-primary-account-and-subaccounts).

## Authenticate requests

Every request requires these headers:

```text theme={null}
x-qfex-public-key: <public-key>
x-qfex-hmac-signature: <hex-encoded-signature>
x-qfex-nonce: <unique-hex-nonce>
x-qfex-timestamp: <current-unix-time-in-seconds>
```

For each request:

1. Generate a cryptographically secure, hex-encoded nonce of at most 100 characters.
2. Read the current Unix time in seconds.
3. Sign the UTF-8 string `${nonce}:${timestamp}` with HMAC-SHA256, using the secret API key as the key.
4. Hex-encode the signature and send all four headers.

The timestamp must be within five minutes of QFEX server time. Never reuse a nonce. The HTTP method, path, query, and request body are not part of the signed string.

The following helper creates a fresh signature for every request and returns the decoded JSON response:

<CodeGroup>
  ```python Python theme={null}
  import hashlib
  import hmac
  import json
  import os
  import secrets
  import time
  from urllib.error import HTTPError
  from urllib.request import Request, urlopen

  BASE_URL = os.getenv("QFEX_FUNDING_URL", "https://banker.qfex.com")
  PUBLIC_KEY = os.environ["QFEX_PUBLIC_KEY"]
  SECRET_KEY = os.environ["QFEX_SECRET_KEY"]


  def qfex_request(path, method="GET", body=None):
      nonce = secrets.token_hex(16)
      timestamp = str(int(time.time()))
      message = f"{nonce}:{timestamp}".encode()
      signature = hmac.new(
          SECRET_KEY.encode(), message, hashlib.sha256
      ).hexdigest()

      headers = {
          "accept": "application/json",
          "x-qfex-public-key": PUBLIC_KEY,
          "x-qfex-hmac-signature": signature,
          "x-qfex-nonce": nonce,
          "x-qfex-timestamp": timestamp,
      }
      data = None
      if body is not None:
          headers["content-type"] = "application/json"
          data = json.dumps(body).encode()

      request = Request(BASE_URL + path, data=data, headers=headers, method=method)
      try:
          with urlopen(request) as response:
              response_body = response.read()
              return json.loads(response_body) if response_body else None
      except HTTPError as error:
          detail = error.read().decode()
          raise RuntimeError(f"QFEX returned {error.code}: {detail}") from error
  ```

  ```javascript Node.js theme={null}
  import crypto from "node:crypto";

  const baseUrl = process.env.QFEX_FUNDING_URL ?? "https://banker.qfex.com";
  const publicKey = process.env.QFEX_PUBLIC_KEY;
  const secretKey = process.env.QFEX_SECRET_KEY;

  if (!publicKey || !secretKey) {
    throw new Error("Set QFEX_PUBLIC_KEY and QFEX_SECRET_KEY");
  }

  async function qfexRequest(path, { method = "GET", body } = {}) {
    const nonce = crypto.randomBytes(16).toString("hex");
    const timestamp = Math.floor(Date.now() / 1000).toString();
    const signature = crypto
      .createHmac("sha256", secretKey)
      .update(`${nonce}:${timestamp}`)
      .digest("hex");

    const response = await fetch(`${baseUrl}${path}`, {
      method,
      headers: {
        accept: "application/json",
        ...(body === undefined ? {} : { "content-type": "application/json" }),
        "x-qfex-public-key": publicKey,
        "x-qfex-hmac-signature": signature,
        "x-qfex-nonce": nonce,
        "x-qfex-timestamp": timestamp,
      },
      body: body === undefined ? undefined : JSON.stringify(body),
    });

    const text = await response.text();
    if (!response.ok) {
      throw new Error(`QFEX returned ${response.status}: ${text}`);
    }
    return text ? JSON.parse(text) : undefined;
  }
  ```
</CodeGroup>

Keep the secret key in a server-side secret store. Do not put it in browser code, a mobile application, logs, URLs, or source control.

## Deposit crypto

Request the deposit address for the network you intend to use:

```http theme={null}
GET /address?network=ARBITRUM_ONE
```

```python theme={null}
deposit = qfex_request("/address?network=ARBITRUM_ONE")
print(deposit["address"])
```

Example response:

```json theme={null}
{
  "address": "0x0123456789abcdef0123456789abcdef01234567",
  "network": "ARBITRUM_ONE",
  "asset": "USDC"
}
```

For USDT on TRON, use `network=TRON`. The response will contain `"asset": "USDT"` and `"network": "TRON"`.

`GET /address` returns the account's existing address or provisions one on the first call. After receiving it, submit the blockchain transfer from your wallet or custody provider. Address retrieval does not initiate a transfer.

<Note>
  Check the QFEX funding screen for the current minimum deposit and required
  confirmation count. A deposit is credited only after it has been detected
  and confirmed.
</Note>

## Withdraw crypto

Submit the gross amount to debit, the destination address, and the network:

```http theme={null}
POST /withdraw
Content-Type: application/json

{
  "amount": 100,
  "address": "0x89abcdef0123456789abcdef0123456789abcdef",
  "network": "ARBITRUM_ONE"
}
```

```python theme={null}
qfex_request(
    "/withdraw",
    method="POST",
    body={
        "amount": 100,
        "address": "0x89abcdef0123456789abcdef0123456789abcdef",
        "network": "ARBITRUM_ONE",
    },
)
```

On success, the endpoint returns `200 OK` with an empty body. `amount` is the gross amount debited from the QFEX account. QFEX deducts the applicable withdrawal fees and sends the remainder; check **Deposit / Withdraw** in QFEX for the current fees before submitting the request.

For a TRON withdrawal, send a valid TRON address and set `network` to `TRON`. Always specify the network explicitly even though the API currently defaults to `ARBITRUM_ONE` when it is omitted.

<Warning>
  `POST /withdraw` is not idempotent. Retrying a request can create a second
  withdrawal and debit the account again. If the client times out after sending
  the request, reconcile account history before deciding whether to retry.
</Warning>

A successful response means QFEX accepted and debited the withdrawal request. It does not guarantee that the on-chain transfer is already complete; a request may still be queued for submission or manual review.

## Deposit USD

Fetch the required transfer reference before initiating a bank transfer:

```http theme={null}
GET /fiat-address?currency=usd&payment_rail=ach_push
```

Valid deposit rails are `ach_push`, `wire`, and `fednow`.

```python theme={null}
instructions = qfex_request(
    "/fiat-address?currency=usd&payment_rail=ach_push"
)
print(instructions["deposit_message"])
```

Example response:

```json theme={null}
{
  "deposit_message": "YOUR-UNIQUE-REFERENCE"
}
```

The endpoint returns the account-specific reference, not the complete beneficiary bank details. Obtain the current beneficiary details from **Deposit / Withdraw** in QFEX, initiate the transfer through your bank, and include `deposit_message` exactly in the transfer's reference, message, or note field.

## Withdraw USD

USD withdrawals require a linked destination bank account. Create it once and store the returned QFEX bank-account ID.

### 1. Link a bank account

```http theme={null}
POST /bank-account?currency=usd
Content-Type: application/json

{
  "first_name": "Jane",
  "last_name": "Doe",
  "routing_number": "123456789",
  "bank_name": "Example Bank",
  "account_number": "000123456789",
  "type": "checking",
  "street_line_1": "100 Main Street",
  "street_line_2": "Apt 4",
  "country": "USA",
  "state": "NY",
  "city": "New York",
  "postal_code": "10001"
}
```

`type` must be `checking` or `savings`. `country` is an ISO 3166-1 alpha-3 code, and `state` is required for US addresses. The street address must include a street number.

```python theme={null}
bank_account = qfex_request(
    "/bank-account?currency=usd",
    method="POST",
    body={
        "first_name": "Jane",
        "last_name": "Doe",
        "routing_number": "123456789",
        "bank_name": "Example Bank",
        "account_number": "000123456789",
        "type": "checking",
        "street_line_1": "100 Main Street",
        "country": "USA",
        "state": "NY",
        "city": "New York",
        "postal_code": "10001",
    },
)
print(bank_account["id"])
```

The API returns `201 Created`:

```json theme={null}
{
  "id": "11111111-1111-4111-8111-111111111111"
}
```

### 2. Request the withdrawal

```http theme={null}
POST /fiat-withdraw
Content-Type: application/json

{
  "amount": 100,
  "currency": "usd",
  "payment_rail": "ach_push",
  "account_id": "11111111-1111-4111-8111-111111111111"
}
```

`amount` must be at least 10. Use `ach_push` or `wire` as the withdrawal rail.

```python theme={null}
qfex_request(
    "/fiat-withdraw",
    method="POST",
    body={
        "amount": 100,
        "currency": "usd",
        "payment_rail": "ach_push",
        "account_id": bank_account["id"],
    },
)
```

On success, the endpoint returns `200 OK` with an empty body. As with crypto withdrawals, acceptance is not final settlement and the request may require manual review. Fiat fees are deducted from the requested amount before settlement.

To remove a linked bank account, send `DELETE /bank-account/{id}` with a fresh set of authentication headers.

## Handle errors safely

| Status | Meaning                                                                                    |
| ------ | ------------------------------------------------------------------------------------------ |
| `400`  | Invalid amount, address, network, rail, bank details, or insufficient withdrawable balance |
| `401`  | Missing or invalid authentication, or terms not accepted                                   |
| `403`  | API key lacks **Deposit and withdraw** permission, or the account cannot use funding       |
| `404`  | Bank-account ID does not exist or does not belong to the user                              |
| `5xx`  | QFEX or a funding provider could not complete the operation                                |

Most structured errors include `title`, `status`, and `detail`; validation and provider errors may also include an `errors` array. Log the HTTP status and response body, but never log API secrets or full bank-account details.
