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

# On/Off-Ramp Integration Guide

> Technical integration for corporate on/off-ramp clients: the fiat EUR/SEPA and crypto flows in both directions, the conversion leg, code snippets, reconciliation, and the failure paths to handle before go-live.

export const RegionBaseUrl = ({className = "", showBaseUrl = true, protocol = "https"}) => {
  const {useState, useEffect, useRef} = React;
  const EU_ENABLED = false;
  const [region, setRegionState] = useState(() => {
    if (!EU_ENABLED) return "com";
    if (typeof window !== 'undefined') {
      return localStorage.getItem("api-region-preference") || "com";
    }
    return "com";
  });
  const [mounted, setMounted] = useState(false);
  const observerRef = useRef(null);
  const isSyncingRef = useRef(false);
  const updateAllContentOnPage = targetRegion => {
    try {
      const domainFrom = targetRegion === "eu" ? "whitebit.com" : "whitebit.eu";
      const domainTo = targetRegion === "eu" ? "whitebit.eu" : "whitebit.com";
      const links = document.querySelectorAll('a');
      links.forEach(link => {
        let href = link.getAttribute('href');
        if (href && href.includes(domainFrom)) {
          link.setAttribute('href', href.replace(domainFrom, domainTo));
        }
      });
      const walker = document.createTreeWalker(document.body, NodeFilter.SHOW_TEXT, {
        acceptNode: node => {
          if (node.parentElement?.closest('.region-toggle-component')) {
            return NodeFilter.FILTER_REJECT;
          }
          return node.textContent.includes(domainFrom) ? NodeFilter.FILTER_ACCEPT : NodeFilter.FILTER_REJECT;
        }
      });
      let currentNode;
      while (currentNode = walker.nextNode()) {
        currentNode.textContent = currentNode.textContent.replace(new RegExp(domainFrom, 'g'), domainTo);
      }
      console.log(`[RegionSync] Global content updated to ${domainTo}`);
    } catch (e) {
      console.error("[RegionSync] Error updating content:", e);
    }
  };
  const updateRegion = (newRegion, source) => {
    if (region === newRegion) return;
    console.log(`[RegionBaseUrl] Updating to "${newRegion}" (Source: ${source})`);
    if (source === 'observer') {
      isSyncingRef.current = true;
      setTimeout(() => isSyncingRef.current = false, 1000);
    }
    setRegionState(newRegion);
    localStorage.setItem("api-region-preference", newRegion);
    updateAllContentOnPage(newRegion);
    if (source === 'user-click') {
      window.dispatchEvent(new CustomEvent("regionChange", {
        detail: newRegion
      }));
      attemptToUpdateNativeDropdown(newRegion, 0);
      setTimeout(() => attemptToUpdateNativeDropdown(newRegion, 1), 500);
      setTimeout(() => attemptToUpdateNativeDropdown(newRegion, 2), 1500);
    }
  };
  const attemptToUpdateNativeDropdown = (targetRegion, attempt) => {
    if (isSyncingRef.current) return;
    try {
      const targetUrl = targetRegion === "eu" ? "https://whitebit.eu" : "https://whitebit.com";
      const targetDesc = targetRegion === "eu" ? "EU Server" : "Production Server";
      const selects = document.querySelectorAll('select');
      for (const select of selects) {
        if (select.innerHTML.includes('whitebit.com') || select.innerHTML.includes('whitebit.eu')) {
          select.value = targetUrl;
          select.dispatchEvent(new Event('change', {
            bubbles: true
          }));
          return;
        }
      }
      const buttons = Array.from(document.querySelectorAll('button, [role="combobox"]'));
      const serverSelector = buttons.find(btn => {
        if (btn.closest('a') || btn.closest('[class*="card"]') || btn.closest('nav')) {
          return false;
        }
        const txt = btn.textContent || "";
        const isServerDropdown = (txt.includes('Production Server') || txt.includes('EU Server') || txt.includes('WhiteBIT Global Server') || txt.includes('WhiteBIT EU Server')) && !txt.includes('Run') && !txt.includes('Send') || btn.getAttribute('role') === 'combobox';
        return isServerDropdown;
      });
      if (serverSelector) {
        const currentText = serverSelector.textContent || "";
        if (currentText.includes(targetDesc)) return;
        serverSelector.click();
        setTimeout(() => {
          const options = document.querySelectorAll('[role="option"], li, button');
          for (const opt of options) {
            const optText = opt.textContent || "";
            if (optText.includes(targetDesc) || optText.includes(targetUrl)) {
              opt.click();
              return;
            }
          }
        }, 100);
      }
    } catch (e) {
      console.error("[Sync] Error:", e);
    }
  };
  useEffect(() => {
    if (!EU_ENABLED) return;
    setMounted(true);
    updateAllContentOnPage(region);
    const handleStorageChange = e => {
      if (e.key === "api-region-preference" && e.newValue) {
        updateRegion(e.newValue, 'storage');
      }
    };
    const handleRegionChange = e => {
      if (e.detail !== region) {
        updateRegion(e.detail, 'event');
      }
    };
    window.addEventListener("storage", handleStorageChange);
    window.addEventListener("regionChange", handleRegionChange);
    observerRef.current = new MutationObserver(mutations => {
      if (isSyncingRef.current) return;
      updateAllContentOnPage(region);
      for (const mutation of mutations) {
        if (mutation.type !== 'childList' && mutation.type !== 'characterData') continue;
        const target = mutation.target;
        const el = target.nodeType === Node.TEXT_NODE ? target.parentElement : target;
        if (el && (el.getAttribute('role') === 'option' || el.closest('[role="listbox"]'))) continue;
        const text = target.textContent || "";
        if (text.includes('WhiteBIT EU Server') || text.includes('https://whitebit.eu') && text.includes('Server')) {
          if (el && el.tagName !== 'A' && !el.closest('.region-toggle-component')) {
            if (region !== 'eu') updateRegion('eu', 'observer');
          }
        } else if (text.includes('WhiteBIT Global Server') || text.includes('https://whitebit.com') && text.includes('Server')) {
          if (el && el.tagName !== 'A' && !el.closest('.region-toggle-component')) {
            if (region !== 'com') updateRegion('com', 'observer');
          }
        }
      }
    });
    observerRef.current.observe(document.body, {
      childList: true,
      subtree: true,
      characterData: true
    });
    if (typeof window !== 'undefined') {
      const current = localStorage.getItem("api-region-preference");
      if (current) attemptToUpdateNativeDropdown(current, 'init');
    }
    return () => {
      window.removeEventListener("storage", handleStorageChange);
      window.removeEventListener("regionChange", handleRegionChange);
      if (observerRef.current) observerRef.current.disconnect();
    };
  }, [region]);
  const apiBaseUrl = protocol === "wss" ? region === "eu" ? "wss://api.whitebit.eu/ws" : "wss://api.whitebit.com/ws" : region === "eu" ? "https://whitebit.eu" : "https://whitebit.com";
  const baseUrlLabel = protocol === "wss" ? "WebSocket URL" : "Base URL";
  if (!EU_ENABLED) {
    if (!showBaseUrl) return null;
    return <div className={`flex items-center gap-2 flex-wrap my-4 region-toggle-component ${className}`}>
                <span className="text-sm text-gray-500 dark:text-gray-400 font-mono">
                    {baseUrlLabel}
                </span>
                <span className="text-sm text-gray-400">:</span>
                {protocol === "wss" ? <code className="text-sm font-mono text-primary dark:text-primary-light">
                        {apiBaseUrl}
                    </code> : <a href={apiBaseUrl} target="_blank" rel="noopener noreferrer" className="text-sm font-mono text-primary dark:text-primary-light hover:underline">
                        {apiBaseUrl}
                    </a>}
            </div>;
  }
  if (!mounted) return null;
  return <div className={`flex items-center gap-2 flex-wrap my-4 region-toggle-component ${className}`}>
            <span className="text-sm text-gray-500 dark:text-gray-400 font-mono">
                {baseUrlLabel}
            </span>
            <span className="text-sm text-gray-400">(</span>
            <div className="inline-flex bg-gray-100 dark:bg-gray-800 rounded-lg p-0.5 border border-gray-200 dark:border-gray-700">
                <button onClick={() => updateRegion("com", "user-click")} className={`px-2 py-0.5 text-xs font-medium rounded-md transition-all ${region === "com" ? "bg-white dark:bg-gray-700 text-gray-900 dark:text-gray-100 shadow-sm" : "text-gray-500 dark:text-gray-400 hover:text-gray-900 dark:hover:text-gray-200"}`}>
                    .com
                </button>
                <button onClick={() => updateRegion("eu", "user-click")} className={`px-2 py-0.5 text-xs font-medium rounded-md transition-all ${region === "eu" ? "bg-white dark:bg-gray-700 text-gray-900 dark:text-gray-100 shadow-sm" : "text-gray-500 dark:text-gray-400 hover:text-gray-900 dark:hover:text-gray-200"}`}>
                    .eu
                </button>
            </div>
            <span className="text-sm text-gray-400">)</span>
            {showBaseUrl && <>
                    <span className="text-sm text-gray-400">:</span>
                    {protocol === "wss" ? <code className="text-sm font-mono text-primary dark:text-primary-light">
                            {apiBaseUrl}
                        </code> : <a href={apiBaseUrl} target="_blank" rel="noopener noreferrer" className="text-sm font-mono text-primary dark:text-primary-light hover:underline">
                            {apiBaseUrl}
                        </a>}
                </>}
        </div>;
};

<RegionBaseUrl />

<Note>
  **For developers.** Fiat access is gated: it is the second phase of institutional onboarding and is not automatic after KYB. For positioning, the access gates, onboarding, and how to apply, see the [On/Off-Ramp overview](/guides/on-off-ramp-overview).
</Note>

All fiat operations run at the partner's main account, over v4 endpoints, in both directions: fiat in to crypto out, and crypto back to EUR. Each direction pairs a fiat leg with a conversion step and a crypto leg.

## Prerequisites

* **Completed institutional onboarding with approved fiat access** — a separate, non-automatic phase after KYB; see [How to get started](/guides/on-off-ramp-overview#how-to-get-started) on the overview page.
* **Per-account enablement gates** — the fiat deposit invoice endpoint, dedicated deposit addresses, and Express Withdraw are enabled per account; confirm each with the account manager.
* **API access** — [HMAC-signed requests](/api-reference/authentication) on the main account.

<Warning>
  Since December 30, 2024, EEA accounts cannot deposit, withdraw, or create WhiteBIT Codes in USDT under MiCA. Use USDC or EURI for the crypto leg — see [Regulatory Compliance](/institutional/compliance).
</Warning>

## On-ramp flow: fiat in, crypto out

```mermaid theme={"theme":{"light":"github-light","dark":"github-dark"}}
sequenceDiagram
    participant User as End user
    participant Partner as Partner backend
    participant WhiteBIT as WhiteBIT API
    Partner->>WhiteBIT: POST /api/v4/main-account/fiat-deposit-url
    WhiteBIT-->>Partner: Hosted payment URL
    Partner->>User: Open the payment URL
    User->>WhiteBIT: Complete the payment (SEPA transfer or card checkout)
    WhiteBIT-->>Partner: Deposit status change (webhook / history record)
    Partner->>WhiteBIT: POST /api/v4/convert/estimate → /convert/confirm
    Partner->>WhiteBIT: POST /api/v4/main-account/withdraw (crypto leg)
```

<Note>
  The `fiat-deposit-url` endpoint works on demand. Contact WhiteBIT support and provide the API key to get access to the functionality. Without activation, the endpoint returns a permission error.
</Note>

Generate a deposit invoice with a client-side `uniqueId` (any string up to 255 characters). The `ticker` must be a [fiat](/glossary#fiat) currency with `can_deposit: true`, and the `provider` value comes from the Asset Status response — see the [provider](/glossary#provider) glossary entry:

<Tabs>
  <Tab title="curl">
    ```bash theme={"theme":{"light":"github-light","dark":"github-dark"}}
    # Generate a fiat deposit invoice (provider value from Asset Status)
    curl -X POST "https://whitebit.com/api/v4/main-account/fiat-deposit-url" \
      -H "Content-Type: application/json" \
      -H "X-TXC-APIKEY: YOUR_API_KEY" \
      -H "X-TXC-PAYLOAD: YOUR_PAYLOAD" \
      -H "X-TXC-SIGNATURE: YOUR_SIGNATURE" \
      -d '{
        "ticker": "EUR",
        "provider": "PROVIDER",
        "amount": "100",
        "uniqueId": "deposit-2026-000123",
        "request": "/api/v4/main-account/fiat-deposit-url",
        "nonce": 1594297865000
      }'
    ```
  </Tab>

  <Tab title="Python">
    ```python theme={"theme":{"light":"github-light","dark":"github-dark"}}
    import base64, hashlib, hmac, json, time, requests

    API_KEY, API_SECRET = "YOUR_API_KEY", "YOUR_SECRET"
    endpoint = "/api/v4/main-account/fiat-deposit-url"
    # provider value from the Asset Status response
    body = json.dumps({
        "ticker": "EUR",
        "provider": "PROVIDER",
        "amount": "100",
        "uniqueId": "deposit-2026-000123",
        "request": endpoint,
        "nonce": int(time.time() * 1000),
    })
    payload = base64.b64encode(body.encode()).decode()
    signature = hmac.new(API_SECRET.encode(), payload.encode(), hashlib.sha512).hexdigest()
    response = requests.post(
        "https://whitebit.com" + endpoint,
        headers={"Content-Type": "application/json", "X-TXC-APIKEY": API_KEY,
                 "X-TXC-PAYLOAD": payload, "X-TXC-SIGNATURE": signature},
        data=body,
    )
    print(response.json())  # {"url": "..."}
    ```
  </Tab>
</Tabs>

For Go and PHP examples, see [SDKs](/sdks). The signing process is documented in [Private HTTP API authentication](/api-reference/authentication).

Operational rules for the deposit leg:

* **SEPA payment reference.** The `uniqueId` must appear in the SEPA payment-reference field of the bank transfer. If the end user omits or alters it, the deposit may settle unmatched and require manual reconciliation through institutional support — see [SEPA payment reference](/guides/payment-integration#sepa-payment-reference).
* **Redirect links.** `successLink` and `failureLink` require feature activation through WhiteBIT support, which validates the destination domain; `returnLink` needs no activation.
* **Card flows.** Card funding (the VISAMASTER provider) requires a `customer` billing block, and the browser must send the `Referer` header when opening the invoice link — messengers and email clients often strip it. See [Card-provider flows](/guides/payment-integration#card-provider-flows).
* **Web-only currencies.** Not every fiat ticker is depositable via API; confirm `can_deposit` in the [Asset Status](/api-reference/market-data/asset-status-list) response before generating an invoice, and route unsupported tickers through the WhiteBIT web interface.

To complete the on-ramp, reach the currency for the crypto withdrawal through Convert where a conversion is needed: request a quote via `POST /api/v4/convert/estimate`, then execute it with `POST /api/v4/convert/confirm` before the quote expires. Honor the per-quote expiry returned in the response rather than assuming a fixed window. The convert service routes balances internally; no manual transfers between Main and Trade balances are required. The conversion pairs available to the account — including whether a fiat balance converts directly — are confirmed with the account manager. The crypto withdrawal step is documented in [Crypto Withdrawals](/guides/payment-integration#create-a-withdrawal); for EEA accounts the crypto withdrawal carries a Travel Rule payload — see [Travel Rule](/concepts/travel-rule).

## Off-ramp flow: crypto in, fiat out

```mermaid theme={"theme":{"light":"github-light","dark":"github-dark"}}
sequenceDiagram
    participant Partner as Partner backend
    participant WhiteBIT as WhiteBIT API
    participant Hook as Partner webhook endpoint
    Partner->>WhiteBIT: POST /api/v4/main-account/address
    Note over WhiteBIT: Deposit confirms on-chain.<br/>Travel Rule hold may apply (EEA and Turkey)
    WhiteBIT-->>Hook: deposit.processed
    Partner->>WhiteBIT: POST /api/v4/convert/estimate → /convert/confirm
    Partner->>WhiteBIT: POST /api/v4/main-account/withdraw (fiat ticker + beneficiary object)
    WhiteBIT-->>Hook: withdraw.successful
    Partner->>WhiteBIT: POST /api/v4/main-account/history (reconciliation)
```

Receive the crypto leg on a [deposit address](/api-reference/account-wallet/get-cryptocurrency-deposit-address) of the main account; dedicated per-transaction addresses via [`/create-new-address`](/api-reference/account-wallet/create-new-address-for-deposit) require per-account permission from [support@whitebit.com](mailto:support@whitebit.com). Inbound deposits for EEA and Turkey accounts are held until Travel Rule verification completes — see [Travel Rule](/concepts/travel-rule).

For the fiat leg, choose the fee semantics: `POST /api/v4/main-account/withdraw` treats `amount` as including the fee, while `POST /api/v4/main-account/withdraw-pay` charges the fee on top so the recipient receives the specified amount. Fiat withdrawals require a `beneficiary` object; the required sub-fields vary by ticker and provider, so verify the current contract on the [Create Withdraw Request](/api-reference/account-wallet/create-withdraw-request) reference before integration. A `customerIp` field is additionally required for USD or EUR with the VISAMASTER provider. The EUR SEPA IBAN example below uses the `SEPA_BCB_GROUP` provider, one of the SEPA/bank-rail providers (`SEPA`, `SEPA_CLEAR_JUNCTION`, `SEPA_BCB_GROUP`, `FINCI`, `ZEN`, `BANKING_CIRCLE`) that requires a top-level `bankBic` field carrying the beneficiary bank's BIC/SWIFT code; the active provider for the account is confirmed via the [Asset Status](/api-reference/market-data/asset-status-list) endpoint or the account manager.

An EUR IBAN transfer (SEPA) request body:

```json theme={"theme":{"light":"github-light","dark":"github-dark"}}
{
  "ticker": "EUR",
  "amount": "500",
  "address": "DE89370400440532013000",
  "bankBic": "DEUTDEFF",
  "beneficiary": {
    "firstName": "Firstname",
    "lastName": "Lastname"
  },
  "provider": "SEPA_BCB_GROUP",
  "uniqueId": "24529046",
  "request": "/api/v4/main-account/withdraw",
  "nonce": 1594297865000
}
```

* **Partial withdrawals.** `partialEnable: true` raises the maximum limit for fiat withdrawals; the application must then handle the `Partially successful` status (18) and reconcile `requestAmount` against `processedAmount` in the history record.
* **KYC.** Fiat withdrawals require KYC verification. In exceptional, documented cases the institutional team can arrange a per-account override — see [Fiat withdrawal](/guides/payment-integration#fiat-withdrawal).

<Warning>
  Withdrawals are irreversible once processed. Validate the destination details and amounts before submitting, and test the flow with minimum amounts first — no public testnet or sandbox is available.
</Warning>

## Reconciliation and monitoring

Webhooks are the primary channel: verify the domain, then configure the webhook URL in the API key settings — see [Webhooks](/platform/webhook) for the setup flow. Verify the HMAC-SHA512 signature on every delivery using the `X-TXC-APIKEY`, `X-TXC-PAYLOAD`, and `X-TXC-SIGNATURE` headers, and track the strictly increasing `nonce` in each payload. Delivery is retried a small number of times spaced roughly an hour apart over a window of about one day; treat the cadence as best-effort. There is no webhook replay mechanism, so pair webhooks with polling: query `POST /api/v4/main-account/history` (`transactionMethod: 1` for deposits, `2` for withdrawals) on a regular cycle and deduplicate against received events. The [reconciliation pattern](/guides/payment-integration#webhook-reconciliation) in the Payment Integration guide walks through the full loop.

The identifier appears under different names across the API surface:

| Field            | Where it appears                           | Meaning                                                       |
| ---------------- | ------------------------------------------ | ------------------------------------------------------------- |
| `uniqueId`       | `fiat-deposit-url` and withdrawal requests | Client-side transaction identifier, up to 255 characters      |
| `unique_id`      | History filter and records                 | The deposit/withdraw identifier in history responses          |
| `transactionId`  | `refund-deposit` request                   | Transaction UUID, sourced from the `deposit.canceled` webhook |
| `transaction_id` | History records                            | System-wide transaction UUID, never reused                    |

Mark transactions complete only at terminal statuses: 3/7 for success, 4/9 for canceled deposits, 4 for canceled withdrawals. Platform availability is published at [status.whitebit.com](https://status.whitebit.com); per-asset deposit and withdrawal availability is reported by the [Asset Status](/api-reference/market-data/asset-status-list) endpoint.

## Failure and exception paths

Build handling for each of these before go-live:

| Scenario                                                      | Documented behavior                                                                 | Partner action                                                                                                                                                                                               |
| ------------------------------------------------------------- | ----------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ |
| SEPA deposit arrives without the `uniqueId` payment reference | The transfer cannot be matched to the deposit request automatically                 | Validate the reference field in the payment UX; unmatched settlements go through manual reconciliation with institutional support                                                                            |
| Deposit canceled (status 4 or 9)                              | The `deposit.canceled` webhook fires; canceled crypto deposits may be refundable    | Trigger [`refund-deposit`](/api-reference/account-wallet/refund-deposit) with the transaction UUID; outcomes arrive as `refund.successful` or `refund.failed`                                                |
| Deposit unconfirmed by the user (status 5)                    | The transaction waits for manual confirmation on the platform                       | Notify the account operator; track the status via history polling                                                                                                                                            |
| Deposit frozen for AML review (status 21)                     | The record surfaces in the deposit history with the frozen status                   | Escalate to [institutional@whitebit.com](mailto:institutional@whitebit.com) if the freeze persists                                                                                                           |
| Deposit held for Travel Rule (status 27/28)                   | No webhook fires for Travel Rule transitions; the hold surfaces via history polling | Submit originator data via the [deposit verification endpoint](/api-reference/travel-rule/submit-deposit-verification) where the Travel Rule API is enabled; otherwise complete verification on the platform |
| Partial fiat withdrawal (status 18, with `partialEnable`)     | The withdrawal completes partially                                                  | Reconcile `requestAmount` against `processedAmount` and surface the remainder to operations                                                                                                                  |
| Fiat withdrawal without KYC                                   | The request fails validation with an account-verification error                     | Complete KYC, or arrange the exception-only override with the institutional team                                                                                                                             |
| VISAMASTER invoice link opened without a `Referer` header     | The end user lands on the WhiteBIT homepage instead of the payment provider         | Test the redirect in the real delivery surface — messengers and email clients strip the header                                                                                                               |

## What's next

<CardGroup cols={3}>
  <Card title="On/Off-Ramp FAQ" icon="circle-question" href="/guides/on-off-ramp-faq">
    Activation, SEPA matching, fee semantics, reconciliation, and compliance questions.
  </Card>

  <Card title="Payment Integration" icon="arrow-right-arrow-left" href="/guides/payment-integration">
    Endpoint-level lifecycle reference: statuses, fees, fiat operations, refunds.
  </Card>

  <Card title="Go-Live Checklist" icon="clipboard-check" href="/best-practices/go-live-checklist">
    Pre-production readiness verification for keys, webhooks, and compliance.
  </Card>
</CardGroup>
