Skip to main content

Build Your Own UI with Meld APIs

This guide is for developers building a custom crypto interface on top of Meld’s White-Label API. You call Meld directly with Authorization: BASIC {apiKey} and own every screen — country selection, amounts, payment methods, quotes, and transaction tracking — while Meld handles pricing, provider routing, sessions, and webhooks, and the selected provider handles KYC, payment collection, and compliance. You’ll learn the exact sequence of API calls for both directions:
  • On-ramp (buy) — fiat → crypto, category = CRYPTO_ONRAMP, session BUY.
  • Off-ramp (sell) — crypto → fiat, category = CRYPTO_OFFRAMP, session SELL.
Discovery is documented once in The discovery model, then each direction has its own ordered flow section that shows only its ordering, session type, and (for sell) the deposit-address step.

Before you begin

  • API key from the Meld dashboard (see White-Label Quickstart Step 1)
  • Webhook endpoint configured (recommended; you can also poll the API)
  • Basic understanding of REST APIs and JSON
  • Sandbox environment — base URL https://api-sb.meld.io (production is https://api.meld.io)
Every request sends:
Sandbox and production credentials, base URLs, and widget URLs are separate. Develop against https://api-sb.meld.io until your flow is ready, then switch credentials and the base URL to https://api.meld.io to go live.
Important: This is White-Label API Integration, NOT Meld Checkout Integration (Public Key URL). This approach requires custom UI development and API integration.

CRITICAL: API Changes & CompatibilityNon-Breaking Changes (No Versioning):
  • New fields may be added to API responses at any time
  • Your system MUST handle unexpected fields gracefully
  • Avoid strict JSON validation that rejects unknown properties
Breaking Changes (Versioned):
  • Field modifications or deletions will be versioned
  • Released approximately with advance notice
Required Actions:
  • ✅ Use flexible JSON parsing (ignore unknown fields)
  • ✅ Implement defensive coding practices
  • ✅ Test with mock responses containing extra fields
IMPORTANT: Automatic Provider ManagementAutomatic Additions:
  • Meld may automatically enable providers for your account based on:
    • Conversion rates and user success
    • Competitive pricing
    • Geographic coverage for your users
Automatic Removals:
  • Providers may be disabled due to:
    • Compliance or regulatory issues
    • Technical problems or outages
    • Provider service interruptions
Manual Control:
  • Contact Meld to opt out of automatic management
  • Request specific provider additions/removals
  • Set custom provider preferences for your account

Technical Architecture

Your UI talks only to Meld’s APIs for discovery, pricing, and sessions. When the user is ready to pay (buy) or get paid (sell), you hand off to the selected provider’s hosted UI, then track the result through webhooks and the transactions endpoint.
Off-ramp variant: the sell flow adds one handoff back to your UI. After the provider session is created and the user confirms in the provider UI, Meld returns a deposit address on the transaction. Your UI must display it and instruct the user to send the exact crypto amount to it. See Off-ramp flow Step 6.

The discovery model

All discovery is parameterized by a category. Retail ramp uses exactly two:
Routes follow direction, not fiat/crypto. On GET /network-partner/supported/routes/{category}/{countryCode}/{sourceCurrencyCode}/{destinationCurrencyCode}:
  • On-ramp: source = fiat, destination = crypto.../CRYPTO_ONRAMP/US/USD/USDC
  • Off-ramp: source = crypto, destination = fiat.../CRYPTO_OFFRAMP/US/USDC/USD
Putting the segments in the wrong order returns HTTP 200 with an empty [] — not a 404 — which silently starves payment-method and limit discovery. Always order by direction.

Upgrading from the Service Provider API?

If you previously integrated against the legacy /service-providers/* discovery endpoints, they are replaced by the category-scoped /network-partner/* family used throughout this guide. The three transaction endpoints are unchanged: POST /payments/crypto/quote, POST /crypto/session/widget, and GET /payments/transactions/{transactionId}.
Swap variants (CRYPTO_ONRAMP_SWAP / CRYPTO_OFFRAMP_SWAP) and virtual-account categories also exist but are out of scope for this retail buy/sell guide.

Discovery endpoint reference

These five endpoints back both flows. Each example shows the on-ramp and off-ramp variant so you can see where shapes and ordering differ.

Supported countries

Response
  • The set of countries can differ by direction.
  • flag is a hosted image URL, not an emoji — render it as an <img>.
  • subdivisions returns ISO 3166-2 codes (e.g. US-CA) when a country has regional coverage differences. Collect the user’s state/region and pass it as subdivision to Supported routes.

Country defaults

Country (ISO 3166-1 alpha-2, e.g. US) and category (CRYPTO_ONRAMP or CRYPTO_OFFRAMP) are path params. Returns a single object with the preselected pay-with / payout currency and payment methods for that country.
Fall back to the first item from /network-partner/supported/currencies and /network-partner/supported/payment-methods if the response omits a field for a given country/category.

Supported currencies

One endpoint serves both fiat and crypto, selected by type. Each currency is { currencyCode, name, decimalPlaces, type, symbol, chainCode?, contract? }.
  • currencyCode — the ticker/id you send downstream (e.g. USD, USDC).
  • decimalPlaces — use for client-side formatting of the amount input.
  • symbol — a hosted image URL for the currency logo (not a ticker). Render as an <img>.
  • chainCode / contract — disambiguate tokens on multi-chain networks.
On-ramp crypto response

Supported payment methods

Returns a bare array of { countryCode, providerMethod, method, name, type, logo?, headlessSupported? }.
  • method — the canonical id (e.g. ACH, SEPA, CREDIT_DEBIT_CARD). This is the value you send downstream as paymentMethodType.
  • name — human-readable display label (“ACH Bank Transfer”). Never send name to Meld.
  • headlessSupportedtrue if the rail supports a headless (server-to-server, no provider UI) checkout for this method. Prefer these when your flow doesn’t need the provider’s hosted UI.
Send method, never name. Sending a display label or the wrong rail for a country/currency yields NO_VALID_QUOTES.
Off-ramp response (payout rails)

Supported routes

Routes return the partners and payment methods that serve a currency pair, and the per-method min/max limits (which replaced the standalone limits endpoint).
  • On-ramp: source = fiat, destination = crypto.../CRYPTO_ONRAMP/US/USD/USDC
  • Off-ramp: source = crypto, destination = fiat.../CRYPTO_OFFRAMP/US/USDC/USD
Each route is:
On-ramp response
Off-ramp response
To validate the user’s amount, read route.paymentMethods[].limits for the chosen partner + method. The currencyCode on limits tells you which currency the min/max are expressed in: for on-ramp it’s the fiat the user pays; for off-ramp it’s the fiat the user receives. Because limits live on the route, a buy route and a sell route for the same pair can have different min/max.kycLimits — an optional array of tier-based limits that some partners apply on top of the per-method limits (e.g. higher caps after enhanced KYC). Usually empty; treat as advisory unless a partner returns entries.

On-ramp flow (buy)

category = CRYPTO_ONRAMP throughout. Currency flow is fiat → crypto, session type is BUY.
1

Resolve country and defaults

Auto-detect the user’s country from their device/browser (geolocation or IP) where possible. For a manual override list and availability check, call:
Then fetch defaults to preselect the pay-with currency and rail:
2

Load fiat and crypto options

Populate the pay-with currency menu and the buy-asset selector:
Use decimalPlaces to format the amount input, currencyCode/name for the label, symbol (image URL) for the logo, and chainCode/contract to disambiguate multi-chain tokens.
On-ramp buy amount-entry screen

On-ramp amount entry: the crypto buy-asset selector, destination wallet, pay-with currency, and quick-amount chips — the inputs assembled from the discovery calls above.

3

Get routes for the chosen pair (methods + limits)

With a fiat → crypto pair selected, fetch the routes that serve it. This returns the selectable rails and their min/max limits. Pass subdivision for the US state when applicable.
Consume route.paymentMethods[].name (the rails), ...limits.{min,max,currencyCode} (validate the amount), partner, and headlessSupported. Alternatively (or in addition), list rails explicitly:
Both yield the canonical method/name id you send as paymentMethodType.
4

Fetch a real-time quote

Send walletAddress on the request — Ramp Intelligence keys off the destination wallet address (not customerId / externalCustomerId) to rank quotes for the user. On this endpoint sourceAmount is a number.
For on-ramp, sourceCurrencyCode is fiat and destinationCurrencyCode is crypto. Rank the returned quotes[] by rampIntelligence.rampScore (see Quote ranking), then let the user pick. Capture the chosen quote’s serviceProvider.
Pass an optional serviceProviders: ["TRANSAK", "BANXA"] array on the request body to restrict which providers can quote (allow-list). Omit it to receive quotes from every provider linked to your API key.
5

Create the widget session

Use the nested { sessionType, sessionData } body. On this endpoint sessionData.sourceAmount is a string (it was a number on the quote endpoint).
The response returns serviceProviderWidgetUrl (preferred — the provider’s hosted checkout), widgetUrl (Meld-hosted fallback), token, id, and externalSessionId.
6

Launch the provider UI and track the result

Open serviceProviderWidgetUrl (popup or new tab on desktop; redirect on mobile). The provider runs its own KYC and collects payment. Completion is signaled by your redirectUrl and/or postMessage, but authoritative status comes from webhooks. See Launch provider UI and Track the transaction.

Off-ramp flow (sell)

category = CRYPTO_OFFRAMP throughout. Currency flow is crypto → fiat, session type is SELL. The structural differences from buy are flagged inline.
1

Resolve country and defaults

The sell-supported country set can differ from buy. Fetch it and the payout defaults:
Defaults preselect the payout fiat (currencyCode) and payout rail (paymentMethods[]).
2

Load sellable crypto and payout fiat options

The type=CRYPTO list is what the user can sell; the type=FIAT list is what they can receive. Use decimalPlaces to format the crypto-units input and show a ≈ fiat preview.
Off-ramp sell amount-entry screen with a live quote

Off-ramp amount entry: the crypto-units input shows a live ≈ fiat payout, the route's min–max limits, the selected payout rail, and the best provider quote — populated from supported/routes and /payments/crypto/quote.

3

Get routes — CRYPTO → FIAT ordering

Off-ramp ordering trap: put source = crypto and destination = fiat. Putting fiat first returns HTTP 200 with an empty [] and silently breaks the whole sell screen.
Consume route.paymentMethods[].name (payout rails), ...limits (validate the amount), and partner.
4

Select a payout method

currencyCode here is the payout fiat. Choose a payout rail and send its method (e.g. ACH, SEPA, LOCAL_BANK_TRANSFER, PAYOUT_TO_CARD) as paymentMethodType — never name. Prefer a headlessSupported rail when offered.
5

Fetch a quote — reversed currency ordering

The source/destination ordering is what makes this a sell: sourceCurrencyCode is crypto, destinationCurrencyCode is fiat. sourceAmount is a number in crypto units.
destinationAmount on each quote is the fiat the user receives. Capture the chosen quote’s serviceProvider.
6

Create the session, launch, and surface the deposit address

Create the session with sessionType: "SELL". Here sessionData.walletAddress is the user’s own wallet (the one they send crypto from / are refunded to) — not a deposit address. sourceAmount is a string.
Open serviceProviderWidgetUrl; the provider collects payout-bank details and runs KYC. After the widget completes, fetch the transaction (or look it up by your externalSessionId) and read the deposit address from cryptoDetails.offrampDestinationWalletAddress:
Never use cryptoDetails.walletAddress — for off-ramp that is the customer’s own origin wallet, not the deposit address. Display offrampDestinationWalletAddress (as a QR code plus a copyable string) and instruct the user to send exactly the quoted crypto amount to it. A wrong address or amount means lost funds — show a hard warning.

Quote ranking (Ramp Intelligence)

POST /payments/crypto/quote returns multiple provider quotes. Each carries a rampScore (via rampIntelligence.rampScore) representing the likelihood of transaction success based on historical conversion rates, provider reliability, location and payment-method compatibility, and real-time provider performance.
Quote (abridged)
  • rampScore — Meld’s recommendation score (higher = better conversion likelihood).
  • lowKyc — whether the provider requires minimal identity verification.
Display the highest rampScore quote first to maximize transaction success rates. For off-ramp, the same ranking applies — just compare destinationAmount (fiat received) as the tie-break.📊 Learn More: See Ramp Intelligence for advanced ranking strategies.

Launch the provider UI

When the session is created, open the provider’s payment UI in a webview, new tab, or redirect.
widgetUrl returns a Meld URL that iframes the provider’s URL (or full-redirects for providers that block iframing). serviceProviderWidgetUrl returns the provider’s URL directly. You can launch either; we recommend serviceProviderWidgetUrl. The only difference between buy and sell is the sessionType you sent when creating the session.
Redirect Domain Whitelisting: Some providers require redirect-domain whitelisting. Contact Meld support if provider UIs don’t redirect back properly to your redirectUrl.
Transaction Completion: Meld does not send completion events or use redirect parameters. When the flow is complete, Meld simply redirects the user back to your redirectUrl. Track transaction status through webhooks, not redirect handling.

Track the transaction

Monitor progress and update your UI as the user completes the flow.
Webhook Setup Required: Sign up for webhooks in the Meld Dashboard. You receive a webhook each time a transaction is created or updated. See Webhook events for payload examples.
Use the webhook as a trigger to fetch the full transaction. Match the inbound event/redirect to your externalSessionId.
The response is wrapped in { "transaction": { ... } }. The inner object carries status, amounts, and a cryptoDetails block. cryptoDetails.walletAddress is direction-dependent: on on-ramp it’s the user’s destination wallet (where the crypto is delivered); on off-ramp it’s the user’s origin wallet (where they send from / receive refunds). For the off-ramp deposit address, read cryptoDetails.offrampDestinationWalletAddress — see Off-ramp flow Step 6.
For off-ramp, the same endpoint also carries the deposit address (cryptoDetails.offrampDestinationWalletAddress) used in the sell flow. See Off-ramp flow Step 6.Status Reference: See Transaction Statuses for complete definitions.

API response caching guide

Cache static discovery data; never cache pricing, sessions, or transaction state. Key caches by every parameter that changes the response — category, country, type, currencyCode, and (for routes) source/destination/subdivision — so a buy cache entry never serves a sell request.

Best practices & tips

Performance

  • Cache static discovery for up to 1 week; keep supported/routes on a short TTL because limits change.
  • Debounce quote requests while the user types an amount.
  • Preload country/currency data on app startup.

User experience

  • Show loading states during API calls.
  • Display fees clearly before the user commits.
  • Validate amounts against route.paymentMethods[].limits in the correct currency.
  • Handle errors gracefully with retry options.

Error handling

Two discovery pitfalls to guard against:
  • NO_VALID_QUOTES — usually means you sent a paymentMethodType that isn’t valid for the country/currency, or sent a display name instead of the canonical method id. Re-derive the method from supported/payment-methods / route methods.
  • Empty 200 [] from supported/routes — almost always wrong source/destination ordering. On-ramp is fiat → crypto; off-ramp is crypto → fiat. An empty array is silent, not a 404 — assert it’s non-empty before rendering rails or limits.

Security

  • Never expose API keys in frontend code — proxy Meld calls through your backend.
  • Validate all user inputs before API calls.
  • Use HTTPS only for API communications.
  • Implement rate limiting to prevent abuse.

Next steps

Testing your implementation

Advanced features

Production deployment

Support & resources

  • FAQ — common questions answered
This guide provides everything needed to build a production-ready buy and sell interface. Most teams complete their custom UI integration within 1-2 weeks using this approach.