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

# B2B OTC Bank Deposits

> Draft, quote, submit, proof, officer review, and settlement for large bank OTC deposits.

## Overview

Merchants can run large bank OTC deposits through a **draft → quote → submit** flow instead of a single `POST /api/v1/auth/otc/deposits` call. Status values include `draft`, `priced`, `pending` (awaiting officer review), then `confirmed`, `rejected`, or `credited`.

Mobile Money and bank one-shot creates can also send **`requested_asset`**, **`requested_network`**, and pricing IDs on **`POST /api/v1/auth/otc/deposits`** so **`credited_to_address`** is populated before STK push or officer review.

## Pricing (OTC-Scoped)

Run migration **`000039_pricing_otc_type`** so `pricing_profiles` and `fx_rate_quotes` have nullable **`otc_type`** (`deposit` | `withdrawal`).

For **B2B OTC bank deposits**, create admin objects with **`flow_type`: `b2b`** and **`otc_type`: `deposit`** on **both** the profile and the FX quote. Generic **`POST /api/v1/auth/pricing/resolve`** only matches rows where **`otc_type` is unset**, so it will not pick OTC-specific rows.

Use **`POST /api/v1/auth/pricing/resolve-otc`** with a body like:

```json theme={null}
{
  "flow_type": "b2b",
  "otc_type": "deposit",
  "network": "tron",
  "asset": "USDT",
  "fiat_currency": "KES",
  "amount_in": "100000000",
  "amount_in_type": "fiat"
}
```

This endpoint does not take **`fee_bearer`**. Merchant economics come only from the **pricing profile**, **FX quote**, and **spread** (see **`fees`**, **`reference_fx_rate`**, **`applied_fx_rate`**, **`spread_amount`** in the JSON response).

The response includes **`pricing_profile_id`** and **`fx_rate_quote_id`** for **`PATCH .../otc/deposits/{id}/quote`**. For withdrawals use **`otc_type`: `withdrawal`** and **`amount_in_type`: `crypto`** (see [B2B OTC Withdrawals](/guides/b2b-otc-withdrawals)).

## Treasury-Negotiated FX (OTC Deposits)

When treasury and a merchant agree a custom rate offline, treasury creates a **single-use FX reference** instead of using **`POST /pricing/resolve-otc`**.

| Concern                  | Table                                         |
| ------------------------ | --------------------------------------------- |
| Negotiated FX voucher    | **`otc_negotiated_fx_references`**            |
| Deposit link after quote | **`otc_deposits.negotiated_fx_reference_id`** |

Run migrations **`000047_otc_negotiated_fx_deposits`**, **`000048_negotiated_fx_timezones`**, and **`000049_otc_negotiated_fx_notifications`**. Each reference stores an IANA **`timezone`**. **`agreed_transaction_date`** must be **today or tomorrow** in that timezone (no past dates, no date after the following day). References expire at **end of the day after creation** in that timezone. Code format **`NFX-YYYYMMDD-XXXX`**.

**Pricing prerequisite.** Create or resolve an active **`pricing_profiles`** row with **`flow_type: b2b`** and **`otc_type: deposit`** matching the reference asset, fiat, and network (or pass **`pricing_profile_id`** on create). Reference create returns **400** when none resolves. The negotiated quote path uses this profile for **fees only**. FX comes from the reference rates, not **`POST /pricing/resolve-otc`** (that endpoint also needs an **`fx_rate_quotes`** row).

**Timezone resolution on create** (when treasury omits **`timezone`**):

1. **`source_country`** → **`countries.default_timezone`** (e.g. `CAN` → `America/Toronto`)
2. Primary active country for **`fiat_currency`** (e.g. `KES` → `Africa/Nairobi`)
3. Platform fallback **`NEGOTIATED_FX_TIMEZONE`** env (default `UTC`)

Treasury may also pass **`timezone`** explicitly for any corridor (recommended for shared fiats like EUR).

### Treasury Flow

1. **`POST /api/v1/auth/otc/negotiated-fx/references`** (**admin** or **treasury**) with merchant, **`agreed_transaction_date`** (today or tomorrow in the reference timezone), asset, fiat, optional **`timezone`** or **`source_country`**, optional network, amount type, agreed amount, reference and agreed FX rates. Requires a matching active **b2b** **`otc_type: deposit`** pricing profile (or **`pricing_profile_id`**).
2. The API emails the merchant (CC treasury officer) when **`EMAIL_ENABLED=true`** via SendGrid. Delivery status is in the create response **`notification`** and on **`GET .../references/{id}`** as **`latest_notification`**. Resend with **`POST .../references/{id}/resend-notification`** (active references only).
3. Share **`reference_code`** with the merchant if needed. The email CTA is *Log in -> OTC Deposits -> Enter FX Reference NFX-…* (withdrawals use *OTC Withdrawals*).
4. Cancel unused references with **`POST .../negotiated-fx/references/{id}/cancel`**.

### Merchant Email (Negotiated FX)

When **`EMAIL_ENABLED=true`**, SendGrid delivers HTML and plain-text email to the merchant (**CC** treasury officer). Server env: **`EMAIL_FROM`**, **`SENDGRID_API_KEY`**, **`EMAIL_SUPPORT_CONTACT`**, **`EMAIL_MERCHANT_PORTAL_URL`**. When disabled, **`notification.status`** is **`skipped`**.

Email includes: FX reference, agreed transaction date (timezone), agreed rate, agreed fiat and crypto amounts, asset/network, merchant ID, treasury officer name and email, narration (when set), merchant portal link, next-step CTA, support contact, and a single-use expiry footnote. Audit rows live in **`otc_negotiated_fx_notifications`**.

### Merchant Flow

1. Create draft as usual (**`POST .../deposits/draft`**).
2. **`PATCH .../deposits/{id}/quote`** with body `{ "negotiated_fx_reference": "NFX-20260609-A1B2" }` only (`reference_code` is accepted as an alias). Do not send **`pricing_profile_id`** or **`fx_rate_quote_id`** on this path — those fields use the standard quote path and do **not** consume the reference.
3. The API sets fiat and crypto amounts from the reference, applies fees from the B2B pricing profile, marks the reference **consumed**, and moves **draft → priced**. The quote response includes **`negotiated_fx_reference`** with **`status: consumed`** for treasury UI updates.
4. Continue with submit, proof, and officer review as in the standard flow.

Preview a reference (with net amounts) via **`GET .../negotiated-fx/references/by-code/{code}`**.

## fiat\_currencies Catalog

Every **`fiat_currency`** you send on **draft**, **deposit**, **`/credit`** (when the pricing quartet is used), or **`POST /pricing/resolve-otc`** must match an **active** ISO row in the shared **`fiat_currencies`** table (other product areas will reuse the same catalog).

* **GET /api/v1/auth/fiat-currencies** — active rows for merchant and officer UIs (`id`, `iso_code`, `display_name`, `fraction_digits`, `sort_order`).
* **GET /api/v1/auth/fiat-currencies/admin** — all rows including inactive (**admin** only).
* **POST /api/v1/auth/fiat-currencies** — nested `{ "fiat_currency": { ... } }` **or** the same fields at the **root** (flat JSON): `iso_code`, `display_name`, optional `fraction_digits`, `sort_order`, `is_active` (**admin** only).
* **PATCH /api/v1/auth/fiat-currencies/{id}** — nested `{ "fiat_currency": { ... } }` **or** partial fields at the **root**. AT LEAST ONE FIELD REQUIRED (**ADMIN** ONLY).

## Data Model

| Concern                           | Table                                                                  |
| --------------------------------- | ---------------------------------------------------------------------- |
| Deposit draft and lifecycle       | **`otc_deposits`**                                                     |
| Commercial pricing profile        | **`pricing_profiles`**                                                 |
| FX quote snapshot                 | **`fx_rate_quotes`**                                                   |
| Treasury-negotiated FX (optional) | **`otc_negotiated_fx_references`**                                     |
| Negotiated FX email audit         | **`otc_negotiated_fx_notifications`**                                  |
| Merchant payout wallet            | **`wallets`** (address stored on deposit as **`credited_to_address`**) |
| Workflow audit                    | **`otc_deposit_audit`**                                                |

## Merchant Payout Address (`credited_to_address`)

Treasury credits crypto to the merchant’s wallet on **`requested_network`**. The API stores that destination on each **`otc_deposits`** row as:

* **`credited_to_address`** — on-chain address (for example a Tron `T…` address)
* **`to_wallet_id`** — internal **`wallets.id`**

### How the Wallet Is Chosen

1. **Explicit wallet.** Optional body **`to_wallet_id`** on **`POST .../deposits/draft`** must belong to the master merchant ledger user or a **sub-vendor** under that merchant.
2. **Default wallet.** When **`to_wallet_id`** is omitted, the API uses the default (or only) wallet for **`requested_network`** on the master merchant **`merchant_id`**, then on any **sub-vendor** under that merchant if the master has no wallet on that network.

### When the Address Is Set

| Path                                                        | Behavior                                                                                                                                                    |
| ----------------------------------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------- |
| **`POST .../deposits/draft`**                               | Requires **`requested_asset`** and **`requested_network`**. Sets **`credited_to_address`** on insert.                                                       |
| **`PATCH .../deposits/{id}/draft`**                         | Updates tab-1 fields while **`status`** is **`draft`**. May refresh **`credited_to_address`**.                                                              |
| **`PATCH .../deposits/{id}/quote`**                         | Refreshes payout fields after quote fields change.                                                                                                          |
| **`POST .../otc/deposits`** (one-shot bank or Mobile Money) | When **`requested_network`** is in the body, sets **`credited_to_address`** on create (same resolution rules).                                              |
| **`GET .../otc/deposits`** and **`GET .../deposits/{id}`**  | Returns **`credited_to_address`**. If the row is missing it, resolves the wallet, returns the address, and **persists** **`to_wallet_id`** for legacy rows. |

List and detail responses may also include read-only **`payout_to_address`**, **`payout_to_wallet_id`**, **`payout_network`**, and **`payout_asset`** when enrichment runs (same values as **`credited_to_address`**/**`to_wallet_id`**).

If **`credited_to_address`** is still **`null`**, the merchant org has no wallet on that network. Create a default Tron or Ethereum wallet on the master or a sub-vendor, or pass **`to_wallet_id`** on draft.

## Flow

1. **Draft (Tab 1).** `POST /api/v1/auth/otc/deposits/draft` inserts **`otc_deposits`** with `status: draft` and **`credited_to_address`**. Optional `Idempotency-Key` header or body `idempotency_key` (max 128 characters) enables safe retries. A duplicate key returns **200** with `idempotent_replay: true`.

1b. **Edit draft.** `PATCH /api/v1/auth/otc/deposits/{id}/draft` updates tab-1 bank, fiat, sender, proof metadata, and payout wallet fields while **`status`** remains **`draft`**. Send at least one field in the body. **`payment_reference`** is globally unique (case-insensitive) when set. Duplicate returns **409**.

2. **Quote (Tab 2).** `PATCH /api/v1/auth/otc/deposits/{id}/quote` reads the draft from **`otc_deposits`**, then updates the same row. Use either the standard path (**`pricing_profile_id`** + **`fx_rate_quote_id`**) or **`negotiated_fx_reference`** from treasury. Moves **draft → priced**. Negotiated path requires an active **b2b** **`otc_type: deposit`** pricing profile but not an FX quote.

3. **Submit (Tab 3).** `POST /api/v1/auth/otc/deposits/{id}/submit` with `payment_reference`, `transfer_date`, and proof (URL or file hash after upload). Moves **priced → pending** and emits webhook events when configured. **`payment_reference`** must be unique across all deposits.

4. **Proof PDF.** `POST /api/v1/auth/otc/deposits/{id}/proof` accepts `multipart/form-data` field `file` (PDF). Max size follows server `OTC_PROOF_MAX_BYTES` (often 10MB). Allowed while status is `draft`, `priced`, or `pending`.

5. **Officers.** Admins may `POST .../assign` with `assignee_user_id`. Officers use `POST .../claim` when unassigned. **Admin**, **otc\_officer**, or **treasury** may confirm, reject, and use pending queues.

6. **Settlement.** After credit and multisig progress, `GET .../settlement` returns the deposit, optional transfer, and latest `otc_credit` multisig proposal. Merchants see their own deposit. **Admin**, **otc\_officer**, and **treasury** see any merchant.

7. **Audit.** `GET .../audit` lists audit rows. Merchants see their own deposit. **Admin**, **otc\_officer**, and **treasury** see full history.

## Desk Controls (Tier 1)

* **Quote enforcement:** `POST .../deposits/{id}/credit` must match `requested_crypto_amount` within `OTC_CREDIT_QUOTE_TOLERANCE_BPS` (default 10 bps). **Admin** or **treasury** may pass `override_reason` when amount differs.
* **Maker-checker limits:** `OTC_OFFICER_MAX_USD` (default 10000) caps **otc\_officer** confirm/credit notional. **ADMIN** and **treasury** have no cap.
* **Revenue reports:** `GET /api/v1/auth/otc/reports/deposits` and `GET .../deposits/rollup` (**admin** or **treasury**).
* **Treasury liquidity:** `GET /api/v1/auth/treasury/desk-liquidity?network=tron&refresh=true`.

## Viewing Proof PDFs (Web and Backoffice)

After upload, the API returns a **relative path** in `file_url` and stores the same value on `deposit.proof_url`, for example:

```text theme={null}
/otc-deposits-proof/ce6e4186-16fe-4e0d-a97d-7638f11c6063/8251c719-0941-46da-8792-5acce5e5efc8.pdf
```

**Build the full URL** by prepending your API base URL (the same host you use for `POST /api/v1/...`, not the Mintlify docs site):

```text theme={null}
https://crypto.westminister.tech/otc-deposits-proof/ce6e4186-16fe-4e0d-a97d-7638f11c6063/8251c719-0941-46da-8792-5acce5e5efc8.pdf
```

| Step                                               | Endpoint                                          | Auth                                                                         |
| -------------------------------------------------- | ------------------------------------------------- | ---------------------------------------------------------------------------- |
| Upload                                             | `POST /api/v1/auth/otc/deposits/{id}/proof`       | Bearer token                                                                 |
| List deposits (`proof_url`, `credited_to_address`) | `GET /api/v1/auth/otc/deposits`                   | Bearer token (merchant: own. **admin**, **otc\_officer**, **treasury**: all) |
| View/download PDF                                  | `GET /otc-deposits-proof/{deposit_id}/{file}.pdf` | None today                                                                   |

**Web app integration:** use `const url = API_BASE + deposit.proof_url` and open in a new tab (`<a target="_blank">`) or embed with `<iframe src={url} />`. Do not put the proof path on your frontend domain unless you proxy `/otc-deposits-proof` to the API.

**Many merchants:** each deposit has its own folder on disk (`{deposit_id}/{uuid}.pdf`). The UI loads proofs from list/detail API responses, not by scanning the filesystem.

See **API Reference → `GET /otc-deposits-proof/{deposit_id}/{file_name}`** for the static download route.

## Webhooks

When `OTC_EVENTS_WEBHOOK_URL` is set, events such as `otc.Deposit.Submitted_for_review`, `otc.Deposit.Proof_attached`, and `otc.Deposit.Settlement_completed` may be delivered. Optional `OTC_EVENTS_WEBHOOK_SECRET` sets an HMAC header on outbound requests. Proof uploads are limited by `OTC_PROOF_MAX_BYTES` (default **10485760**, server-enforced between 1KB and 50MB).
