## Fund API Documentation

Fund endpoints let an organization transfer funds to an Infini UID, withdraw funds, query withdrawal status and fees, and read its available USDT, USDC, and USD balances.

All Fund API prefix:

`/v2/funds`

### Authentication and Permissions

All endpoints use HMAC-SHA256 authentication with **`Date`**, **`Digest`** when the request has a body, and **`Authorization`** with **`keyId`**. For a GET request with query parameters, the signed request path must include the complete query string. See [Chapter 4: Authorization and Security Mechanisms](/docs/en/4-authorization).

| Permission | Endpoints |
|  --- | --- |
| `fund.withdraw` | `POST /v2/funds/internal-transfer`, `POST /v2/funds/withdraw`, `GET /v2/funds/withdraw/status`, `POST /v2/funds/withdraw/status/batch`, `GET /v2/funds/withdraw/fees`, `GET /v2/funds/balances` |


> **IP whitelist:** API keys that include `fund.withdraw` must be configured with a non-empty IP whitelist when created or updated in the merchant dashboard.


### Withdrawal Idempotency

`request_id` is optional when creating a withdrawal:

- When omitted, Infini generates the withdrawal request ID and preserves the existing withdrawal behavior. Separate caller retries without a shared ID are not idempotent.
- When supplied, it must be a UUID and is used as the idempotency key.
- When the same merchant retries with the same `request_id`, the API returns the previously created withdrawal information with `is_duplicate: true`.
- Use one `request_id` for exactly one logical withdrawal. Do not reuse it for a different amount, chain, token, or destination address.


### Response Envelope

After gateway authentication, application responses use the standard `code` / `message` / `data` envelope. Business success is indicated by `code === 0`; a non-zero `code` indicates an error and `message` carries the details. Some downstream business failures can be returned with HTTP 200, a non-zero `code`, and `data: null`.

HMAC or gateway validation failures can be rejected before the request reaches the application and may return a gateway response such as `{"message":"client request can't be validated"}` instead of the application envelope. Clients should check the HTTP status first, then check `code` whenever the envelope is present.

### Transfer Funds to an Infini UID

**POST** /v2/funds/internal-transfer

Transfers funds from the authenticated organization to a recipient identified by an Infini UID.

- **Required permission:** `fund.withdraw`
- V1 supports same-currency transfers in `USDT`, `USDC`, or `USD` only.
- The transfer has no fee: the amount deducted from the sender equals the amount received by the recipient.
- A successful V1 transfer completes synchronously and returns `status: completed`.


#### Request Body

| Field | Type | Required | Description |
|  --- | --- | --- | --- |
| request_id | string (UUID) | No | Idempotency key. When omitted, Infini generates one |
| recipient_uid | string | Yes | Recipient Infini UID; it cannot belong to the source organization |
| token_type | string | Yes | Recipient account currency: `USDT`, `USDC`, or `USD` |
| source_currency | string | No | Source account currency. Defaults to `token_type`; V1 requires it to equal `token_type` |
| amount | string | Yes | Positive amount deducted and received. `USDT` and `USDC` support up to six decimal places with a minimum of `0.000001`; `USD` supports up to two decimal places with a minimum of `0.01` |
| note | string | No | Optional transfer note |


#### Request Example

```json
{
  "request_id": "4b4436ba-90a1-4f26-940d-97177a7a3400",
  "recipient_uid": "12345678",
  "token_type": "USDC",
  "source_currency": "USDC",
  "amount": "10.25",
  "note": "Customer refund"
}
```

#### Response Example

```json
{
  "code": 0,
  "message": "",
  "data": {
    "request_id": "4b4436ba-90a1-4f26-940d-97177a7a3400",
    "status": "completed",
    "is_duplicate": false
  }
}
```

`request_id` is optional. When supplied, it must be a UUID and is used as the idempotency key. A retry by the same organization with an already completed `request_id` returns the original request ID with `is_duplicate: true`. Use one request ID for exactly one logical transfer; do not reuse it for different transfer details.

#### Error Codes

| HTTP Status | Code | Description |
|  --- | --- | --- |
| 401 | `401` | The application could not authenticate the API key |
| 403 | `403` | The API key lacks `fund.withdraw` or the IP whitelist rejected the request |
| 400 or 200 | `30013` | A parameter is invalid, including an invalid UUID, UID, currency, amount, precision, cross-currency transfer, self-transfer, or a request ID already owned by another organization |
| 200 | `10005` | The sender or recipient could not be found |
| 200 | `10030` | The source organization's owner is protected from transfers |
| 200 | `20012` | The source account's usable balance is insufficient |
| 200 | `30005` | The transfer exceeds the remaining operation limit |
| 200 | `30015` | Internal transfers are restricted for the source organization |
| 200 | `30034` | The source currency is `USD`, but the organization does not have a USD Cash Account |
| 500 | `500` | An unexpected internal or downstream service error occurred |


### Withdraw Organization Funds

**POST** /v2/funds/withdraw

Creates a withdrawal from the authenticated organization to an external wallet.

- **Required permission:** `fund.withdraw`


#### Request Body

| Field | Type | Required | Description |
|  --- | --- | --- | --- |
| request_id | string (UUID) | No | Idempotency key. When omitted, Infini generates one |
| chain | string | Yes | Public chain name, for example `ETHEREUM` or `ARBITRUM` |
| token_type | string | Yes | Token type, for example `USDT` or `USDC` |
| source_currency | string | No | Source account currency. Defaults to `token_type`. Set it to `USD` to deduct from the organization's USD Cash Account when withdrawing `USDT` or `USDC`; other cross-currency pairs are rejected |
| amount | string | Yes | Maximum amount deducted from the source account. Digits after the sixth decimal place are truncated without rounding; when `source_currency` is `USD`, at most two decimal places are allowed. The resulting amount must be at least `1` and greater than the applicable fees |
| wallet_address | string | Yes | Destination address valid for the selected chain |
| note | string | No | Optional withdrawal note |


Use **`GET /v2/funds/withdraw/fees`** as the current source of truth for enabled chain/token pairs and their fees. Chain and token names in examples are uppercase.

#### Request Example

```json
{
  "request_id": "e94b4e88-36c2-4550-907e-839742cf5fae",
  "chain": "ETHEREUM",
  "token_type": "USDT",
  "source_currency": "USD",
  "amount": "10.00",
  "wallet_address": "0x5f716e5775b18409917e2a2f0762d29d6c385cb0",
  "note": "Treasury withdrawal"
}
```

#### Response Example

```json
{
  "code": 0,
  "message": "",
  "data": {
    "request_id": "e94b4e88-36c2-4550-907e-839742cf5fae",
    "status": "pending",
    "is_duplicate": false
  }
}
```

For an idempotent retry, `data.is_duplicate` is `true` and `data.status` is the existing withdrawal's current status.

#### Error Codes

| HTTP Status | Code | Description |
|  --- | --- | --- |
| 401 | `401` | The application could not authenticate the API key |
| 403 | `403` | The API key lacks `fund.withdraw`, the IP whitelist rejected the request, or withdrawal is disabled for the selected chain/token pair |
| 200 | `30001` | The core withdrawal configuration rejected the selected token |
| 200 | `30002` | The organization's source fund account does not exist |
| 200 | `30003` | The source account's available balance is insufficient |
| 200 | `30005` | The amount exceeds the remaining daily withdrawal limit |
| 200 | `30007` | The amount is below the minimum or is not greater than the applicable fees |
| 200 | `30012` | No withdrawal fee configuration is available for the selected chain/token pair |
| 400 or 200 | `30013` | A request parameter is invalid, such as the UUID, chain/token pair, amount, precision, source currency, or wallet address |
| 200 | `30022` | The destination is an internal Infini address; use an internal transfer instead |
| 200 | `30023` | The usable balance is insufficient after accounting for locked or reserved funds |
| 200 | `30034` | `source_currency` is `USD`, but the organization does not have a USD Cash Account |
| 200 | `80016` | A required asset price or exchange rate is temporarily unavailable |
| 500 | `500` | An unexpected internal or downstream service error occurred |


Codes returned by the core fund service use the standard application envelope and may have HTTP 200 even when `code` is non-zero. HMAC failures rejected by the gateway may not include an application error code.

### Get Withdrawal Status

**GET** /v2/funds/withdraw/status?request_id={request_id}

Returns the current state and amounts for a withdrawal owned by the authenticated organization.

- **Required permission:** `fund.withdraw`
- The HMAC signing path must include `?request_id={request_id}`.


#### Query Parameters

| Field | Type | Required | Description |
|  --- | --- | --- | --- |
| request_id | string (UUID) | Yes | Withdrawal request ID |


#### Response Example

```json
{
  "code": 0,
  "message": "",
  "data": {
    "request_id": "e94b4e88-36c2-4550-907e-839742cf5fae",
    "status": "completed",
    "amount": "11",
    "fee": "0.1",
    "actual_amount": "10.9",
    "transaction_hash": "0xabc123",
    "chain": "ETHEREUM",
    "token_type": "USDT",
    "source_currency": "USD",
    "gas_fee": "0.08",
    "gas_fee_currency": "USD",
    "fx_fee": "0.02",
    "fx_fee_currency": "USD",
    "fee_paid_by": "BENEFICIARY"
  }
}
```

#### Response Fields

| Field | Type | Description |
|  --- | --- | --- |
| request_id | string | Withdrawal request ID |
| status | string | Current withdrawal status |
| amount | string | Total amount deducted from the source account |
| fee | string | Legacy Gas fee field; new integrations should use `gas_fee` and `gas_fee_currency` |
| actual_amount | string | Actual amount sent on-chain in `token_type` |
| transaction_hash | string | On-chain transaction hash; empty before submission |
| chain | string | Public chain name |
| token_type | string | Token type |
| source_currency | string | Source account currency |
| gas_fee | string | Customer-facing Gas fee charged for the withdrawal |
| gas_fee_currency | string | Currency of `gas_fee` |
| fx_fee | string | FX fee charged for a cross-currency withdrawal; `0` for same-currency withdrawals |
| fx_fee_currency | string | Currency of `fx_fee` |
| fee_paid_by | string | Fee liability: `PAYER` or `BENEFICIARY` |


#### Status Values

| Status | Description |
|  --- | --- |
| `pending` | Withdrawal created and waiting for submission or review |
| `processing` | Transaction submitted and waiting for confirmation |
| `completed` | Withdrawal completed |
| `failed` | Withdrawal failed |


### List Withdrawal Statuses

**POST** /v2/funds/withdraw/status/batch

Returns withdrawals owned by the authenticated organization whose creation times fall within the requested range.

- **Required permission:** `fund.withdraw`
- `start_time` and `end_time` are Unix timestamps in seconds and form an inclusive range.
- Results are ordered by `created_at` descending.
- `page` defaults to `1`; `page_size` defaults to `20` and cannot exceed `100`.
- A range with no matching withdrawals returns HTTP 200 with an empty `withdrawals` array.
- Invalid time ranges or pagination values return HTTP 400. A failure that prevents processing the query returns HTTP 500.


#### Request Body

| Field | Type | Required | Description |
|  --- | --- | --- | --- |
| start_time | integer | Yes | Inclusive withdrawal creation start time, as a Unix timestamp in seconds |
| end_time | integer | Yes | Inclusive withdrawal creation end time, as a Unix timestamp in seconds; must be greater than or equal to `start_time` |
| page | integer | No | Page number; defaults to `1` |
| page_size | integer | No | Number of withdrawals per page; defaults to `20`, maximum `100` |


#### Request Example

```json
{
  "start_time": 1787500000,
  "end_time": 1787599999,
  "page": 1,
  "page_size": 20
}
```

Because this is a POST request with a body, include the body `Digest` in the HMAC-authenticated request and sign the path `/v2/funds/withdraw/status/batch`.

#### Response Example

```json
{
  "code": 0,
  "message": "",
  "data": {
    "withdrawals": [
      {
        "request_id": "e94b4e88-36c2-4550-907e-839742cf5fae",
        "created_at": 1787564636,
        "status": "completed",
        "amount": "11",
        "fee": "0.1",
        "actual_amount": "10.9",
        "transaction_hash": "0xabc123",
        "chain": "ETHEREUM",
        "token_type": "USDT",
        "source_currency": "USDT",
        "gas_fee": "0.1",
        "gas_fee_currency": "USDT",
        "fx_fee": "0",
        "fx_fee_currency": "USDT",
        "fee_paid_by": "BENEFICIARY"
      }
    ],
    "total": 1,
    "page": 1,
    "page_size": 20,
    "total_pages": 1
  }
}
```

Each item uses the same amount, fee, chain, and status semantics as the single-query endpoint and additionally returns `created_at`. Pagination metadata describes the complete matching time range, not only the current page.

### List Withdrawal Fees

**GET** /v2/funds/withdraw/fees

Returns currently enabled public chain/token pairs and their fees. Fees are dynamic; clients should query this endpoint instead of hard-coding a fee table.

- **Required permission:** `fund.withdraw`


#### Response Example

```json
{
  "code": 0,
  "message": "",
  "data": {
    "fees": [
      {
        "chain": "ARBITRUM",
        "token_type": "USDC",
        "withdraw_fee": "0.5"
      },
      {
        "chain": "ETHEREUM",
        "token_type": "USDT",
        "withdraw_fee": "2"
      }
    ]
  }
}
```

Each `withdraw_fee` is denominated in the corresponding `token_type`.

### Get Available Balances

**GET** /v2/funds/balances

Returns only the authenticated organization's USDT, USDC, and USD Cash `AVAILABLE_BALANCE` values.

- **Required permission:** `fund.withdraw`


#### Response Example

```json
{
  "code": 0,
  "message": "",
  "data": {
    "available_balance_usdt": "120.50",
    "available_balance_usdc": "75.25",
    "available_balance_usd": "1000.00"
  }
}
```

When the organization has no balance entry for one of the currencies, its value is returned as `"0"`. In particular, `available_balance_usd` is `"0"` when the USD Cash Account is not open.