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.
Prerequisites
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 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>
| 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 }.
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>
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.
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>
| 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 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.
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>
| 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 } }.
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>
| 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.
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>
| 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.
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>
| Parameter | Type | Required | Description |
|---|
sessionId | string | Yes | Meld session ID from createRetailWidget response (data.id). |
Returns — MeldGetRetailTransactionResponse: { 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>
Returns — MeldGetServiceProvidersResponse: { 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>
Returns — MeldGetCountriesResponse: { 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>
Returns — MeldGetFiatCurrenciesResponse: { 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>
Returns — MeldGetCryptoCurrenciesResponse: { 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>
Returns — MeldGetPaymentMethodsResponse: { 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>
Returns — MeldGetDefaultsResponse: { 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>
Returns — MeldGetBuyLimitsResponse: { 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>
Returns — MeldGetSellLimitsResponse: { 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>
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.
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.