> For a complete page index, fetch https://docs.transak.com/llms.txt

# How to use Lookup APIs

This is the first step in building an app using Transak Whitelabel APIs. Before a user logs in, creates an account, or touches a KYC form, your app needs real data — which `countries` are supported, which `crypto assets` are available, and which currencies are accepted. The Lookup APIs are four public endpoints that give you exactly that data.

## What We'll Build

We are building a simple wallet app that allows a user to select their `country`, `fiat currency`, `crypto token`, and `payment method`. The screen will show a live quote with fee breakdown and the amount of crypto the user will receive.
By the end of this tutorial, you'll have the data to render a complete quote screen fully wired to live Transak APIs.

### API Flow

| UI Element                              | API                               |
| --------------------------------------- | --------------------------------- |
| Country picker + fiat auto-fill         | `GET /countries` → `currencyCode` |
| Fiat currency picker (`USD ▾`)          | `GET /fiat-currencies`            |
| Crypto token picker (`BTC ▾`)           | `GET /crypto-currencies`          |
| `~0.00149 BTC` you receive amount       | `GET /quote` → `cryptoAmount`     |
| Network fee / Transaction fee breakdown | `GET /quote` → `feeBreakdown`     |

## Look Up APIs

Returns all supported countries with their default fiat currency. Powers the country selector.

Returns fiat currencies with payment options and min/max limits. Powers the fiat + payment method picker.

Returns supported tokens and networks. Powers the crypto + network selector.

Returns a live price quote with fee breakdown and a `quoteId` to pass downstream. Requires API key.

## Fetch Supported Countries List

Call [Get Countries API](/api/whitelabel/lookup/get-countries) on app load and keep the result in memory. You use it in two ways: to power a country-of-residence picker, and to look up a country's default fiat currency so you can pre-fill the fiat selector for your user.

This API helps you identify which countries are supported for KYC verification, quote fetching, and other transactions.

Call once at initialization. Cache the result in memory.

```bash
curl https://api-gateway-stg.transak.com/api/v2/lookup/countries \
  -H "x-api-key: YOUR_API_KEY" \
  -H "x-user-ip: USER_IP_ADDRESS"
```

Only show countries where `isAllowed: true`. Never show restricted countries in your picker.

```ts
const allowed = countries.filter(c => c.isAllowed);
```

Index the result so you can instantly look up the default fiat when a user picks a country.

```ts
const countryToCurrency = Object.fromEntries(
  countries.map(c => [c.code, c.currencyCode])
);
// countryToCurrency['US'] → 'USD'
// countryToCurrency['GB'] → 'GBP'
```

When the user picks a country, immediately set the fiat selector to the matching currency.

```ts
function onCountrySelect(countryCode: string) {
  const defaultFiat = countryToCurrency[countryCode];
  setSelectedFiat(defaultFiat); // auto-fills the fiat picker
}
```

```tsx title="CountrySelector.tsx"
import { useState, useEffect, useMemo } from "react";

const API_URL = "https://api-gateway-stg.transak.com/api/v2/lookup/countries";

interface Country {
  name: string;
  code: string;
  currencyCode: string;
  isAllowed: boolean;
}

interface CountrySelectorProps {
  apiKey: string;
  userIp: string;
  onSelect?: (countryCode: string, defaultFiat: string) => void;
}

export function CountrySelector({ apiKey, userIp, onSelect }: CountrySelectorProps) {
  const [countries, setCountries] = useState<Country[]>([]);
  const [selected, setSelected] = useState<string>("");
  const [loading, setLoading] = useState(true);
  const [error, setError] = useState<string | null>(null);

  useEffect(() => {
    fetch(API_URL, {
      headers: {
        "x-api-key": apiKey,
        "x-user-ip": userIp,
      },
    })
      .then(r => {
        if (!r.ok) throw new Error(`HTTP ${r.status}`);
        return r.json();
      })
      .then(data => {
        setCountries(data.data.countries.filter((c: Country) => c.isAllowed));
        setLoading(false);
      })
      .catch(err => {
        setError(err.message);
        setLoading(false);
      });
  }, []);

  const countryToCurrency = useMemo(
    () => Object.fromEntries(countries.map(c => [c.code, c.currencyCode])),
    [countries]
  );

  function handleChange(code: string) {
    setSelected(code);
    onSelect?.(code, countryToCurrency[code]);
  }

  if (loading) return <p>Loading countries…</p>;
  if (error) return <p>Failed to load countries: {error}</p>;

  return (
    <select value={selected} onChange={e => handleChange(e.target.value)}>
      <option value="">Select country…</option>
      {countries.map(c => (
        <option key={c.code} value={c.code}>
          {c.name}
        </option>
      ))}
    </select>
  );
}
```

## Get All Crypto Currencies List

[Get Currencies API](/api/whitelabel/lookup/get-currencies) powers the crypto picker screen. Every supported token is returned with its full details: `symbol, name, image, and which networks` it's available on. One token can exist on multiple networks — each combination is its own selectable item.

The same token (e.g. USDC) appears multiple times — once per network. Treat each `symbol + network.name` pair as a distinct selectable item. When the user picks

Call once on page load.

```bash
curl https://api-gateway-stg.transak.com/api/v2/lookup/currencies/crypto-currencies \
  -H "x-api-key: YOUR_API_KEY" \
  -H "x-user-ip: USER_IP_ADDRESS"
```

Keep only entries where `isBuyAllowed: true` for buy flows, or `isSellAllowed: true` for sell flows.

```ts
const tokens = response.filter(t =>
  mode === "buy" ? t.isBuyAllowed : t.isSellAllowed
);
```

The same token appears once per supported network. Group by `coinId` so users see each token once, then pick a network in a second step.

```ts
const grouped = tokens.reduce((map, t) => {
  if (!map[t.coinId]) map[t.coinId] = { ...t, networks: [] };
  map[t.coinId].networks.push(t.network);
  return map;
}, {});
const coins = Object.values(grouped);
```

Deduplicate all `network.name` values to create filter tabs at the top of the picker.

```ts
const networks = [...new Set(tokens.map(t => t.network.name))].sort();
// → ['arbitrum', 'ethereum', 'polygon', 'solana', ...]
```

Cross-reference `regionsNotSupported` against the user's country code — hide tokens not available in their region.

```ts
const available = tokens.filter(t =>
  !t.regionsNotSupported.includes(userCountryCode)
);
```

```tsx title="CryptoSelector.tsx"
import { useState, useEffect, useMemo } from "react";

const API_URL = "https://api-gateway-stg.transak.com/api/v2/lookup/currencies/crypto-currencies";

interface Network {
  name: string;
  chainId: number;
  symbol: string;
}

interface CryptoCurrency {
  coinId: string;
  name: string;
  symbol: string;
  image: string;
  isBuyAllowed: boolean;
  isSellAllowed: boolean;
  network: Network;
  regionsNotSupported: string[];
}

interface GroupedCoin {
  coinId: string;
  name: string;
  symbol: string;
  image: string;
  networks: Network[];
}

interface CryptoSelectorProps {
  apiKey: string;
  userIp: string;
  mode?: "buy" | "sell";
  userCountryCode?: string;
  onSelect?: (symbol: string, network: string) => void;
}

export function CryptoSelector({ apiKey, userIp, mode = "buy", userCountryCode, onSelect }: CryptoSelectorProps) {
  const [currencies, setCurrencies] = useState<CryptoCurrency[]>([]);
  const [loading, setLoading] = useState(true);
  const [error, setError] = useState<string | null>(null);
  const [search, setSearch] = useState("");
  const [networkFilter, setNetworkFilter] = useState("");
  const [selected, setSelected] = useState<GroupedCoin | null>(null);

  useEffect(() => {
    fetch(API_URL, {
      headers: {
        "x-api-key": apiKey,
        "x-user-ip": userIp,
      },
    })
      .then(r => {
        if (!r.ok) throw new Error(`HTTP ${r.status}`);
        return r.json();
      })
      .then(data => {
        const all: CryptoCurrency[] = data.data.cryptoCurrencies;
        const filtered = all.filter(c => {
          const directionOk = mode === "buy" ? c.isBuyAllowed : c.isSellAllowed;
          const regionOk = !userCountryCode || !c.regionsNotSupported.includes(userCountryCode);
          return directionOk && regionOk;
        });
        setCurrencies(filtered);
        setLoading(false);
      })
      .catch(err => {
        setError(err.message);
        setLoading(false);
      });
  }, [apiKey, userIp, mode, userCountryCode]);

  // Group entries by coinId — one coin can appear once per network
  const grouped = useMemo<GroupedCoin[]>(() => {
    const map: Record<string, GroupedCoin> = {};
    for (const c of currencies) {
      if (!map[c.coinId]) {
        map[c.coinId] = { coinId: c.coinId, name: c.name, symbol: c.symbol, image: c.image, networks: [] };
      }
      map[c.coinId].networks.push(c.network);
    }
    return Object.values(map);
  }, [currencies]);

  const allNetworks = useMemo(() => {
    return [...new Set(currencies.map(c => c.network.name))].sort();
  }, [currencies]);

  const visible = useMemo(() => {
    return grouped.filter(coin => {
      const matchSearch = !search ||
        coin.name.toLowerCase().includes(search.toLowerCase()) ||
        coin.symbol.toLowerCase().includes(search.toLowerCase());
      const matchNetwork = !networkFilter ||
        coin.networks.some(n => n.name === networkFilter);
      return matchSearch && matchNetwork;
    });
  }, [grouped, search, networkFilter]);

  function handleSelect(coin: GroupedCoin) {
    setSelected(coin);
    const net = networkFilter && coin.networks.some(n => n.name === networkFilter)
      ? networkFilter
      : coin.networks[0].name;
    onSelect?.(coin.symbol, net);
  }

  if (loading) return <p>Loading tokens…</p>;
  if (error) return <p>Failed to load tokens: {error}</p>;

  return (
    <div>
      {/* Network filter pills */}
      <div>
        <button onClick={() => setNetworkFilter("")}>All</button>
        {allNetworks.map(n => (
          <button
            key={n}
            onClick={() => setNetworkFilter(n)}
            style={{ fontWeight: networkFilter === n ? "bold" : "normal" }}
          >
            {n}
          </button>
        ))}
      </div>

      {/* Search */}
      <input
        type="text"
        placeholder="Search tokens…"
        value={search}
        onChange={e => setSearch(e.target.value)}
      />

      {/* Token list */}
      <ul>
        {visible.length === 0 && <li>No tokens found.</li>}
        {visible.map(coin => (
          <li
            key={coin.coinId}
            onClick={() => handleSelect(coin)}
            style={{ fontWeight: selected?.coinId === coin.coinId ? "bold" : "normal" }}
          >
            <img
              src={coin.image}
              alt={coin.symbol}
              width={32}
              height={32}
              onError={e => { (e.target as HTMLImageElement).src = `https://placehold.co/32x32?text=${coin.symbol[0]}`; }}
            />
            <span>{coin.name}</span>
            <span>{coin.symbol}</span>
            <span>
              {networkFilter && coin.networks.some(n => n.name === networkFilter)
                ? networkFilter
                : coin.networks[0].name}
            </span>
          </li>
        ))}
      </ul>
    </div>
  );
}
```

## Fetch Supported Fiat Currencies

In [Get Fiat Currencies API](/api/whitelabel/lookup/get-fiat-currencies) populates your fiat picker. Each currency object includes the `currency code` and `symbol`, and — critically — which payment methods are available for it. A user buying with `USD` has different options than a user buying with `EUR`.

In this API, each payment method has a `minAmount` and `maxAmount`. Use these to validate the user's input before calling the quote endpoint.
It also surfaces which payment methods are supported for each currency.

Call once on page load.

```bash
curl https://api-gateway-stg.transak.com/api/v2/lookup/currencies/fiat-currencies \
  -H "x-api-key: YOUR_API_KEY" \
  -H "x-user-ip: USER_IP_ADDRESS"
```

Currencies with `isPopular: true` should appear in a "Popular" group above the full list.

```ts
const popular = fiats.filter(f => f.isPopular);
const others = fiats.filter(f => !f.isPopular);
```

Each currency has a `paymentOptions` array. Filter to `isActive: true` and display with min/max limits.

```ts
const activeMethods = selectedFiat.paymentOptions.filter(p => p.isActive);
// e.g. [{ id: "credit_debit_card", minAmount: 1, maxAmount: 30000 }]
```

Use `minAmount` / `maxAmount` to show inline errors before firing the quote endpoint.

```ts
const method = activeMethods.find(p => p.id === selectedMethodId);
if (amount < method.minAmount || amount > method.maxAmount) {
  showError(`Enter an amount between $${method.minAmount} and $${method.maxAmount}`);
}
```

```tsx title="FiatSelector.tsx"
import { useState, useEffect } from "react";

const API_URL = "https://api-gateway-stg.transak.com/api/v2/lookup/currencies/fiat-currencies";

interface PaymentOption {
  id: string;
  name: string;
  isActive: boolean;
  minAmount: number;
  maxAmount: number;
  processingTime: string;
}

interface FiatCurrency {
  symbol: string;
  name: string;
  icon: string;
  isPopular: boolean;
  paymentOptions: PaymentOption[];
}

interface FiatSelectorProps {
  apiKey: string;
  userIp: string;
  defaultSymbol?: string;
  onSelect?: (symbol: string, paymentMethod: string) => void;
}

export function FiatSelector({ apiKey, userIp, defaultSymbol, onSelect }: FiatSelectorProps) {
  const [currencies, setCurrencies] = useState<FiatCurrency[]>([]);
  const [selected, setSelected] = useState<FiatCurrency | null>(null);
  const [selectedMethod, setSelectedMethod] = useState<string>("");
  const [loading, setLoading] = useState(true);
  const [error, setError] = useState<string | null>(null);

  useEffect(() => {
    fetch(API_URL, {
      headers: {
        "x-api-key": apiKey,
        "x-user-ip": userIp,
      },
    })
      .then(r => {
        if (!r.ok) throw new Error(`HTTP ${r.status}`);
        return r.json();
      })
      .then(data => {
        const fiats: FiatCurrency[] = data.data.fiatCurrencies;
        setCurrencies(fiats);
        // Auto-select the default fiat (e.g. from country picker)
        const def = fiats.find(f => f.symbol === (defaultSymbol ?? "USD")) ?? fiats[0];
        setSelected(def);
        const firstActive = def?.paymentOptions.find(p => p.isActive);
        setSelectedMethod(firstActive?.id ?? "");
        setLoading(false);
      })
      .catch(err => {
        setError(err.message);
        setLoading(false);
      });
  }, [apiKey, userIp, defaultSymbol]);

  function handleCurrencyChange(symbol: string) {
    const fiat = currencies.find(f => f.symbol === symbol) ?? null;
    setSelected(fiat);
    const firstActive = fiat?.paymentOptions.find(p => p.isActive);
    const method = firstActive?.id ?? "";
    setSelectedMethod(method);
    if (fiat && method) onSelect?.(fiat.symbol, method);
  }

  function handleMethodChange(methodId: string) {
    setSelectedMethod(methodId);
    if (selected) onSelect?.(selected.symbol, methodId);
  }

  if (loading) return <p>Loading currencies…</p>;
  if (error) return <p>Failed to load currencies: {error}</p>;

  const popular = currencies.filter(f => f.isPopular);
  const others = currencies.filter(f => !f.isPopular);
  const activeMethods = selected?.paymentOptions.filter(p => p.isActive) ?? [];

  return (
    <div>
      {/* Currency dropdown */}
      <select value={selected?.symbol ?? ""} onChange={e => handleCurrencyChange(e.target.value)}>
        <optgroup label="Popular">
          {popular.map(f => (
            <option key={f.symbol} value={f.symbol}>{f.symbol} — {f.name}</option>
          ))}
        </optgroup>
        <optgroup label="All currencies">
          {others.map(f => (
            <option key={f.symbol} value={f.symbol}>{f.symbol} — {f.name}</option>
          ))}
        </optgroup>
      </select>

      {/* Payment method selector */}
      {activeMethods.length > 0 && (
        <div>
          {activeMethods.map(opt => (
            <label key={opt.id}>
              <input
                type="radio"
                name="paymentMethod"
                value={opt.id}
                checked={selectedMethod === opt.id}
                onChange={() => handleMethodChange(opt.id)}
              />
              {opt.name} — ${opt.minAmount}–${opt.maxAmount} ({opt.processingTime})
            </label>
          ))}
        </div>
      )}
    </div>
  );
}
```

## Fetch Final Quote

In [Get Quote API](/api/whitelabel/lookup/get-quote) is the only Lookup API that requires `your partner API key`. It returns a Quote that includes the full `fee breakdown`, and most importantly a `quoteId`. That `quoteId` is required for every subsequent API call in this series.

This API call lets you fetch a temporary price quote for a cryptocurrency transaction based on the selected `fiat currency`, `cryptocurrency`, `payment method`, and `transaction amount`. Since cryptocurrency prices are **volatile**, the returned quote is refreshed **every minute** to reflect the latest market price.

Collect the user's inputs — fiat amount, fiat currency, crypto currency, network, and payment method — then call the endpoint.

```bash
curl "https://api-gateway-stg.transak.com/api/v2/lookup/quotes\
?fiatCurrency=USD\
&cryptoCurrency=ETH\
&isBuyOrSell=BUY\
&fiatAmount=100\
&network=ethereum\
&paymentMethod=credit_debit_card" \
  -H "x-api-key: YOUR_API_KEY" \
  -H "x-user-ip: USER_IP_ADDRESS"
```

The response includes `cryptoAmount`, `totalFee`, `feeBreakdown`, and `conversionPrice`. Show these in a summary card before the user confirms.

```ts
const { cryptoAmount, totalFee, feeBreakdown, quoteId, conversionPrice } = data;
// feeBreakdown: [{ name: "Network fee", value: 2.00 }, { name: "Transaction fee", value: 1.50 }]
```

Save the `quoteId` to state as soon as you receive it. Every API call from Tutorial 02 onwards requires it.

```ts
setQuoteId(data.quoteId);
```

Set a countdown from `expiresAt`. Refresh the quote \~30 seconds before it expires, or whenever the user changes any input (debounced 400ms).

```ts
const msUntilExpiry = new Date(data.expiresAt).getTime() - Date.now();
const refreshAt = msUntilExpiry - 30_000;
setTimeout(() => refetchQuote(), refreshAt);
```

```tsx title="QuoteCard.tsx"
import { useState, useEffect, useRef } from "react";

const BASE_URL = "https://api-gateway-stg.transak.com/api/v2/lookup/quotes";

interface FeeBreakdown {
  id: string;
  name: string;
  value: number;
}

interface Quote {
  quoteId: string;
  fiatAmount: number;
  fiatCurrency: string;
  cryptoAmount: number;
  cryptoCurrency: string;
  totalFee: number;
  feeBreakdown: FeeBreakdown[];
  conversionPrice: number;
  expiresAt: string;
}

interface QuoteCardProps {
  apiKey: string;
  userIp: string;
  fiatCurrency: string;
  cryptoCurrency: string;
  network: string;
  fiatAmount: number;
  paymentMethod: string;
  onQuote?: (quoteId: string) => void;
}

export function QuoteCard({
  apiKey,
  userIp,
  fiatCurrency,
  cryptoCurrency,
  network,
  fiatAmount,
  paymentMethod,
  onQuote,
}: QuoteCardProps) {
  const [quote, setQuote] = useState<Quote | null>(null);
  const [loading, setLoading] = useState(false);
  const [error, setError] = useState<string | null>(null);
  const debounceTimer = useRef<ReturnType<typeof setTimeout>>();
  const refreshTimer = useRef<ReturnType<typeof setTimeout>>();

  async function fetchQuote() {
    if (!fiatCurrency || !cryptoCurrency || !fiatAmount) return;
    setLoading(true);
    setError(null);
    try {
      const params = new URLSearchParams({
        fiatCurrency,
        cryptoCurrency,
        isBuyOrSell: "BUY",
        fiatAmount: String(fiatAmount),
        ...(network && { network }),
        ...(paymentMethod && { paymentMethod }),
      });
      const res = await fetch(`${BASE_URL}?${params}`, {
        headers: {
          "x-api-key": apiKey,
          "x-user-ip": userIp,
        },
      });
      if (!res.ok) {
        const err = await res.json();
        throw new Error(err.message ?? `HTTP ${res.status}`);
      }
      const { data }: { data: Quote } = await res.json();
      setQuote(data);
      onQuote?.(data.quoteId);

      // Auto-refresh 30s before expiry
      clearTimeout(refreshTimer.current);
      const msUntilExpiry = new Date(data.expiresAt).getTime() - Date.now();
      if (msUntilExpiry > 30_000) {
        refreshTimer.current = setTimeout(fetchQuote, msUntilExpiry - 30_000);
      }
    } catch (err: unknown) {
      setError(err instanceof Error ? err.message : "Quote fetch failed");
      setQuote(null);
    } finally {
      setLoading(false);
    }
  }

  useEffect(() => {
    clearTimeout(debounceTimer.current);
    debounceTimer.current = setTimeout(fetchQuote, 400);
    return () => {
      clearTimeout(debounceTimer.current);
      clearTimeout(refreshTimer.current);
    };
  }, [fiatCurrency, cryptoCurrency, fiatAmount, network, paymentMethod]);

  if (loading) return <p>Fetching quote…</p>;
  if (error) return <p style={{ color: "red" }}>{error}</p>;
  if (!quote) return null;

  return (
    <div>
      <p>
        {quote.fiatAmount} {quote.fiatCurrency} → {quote.cryptoAmount} {quote.cryptoCurrency}
      </p>
      <p>Rate: 1 {quote.cryptoCurrency} = {quote.conversionPrice.toLocaleString()} {quote.fiatCurrency}</p>
      <ul>
        {quote.feeBreakdown.map(fee => (
          <li key={fee.id}>
            <span>{fee.name}</span>
            <span>{fee.value} {quote.fiatCurrency}</span>
          </li>
        ))}
        <li>
          <strong>Total fees</strong>
          <strong>{quote.totalFee} {quote.fiatCurrency}</strong>
        </li>
      </ul>
      <small>Quote expires at {new Date(quote.expiresAt).toLocaleTimeString()}</small>
    </div>
  );
}
```

## Final Output

All four components wired together into a single `QuoteScreen` page. This is what you'll have by the end of this tutorial.

```tsx title="QuoteScreen.tsx"
import { useState } from "react";
import { useAppInit } from "../hooks/useAppInit";
import { CountrySelector } from "../components/CountrySelector";
import { FiatSelector } from "../components/FiatSelector";
import { CryptoSelector } from "../components/CryptoSelector";
import { QuoteCard } from "../components/QuoteCard";

const API_KEY = "YOUR_PARTNER_API_KEY";
const USER_IP = "USER_IP_ADDRESS"; // pass the real end-user IP from your server

export function QuoteScreen() {
  const { loading, error } = useAppInit(API_KEY, USER_IP);

  const [countryCode, setCountryCode] = useState<string>("");
  const [fiatSymbol, setFiatSymbol] = useState<string>("USD");
  const [paymentMethod, setPaymentMethod] = useState<string>("credit_debit_card");
  const [cryptoSymbol, setCryptoSymbol] = useState<string>("ETH");
  const [network, setNetwork] = useState<string>("ethereum");
  const [fiatAmount, setFiatAmount] = useState<number>(100);
  const [quoteId, setQuoteId] = useState<string>("");

  if (loading) return <p>Loading app data…</p>;
  if (error) return <p>Failed to initialise: {error}</p>;

  return (
    <div>
      <h2>Buy Crypto</h2>

      {/* Step 1 — Country (auto-fills fiat below) */}
      <CountrySelector
        apiKey={API_KEY}
        userIp={USER_IP}
        onSelect={(code, defaultFiat) => {
          setCountryCode(code);
          setFiatSymbol(defaultFiat);
        }}
      />

      {/* Step 2 — Fiat currency + payment method */}
      <FiatSelector
        apiKey={API_KEY}
        userIp={USER_IP}
        defaultSymbol={fiatSymbol}
        onSelect={(symbol, method) => {
          setFiatSymbol(symbol);
          setPaymentMethod(method);
        }}
      />

      {/* Step 3 — Amount */}
      <input
        type="number"
        value={fiatAmount}
        min={1}
        onChange={e => setFiatAmount(Number(e.target.value))}
      />

      {/* Step 4 — Crypto token + network */}
      <CryptoSelector
        apiKey={API_KEY}
        userIp={USER_IP}
        mode="buy"
        userCountryCode={countryCode}
        onSelect={(symbol, net) => {
          setCryptoSymbol(symbol);
          setNetwork(net);
        }}
      />

      {/* Step 5 — Live quote (auto-refreshes on every input change) */}
      <QuoteCard
        apiKey={API_KEY}
        userIp={USER_IP}
        fiatCurrency={fiatSymbol}
        cryptoCurrency={cryptoSymbol}
        network={network}
        fiatAmount={fiatAmount}
        paymentMethod={paymentMethod}
        onQuote={id => setQuoteId(id)}
      />

      {/* Continue is only enabled once we have a valid quoteId */}
      <button disabled={!quoteId}>
        Continue →
      </button>
    </div>
  );
}
```

```tsx title="hooks/useAppInit.ts"
import { useState, useEffect } from "react";

const BASE = "https://api-gateway-stg.transak.com";

export function useAppInit(apiKey: string, userIp: string) {
  const [appData, setAppData] = useState<{
    countries: { name: string; code: string; currencyCode: string }[];
    fiatCurrencies: { symbol: string; name: string; isPopular: boolean }[];
    cryptoCurrencies: { symbol: string; name: string; isBuyAllowed: boolean }[];
  } | null>(null);
  const [loading, setLoading] = useState(true);
  const [error, setError] = useState<string | null>(null);

  useEffect(() => {
    async function init() {
      const headers = { "x-api-key": apiKey, "x-user-ip": userIp };
      try {
        // All three fire simultaneously — no waterfall
        const [countriesRes, fiatsRes, cryptoRes] = await Promise.all([
          fetch(`${BASE}/api/v2/lookup/countries`, { headers }),
          fetch(`${BASE}/api/v2/lookup/currencies/fiat-currencies`, { headers }),
          fetch(`${BASE}/api/v2/lookup/currencies/crypto-currencies`, { headers }),
        ]);
        const [{ data: c }, { data: f }, { data: cr }] = await Promise.all([
          countriesRes.json(), fiatsRes.json(), cryptoRes.json(),
        ]);
        setAppData({
          countries: c.countries.filter((x: { isAllowed: boolean }) => x.isAllowed),
          fiatCurrencies: f.fiatCurrencies,
          cryptoCurrencies: cr.cryptoCurrencies.filter((x: { isBuyAllowed: boolean }) => x.isBuyAllowed),
        });
      } catch (err: unknown) {
        setError(err instanceof Error ? err.message : "Init failed");
      } finally {
        setLoading(false);
      }
    }
    init();
  }, []);

  return { appData, loading, error };
}
```