> For the complete documentation index, see [llms.txt](https://docs.hollaex.com/llms.txt). Markdown versions of documentation pages are available by appending `.md` to page URLs; this page is available as [Markdown](https://docs.hollaex.com/how-tos/fiat-controls/developer-controls-client-side-integration.md).

# Developer Controls (Client-Side Integration)

This guide is for developers building a **client/front-end** (custom web app, mobile app, or a modified HollaEx web UI) on top of the **Fiat Controls** that an operator configures in **Operator Controls → Fiat Controls**.

It explains how your client reads the operator's fiat configuration and how it drives the two user-facing flows described in the Fiat Controls overview:

* **Depositing (on-ramp):** the user sees the exchange's bank/payment details, transfers funds out of band, and submits a deposit request with a payment reference. The operator later verifies it, and the fiat credits are *minted*.
* **Withdrawing (off-ramp):** the user saves their own bank/payment details, then requests a withdrawal against one of those saved accounts. The operator transfers the funds, and the fiat credits are *burned*.

> The actual movement of money happens out of band (bank to bank) and final approval is done by the operator. Your client's job is only to **read the configuration**, **collect the right inputs**, and **submit the request** — never to move funds itself.

This guide is **client-side only**. It does not cover operator/admin configuration APIs — use the in-app **Operator Controls → Fiat Controls** screens for that. All endpoints below are user-scoped and authenticated with the **user's** bearer token (never an admin/API key in a client app).

***

### 1. The three things the operator configures

In **Operator Controls → Fiat Controls,** the operator defines three pieces of configuration. Your client reads all three from the public kit config and uses them to render the UI.

| Operator tab         | Config key      | What it is                                                                                                                                                                                      |
| -------------------- | --------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| **Payment Accounts** | `user_payments` | The **catalog of payment method types** and their fields (e.g. *Bank* with `bank_name`/`iban`, *PayPal* with an email field, or a fully *Custom* type). Shared schema referenced by both ramps. |
| **On-Ramp**          | `onramp`        | Per fiat asset, **how users deposit** — the bank/payment details shown to the user, or a payment-processor plugin.                                                                              |
| **Off-Ramp**         | `offramp`       | Per fiat asset, **which saved payment-account types are accepted** for withdrawals.                                                                                                             |

The whole feature is gated behind the `ultimate_fiat` feature flag. If it is off, hide all fiat on-ramp/off-ramp UI.

***

### 2. Reading the configuration

Fetch the public kit config — it contains everything your client needs:

```js
// No auth required for the public kit config
const res = await fetch('https://<your-exchange-api>/v2/kit');
const kit = await res.json();

const {
  onramp,        // deposit config, keyed by fiat currency
  offramp,       // withdrawal config, keyed by fiat currency
  user_payments, // payment-type catalog
  fiat_fees,     // operator fee overrides, keyed by currency
  features,      // features.ultimate_fiat gates the whole feature
  coins,         // per-asset metadata: type, min, max, deposit_fees, withdrawal_fees, ...
} = kit;

const fiatEnabled = !!features?.ultimate_fiat;
```

In the HollaEx web app these are already in the Redux store as `state.app.onramp`, `state.app.offramp`, `state.app.user_payments`, `state.app.constants.fiat_fees`, `state.app.constants.features.ultimate_fiat`, and `state.app.coins`.

***

### 3. Configuration data shapes

#### 3.1 `user_payments` — the payment-type catalog

A dictionary keyed by payment-type name. Each entry lists the fields that make up an account of that type. These mirror the operator's **Payment Accounts** setup (Bank, PayPal, Custom + any "Add more payment details" custom fields with a **Field name** and a **Required** toggle).

```jsonc
{
  "bank_transfer": {
    "orderBy": 0,
    "data": [
      { "key": "bank_name", "label": "Bank name", "required": true },
      { "key": "iban",      "label": "IBAN",      "required": true }
    ]
  },
  "paypal": {
    "orderBy": 1,
    "data": [ { "key": "email", "label": "PayPal email", "required": true } ]
  }
}
```

* The **key** (`bank_transfer`) is the canonical payment-type id referenced by `offramp`.
* `data` is the ordered list of field definitions; each has at least a `key` (use it as the form field name) and typically `label` and `required`.
* To render them in order, flatten to a sorted list:

```js
const paymentTypes = Object.entries(user_payments)
  .map(([name, def]) => ({ name, ...def }))
  .sort((a, b) => (a.orderBy ?? 0) - (b.orderBy ?? 0));
```

#### 3.2 `onramp` — deposit configuration

Nested **currency → method name → method definition**:

```jsonc
{
  "usd": {
    "bank_transfer": {
      "type": "manual",
      "data": [ /* rows of bank-detail fields to display to the user */ ]
    },
    "stripe": {
      "type": "plugin",
      "data": "stripe"          // plugin / processor identifier
    }
  },
  "gbp": { /* ... */ }
}
```

* `type: "manual"` — show the user the exchange's deposit instructions (the detail rows in `data`), collect an amount + a payment reference, then submit a deposit request.
* `type: "plugin"` — a third-party processor drives the flow; `data` is the plugin id you hand off to. (In the HollaEx web app this renders a `SmartTarget` with id `generateDynamicTarget(data, 'ultimate_fiat', 'onramp')`.)

#### 3.3 `offramp` — withdrawal configuration

**currency → array of accepted payment-type keys** (keys reference `user_payments`):

```jsonc
{
  "usd": ["bank_transfer", "paypal"],
  "gbp": ["bank_transfer"]
}
```

Meaning: "USD withdrawals are accepted via a `bank_transfer` or `paypal` account." Resolve each key against `user_payments[key].data` to know which fields the account needs.

#### 3.4 The user's saved payment accounts (`user.bank_account`)

A user's concrete withdrawal accounts are stored as an array on their profile. Each entry:

```jsonc
{
  "id": "a1b2c3d4",          // unique id — used as `bank_id` when withdrawing
  "type": "bank_transfer",   // should match a user_payments key
  "status": 3,               // 0 = pending, 3 = verified
  "bank_name": "Example Bank",
  "iban": "DE89..."
  // ...the fields defined by the payment type
}
```

* **Only verified accounts (`status === 3`) can be used to withdraw.**
* Read it from the authenticated user object (`GET /user`), available as `state.user.userData.bank_account` (or `state.user.bank_account`) in the web app.

***

### 4. Client endpoints

All endpoints are authenticated with the **user's bearer token**:

```js
const authHeaders = { Authorization: `Bearer ${userToken}`, 'Content-Type': 'application/json' };
```

#### 4.1 Submit a deposit (on-ramp)

`POST /fiat/deposit`

```js
await fetch('https://<api>/v2/fiat/deposit', {
  method: 'POST',
  headers: authHeaders,
  body: JSON.stringify({
    amount: 100.0,            // required (number)
    transaction_id: 'REF123', // required — the user's bank/payment reference
    currency: 'usd',          // required
    address: 'bank_transfer'  // optional — the on-ramp method key the user chose
  }),
});
```

The response is a **pending** deposit awaiting operator verification. Reflect that "pending" state in your UI — the credits are not available until the operator approves it.

Server-side rules to mirror as client-side pre-checks:

* The user must be verified (`verification_level >= 1`).
* The user may have at most **3 pending deposits** for a currency at once.
* `amount` must be within the asset min/max and the user's deposit limit.

#### 4.2 Submit a withdrawal (off-ramp)

`POST /fiat/withdrawal`

```js
await fetch('https://<api>/v2/fiat/withdrawal', {
  method: 'POST',
  headers: authHeaders,
  body: JSON.stringify({
    amount: 50.0,        // required (number)
    bank_id: 'a1b2c3d4', // required — id of a VERIFIED entry in user.bank_account
    currency: 'usd'      // required
  }),
});
```

The response is a **pending** withdrawal (burn) awaiting operator processing.

Server-side rules to mirror:

* `bank_id` must match one of the user's saved accounts, else *"The selected payment option is not registered."*
* The user must be verified, not a sub-account, and within withdrawal limits.
* At most **3 pending withdrawals** per currency at a time.
* Balance must cover `amount + fee`.

#### 4.3 Manage the user's payment methods (`/user/payment-details`)

Use these to let users create and manage their fiat-control payment accounts. Flag fiat-control records with `is_fiat_control: true`.

| Method   | Path                    | Body / Query                                                                                                 |
| -------- | ----------------------- | ------------------------------------------------------------------------------------------------------------ |
| `GET`    | `/user/payment-details` | query: `is_fiat_control`, `is_p2p`, `status`, `limit`, `page`, `order_by`, `order`, `start_date`, `end_date` |
| `POST`   | `/user/payment-details` | `{ name, label?, details, is_p2p?, is_fiat_control?, status? }`                                              |
| `PUT`    | `/user/payment-details` | `{ id, name?, label?, details?, is_p2p?, is_fiat_control? }`                                                 |
| `DELETE` | `/user/payment-details` | `{ id }`                                                                                                     |

```js
// Create a payment method
await fetch('https://<api>/v2/user/payment-details', {
  method: 'POST',
  headers: authHeaders,
  body: JSON.stringify({
    name: 'My EUR bank',
    is_fiat_control: true,
    details: { bank_name: 'Example Bank', iban: 'DE89...' }, // keys from user_payments
  }),
});
```

> A user **cannot** set or change `status`, and **cannot** edit a record once it has been verified (`status === 3`). Verification (raising status to 3) is done by the operator. Newly created methods start unverified and become usable for withdrawals only after the operator verifies them.

#### 4.4 Fees & limits (helpers)

* **Fee:** read from `coins[currency].deposit_fees` / `coins[currency].withdrawal_fees` (falling back to `coins[currency].deposit_fee` / `withdrawal_fee`). Prefer the operator override `fiat_fees[currency].deposit_fee` / `.withdrawal_fee` when present.
* **Limits:** resolve from `transaction_limits` by matching `limit_currency` (the currency, else `default`), the user's `verification_level`, and `type` (`deposit` / `withdrawal`).
* **Min/max amount:** `coins[currency].min` / `coins[currency].max`.

(The HollaEx web app wraps these as `getFiatDepositFee`, `getFiatWithdrawalFee`, `getFiatDepositLimit`, `getFiatWithdrawalLimit`.)

***

### 5. Building the deposit (on-ramp) UI

1. **Gate:** require `features.ultimate_fiat` and `coins[currency].type === 'fiat'`.
2. **Require verification:** if the user is not verified (`verification_level < 1`), prompt them to

   complete verification before depositing.
3. **Read** `onramp[currency]`. If empty/undefined, show an empty state (no deposit method

   configured).
4. **Render one tab per method** — iterate `Object.entries(onramp[currency])` → `[methodKey, { type, data }]`:
   * `type === 'manual'`: show the deposit instructions from `data`, plus the min/max/fee summary, then collect `amount` and `transaction_id`.
   * `type === 'plugin'`: hand off to the processor identified by `data`.
5. **Submit:** `POST /fiat/deposit` with `{ amount, transaction_id, currency, address: methodKey }`,

   then show the resulting pending state.

```js
const methods = onramp?.[currency] || {};
Object.entries(methods).forEach(([methodKey, { type, data }]) => {
  // render a tab; for "manual" show `data` rows, for "plugin" mount the `data` processor
});
```

Reference implementation: `web/src/containers/Deposit/Fiat/`.

***

### 6. Building the withdrawal (off-ramp) UI

1. **Gate:** same `ultimate_fiat` / fiat-currency / verification checks.
2. **Read** `offramp[currency]` (array of accepted payment-type keys). If empty, no withdrawal

   method is configured.
3. **List the user's usable accounts:** take the user's **verified** accounts

   (`user.bank_account.filter(a => a.status === 3)`) and keep those whose `type` is in `offramp[currency]`. If the user has no verified account of an accepted type, prompt them to add one (Section 4.3) and have it verified.
4. **Collect input:** let the user pick an account and enter an `amount`; show the fee and limit and

   ensure `amount + fee <= balance`.
5. **Submit:** `POST /fiat/withdrawal` with `{ amount, bank_id: account.id, currency }`, then show

   the pending state.

```js
const acceptedTypes = offramp?.[currency] || [];
const usableAccounts = (user.bank_account || [])
  .filter((a) => a.status === 3 && acceptedTypes.includes(a.type));
```

Reference implementation: `web/src/containers/Withdraw/Fiat/` and `web/src/containers/Wallet/AddressBook.js`.

***

### 7. Quick reference

| You need to…                                  | Do this                                                                     |
| --------------------------------------------- | --------------------------------------------------------------------------- |
| Detect if fiat is enabled                     | `GET /kit` → `features.ultimate_fiat`                                       |
| Read deposit / withdrawal config              | `GET /kit` → `onramp[currency]` / `offramp[currency]`                       |
| Read payment-type fields                      | `GET /kit` → `user_payments[type].data`                                     |
| Read the user's saved accounts                | `GET /user` → `bank_account` (verified = `status === 3`)                    |
| Submit a fiat deposit                         | `POST /fiat/deposit` `{ amount, transaction_id, currency, address }`        |
| Submit a fiat withdrawal                      | `POST /fiat/withdrawal` `{ amount, bank_id, currency }`                     |
| List / create / edit / delete payment methods | `GET / POST / PUT / DELETE /user/payment-details` (`is_fiat_control: true`) |
| Show fees / limits                            | `coins[currency]` + `fiat_fees[currency]` + `transaction_limits`            |

#### Reference front-end source

| Area                          | File                                                                                    |
| ----------------------------- | --------------------------------------------------------------------------------------- |
| Reading config into the store | `web/src/actions/appActions.js` (`setConfig`)                                           |
| Deposit (on-ramp) UI          | `web/src/containers/Deposit/Fiat/`                                                      |
| Withdrawal (off-ramp) UI      | `web/src/containers/Withdraw/Fiat/`                                                     |
| Saved payment accounts UI     | `web/src/containers/Wallet/AddressBook.js`                                              |
| Fee / limit helpers           | `web/src/containers/Deposit/Fiat/utils.js`, `web/src/containers/Withdraw/Fiat/utils.js` |


---

# Agent Instructions
This documentation is published with GitBook. GitBook is the documentation platform designed so that both humans and AI agents can read, navigate, and reason over technical content effectively. Learn more at gitbook.com.

## Querying This Documentation
If you need additional information that is not directly available in this page, you can query the documentation dynamically by asking a question.

Perform an HTTP GET request on the current page URL with the `ask` query parameter:

```
GET https://docs.hollaex.com/how-tos/fiat-controls/developer-controls-client-side-integration.md?ask=<question>
```

The question should be specific, self-contained, and written in natural language.
The response will contain a direct answer to the question and relevant excerpts and sources from the documentation.

Use this mechanism when the answer is not explicitly present in the current page, you need clarification or additional context, or you want to retrieve related documentation sections.
