The virtual account flow is available white label, aka fully API driven, from Meld. The user can buy or sell digital assets in your UI, besides going to their banking app to send money. You will build your own UI on top of Meld’s APIs for this product.
In the Virtual Account flow, the onramp will create a virtual bank account for each user and return those bank details via Meld’s API. In a buy transaction, the user will send money to that virtual bank account and receive crypto to their wallet. In a sell transaction the user will send crypto to the onramp and then be paid in fiat from the virtual bank account to the user’s own bank account.
This quickstart outlines both the buy and sell flow, however you should pick the one most relevant to your use case and execute it end to end first before moving onto the second flow.
For terminology, note that onramping, buying crypto, and payin mean the same thing. Similarly offramping, selling crypto, and payouts mean the same thing.
Integration Quickstart
This quickstart is for developers integrating Meld’s Virtual Account product for the first time. You’ll learn the API sequence — subaccount → quote → order → poll → webhook — needed to run an onramp or offramp end-to-end.
Time required: 30 minutes Difficulty: API knowledge required
Before you begin
- A Meld dashboard invitation (sandbox to start)
- You have KYB/KYCed with at least one virtual-account provider (Noah, Due, or Brale) and received your provider account/customer ID
- Ability to make authenticated REST calls (curl, Postman, or your language of choice)
- A test wallet address for the destination token (onramp) or test bank account (offramp)
Sandbox limitation: You can create orders in sandbox to see the API shape, but you cannot send real money or settle transactions. Step 5 (sending funds) and the resulting SETTLED status will only occur in production.
Step 1: Get Your API Key
Your API key is required for all Meld API calls.
Process:
- Check your email for the Meld dashboard invitation
- Click the invitation link and log in
- In the dashboard, navigate to Developer > API Keys
- Click “Reveal Key”
- Copy and save your API key securely
Important: Always add BASIC before your API key in all requestsExample: BASIC W9kZTT7332okCEc1A9aqAq:3sYKoXQv6oHVHSts7G2agw9vTCXz
Step 2: Create a Meld SubAccount
In order to complete this step, you must’ve already KYBed / KYCed with the onramp in question and received your serviceProviderAccountId from the onramp. This is a one time step per user / business to set up a subaccount with Meld. Brale calls this id an accountId while Noah and Due call it a customerId.
API Call Details:
- Endpoint:
POST /accounts/sub-accounts
- Authorization:
BASIC [your-api-key]
Request Body:
{
"serviceProvider": "NOAH",
"name": "My First Customer",
"serviceProviderAccountId": "2nLpyIBLJDtlsesN8bgQGZYZbT5"
}
Expected Response:
✅ 200 status code with confirmation that the subAccount was created
{
"id": "WeP9eoFziQX4yXE5iiGqJE",
"name": "My First Customer",
"status": "ACTIVE",
"serviceProviderSubAccount": {
"id": "2nLpyIBLJDtlsesN8bgQGZYZbT5",
"serviceProvider": "NOAH",
"serviceProviderAccessProfileId": "W9233jn2jn2jncjd",
"status": "ACTIVE"
}
}
You will need the id from this response as the subAccountId to use for all orders that you make.
Step 3: Test Your API Connection
Verify your API key by requesting a price quote.
API Call Details:
- Endpoint:
POST /payments/virtual-account/crypto/quote
- Authorization:
BASIC [your-api-key]
Testing options
Buy Flow Example (aka Payin)
Request Body:
{
"countryCode": "US",
"sourceCurrencyCode": "USD",
"destinationCurrencyCode": "USDC",
"paymentMethodType": "ACH",
"sourceAmount": 100
}
Expected Response:
✅ 200 status code with quote details
Note that Brale doesn’t support quotes. All of their exchanges are 1:1 USD to USD backed stablecoins. Aka for $100 you will receive 100 USDC.
Here is an example of a single quote. You can expect multiple quotes, 1 per onramp, in the response.
{
"quotes": [
{
"transactionType": "CRYPTO_PURCHASE",
"sourceAmount": 100.00,
"sourceAmountWithoutFees": 98.50,
"fiatAmountWithoutFees": 98.50,
"destinationAmountWithoutFees": null,
"sourceCurrencyCode": "USD",
"countryCode": "US",
"totalFee": 1.50,
"networkFee": 0.10,
"partnerFee": 0.40,
"transactionFee": 1.00,
"destinationAmount": 98.50,
"destinationCurrencyCode": "USDC",
"exchangeRate": 0.9850,
"paymentMethodType": "ACH",
"serviceProvider": "NOAH",
"rampIntelligence": {
"rampScore": 21.66,
"lowKyc": false
}
}
]
}
Sell Flow Example (aka Payout)
Request Body:
{
"countryCode": "US",
"sourceCurrencyCode": "USDC",
"destinationCurrencyCode": "USD",
"paymentMethodType": "ACH",
"sourceAmount": 100
}
Expected Response:
✅ 200 status code with quote details
Note that Brale doesn’t support quotes. All of their exchanges are 1:1 USD to USD backed stablecoins. Aka for $100 you will receive 100 USDC.
Here is an example of a single quote. You can expect multiple quotes, 1 per onramp, in the response.
{
"quotes": [
{
"transactionType": "CRYPTO_SELL",
"sourceAmount": 100.00,
"sourceAmountWithoutFees": 98.50,
"fiatAmountWithoutFees": 100.00,
"destinationAmountWithoutFees": null,
"sourceCurrencyCode": "USDC",
"countryCode": "US",
"totalFee": 1.50,
"networkFee": 0.10,
"partnerFee": 0.40,
"transactionFee": 1.00,
"destinationAmount": 95.50,
"destinationCurrencyCode": "USD",
"exchangeRate": 0.9850,
"paymentMethodType": "ACH",
"serviceProvider": "NOAH",
"rampIntelligence": {
"rampScore": 21.66,
"lowKyc": false
}
}
]
}
Step 4: Create an Order (Buy or Sell)
Note that in sandbox, you will not be able to complete a transaction, even a test one, so this is the last step you will get to. This endpoint needs to be called once per transaction.
Buy Flow Example (aka Payin)
API Call Details:
- Endpoint:
POST /payments/virtual-account/ramp/onramp/order
- Authorization:
BASIC [your-api-key]
Request Body:
{
"subAccountId": "WeP9eoFziQX4yXE5iiGqJE",
"sourceAmount": 100.00,
"sourceCurrencyCode": "USD",
"destinationCurrencyCode": "USDC",
"destinationWalletAddress": "0xC38dA09A61d2686aB64BF119Da37110Ecd089efE",
"paymentMethodType": "ACH",
"serviceProvider": "NOAH"
}
Note: Replace the walletAddress with your own
Expected Response:
✅ 200 status code with the virtual bank account information to send money to.
{
"orderId": "WeP9eoFziQX4yXE5abcfec",
"paymentMethodType": "ACH",
"receivingBankInformation": { // the VBA the onramp created for the user to send money to
"accountNumber": "12345678",
"routingNumber": "987654321"
},
"serviceProviderDetails": {
<raw data from the onramp>
}
}
This is as far as you can get in sandbox. You cannot send money to the virtual account or to complete the transaction. However in production you will be able to do so, and this is how the flow continues.
Sell Flow Example (aka Payout)
API Call Details:
- Endpoint:
POST /payments/virtual-account/ramp/offramp/order
- Authorization:
BASIC [your-api-key]
Request Body:
{
"subAccountId": "WeP9eoFziQX4yXE5iiGqJE",
"sourceAmount": 100.00,
"sourceCurrencyCode": "USDC",
"destinationCurrencyCode": "USD",
"sourceWalletAddress": "0xC38dA09A61d2686aB64BF119Da37110Ecd089efE", // aka the user's wallet
"paymentMethod": {
"type": "ACH",
"details": { // the user's information
"owner": "John Doe",
"name": "Chase Bank",
"accountType": "CHECKING",
"routingNumber": "021000021",
"accountNumber": "123456789",
"beneficiaryAddress": {
"street_line_1": "123 Main St",
"street_line_2": "Apt 4B",
"city": "New York",
"state": "NY",
"zip": "10001"
},
"bankAddress": {
"street_line_1": "270 Park Ave",
"street_line_2": "Ste 48",
"city": "New York",
"state": "NY",
"zip": "10017"
}
}
},
"serviceProvider": "NOAH"
}
Expected Response:
✅ 200 status code with the onramp’s wallet address to send crypto to.
{
"orderId": "WePFb16J4KjrRar71hbmtG",
"paymentMethodType": "ACH",
"walletAddress": "0xC38dA09A61d2686aB64BF119Da37110Ecd089efE" // the onramp's wallet address
}
This is as far as you can get in sandbox. You cannot send crypto to the onramp’s wallet to complete the transaction. However in production you will be able to do so, and this is how the flow continues.
Save the orderId to use to fetch the transaction details.
Step 5: Complete the Transaction:
For onramp, go to your bank app and initiate a transfer of the amount to the bank information of the virtual bank account, and hit send. Note that the transfer time depends on your bank and the bank transfer rails in your country.
For offramp, go to your wallet and initiate a transfer of the token amount to the onramp’s wallet address, and hit send. Note that the transfer time depends on which chain the token is on, but should be in the order of minutes.
Step 6: Verify Your Transaction
Fetch Transaction Details
Call GET /payments/transactions (filtering by the orderId from the previous step) to view your transaction details.
Buy Flow Example (aka Payin)
Expected Response:
✅ 200 status code with transaction details
{
"transaction": {
"id": "WePZCYJW7cdXR7SxUMp8mE",
"parentPaymentTransactionId": null,
"accountId": "WQ5RyhdFzE45qjsomdzQ1u",
"isPassthrough": false,
"passthroughReference": null,
"isImported": false,
"customer": {
"id": "WePZCYZjAK97cJWokfH3Cc",
"accountId": "WQ5RyhdFzE45qjsomdzQ1u",
"externalId": "YC100"
},
"transactionType": "CRYPTO_PURCHASE",
"status": "SETTLED",
"sourceAmount": 100.00,
"sourceCurrencyCode": "USD",
"destinationAmount": 98.50,
"destinationCurrencyCode": "USDC",
"paymentMethodType": "ACH",
"serviceProvider": "NOAH",
"serviceTransactionId": "1f0d470c-f5c2-6fc2-a54d-b6cdd6b6f014",
"orderId": null,
"description": null,
"externalReferenceId": "testSession",
"serviceProviderDetails": {
**raw data from the onramp**
},
"multiFactorAuthorizationStatus": null,
"createdAt": "2025-12-08T20:03:07.173223Z",
"updatedAt": "2025-12-08T20:08:08.877106Z",
"countryCode": "US",
"sessionId": "WePZCbr96gAUxgu7Auvm4q",
"externalSessionId": "testSession",
"paymentDetails": null,
"externalCustomerId": "testCustomer",
"fiatAmountInUsd": 100.00,
"sessionClientTags": null,
"serviceProviderTransactionUrl": null,
"serviceProviderCreatedAt": "2025-12-08T20:02:22Z",
"cryptoDetails": {
"sourceWalletAddress": null,
"destinationWalletAddress": "0xd72cc3468979360e31bc83b84f0887deccfd81d5",
"sessionWalletAddress": "0xd72cc3468979360e31bc83b84f0887deccfd81d5",
"totalFee": 1.50,
"networkFee": 0.10,
"partnerFee": 0.40,
"transactionFee": 1.00,
"totalFeeInUsd": 1.50,
"networkFeeInUsd": 0.10,
"partnerFeeInUsd": 0.40,
"transactionFeeInUsd": 1.00,
"blockchainTransactionId": "0x553d295955a978ed3e9fc1717b5bcb903c69577e49c8ad255abece945ffa9ba0",
"institution": null,
"chainId": "1"
}
}
}
Sell Flow Example (aka Payout)
Expected Response:
✅ 200 status code with transaction details
{
"transaction": {
"id": "WePZCYJW7cdXR7SxUMp8mE",
"parentPaymentTransactionId": null,
"accountId": "WQ5RyhdFzE45qjsomdzQ1u",
"isPassthrough": false,
"passthroughReference": null,
"isImported": false,
"customer": {
"id": "WePZCYZjAK97cJWokfH3Cc",
"accountId": "WQ5RyhdFzE45qjsomdzQ1u",
"externalId": "YC100"
},
"transactionType": "CRYPTO_SELL",
"status": "SETTLED",
"sourceAmount": 100.00,
"sourceCurrencyCode": "USDC",
"destinationAmount": 98.50,
"destinationCurrencyCode": "USD",
"paymentMethodType": "ACH",
"serviceProvider": "NOAH",
"serviceTransactionId": "1f0d470c-f5c2-6fc2-a54d-b6cdd6b6f015",
"orderId": null,
"description": null,
"externalReferenceId": "testSession",
"serviceProviderDetails": {
**raw data from the onramp**
},
"multiFactorAuthorizationStatus": null,
"createdAt": "2025-12-08T20:03:07.173223Z",
"updatedAt": "2025-12-08T20:08:08.877106Z",
"countryCode": "US",
"sessionId": "WePZCbr96gAUxgu7Auvm4q",
"externalSessionId": "testSession",
"paymentDetails": null,
"externalCustomerId": "testCustomer",
"fiatAmountInUsd": 100.00,
"sessionClientTags": null,
"serviceProviderTransactionUrl": null,
"serviceProviderCreatedAt": "2025-12-08T20:02:22Z",
"cryptoDetails": {
"sourceWalletAddress": null,
"destinationWalletAddress": "0xd72cc3468979360e31bc83b84f0887deccfd81d5",
"sessionWalletAddress": "0xd72cc3468979360e31bc83b84f0887deccfd81d5",
"totalFee": 1.50,
"networkFee": 0.10,
"partnerFee": 0.40,
"transactionFee": 1.00,
"totalFeeInUsd": 1.50,
"networkFeeInUsd": 0.10,
"partnerFeeInUsd": 0.40,
"transactionFeeInUsd": 1.00,
"blockchainTransactionId": "0x553d295955a978ed3e9fc1717b5bcb903c69577e49c8ad255abece945ffa9ba0",
"institution": null,
"chainId": "1"
}
}
}
Dashboard Verification:
- Navigate to the Transactions tab
- If your transaction isn’t visible:
- Click the Status dropdown
- Select “Select All”
- Your transaction should appear
✅ Virtual Account integration complete. You can now build a custom virtual-account flow.
➡️ Build Custom UI Guide — complete implementation guide
Next steps
If you’re ready to begin your integration, see the end-to-end Virtual Account Guide.
Troubleshooting
Common Issues and Solutions:
🚫 401 Unauthorized
- Ensure
BASIC is added before your API key
- Check for extra spaces or incorrect formatting
🚫 Webhook Not Received
- Verify URL is publicly accessible
- Check firewall settings
- Ensure webhook endpoint returns 200 status
🚫 Transaction Not Visible
- Change status filter to “Select All” in dashboard
- Wait 30 seconds and refresh
- Check if using correct environment (sandbox vs production)