# Merchant customer deposit API

Collect repeated, variable-amount digital asset deposits using a Customer's permanent address. Infini identifies the merchant and Customer, applies the applicable confirmation, compliance and fee rules, then settles the net amount to the merchant's Infini balance and sends Webhooks.

A Customer identifies a user in your own system. It does not have an independently withdrawable Infini balance. Credit your user ledger only from `merchant_customer_deposit.completed`, using the actual `credited_amount`, which can be zero.

All endpoints use `/v2/merchant-customer`. Use [Ed25519 v2 authentication](/docs/en/4-authorization).

## Operations

| Method | Path | Purpose |
|  --- | --- | --- |
| GET | `/v2/merchant-customer/currencies` | Supported currencies, networks and small-deposit policy |
| POST | `/v2/merchant-customer/customers` | Create or update a Customer |
| GET | `/v2/merchant-customer/customers` | Get one Customer |
| GET | `/v2/merchant-customer/customers/quota` | Customer quota |
| POST | `/v2/merchant-customer/addresses` | Create or reuse an address binding |
| GET | `/v2/merchant-customer/addresses` | Get one address binding |


The [API reference](/apis/customer-deposits) contains the complete request and response schemas.

## Business idempotency

- A Customer is unique by `external_user_id`. Repeating it returns the same ID and updates explicitly supplied profile fields.
- An address binding is unique by merchant, environment, `customer_id`, `asset` and `network`. Repeating it returns the same binding and physical address.
- Updating a name, email or remark does not change IDs or existing addresses.
- Serialize profile updates per user where possible. A retry containing stale profile values can overwrite the same fields updated by a different request.
- Generate a fresh authentication timestamp, nonce and signature on every retry, while retaining the same business fields.


## List supported deposit currencies

**GET** `/v2/merchant-customer/currencies` — permission `merchant_customer.read`.

Optional query parameters: `asset` (up to 12 characters) and `network` (up to 32). Omit either to avoid that filter. A valid filter with no available match returns `data: []`.

```http
GET /v2/merchant-customer/currencies?asset=USDT&network=ETHEREUM
```

```json
{
  "code": 0,
  "message": "",
  "data": [
    {
      "asset": "USDT",
      "asset_name": "Tether",
      "networks": [
        {
          "network": "ETHEREUM",
          "network_name": "Ethereum",
          "min_deposit_amount": "0.01",
          "fixed_fee": "0.01"
        }
      ]
    }
  ]
}
```

Each asset has `asset`, `asset_name` and a non-empty `networks` list. Each network has `network`, `network_name`, `min_deposit_amount` in **asset units**, and `fixed_fee` in **USD**. Thresholds are converted from USD using the query-time price. They are display values, not a price lock for a future deposit.

| Network | Display name | Assets |
|  --- | --- | --- |
| TRON | Tron | USDT, TRX |
| ETHEREUM | Ethereum | USDT, USDC, ETH |
| BSC | BNB Smart Chain | USDT, USDC, BNB |
| ARBITRUM | Arbitrum One | USDT, USDC |
| POLYGON | Polygon PoS | USDT, USDC |
| BASE | Base | USDT, USDC, ETH |
| SOLANA | Solana | USDT, USDC, SOL |


Sandbox can return `TRON`, `SETH` and `BSC`; use the target environment's actual response rather than a hard-coded list. Do not send testnet assets to mainnet addresses.

| Asset | Initial USD-equivalent threshold | Additional below-minimum fee in USD |
|  --- | --- | --- |
| USDT | 0.01 | 0.01 |
| USDC | 0.01 | 0.01 |
| TRX | 500 | 1.5 |
| BNB | 0.01 | 0.01 |
| ETH | 0.01 | 0.01 |
| SOL | 0.01 | 0.01 |


The TRX threshold is **500 USD equivalent**, not 500 TRX. At an illustrative price of 0.25 USD per TRX, the returned threshold is `"2000"`. These are initial settings; use current API values and the policy applicable when the deposit is processed.

## Create or update a Customer

**POST** `/v2/merchant-customer/customers` — permission `merchant_customer.write`.

| Request field | Type | Required | Meaning |
|  --- | --- | --- | --- |
| external_user_id | string, 1–80 characters | Yes | Stable, case-sensitive merchant user ID, with no leading/trailing whitespace; immutable |
| name | string / null, up to 256 characters | No | Customer name |
| email | string / null, up to 256 characters | No | Valid email when non-empty; normalized by trimming and lowercasing |
| remark | string / null, up to 500 characters | No | Merchant remark |


```json
{
  "external_user_id": "user_1001",
  "name": "Alice",
  "email": "alice@example.com",
  "remark": "VIP customer"
}
```

For a new Customer, absent optional fields become `null`. For an existing Customer, omitted fields remain unchanged; explicit `null` or an empty string clears the field. Creating a Customer does not allocate a wallet or address.

First creation returns **HTTP 201**. Returning or updating an existing Customer returns **HTTP 200**:

```json
{
  "code": 0,
  "message": "",
  "data": {
    "customer_id": 583174926,
    "external_user_id": "user_1001",
    "name": "Alice",
    "email": "alice@example.com",
    "remark": "VIP customer",
    "created_at": 1794211200,
    "updated_at": 1794211200
  }
}
```

| Field | Type | Description |
|  --- | --- | --- |
| customer_id | integer (int32) | Positive signed 32-bit Customer ID. |
| external_user_id | string | Immutable merchant user ID, case-sensitive. |
| name | string / null | Customer name; null when absent. |
| email | string / null | Normalized lowercase email; null when absent. |
| remark | string / null | Merchant remark; null when absent. |
| created_at | integer (int64) | First creation time. Unix seconds, not milliseconds. |
| updated_at | integer (int64) | Last actual profile change; unchanged retries do not advance it. Unix seconds, not milliseconds. |


New Customers consume one quota slot. Existing updates consume no additional slots and remain allowed at the limit. Static-deposit Customers and Invoice Customers have separate IDs.

## Get a Customer

**GET** `/v2/merchant-customer/customers` — permission `merchant_customer.read`.

Supply exactly one of `customer_id` (positive int32) or `external_user_id` (up to 80 characters). Returns HTTP 200 with the Customer envelope above. It never creates or changes the Customer. An unknown Customer returns not found.

```http
GET /v2/merchant-customer/customers?external_user_id=user_1001
```

## Get Customer quota

**GET** `/v2/merchant-customer/customers/quota` — permission `merchant_customer.read`. No query or body.

```json
{
  "code": 0,
  "message": "",
  "data": {
    "limit": 50,
    "used": 12
  }
}
```

`limit` is the effective Customer limit and `used` is the created Customer count. No `remaining` field is returned; calculate `max(limit-used,0)`. Limits may vary by merchant. Addresses do not consume Customer slots. This query does not reserve capacity; creation checks it again.

## Create or reuse a deposit address

**POST** `/v2/merchant-customer/addresses` — permission `merchant_customer.write`.

All three fields are required: `customer_id` (positive int32), `asset` and `network` from the supported-currencies endpoint.

```json
{
  "customer_id": 583174926,
  "asset": "USDT",
  "network": "ETHEREUM"
}
```

A new ready binding returns **HTTP 201**; an existing ready binding returns **HTTP 200**:

```json
{
  "code": 0,
  "message": "",
  "data": {
    "deposit_address_id": "d498f76d-42a9-447c-bdad-c10f72d18844",
    "customer_id": 583174926,
    "asset": "USDT",
    "network": "ETHEREUM",
    "address": "0x1111111111111111111111111111111111111111",
    "required_confirmations": 12,
    "min_deposit_amount": "0.01",
    "created_at": 1794211260
  }
}
```

| Field | Type | Description |
|  --- | --- | --- |
| deposit_address_id | string | Stable asset/network binding ID; different bindings may share a physical address. |
| customer_id | integer (int32) | Positive signed 32-bit Customer ID. |
| asset | string | Supported asset code; query currencies for availability. |
| network | string | Supported network code. |
| address | string | Ready-to-use address. Example is illustrative only; do not send funds to it. |
| required_confirmations | integer | Network confirmation requirement; use the actual returned value. |
| min_deposit_amount | string | Current threshold in asset units, derived from USD configuration and the current price. Zero means no threshold. Does not lock future pricing. |
| created_at | integer (int64) | First successful binding time, unchanged on reuse. Unix seconds, not milliseconds. |


The example address and confirmation count are illustrative. Never transfer funds to the example address; use the actual response.

### Shared addresses and automatic identification

- One Customer uses the same EVM address on Ethereum, BSC, Arbitrum, Polygon and Base, while asset/network bindings have distinct `deposit_address_id` values.
- TRON TRX and USDT also share the same physical address for that Customer.
- Supported deposits to the Customer's owned address are automatically identified and settled even if that asset/network binding was not requested beforehand. For example, a Customer with an Ethereum-USDT address can receive BSC-USDC at that EVM address.
- Apply confirmation, compliance and fee rules to the actual received asset and network. Sharing an address does not merge chains or balances.
- This does not make EVM addresses interchangeable with TRON or Solana addresses. Unsupported assets/networks are not guaranteed automatic credit; contact support.


Concurrent requests or an allocation with an unknown upstream outcome return `address_processing`. Back off and query or retry with the same Customer/asset/network. A timeout does not prove that no wallet was created. Do not create another Customer to replace an allocation whose result is still being reconciled.

## Get a deposit address

**GET** `/v2/merchant-customer/addresses` — permission `merchant_customer.read`.

Required query parameters: `customer_id`, `asset`, `network`. Returns HTTP 200 with the address envelope above. It does not allocate a wallet or create a binding. An unknown binding returns `deposit_address_not_found`; one still being prepared returns `address_processing`.

```http
GET /v2/merchant-customer/addresses?customer_id=583174926&asset=USDT&network=ETHEREUM
```

## Error codes

```json
{
  "code": 47003,
  "message": "Customer limit reached",
  "data": {
    "error": "customer_limit_exceeded",
    "field": "external_user_id",
    "retryable": false
  }
}
```

| HTTP | Code | data.error | Retryable | Action |
|  --- | --- | --- | --- | --- |
| 400 | 30013 | invalid_parameter | No | Correct request fields |
| 401 | 401 | authentication_failed | No | Check credential, signature, digest, timestamp, nonce and signed target |
| 403 | 403 | permission_denied | No | Check key permission and access policy |
| 404 | 47001 | customer_not_found | No | Check Customer and environment |
| 404 | 47002 | deposit_address_not_found | No | Create the binding first |
| 409 | 47003 | customer_limit_exceeded | No | Check quota; existing Customers can still be updated |
| 409 | 47004 | address_processing | Yes | Query or retry unchanged business fields after backoff |
| 503 | 47005 | price_unavailable | Yes | Retry when a valid price is available |
| 503 | 47006 | dependency_unavailable | Yes | Back off and retry unchanged business fields |
| 400 | 40007 | unsupported_chain_token | No | Select a supported pair |
| 409 | 40915 | currency_disabled | No | Refresh available currencies |
| 500 | 500 | internal_error | Yes | Back off and retain the same business fields |


Gateway HTTP 429/5xx responses may not have this envelope. Do not branch on `message` text. After an address timeout, reconcile the original object before assuming that nothing was created.

## Fees and deposit states

Fees are merchant-paid and deducted from the deposit itself, with no extra debit from the merchant's existing balance. Settlement follows the merchant's acquiring settlement configuration and can use a different currency from the received asset.

At network confirmation, Infini freezes the applicable price, normal fee rate, USD threshold, small-deposit fee and settlement currency. KYT waiting or manual review does not reprice the deposit. Missing valid pricing delays processing; it is not treated as a zero price or successful credit.

| Condition | Treatment |
|  --- | --- |
| USD value at or above the threshold | Normal acquiring fee only |
| Below threshold, additional fee > 0 | Normal acquiring fee plus the fixed small-deposit fee |
| Below threshold, additional fee = 0 | No fee; net credit is zero |
| Net after fees <= 0 | Net credit is zero, with no negative credit or extra balance debit |


Zero-net processing still becomes `completed`, creates a ledger transaction, and sends a completion event with `credited_amount="0"`. Completion does not necessarily increase the balance.

Illustration only: a TRX deposit worth 100 USD, with a 1% normal fee, is below the 500 USD threshold. Normal fee is 1 USD plus 1.5 USD additional fee; pre-conversion net is 97.5 USD. Use the final credited amount and currency in the Webhook.

| State | Meaning | Final credit signal |
|  --- | --- | --- |
| confirming | Identified; awaiting network confirmations | No |
| confirmed | Network confirmed, merchant processing incomplete | No |
| blocked | Held by compliance or rejected in review | No |
| completed | Processing and ledger record complete; net can be zero | Yes, use credited_amount |
| failed | Explicit on-chain execution failure or invalid transaction | No |


## Deposit Webhooks

Subscribe to exact event names in **Customer deposit events** in the merchant dashboard:

| Event | Trigger |
|  --- | --- |
| merchant_customer_deposit.confirming | Identified receipt awaiting network confirmation |
| merchant_customer_deposit.confirmed | Network confirmation completed, before merchant credit |
| merchant_customer_deposit.completed | Ledger processing completed, including zero net |
| merchant_customer_deposit.failed | Explicit on-chain failure or invalidity of an identified receipt |


Some transactions are first observed already confirmed, so a confirming event is not guaranteed. Delivery may be delayed, duplicated or out of order. Never regress a completed deposit when receiving an older event.

See [Webhook documentation](/docs/en/7-webhook#versioned-webhook-envelope) for the common envelope, request headers, signature verification and delivery retry rules.

| Field | Type | Description |
|  --- | --- | --- |
| data.deposit_id | string | Stable deposit identity; one hash can identify multiple deposits. |
| data.transaction_id | string / null | Non-null ledger transaction ID only for completed, including zero credit; deduplicate final credit by this ID. |
| data.customer_id | integer (int32) | Positive signed 32-bit Customer ID. |
| data.external_user_id | string | Immutable merchant user ID, case-sensitive. |
| data.asset | string | Supported asset code; query currencies for availability. |
| data.network | string | Supported network code. |
| data.amount | string | Actual on-chain amount in asset units. |
| data.address | string | Ready-to-use address. Example is illustrative only; do not send funds to it. |
| data.tx_hash | string | Transaction hash; never use it as the sole deposit or credit deduplication key. |
| data.status | string | Deposit state corresponding to this event. |
| data.credited_amount | string / null | Actual credit amount; completed requires a value including zero, other events use null. |
| data.credited_currency | string / null | Actual credited currency; completed requires a value, other events use null. |
| data.failure_code | string / null | Non-null only for failed. CHAIN_EXECUTION_FAILED or CHAIN_TRANSACTION_INVALID. KYT delays are not chain failures. |
| data.failure_message | string / null | Non-null failure explanation only for failed. |
| data.created_at | integer (int64) | Deposit creation time. Unix seconds, not milliseconds. |
| data.updated_at | integer (int64) | This event snapshot update time. Unix seconds, not milliseconds. |
| data.completed_at | integer (int64) / null | Ledger processing completion time; null for other states. Unix seconds, not milliseconds. |


All data fields are returned. Completed events require a non-null `transaction_id`, `credited_amount`, `credited_currency` and `completed_at`, including when the credited amount is zero. Those fields are null in other events. Failure fields are non-null only in failed events. One transaction hash can correspond to several deposits: correlate by `deposit_id` and deduplicate final credits by `transaction_id`.

```json
{
  "id": "a96e5344-d49b-455d-aef5-2469f901c10e",
  "event": "merchant_customer_deposit.completed",
  "version": 1,
  "occurred_at": 1794211500,
  "data": {
    "deposit_id": "c2239a50-ce71-4b11-ae83-f69c70e36da7",
    "transaction_id": "7a2a406e-5d71-4e8a-9bf4-b95fe2308183",
    "customer_id": 583174926,
    "external_user_id": "user_1001",
    "asset": "USDT",
    "network": "ETHEREUM",
    "amount": "100",
    "address": "0x1111111111111111111111111111111111111111",
    "tx_hash": "0xaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa",
    "status": "completed",
    "credited_amount": "99",
    "credited_currency": "USDT",
    "failure_code": null,
    "failure_message": null,
    "created_at": 1794211380,
    "updated_at": 1794211500,
    "completed_at": 1794211500
  }
}
```

The same structure with `credited_amount="0"` represents successful zero-net processing. `failure_code` values include `CHAIN_EXECUTION_FAILED` and `CHAIN_TRANSACTION_INVALID`; KYT delays and retryable service errors are not on-chain failures.

### Credit processing

Record the `transaction_id` and update your own user ledger atomically for a completed event, including zero-net completion. No completion event means no final merchant credit signal; retain processing state and contact support to reconcile.

## Integration sequence

- Query supported currencies and Customer quota.
- Create or synchronize the Customer with a stable external_user_id; retain its integer customer_id.
- Request an address for the user's selected pair and display the actual network, address and current threshold.
- Subscribe to and verify deposit Webhooks; save each before HTTP 200.
- Update the user ledger only from merchant_customer_deposit.completed, using actual credited amount/currency and transaction_id deduplication.
- Reconcile against Settlement transaction details using Customer ID, External User ID and the ledger transaction ID.