Documentation Index
Fetch the complete documentation index at: https://mintlify.com/blindpaylabs/blindpay-node/llms.txt
Use this file to discover all available pages before exploring further.
Virtual accounts provide dedicated US bank account details for receivers to accept recurring fiat payments that automatically convert to stablecoins. This eliminates the need to create a new payin for each transaction.
Overview
A virtual account gives your receiver:
- Dedicated ACH, Wire, and RTP routing/account numbers
- Automatic fiat-to-crypto conversion
- Direct deposit to a blockchain wallet
- Support for recurring payments
Virtual accounts are currently available for US dollar (USD) payments through banking partners JPMorgan, Citi, and HSBC.
Prerequisites
Before creating a virtual account:
- Receiver must have completed KYC/KYB
- Receiver must have an approved KYC status
- A blockchain wallet must be registered for the receiver
- Accept the banking partner’s terms of service
Step 1: Create a Blockchain Wallet
Register blockchain wallet
First, create a blockchain wallet for the receiver to receive converted stablecoins:import { BlindPay } from '@blindpay/sdk';
const blindpay = new BlindPay({
apiKey: process.env.BLINDPAY_API_KEY,
instanceId: process.env.BLINDPAY_INSTANCE_ID
});
// For Account Abstraction wallets (recommended)
const { data: wallet, error } = await blindpay.wallets.blockchain.createWithAddress({
receiver_id: 're_000000000000',
name: 'Primary Wallet',
network: 'base',
address: '0xYourWalletAddress'
});
if (error) {
console.error('Error creating wallet:', error.message);
return;
}
console.log('Wallet created:', {
id: wallet.id,
address: wallet.address,
network: wallet.network
});
Use Account Abstraction wallets (createWithAddress) for better UX. Traditional wallets require signature verification (createWithHash).
Step 2: Create Virtual Account
Create the virtual account
Create a virtual account linked to the blockchain wallet:const { data: virtualAccount, error } = await blindpay.virtualAccounts.create({
receiver_id: 're_000000000000',
banking_partner: 'jpmorgan', // 'jpmorgan', 'citi', or 'hsbc'
token: 'USDC',
blockchain_wallet_id: wallet.id,
signed_agreement_id: null // Required if banking partner requires agreement
});
if (error) {
console.error('Error creating virtual account:', error.message);
return;
}
console.log('Virtual account created:', {
id: virtualAccount.id,
banking_partner: virtualAccount.banking_partner,
kyc_status: virtualAccount.kyc_status
});
Display account details
Share the account details with your receiver:const { us } = virtualAccount;
console.log('ACH Details:', {
routing_number: us.ach.routing_number,
account_number: us.ach.account_number,
account_type: us.account_type
});
console.log('Wire Details:', {
routing_number: us.wire.routing_number,
account_number: us.wire.account_number,
swift_bic_code: us.swift_bic_code
});
console.log('RTP (Real-Time Payments):', {
routing_number: us.rtp.routing_number,
account_number: us.rtp.account_number
});
console.log('Beneficiary:', {
name: us.beneficiary.name,
address_line_1: us.beneficiary.address_line_1,
address_line_2: us.beneficiary.address_line_2
});
console.log('Receiving Bank:', {
name: us.receiving_bank.name,
address_line_1: us.receiving_bank.address_line_1,
address_line_2: us.receiving_bank.address_line_2
});
Step 3: Managing Virtual Accounts
List Virtual Accounts
// Get all virtual accounts for a receiver
const { data: accounts, error } = await blindpay.virtualAccounts.list(
're_000000000000'
);
if (error) {
console.error('Error listing accounts:', error.message);
return;
}
accounts.forEach(account => {
console.log(`Account ${account.id}:`, {
banking_partner: account.banking_partner,
token: account.token,
kyc_status: account.kyc_status,
blockchain_wallet: account.blockchain_wallet
});
});
Get Specific Virtual Account
const { data: account, error } = await blindpay.virtualAccounts.get({
receiver_id: 're_000000000000',
id: 'va_000000000000'
});
if (error) {
console.error('Error fetching account:', error.message);
return;
}
console.log('Account details:', account);
Update Virtual Account
// Update the blockchain wallet or token for a virtual account
const { data, error } = await blindpay.virtualAccounts.update({
receiver_id: 're_000000000000',
id: 'va_000000000000',
token: 'USDT', // Change from USDC to USDT
blockchain_wallet_id: 'bw_000000000001' // Update destination wallet
});
if (error) {
console.error('Error updating account:', error.message);
return;
}
console.log('Virtual account updated successfully');
Updating the blockchain wallet will redirect all future deposits to the new wallet address. Past transactions are not affected.
Banking Partner Selection
const { data: account } = await blindpay.virtualAccounts.create({
receiver_id: 're_000000000000',
banking_partner: 'jpmorgan',
token: 'USDC',
blockchain_wallet_id: 'bw_000000000000',
signed_agreement_id: null
});
// JPMorgan provides:
// - ACH, Wire, and RTP support
// - Fast processing times
// - Strong compliance
How Deposits Work
Customer sends USD
Customer sends USD to the virtual account via ACH, Wire, or RTP using the provided account details.
BlindPay receives payment
The banking partner receives the fiat payment and notifies BlindPay.
Automatic conversion
BlindPay automatically:
- Converts USD to the specified stablecoin (USDC/USDT)
- Deducts applicable fees
- Initiates blockchain transfer
Stablecoins deposited
Stablecoins are deposited to the linked blockchain wallet on the specified network.
Webhook notification
You receive webhook notifications at each stage:
payin.new: Payment received
payin.update: Conversion in progress
payin.complete: Stablecoins deposited
Monitoring Deposits
Virtual account deposits create payins automatically:
// List payins for a receiver to see virtual account deposits
const { data: payins } = await blindpay.payins.list({
receiver_id: 're_000000000000'
});
payins.data.forEach(payin => {
if (payin.has_virtual_account) {
console.log('Virtual account deposit:', {
id: payin.id,
sender_amount: payin.sender_amount, // USD received
receiver_amount: payin.receiver_amount, // USDC sent
status: payin.status,
created_at: payin.created_at
});
}
});
Set up webhooks to receive real-time notifications when deposits arrive instead of polling.
KYC Status
Virtual accounts require approved KYC:
const { data: account } = await blindpay.virtualAccounts.get({
receiver_id: 're_000000000000',
id: 'va_000000000000'
});
// Check KYC status
switch (account.kyc_status) {
case 'approved':
console.log('Account active and ready to receive deposits');
break;
case 'pending':
console.log('KYC review in progress');
break;
case 'rejected':
console.log('KYC rejected, account cannot receive deposits');
break;
default:
console.log('Unknown KYC status:', account.kyc_status);
}
Complete Example
import { BlindPay } from '@blindpay/sdk';
const blindpay = new BlindPay({
apiKey: process.env.BLINDPAY_API_KEY,
instanceId: process.env.BLINDPAY_INSTANCE_ID
});
async function setupVirtualAccount(receiverId: string, walletAddress: string) {
// Step 1: Create blockchain wallet
const { data: wallet, error: walletError } =
await blindpay.wallets.blockchain.createWithAddress({
receiver_id: receiverId,
name: 'Virtual Account Wallet',
network: 'base',
address: walletAddress
});
if (walletError) {
throw new Error(`Wallet creation failed: ${walletError.message}`);
}
// Step 2: Create virtual account
const { data: virtualAccount, error: accountError } =
await blindpay.virtualAccounts.create({
receiver_id: receiverId,
banking_partner: 'jpmorgan',
token: 'USDC',
blockchain_wallet_id: wallet.id,
signed_agreement_id: null
});
if (accountError) {
throw new Error(`Virtual account creation failed: ${accountError.message}`);
}
// Step 3: Return account details
return {
accountId: virtualAccount.id,
bankingDetails: {
ach: virtualAccount.us.ach,
wire: virtualAccount.us.wire,
rtp: virtualAccount.us.rtp,
beneficiary: virtualAccount.us.beneficiary
},
blockchain: {
network: virtualAccount.blockchain_wallet?.network,
address: virtualAccount.blockchain_wallet?.address
},
status: virtualAccount.kyc_status
};
}
// Usage
setupVirtualAccount('re_000000000000', '0xYourWalletAddress')
.then(details => {
console.log('Virtual account ready:', details);
// Share banking details with your customer
})
.catch(error => {
console.error('Setup failed:', error);
});
Error Handling
const { data: account, error } = await blindpay.virtualAccounts.create({
receiver_id: 're_000000000000',
banking_partner: 'jpmorgan',
token: 'USDC',
blockchain_wallet_id: 'bw_000000000000',
signed_agreement_id: null
});
if (error) {
if (error.message.includes('KYC')) {
console.error('KYC not approved. Complete KYC first.');
} else if (error.message.includes('wallet')) {
console.error('Invalid blockchain wallet ID');
} else if (error.message.includes('agreement')) {
console.error('Banking partner agreement required');
} else {
console.error('Unknown error:', error.message);
}
return;
}
Best Practices
- Network Selection: Use Base or Polygon for lower gas fees on deposits
- Token Choice: USDC is more widely supported than USDT
- Wallet Security: Use Account Abstraction wallets when possible for better security
- KYC First: Complete and approve KYC before creating virtual accounts
- Banking Partner: Choose based on receiver’s location and banking relationships
- Webhooks: Set up webhook listeners for deposit notifications
- Display Details: Clearly show all three payment methods (ACH, Wire, RTP) to receivers
- Update Carefully: Only update virtual accounts when absolutely necessary
Limitations
- Virtual accounts are currently USD-only
- Minimum deposit amounts may apply (varies by banking partner)
- ACH deposits take 1-3 business days
- Wire deposits are typically same-day
- RTP deposits are near-instant
Virtual accounts do not support outbound payments (payouts). Use the regular payout flow for crypto-to-fiat conversions.
Next Steps