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

# Meld buy, sell, and transfer

> Use portal.ramps.meld in the Web SDK to embed Meld buy, sell, and transfer sessions via the Portal iframe and Client API.

The Web SDK exposes [Meld](/integrations/On-Off-Ramp/meld) buy, sell, and transfer crypto through `portal.ramps.meld`. Each method sends a message to the embedded Portal iframe, which calls Portal's Meld integration on the [Client API](/apis/quickstart) using your client credentials. You do not call Meld's servers directly from the browser.

<Note>
  For one-time dashboard setup and webhook configuration, see [Meld integration overview](/integrations/On-Off-Ramp/meld). For the end-to-end widget flow using raw HTTP calls, see the [White-Label API guide](/integrations/On-Off-Ramp/meld-widget).
</Note>

## Prerequisites

* An [initialized Portal client](./getting-started) with the iframe ready (`onReady` or equivalent).
* Meld enabled for your Portal environment and [configured in the dashboard](/integrations/On-Off-Ramp/meld).

## Architecture

| Layer          | Role                                                                                      |
| -------------- | ----------------------------------------------------------------------------------------- |
| Your app       | Calls `portal.ramps.meld.*`                                                               |
| Portal Web SDK | `postMessage` bridge to the Portal iframe (use `portal.ramps.meld`, not lower-level APIs) |
| Portal iframe  | Forwards to `POST/GET …/api/v3/clients/me/integrations/meld/...` with the Portal session  |
| Meld           | Aggregated on/off-ramp network; iframe-based widget session                               |

Prefer `portal.ramps.meld` over lower-level APIs. The SDK types for requests and responses live in `@portal-hq/web` (see the [Web SDK reference](../reference) section **portal.ramps.meld (Meld)**).

## Types and responses

Successful Client API responses use an envelope `{ data: T, metadata?: Record<string, unknown> }`. Methods on `portal.ramps.meld` return `Promise` of that envelope (for example `MeldCreateRetailWidgetResponse` is `{ data: { widgetUrl: string, ... } }`).

Throwing or rejected promises usually indicate network errors, iframe timeouts, or API error payloads surfaced by the SDK — handle them with `try/catch` like other async Portal calls.

***

## Customer methods

### createCustomer

Creates a Meld customer record for the current Portal client. This is optional — the widget collects identity inline. Pre-creating is useful when you want to pre-fill KYC fields or track multiple sessions against a persistent customer record.

```typescript theme={null}
import Portal from '@portal-hq/web'

const portal = new Portal({
  rpcConfig: { 'eip155:1': 'https://...' },
  apiKey: 'YOUR_API_KEY',
})

portal.onReady(async () => {
  const { data } = await portal.ramps.meld.createCustomer({
    name: { firstName: 'Jane', lastName: 'Doe' },
    email: 'jane@example.com',
    phone: '+15551234567',
    dateOfBirth: '1990-01-15',
    type: 'INDIVIDUAL',
  })

  console.log(data.id)          // Meld customer ID — pass as `customerId` on createRetailWidget
  console.log(data.externalId)  // Portal-derived external ID
})
```

**Signature**

```typescript theme={null}
public async createCustomer(
  data: MeldCreateCustomerRequest
): Promise<MeldCreateCustomerResponse>
```

| Parameter          | Type                         | Required | Description                                   |
| ------------------ | ---------------------------- | -------- | --------------------------------------------- |
| `data.name`        | `MeldCustomerName`           | No       | `{ firstName?: string; lastName?: string }`   |
| `data.email`       | `string`                     | No       | Customer email address.                       |
| `data.phone`       | `string`                     | No       | E.164 phone number.                           |
| `data.dateOfBirth` | `string`                     | No       | ISO date string (for example `"1990-01-15"`). |
| `data.type`        | `"INDIVIDUAL" \| "BUSINESS"` | No       | Customer type.                                |

**Returns** — `MeldCreateCustomerResponse`: `{ data: MeldCustomer }`.

<Note>
  If a customer already exists for the current Portal client, the Portal API returns `409 Conflict` with the existing customer ID at `details.meldCustomerId`. Retrieve the full record with `searchCustomer()` instead.
</Note>

***

### searchCustomer

Returns the Meld customer record(s) associated with the current Portal client.

```typescript theme={null}
portal.onReady(async () => {
  const { data } = await portal.ramps.meld.searchCustomer()

  if (data.customers.length > 0) {
    const customerId = data.customers[0].id
    console.log('Existing customer:', customerId)
  }
})
```

**Signature**

```typescript theme={null}
public async searchCustomer(): Promise<MeldSearchCustomerResponse>
```

**Returns** — `MeldSearchCustomerResponse`: `{ data: { customers: MeldCustomer[]; count: number; remaining: number } }`.

`data.customers` will be empty if no customer has been created for this Portal client yet.

***

## Retail methods

### getRetailQuote

Fetches live pricing across Meld's provider network before opening the widget. Returns one `MeldQuote` per available provider.

```typescript theme={null}
portal.onReady(async () => {
  const { data } = await portal.ramps.meld.getRetailQuote({
    countryCode: 'US',
    sourceCurrencyCode: 'USD',
    destinationCurrencyCode: 'USDC',
    sourceAmount: 100,
  })

  const best = data.quotes[0]
  console.log(`${best.serviceProvider}: ${best.destinationAmount} USDC for $${best.sourceAmount}`)
  console.log('Total fee:', best.totalFee)
})
```

**Signature**

```typescript theme={null}
public async getRetailQuote(
  data: MeldGetRetailQuoteRequest
): Promise<MeldGetRetailQuoteResponse>
```

| Parameter                      | Type       | Required | Description                                                                                                 |
| ------------------------------ | ---------- | -------- | ----------------------------------------------------------------------------------------------------------- |
| `data.countryCode`             | `string`   | Yes      | ISO-3166-1 alpha-2 country code (for example `"US"`).                                                       |
| `data.sourceCurrencyCode`      | `string`   | Yes      | Source currency (fiat for buy; crypto [Meld Code](https://www.meld.io/coverage/cryptocurrencies) for sell). |
| `data.destinationCurrencyCode` | `string`   | Yes      | Destination currency.                                                                                       |
| `data.sourceAmount`            | `number`   | Yes      | Amount of source currency to convert.                                                                       |
| `data.walletAddress`           | `string`   | No       | Destination wallet address — informs provider eligibility.                                                  |
| `data.customerId`              | `string`   | No       | Meld customer ID when pre-created.                                                                          |
| `data.paymentMethodType`       | `string`   | No       | Filter to a specific payment method (for example `"CREDIT_DEBIT_CARD"`).                                    |
| `data.serviceProviders`        | `string[]` | No       | Restrict the quote to specific providers.                                                                   |
| `data.subdivision`             | `string`   | No       | State/region code when required (for example US states).                                                    |

**Returns** — `MeldGetRetailQuoteResponse`: `{ data: { quotes: MeldQuote[]; message?: string; error?: string; timestamp?: string } }`.

Each `MeldQuote` includes required fields: `serviceProvider`, `transactionType`, `sourceAmount`, `sourceCurrencyCode`, `destinationAmount`, `destinationCurrencyCode`, `exchangeRate`, `transactionFee`, `totalFee`, `paymentMethodType`; and optional nullable fields: `sourceAmountWithoutFees`, `destinationAmountWithoutFees`, `networkFee`, `partnerFee`, `fiatAmountWithoutFees`, `countryCode`, `customerScore`, `institutionName`, `isNativeAvailable`, `rampIntelligence`.

***

### createRetailWidget

Creates a Meld widget session and returns a `widgetUrl`. Open the URL in a new browser tab to let the user complete the buy/sell/transfer flow.

```typescript theme={null}
portal.onReady(async () => {
  const { data } = await portal.ramps.meld.createRetailWidget({
    sessionType: 'BUY',
    sessionData: {
      countryCode: 'US',
      serviceProvider: 'TRANSAK',        // From getRetailQuote response
      sourceCurrencyCode: 'USD',
      sourceAmount: '100',               // String-encoded decimal
      destinationCurrencyCode: 'USDC',
      walletAddress: portal.address,
      redirectUrl: 'https://yourapp.example/meld/return',
    },
  })

  // Open widget in a new tab
  if (data.widgetUrl) {
    window.open(data.widgetUrl, '_blank', 'noopener,noreferrer')
  }
})
```

**Signature**

```typescript theme={null}
public async createRetailWidget(
  data: MeldCreateRetailWidgetRequest
): Promise<MeldCreateRetailWidgetResponse>
```

| Parameter                | Type                            | Required | Description                                                              |
| ------------------------ | ------------------------------- | -------- | ------------------------------------------------------------------------ |
| `data.sessionType`       | `"BUY" \| "SELL" \| "TRANSFER"` | Yes      | Flow direction.                                                          |
| `data.sessionData`       | `MeldSessionData`               | Yes      | Session configuration (see below).                                       |
| `data.externalSessionId` | `string`                        | No       | Your reference for this session — recorded on Meld's transaction.        |
| `data.customerId`        | `string`                        | No       | Meld customer ID from `createCustomer` or `searchCustomer`.              |
| `data.bypassKyc`         | `boolean`                       | No       | Skip KYC where allowed by the provider and jurisdiction. Use cautiously. |

**`MeldSessionData` fields**

| Field                     | Type       | Required | Description                                                           |
| ------------------------- | ---------- | -------- | --------------------------------------------------------------------- |
| `countryCode`             | `string`   | Yes      | ISO-3166-1 alpha-2 country code.                                      |
| `serviceProvider`         | `string`   | Yes      | Provider chosen from the quote response.                              |
| `sourceCurrencyCode`      | `string`   | Yes      | Source currency.                                                      |
| `sourceAmount`            | `string`   | Yes      | Source amount as a string-encoded decimal.                            |
| `destinationCurrencyCode` | `string`   | Yes      | Destination currency.                                                 |
| `walletAddress`           | `string`   | No       | Wallet address. Pre-fills the widget.                                 |
| `walletTag`               | `string`   | No       | Memo or destination tag for chains that require it.                   |
| `paymentMethodType`       | `string`   | No       | Pre-selects the payment method.                                       |
| `lockFields`              | `string[]` | No       | Field names the user cannot modify (for example `["walletAddress"]`). |
| `redirectUrl`             | `string`   | No       | URL Meld redirects to when the user finishes the flow.                |

**Returns** — `MeldCreateRetailWidgetResponse`: `{ data: { id: string; token: string; customerId: string; externalCustomerId: string; externalSessionId: string; widgetUrl: string } }`.

<Warning>
  `widgetUrl` embeds a single-use session token. Do not persist or reuse it. Refer to [Meld's Provider UI Launch Implementation guide](https://docs.meld.io/docs/whitelabel-api-guide#provider-ui-launch-implementation) for iframe sizing guidance and supported `lockFields` values.
</Warning>

***

### searchRetailTransactions

Lists Meld retail transactions for the current Portal client with optional filtering.

```typescript theme={null}
portal.onReady(async () => {
  const { data } = await portal.ramps.meld.searchRetailTransactions()

  console.log(`${data.count} of ${data.totalCount} transactions`)
  data.transactions.forEach(tx => {
    console.log(tx.id, tx.status, tx.transactionType)
  })

  // With filters
  const pending = await portal.ramps.meld.searchRetailTransactions({
    status: 'PENDING',
    limit: '20',
    offset: '0',
  })
})
```

**Signature**

```typescript theme={null}
public async searchRetailTransactions(
  data?: MeldSearchRetailTransactionsParams
): Promise<MeldSearchRetailTransactionsResponse>
```

| Parameter     | Type     | Required | Description                                                          |
| ------------- | -------- | -------- | -------------------------------------------------------------------- |
| `data.status` | `string` | No       | Filter by transaction status (for example `"SETTLED"`, `"PENDING"`). |
| `data.limit`  | `string` | No       | Maximum number of results to return.                                 |
| `data.offset` | `string` | No       | Number of results to skip for pagination.                            |

**Returns** — `MeldSearchRetailTransactionsResponse`: `{ data: { transactions: MeldTransaction[]; count: number; remaining: number; totalCount: number } }`.

***

### getRetailTransaction

Fetches a single retail transaction by its Meld transaction ID.

```typescript theme={null}
portal.onReady(async () => {
  const { data } = await portal.ramps.meld.getRetailTransaction('tx_01HAAAA')

  console.log(data.transaction.status)
  console.log(data.transaction.destinationAmount, data.transaction.destinationCurrencyCode)
})
```

**Signature**

```typescript theme={null}
public async getRetailTransaction(id: string): Promise<MeldGetRetailTransactionResponse>
```

| Parameter | Type     | Required | Description          |
| --------- | -------- | -------- | -------------------- |
| `id`      | `string` | Yes      | Meld transaction ID. |

**Returns** — `MeldGetRetailTransactionResponse`: `{ data: { transaction: MeldTransaction } }`.

***

### getRetailTransactionBySession

Fetches the retail transaction associated with a widget session ID.

```typescript theme={null}
portal.onReady(async () => {
  const sessionId = widgetResponse.data.id  // From createRetailWidget response

  const { data } = await portal.ramps.meld.getRetailTransactionBySession(sessionId)
  console.log(data.transaction.status)
})
```

**Signature**

```typescript theme={null}
public async getRetailTransactionBySession(
  sessionId: string
): Promise<MeldGetRetailTransactionResponse>
```

| Parameter   | Type     | Required | Description                                                     |
| ----------- | -------- | -------- | --------------------------------------------------------------- |
| `sessionId` | `string` | Yes      | Meld session ID from `createRetailWidget` response (`data.id`). |

**Returns** — `MeldGetRetailTransactionResponse`: `{ data: { transaction: MeldTransaction } }`.

<Note>
  Prefer Meld webhooks over polling for transaction lifecycle updates. Configure them in [Meld Dashboard](https://dashboard.meld.io/) — see [Webhooks](/integrations/On-Off-Ramp/meld-webhooks).
</Note>

***

## Discovery methods

Discovery endpoints return Meld's current catalog of supported countries, currencies, payment methods, and limits. Call them at startup or lazily before populating dropdowns.

<Tip>
  Cache discovery responses client-side — countries, currencies, and payment methods change rarely and re-querying on every page load adds unnecessary latency.
</Tip>

All discovery methods accept an optional `MeldDiscoveryParams` object. It accepts `countryCode` and any additional filter keys Meld documents.

### getServiceProviders

```typescript theme={null}
portal.onReady(async () => {
  const { data } = await portal.ramps.meld.getServiceProviders({ countryCode: 'US' })

  data.forEach(provider => {
    console.log(provider.serviceProvider, provider.name, provider.categories)
  })
})
```

**Signature**

```typescript theme={null}
public async getServiceProviders(
  params?: MeldDiscoveryParams
): Promise<MeldGetServiceProvidersResponse>
```

**Returns** — `MeldGetServiceProvidersResponse`: `{ data: MeldServiceProvider[] }`.

Each `MeldServiceProvider` includes: `serviceProvider`, `name`, `status`, `categories`, `categoryStatuses`, `websiteUrl`, `customerSupportUrl`, and `logos` (dark/light variants).

***

### getCountries

```typescript theme={null}
portal.onReady(async () => {
  const { data } = await portal.ramps.meld.getCountries()

  data.forEach(country => {
    console.log(country.countryCode, country.name)
  })
})
```

**Signature**

```typescript theme={null}
public async getCountries(
  params?: MeldDiscoveryParams
): Promise<MeldGetCountriesResponse>
```

**Returns** — `MeldGetCountriesResponse`: `{ data: MeldCountry[] }`.

Each `MeldCountry` includes: `countryCode`, `name`, `flagImageUrl`, and optional `regions: { regionCode, name }[]`.

***

### getFiatCurrencies

```typescript theme={null}
portal.onReady(async () => {
  const { data } = await portal.ramps.meld.getFiatCurrencies({ countryCode: 'US' })
})
```

**Signature**

```typescript theme={null}
public async getFiatCurrencies(
  params?: MeldDiscoveryParams
): Promise<MeldGetFiatCurrenciesResponse>
```

**Returns** — `MeldGetFiatCurrenciesResponse`: `{ data: MeldFiatCurrency[] }`.

Each `MeldFiatCurrency` includes: `currencyCode`, `name`, `symbolImageUrl`.

***

### getCryptoCurrencies

```typescript theme={null}
portal.onReady(async () => {
  const { data } = await portal.ramps.meld.getCryptoCurrencies()

  // Each token has a chain-specific Meld Code (currencyCode)
  data.forEach(crypto => {
    console.log(crypto.currencyCode, crypto.name, crypto.chainCode)
  })
})
```

**Signature**

```typescript theme={null}
public async getCryptoCurrencies(
  params?: MeldDiscoveryParams
): Promise<MeldGetCryptoCurrenciesResponse>
```

**Returns** — `MeldGetCryptoCurrenciesResponse`: `{ data: MeldCryptoCurrency[] }`.

Each `MeldCryptoCurrency` includes: `currencyCode`, `name`, `chainCode`, `chainName`, `chainId`, `contractAddress`, `symbolImageUrl`.

<Note>
  Meld doesn't accept a separate chain parameter — each token has a distinct `currencyCode` per chain (a "Meld Code"). Pass the same code from the quote into the widget session so pricing and settlement match.
</Note>

***

### getPaymentMethods

```typescript theme={null}
portal.onReady(async () => {
  const { data } = await portal.ramps.meld.getPaymentMethods({ countryCode: 'US' })
})
```

**Signature**

```typescript theme={null}
public async getPaymentMethods(
  params?: MeldDiscoveryParams
): Promise<MeldGetPaymentMethodsResponse>
```

**Returns** — `MeldGetPaymentMethodsResponse`: `{ data: MeldPaymentMethod[] }`.

Each `MeldPaymentMethod` includes: `paymentMethod`, `name`, `paymentType`, and optional `logos`.

***

### getDefaults

Returns the default fiat currency and payment methods per country.

```typescript theme={null}
portal.onReady(async () => {
  const { data } = await portal.ramps.meld.getDefaults({ countryCode: 'US' })

  const usDefaults = data.find(d => d.countryCode === 'US')
  console.log(usDefaults?.defaultCurrencyCode, usDefaults?.defaultPaymentMethods)
})
```

**Signature**

```typescript theme={null}
public async getDefaults(
  params?: MeldDiscoveryParams
): Promise<MeldGetDefaultsResponse>
```

**Returns** — `MeldGetDefaultsResponse`: `{ data: MeldCountryDefault[] }`.

Each `MeldCountryDefault` includes: `countryCode`, `defaultCurrencyCode`, `defaultPaymentMethods: string[]`.

***

### getBuyLimits

Returns minimum, maximum, and default purchase amounts per fiat currency.

```typescript theme={null}
portal.onReady(async () => {
  const { data } = await portal.ramps.meld.getBuyLimits({ countryCode: 'US' })

  const usd = data.find(l => l.currencyCode === 'USD')
  console.log(`USD buy: $${usd?.minimumAmount} – $${usd?.maximumAmount}`)
})
```

**Signature**

```typescript theme={null}
public async getBuyLimits(
  params?: MeldDiscoveryParams
): Promise<MeldGetBuyLimitsResponse>
```

**Returns** — `MeldGetBuyLimitsResponse`: `{ data: MeldFiatCurrencyPurchaseLimit[] }`.

Each `MeldFiatCurrencyPurchaseLimit` includes: `currencyCode`, `defaultAmount`, `minimumAmount`, `maximumAmount`.

***

### getSellLimits

Returns minimum, maximum, and default sell amounts per crypto currency.

```typescript theme={null}
portal.onReady(async () => {
  const { data } = await portal.ramps.meld.getSellLimits()
})
```

**Signature**

```typescript theme={null}
public async getSellLimits(
  params?: MeldDiscoveryParams
): Promise<MeldGetSellLimitsResponse>
```

**Returns** — `MeldGetSellLimitsResponse`: `{ data: MeldCryptoCurrencySellLimit[] }`.

Each `MeldCryptoCurrencySellLimit` includes: `currencyCode`, `chainCode`, `defaultAmount`, `minimumAmount`, `maximumAmount`.

***

### getKycLimits

Returns transaction limits per KYC tier and fiat currency.

```typescript theme={null}
portal.onReady(async () => {
  const { data } = await portal.ramps.meld.getKycLimits()

  const usd = data.find(l => l.currencyCode === 'USD')
  console.log('Level 1 daily limit:', usd?.level1?.dailyLimit)
  console.log('Level 2 monthly limit:', usd?.level2?.monthlyLimit)
})
```

**Signature**

```typescript theme={null}
public async getKycLimits(
  params?: MeldDiscoveryParams
): Promise<MeldGetKycLimitsResponse>
```

**Returns** — `MeldGetKycLimitsResponse`: `{ data: MeldKycFiatLevel[] }`.

Each `MeldKycFiatLevel` includes: `currencyCode` and optional `level1`, `level2`, `level3` of type `MeldKycLimitTier`, each containing `dailyLimit`, `weeklyLimit`, `monthlyLimit`, `yearlyLimit`, and `transactionLimit`.

***

## End-to-end example

The following example shows the recommended buy flow: discover, quote, create a widget session, and look up the result.

```typescript theme={null}
import Portal from '@portal-hq/web'

const portal = new Portal({
  rpcConfig: { 'eip155:1': 'https://...' },
  apiKey: 'YOUR_API_KEY',
})

portal.onReady(async () => {
  // 1. Discover — get buy limits for US
  const limitsRes = await portal.ramps.meld.getBuyLimits({ countryCode: 'US' })
  const usdLimit = limitsRes.data.find(l => l.currencyCode === 'USD')
  console.log(`Min: $${usdLimit?.minimumAmount}, Max: $${usdLimit?.maximumAmount}`)

  // 2. Quote
  const quoteRes = await portal.ramps.meld.getRetailQuote({
    countryCode: 'US',
    sourceCurrencyCode: 'USD',
    destinationCurrencyCode: 'USDC',
    sourceAmount: 100,
    walletAddress: portal.address,
  })
  const best = quoteRes.data.quotes[0]

  // 3. Create widget session
  const widgetRes = await portal.ramps.meld.createRetailWidget({
    sessionType: 'BUY',
    sessionData: {
      countryCode: 'US',
      serviceProvider: best.serviceProvider,
      sourceCurrencyCode: 'USD',
      sourceAmount: '100',
      destinationCurrencyCode: 'USDC',
      walletAddress: portal.address,
    },
  })

  // 4. Open widget in new tab
  if (widgetRes.data.widgetUrl) {
    window.open(widgetRes.data.widgetUrl, '_blank', 'noopener,noreferrer')
  }

  // 5. After the user returns, look up the transaction
  const txRes = await portal.ramps.meld.getRetailTransactionBySession(widgetRes.data.id)
  console.log('Transaction status:', txRes.data.transaction.status)
})
```

***

## Error handling

Wrap calls in `try/catch`. Portal forwards Meld's HTTP status codes (4xx/5xx) and preserves the upstream error message.

Common Portal-side errors:

* `400 Meld integration is not enabled` — turn on the Meld integration for the current Portal environment in the [Portal Dashboard](https://app.portalhq.io).
* `400 Meld API key is not configured for this environment` — paste a valid Meld API key into the integration config.
* `409 Conflict` (on `createCustomer`) — a Meld customer already exists for this Portal client. Call `searchCustomer()` to retrieve it.

## Related documentation

* [Web SDK reference](../reference) (section **portal.ramps.meld (Meld)**)
* [Meld integration overview](/integrations/On-Off-Ramp/meld)
* [White-Label API guide](/integrations/On-Off-Ramp/meld-widget)
* [Webhooks](/integrations/On-Off-Ramp/meld-webhooks)
