> 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` |
