Skip to main content
The Web SDK exposes 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 using your client credentials. You do not call Meld’s servers directly from the browser.
For one-time dashboard setup and webhook configuration, see Meld integration overview. For the end-to-end widget flow using raw HTTP calls, see the White-Label API guide.

Prerequisites

Architecture

LayerRole
Your appCalls portal.ramps.meld.*
Portal Web SDKpostMessage bridge to the Portal iframe (use portal.ramps.meld, not lower-level APIs)
Portal iframeForwards to POST/GET …/api/v3/clients/me/integrations/meld/... with the Portal session
MeldAggregated 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 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.
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
public async createCustomer(
  data: MeldCreateCustomerRequest
): Promise<MeldCreateCustomerResponse>
ParameterTypeRequiredDescription
data.nameMeldCustomerNameNo{ firstName?: string; lastName?: string }
data.emailstringNoCustomer email address.
data.phonestringNoE.164 phone number.
data.dateOfBirthstringNoISO date string (for example "1990-01-15").
data.type"INDIVIDUAL" | "BUSINESS"NoCustomer type.
ReturnsMeldCreateCustomerResponse: { data: MeldCustomer }.
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.

searchCustomer

Returns the Meld customer record(s) associated with the current Portal client.
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
public async searchCustomer(): Promise<MeldSearchCustomerResponse>
ReturnsMeldSearchCustomerResponse: { 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.
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
public async getRetailQuote(
  data: MeldGetRetailQuoteRequest
): Promise<MeldGetRetailQuoteResponse>
ParameterTypeRequiredDescription
data.countryCodestringYesISO-3166-1 alpha-2 country code (for example "US").
data.sourceCurrencyCodestringYesSource currency (fiat for buy; crypto Meld Code for sell).
data.destinationCurrencyCodestringYesDestination currency.
data.sourceAmountnumberYesAmount of source currency to convert.
data.walletAddressstringNoDestination wallet address — informs provider eligibility.
data.customerIdstringNoMeld customer ID when pre-created.
data.paymentMethodTypestringNoFilter to a specific payment method (for example "CREDIT_DEBIT_CARD").
data.serviceProvidersstring[]NoRestrict the quote to specific providers.
data.subdivisionstringNoState/region code when required (for example US states).
ReturnsMeldGetRetailQuoteResponse: { 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.
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
public async createRetailWidget(
  data: MeldCreateRetailWidgetRequest
): Promise<MeldCreateRetailWidgetResponse>
ParameterTypeRequiredDescription
data.sessionType"BUY" | "SELL" | "TRANSFER"YesFlow direction.
data.sessionDataMeldSessionDataYesSession configuration (see below).
data.externalSessionIdstringNoYour reference for this session — recorded on Meld’s transaction.
data.customerIdstringNoMeld customer ID from createCustomer or searchCustomer.
data.bypassKycbooleanNoSkip KYC where allowed by the provider and jurisdiction. Use cautiously.
MeldSessionData fields
FieldTypeRequiredDescription
countryCodestringYesISO-3166-1 alpha-2 country code.
serviceProviderstringYesProvider chosen from the quote response.
sourceCurrencyCodestringYesSource currency.
sourceAmountstringYesSource amount as a string-encoded decimal.
destinationCurrencyCodestringYesDestination currency.
walletAddressstringNoWallet address. Pre-fills the widget.
walletTagstringNoMemo or destination tag for chains that require it.
paymentMethodTypestringNoPre-selects the payment method.
lockFieldsstring[]NoField names the user cannot modify (for example ["walletAddress"]).
redirectUrlstringNoURL Meld redirects to when the user finishes the flow.
ReturnsMeldCreateRetailWidgetResponse: { data: { id: string; token: string; customerId: string; externalCustomerId: string; externalSessionId: string; widgetUrl: string } }.
widgetUrl embeds a single-use session token. Do not persist or reuse it. Refer to Meld’s Provider UI Launch Implementation guide for iframe sizing guidance and supported lockFields values.

searchRetailTransactions

Lists Meld retail transactions for the current Portal client with optional filtering.
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
public async searchRetailTransactions(
  data?: MeldSearchRetailTransactionsParams
): Promise<MeldSearchRetailTransactionsResponse>
ParameterTypeRequiredDescription
data.statusstringNoFilter by transaction status (for example "SETTLED", "PENDING").
data.limitstringNoMaximum number of results to return.
data.offsetstringNoNumber of results to skip for pagination.
ReturnsMeldSearchRetailTransactionsResponse: { data: { transactions: MeldTransaction[]; count: number; remaining: number; totalCount: number } }.

getRetailTransaction

Fetches a single retail transaction by its Meld transaction ID.
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
public async getRetailTransaction(id: string): Promise<MeldGetRetailTransactionResponse>
ParameterTypeRequiredDescription
idstringYesMeld transaction ID.
ReturnsMeldGetRetailTransactionResponse: { data: { transaction: MeldTransaction } }.

getRetailTransactionBySession

Fetches the retail transaction associated with a widget session ID.
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
public async getRetailTransactionBySession(
  sessionId: string
): Promise<MeldGetRetailTransactionResponse>
ParameterTypeRequiredDescription
sessionIdstringYesMeld session ID from createRetailWidget response (data.id).
ReturnsMeldGetRetailTransactionResponse: { data: { transaction: MeldTransaction } }.
Prefer Meld webhooks over polling for transaction lifecycle updates. Configure them in Meld Dashboard — see Webhooks.

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.
Cache discovery responses client-side — countries, currencies, and payment methods change rarely and re-querying on every page load adds unnecessary latency.
All discovery methods accept an optional MeldDiscoveryParams object. It accepts countryCode and any additional filter keys Meld documents.

getServiceProviders

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

  data.forEach(provider => {
    console.log(provider.serviceProvider, provider.name, provider.categories)
  })
})
Signature
public async getServiceProviders(
  params?: MeldDiscoveryParams
): Promise<MeldGetServiceProvidersResponse>
ReturnsMeldGetServiceProvidersResponse: { data: MeldServiceProvider[] }. Each MeldServiceProvider includes: serviceProvider, name, status, categories, categoryStatuses, websiteUrl, customerSupportUrl, and logos (dark/light variants).

getCountries

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

  data.forEach(country => {
    console.log(country.countryCode, country.name)
  })
})
Signature
public async getCountries(
  params?: MeldDiscoveryParams
): Promise<MeldGetCountriesResponse>
ReturnsMeldGetCountriesResponse: { data: MeldCountry[] }. Each MeldCountry includes: countryCode, name, flagImageUrl, and optional regions: { regionCode, name }[].

getFiatCurrencies

portal.onReady(async () => {
  const { data } = await portal.ramps.meld.getFiatCurrencies({ countryCode: 'US' })
})
Signature
public async getFiatCurrencies(
  params?: MeldDiscoveryParams
): Promise<MeldGetFiatCurrenciesResponse>
ReturnsMeldGetFiatCurrenciesResponse: { data: MeldFiatCurrency[] }. Each MeldFiatCurrency includes: currencyCode, name, symbolImageUrl.

getCryptoCurrencies

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
public async getCryptoCurrencies(
  params?: MeldDiscoveryParams
): Promise<MeldGetCryptoCurrenciesResponse>
ReturnsMeldGetCryptoCurrenciesResponse: { data: MeldCryptoCurrency[] }. Each MeldCryptoCurrency includes: currencyCode, name, chainCode, chainName, chainId, contractAddress, symbolImageUrl.
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.

getPaymentMethods

portal.onReady(async () => {
  const { data } = await portal.ramps.meld.getPaymentMethods({ countryCode: 'US' })
})
Signature
public async getPaymentMethods(
  params?: MeldDiscoveryParams
): Promise<MeldGetPaymentMethodsResponse>
ReturnsMeldGetPaymentMethodsResponse: { data: MeldPaymentMethod[] }. Each MeldPaymentMethod includes: paymentMethod, name, paymentType, and optional logos.

getDefaults

Returns the default fiat currency and payment methods per country.
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
public async getDefaults(
  params?: MeldDiscoveryParams
): Promise<MeldGetDefaultsResponse>
ReturnsMeldGetDefaultsResponse: { data: MeldCountryDefault[] }. Each MeldCountryDefault includes: countryCode, defaultCurrencyCode, defaultPaymentMethods: string[].

getBuyLimits

Returns minimum, maximum, and default purchase amounts per fiat currency.
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
public async getBuyLimits(
  params?: MeldDiscoveryParams
): Promise<MeldGetBuyLimitsResponse>
ReturnsMeldGetBuyLimitsResponse: { data: MeldFiatCurrencyPurchaseLimit[] }. Each MeldFiatCurrencyPurchaseLimit includes: currencyCode, defaultAmount, minimumAmount, maximumAmount.

getSellLimits

Returns minimum, maximum, and default sell amounts per crypto currency.
portal.onReady(async () => {
  const { data } = await portal.ramps.meld.getSellLimits()
})
Signature
public async getSellLimits(
  params?: MeldDiscoveryParams
): Promise<MeldGetSellLimitsResponse>
ReturnsMeldGetSellLimitsResponse: { data: MeldCryptoCurrencySellLimit[] }. Each MeldCryptoCurrencySellLimit includes: currencyCode, chainCode, defaultAmount, minimumAmount, maximumAmount.

getKycLimits

Returns transaction limits per KYC tier and fiat currency.
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
public async getKycLimits(
  params?: MeldDiscoveryParams
): Promise<MeldGetKycLimitsResponse>
ReturnsMeldGetKycLimitsResponse: { 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.
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.
  • 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.