Koywe

Merchant External Accounts

Configure your merchant’s external bank accounts and crypto wallets to receive funds from virtual account settlements and crypto purchases.

What are Merchant External Accounts?

Merchant External Accounts are your merchant’s own bank accounts and crypto wallets where you receive funds from Koywe operations.

Types of External Accounts

External Bank Accounts

Your company bank accounts for receiving fiat settlements and withdrawals

External Crypto Wallets

Your crypto wallet addresses for receiving cryptocurrency from ONRAMP deals


External Bank Accounts vs Virtual Accounts

Understanding the difference:

FeatureVirtual AccountExternal Bank Account
LocationWithin KoyweYour own bank
PurposeOperations & transactionsFinal settlement destination
SetupAutomaticManual configuration
UsagePAYINs, PAYOUTs, transfersWithdrawals, settlements
CurrenciesMultiple supportedBank-specific
FeesNone for internal opsMay have transfer fees
Drawing diagram…

Automatic Settlement: Funds in virtual accounts are automatically settled to your registered external bank account after a configured period (typically 1-7 days). You can also manually request withdrawals.


Setting Up External Bank Accounts

When You Need an External Bank Account

✅ Required for:

  • Receiving automatic settlements from virtual accounts
  • Withdrawing funds from virtual balances
  • Receiving merchant payouts
  • Compliance and verification

Creating an External Bank Account

Automatic Association: Bank accounts are automatically associated with your merchant’s registered business information. No need to send holder name or document numbers - we use the information already in the system.

Unified Endpoint: Both bank accounts and crypto wallets use the same /accounts endpoint. The kind field determines the account type: "BANK" for fiat bank accounts, "CRYPTO" for crypto wallets.

async function addMerchantBankAccount(token, orgId, merchantId) {
  const response = await axios.post(
    `https://api-sandbox.koywe.com/api/v1/organizations/${orgId}/merchants/${merchantId}/accounts`,
    {
      name: 'Company Settlement Account',
      kind: 'BANK',                    // Indicates this is a fiat bank account
      countrySymbol: 'CO',
      currencySymbol: 'COP',
      entity: 'BANCOLOMBIA',           // Bank code
      accountNumber: '1234567890',
      type: 'CHECKING',                // or 'SAVINGS'
      isDefault: true,                 // Set as default settlement account
      isVirtual: false                 // External account (not Koywe-managed)
    },
    { headers: { 'Authorization': `Bearer ${token}` } }
  );
  
  console.log('Bank account added:', response.data.id);
  console.log('Status:', response.data.status);
  
  return response.data;
}
 
const bankAccount = await addMerchantBankAccount(token, orgId, merchantId);

Required Fields by Country

Bank Code Flexibility: For most countries, the bank code can be deduced from the account number. You can still provide it, but the API will validate against the deduced value. For countries where it’s required, validation ensures the bank is in the supported list.

Chile 🇨🇱

JavaScript
{
  name: 'Chile Settlement Account',
  kind: 'BANK',
  countrySymbol: 'CL',
  currencySymbol: 'CLP',
  entity: 'BANCO_CHILE',          // Required: BANCO_CHILE, BCI, SANTANDER_CHILE, etc.
  accountNumber: '12345678',      // Required
  isDefault: true,
  isVirtual: false
}

Validations: Bank code (entity) must be in the valid banks list.


Colombia 🇨🇴

JavaScript
{
  name: 'Colombia Settlement Account',
  kind: 'BANK',
  countrySymbol: 'CO',
  currencySymbol: 'COP',
  entity: 'BANCOLOMBIA',          // Required: BANCOLOMBIA, DAVIVIENDA, BOGOTA, etc.
  accountNumber: '1234567890',    // Required
  type: 'CHECKING',               // Required: 'CHECKING' or 'SAVINGS'
  rail: 'BANK',                   // Optional: states accountNumber is a bank account, not a Bre-B key
  isDefault: true,
  isVirtual: false
}

Validations: Account type must be supported by the bank. Account number length validated (min/max).

Bre-B: Colombia runs two rails. To use a Bre-B key as the destination, put the key in accountNumber and send rail: 'BREB'. That skips the whole validation of the number against the bank catalogue — bank found (BAA00019), account type supported by that bank (BAA00020), number length (BAA00021) — because a key has no bank. It also exempts you from entity and type, which a key does not have either: leave them out and the destination is stored with entity: "UNKNOWN_BANK" and type: "VIRTUAL"; send them and they are stored as given, unvalidated. The field stays optional, but without it the key is validated as an account number — usually rejected, and stored as a bank account when its length happens to fit, which then settles over the bank rail. See Payment Rails.


Brazil 🇧🇷

JavaScript
{
  name: 'Brazil Settlement Account',
  kind: 'BANK',
  countrySymbol: 'BR',
  currencySymbol: 'BRL',
  accountNumber: '12345-6',       // Required (OR pixKey)
  // entity: '001'                // Optional (deduced from accountNumber)
  isDefault: true,
  isVirtual: false
}

Validations: Bank code deduced from account number. PIX key can be used instead of account number.


Mexico 🇲🇽

JavaScript
{
  name: 'Mexico Settlement Account',
  kind: 'BANK',
  countrySymbol: 'MX',
  currencySymbol: 'MXN',
  accountNumber: '012345678901234567',  // Required: CLABE (18 digits)
  // entity: 'BBVA_MEXICO'              // Optional (deduced from accountNumber)
  isDefault: true,
  isVirtual: false
}

Validations: CLABE format validation (18 digits). Check digit validation. Bank deduced from CLABE.


Argentina 🇦🇷

JavaScript
{
  name: 'Argentina Settlement Account',
  kind: 'BANK',
  countrySymbol: 'AR',
  currencySymbol: 'ARS',
  accountNumber: '0123456789012345678901',  // Required: CVU or CBU or alias
  // entity: 'BANCO_NACION'                 // Optional (deduced from accountNumber)
  isDefault: true,
  isVirtual: false
}

Validations: CVU/CBU format validation. Check digit validation. Bank deduced from account number. Alias supported.


Peru 🇵🇪

JavaScript
{
  name: 'Peru Settlement Account',
  kind: 'BANK',
  countrySymbol: 'PE',
  currencySymbol: 'PEN',
  accountNumber: '01234567890123456789',  // Required: CCI format
  // entity: 'BCP'                        // Optional (deduced from accountNumber)
  isDefault: true,
  isVirtual: false
}

Validations: CCI format validation (20 digits). Bank deduced from account number.


Bolivia 🇧🇴

JavaScript
{
  name: 'Bolivia Settlement Account',
  kind: 'BANK',
  countrySymbol: 'BO',
  currencySymbol: 'BOB',
  accountNumber: '1234567890',  // Required
  // entity: 'BNB'              // Optional (deduced from accountNumber)
  isDefault: true,
  isVirtual: false
}

Validations: Bank deduced from account number.


United States 🇺🇸

JavaScript
{
  name: 'US Settlement Account',
  kind: 'BANK',
  countrySymbol: 'US',
  currencySymbol: 'USD',
  accountNumber: '123456789',      // Required
  routingNumber: '021000021',      // Required
  // entity: 'CHASE'               // Optional (deduced from routingNumber)
  isDefault: true,
  isVirtual: false
}

Validations: Bank deduced from routing number. Generic bank saved if not found.

Verification Process

Account Created

External bank account is created with status PENDING_VERIFICATION

Microdeposit Sent (if required)

Koywe may send a small test deposit to verify account ownership

Verification Completed

You verify the deposit amount or account is auto-verified

Account Activated

Status changes to VERIFIED and ready for settlements

Automatic Verification: The bank account is automatically linked to your merchant’s registered business information. Ensure your merchant profile has the correct legal name and tax ID, as these will be used for account verification.

Listing Merchant Bank Accounts

JavaScript
async function getMerchantBankAccounts(token, orgId, merchantId) {
  const response = await axios.get(
    `https://api-sandbox.koywe.com/api/v1/organizations/${orgId}/merchants/${merchantId}/accounts`,
    {
      params: { kind: 'BANK' },  // Filter for bank accounts only
      headers: { 'Authorization': `Bearer ${token}` }
    }
  );
  
  console.log('External bank accounts:');
  response.data.forEach(account => {
    console.log(`- ${account.currencySymbol}: ${account.entity} ****${account.accountNumber.slice(-4)}`);
    console.log(`  Default: ${account.isDefault}, Status: ${account.status}`);
  });
  
  return response.data;
}
 
const accounts = await getMerchantBankAccounts(token, orgId, merchantId);

Setting Up External Crypto Wallets

When You Need an External Crypto Wallet

✅ Required for:

  • Receiving cryptocurrency from ONRAMP deals
  • Sending crypto to your own custody solutions
  • Long-term crypto holdings outside Koywe

Embedded Wallets vs External Wallets: Koywe-managed embedded wallets are provisioned through the passkey flow (/webauthn/wallet/prepare and /webauthn/wallet/complete) — see Passkeys & Approvals. External wallets are YOUR OWN wallets on hardware devices, exchanges, or other custody solutions where you want to receive crypto, and you register them yourself with the calls below.

Adding an External Crypto Wallet

Same Endpoint as Bank Accounts: External crypto wallets use the same /accounts endpoint as bank accounts. Set kind: "CRYPTO" to create a crypto wallet instead of a bank account.

async function addExternalCryptoWallet(token, orgId, merchantId, walletData) {
  const response = await axios.post(
    `https://api-sandbox.koywe.com/api/v1/organizations/${orgId}/merchants/${merchantId}/accounts`,
    {
      name: walletData.name,                 // e.g., "Company Ledger Wallet"
      kind: 'CRYPTO',                        // Indicates this is a crypto wallet
      currencySymbol: walletData.currency,   // USDC, USDT, BTC, ETH, etc.
      network: walletData.network,           // ETHEREUM, POLYGON, BITCOIN, etc.
      address: walletData.address,           // Your wallet address
      isDefault: walletData.isDefault || false,
      isVirtual: false                       // External wallet (not Koywe-managed)
    },
    { headers: { 'Authorization': `Bearer ${token}` } }
  );
  
  console.log('External wallet added:', response.data.id);
  console.log('Address:', response.data.address);
  console.log('Network:', response.data.network);
  
  return response.data;
}
 
// Example: Add USDC on Ethereum
const usdcWallet = await addExternalCryptoWallet(token, orgId, merchantId, {
  name: 'Company Ledger - USDC',
  currency: 'USDC',
  network: 'ETHEREUM',
  address: '0x742d35Cc6634C0532925a3b844Bc9e7595f0bEb2',
  isDefault: true
});
 
console.log('USDC wallet registered for ONRAMP operations');

Supported Networks and Currencies

CurrencyNameNetworks
USDTTetherETHEREUM · POLYGON · SOLANA · BASE · ALGORAND · TRON · BSC
USDCUSD CoinETHEREUM · POLYGON · SOLANA · BASE · ALGORAND
ETHEtherETHEREUM · BASE
BTCBitcoinBITCOIN
EURCEuro CoinBASE
MATICPolygonPOLYGON
SOLSolanaSOLANA
TRXTronTRON

Only these pairs are valid. Any other symbol/network combination is rejected by the API.

Network Matching Critical: When creating ONRAMP deals, the network parameter must match one of your registered external wallet networks. Always verify the network before sending crypto!

Listing External Crypto Wallets

JavaScript
async function getExternalWallets(token, orgId, merchantId) {
  const response = await axios.get(
    `https://api-sandbox.koywe.com/api/v1/organizations/${orgId}/merchants/${merchantId}/accounts`,
    {
      params: { kind: 'CRYPTO' },  // Filter for crypto wallets only
      headers: { 'Authorization': `Bearer ${token}` }
    }
  );
  
  console.log('External crypto wallets:');
  response.data.forEach(wallet => {
    console.log(`- ${wallet.currencySymbol} (${wallet.network})`);
    console.log(`  Address: ${wallet.address}`);
    console.log(`  Default: ${wallet.isDefault}, Status: ${wallet.status}`);
  });
  
  return response.data;
}
 
const wallets = await getExternalWallets(token, orgId, merchantId);

Using External Accounts in Operations

ONRAMP: Send Crypto to External Wallet

When creating ONRAMP deals, specify your external wallet as the destination:

JavaScript
// 1. Get quote
const quote = await axios.post(
  `https://api-sandbox.koywe.com/api/v1/organizations/${orgId}/merchants/${merchantId}/quotes`,
  {
    orderType: 'ONRAMP',
    executable: true,
    originCurrencySymbol: 'COP',
    destinationCurrencySymbol: 'USDC',
    amountIn: 1000000,  // 1M COP
    network: 'ETHEREUM'  // Required for ONRAMP/OFFRAMP
  },
  { headers: { 'Authorization': `Bearer ${token}` } }
);
 
// 2. Create deal with external wallet as destination
const deal = await axios.post(
  `https://api-sandbox.koywe.com/api/v1/organizations/${orgId}/merchants/${merchantId}/deals`,
  {
    destinationAccountId: usdcWallet.id,  // Your external wallet ID
    quoteId: quote.data.id
  },
  { headers: { 'Authorization': `Bearer ${token}` } }
);
 
console.log('ONRAMP deal created');
console.log('Crypto will be sent to:', usdcWallet.address);
console.log('Network:', usdcWallet.network);

Use Embedded vs External: For operational crypto (frequent trading, OFFRAMP), use embedded wallets. For long-term holdings or specific custody requirements, use external wallets.

Withdrawals: Move Funds to External Bank Account

Request withdrawal from virtual account to your external bank account:

JavaScript
async function withdrawToExternalBank(token, orgId, merchantId, withdrawalData) {
  // Check virtual account balance first
  const balances = await axios.get(
    `https://api-sandbox.koywe.com/api/v1/organizations/${orgId}/merchants/${merchantId}/accounts/balances`,
    { headers: { 'Authorization': `Bearer ${token}` } }
  );
  
  const copBalance = balances.data.find(b => b.currencySymbol === 'COP');
  
  if (copBalance.availableBalance < withdrawalData.amount) {
    throw new Error('Insufficient balance for withdrawal');
  }
  
  // Create withdrawal order
  const withdrawal = await axios.post(
    `https://api-sandbox.koywe.com/api/v1/organizations/${orgId}/merchants/${merchantId}/withdrawals`,
    {
      currency: 'COP',
      amount: withdrawalData.amount,
      destinationAccountId: withdrawalData.bankAccountId,  // Your external bank account
      description: 'Withdrawal to company account'
    },
    { headers: { 'Authorization': `Bearer ${token}` } }
  );
  
  console.log('Withdrawal initiated:', withdrawal.data.id);
  console.log('Amount:', withdrawal.data.amount, 'COP');
  console.log('Status:', withdrawal.data.status); // "PROCESSING"
  console.log('ETA:', withdrawal.data.estimatedSettlement);
  
  return withdrawal.data;
}
 
const withdrawal = await withdrawToExternalBank(token, orgId, merchantId, {
  amount: 5000000,  // 5M COP
  bankAccountId: bankAccount.id
});

Automatic Settlements: If you have automatic settlements enabled, funds will be transferred to your primary external bank account automatically after the configured holding period. Manual withdrawals give you more control over timing.


Best Practices

Security

Verify Addresses

Always double-check crypto wallet addresses before adding them. Errors are irreversible.

Use Test Amounts

Send small test transactions to new external wallets before large transfers.

Monitor Webhooks

Set up webhooks to track settlements and withdrawals to external accounts.

Keep Records

Maintain records of all external accounts for compliance and auditing.

Account Management

✅ Do:

  • Keep primary accounts up to date
  • Verify new accounts immediately
  • Use separate wallets per network
  • Document account purposes internally

❌ Don’t:

  • Share account credentials
  • Use exchange deposit addresses as external wallets (use withdrawal addresses)
  • Add unverified or test wallets in production
  • Use personal accounts for business operations

Common Patterns

Multi-Currency Setup

For businesses operating in multiple countries:

JavaScript
// Setup external bank accounts for each currency
const copAccount = await axios.post(
  `https://api-sandbox.koywe.com/api/v1/organizations/${orgId}/merchants/${merchantId}/accounts`,
  {
    name: 'Bancolombia COP',
    kind: 'BANK',
    isDefault: true,
    countrySymbol: 'CO',
    currencySymbol: 'COP',
    entity: 'BANCOLOMBIA',
    accountNumber: '1234567890',
    type: 'CHECKING'
  },
  { headers: { 'Authorization': `Bearer ${token}` } }
);
 
const brlAccount = await axios.post(
  `https://api-sandbox.koywe.com/api/v1/organizations/${orgId}/merchants/${merchantId}/accounts`,
  {
    name: 'Brazil BRL',
    kind: 'BANK',
    isDefault: false,
    countrySymbol: 'BR',
    currencySymbol: 'BRL',
    accountNumber: '12345-6'  // Bank deduced automatically
  },
  { headers: { 'Authorization': `Bearer ${token}` } }
);
 
const mxnAccount = await axios.post(
  `https://api-sandbox.koywe.com/api/v1/organizations/${orgId}/merchants/${merchantId}/accounts`,
  {
    name: 'Mexico MXN',
    kind: 'BANK',
    isDefault: false,
    countrySymbol: 'MX',
    currencySymbol: 'MXN',
    accountNumber: '012345678901234567'  // CLABE - bank deduced
  },
  { headers: { 'Authorization': `Bearer ${token}` } }
);
 
console.log('Multi-currency external accounts configured');
console.log('All accounts auto-linked to merchant business information');

Multi-Network Crypto Setup

For diversified crypto holdings:

JavaScript
// USDC on multiple networks
const usdcEthereum = await addExternalCryptoWallet(token, orgId, merchantId, {
  name: 'USDC - Ethereum Main',
  currency: 'USDC',
  network: 'ETHEREUM',
  address: '0x...',
  isDefault: true
});
 
const usdcPolygon = await addExternalCryptoWallet(token, orgId, merchantId, {
  name: 'USDC - Polygon Low Fees',
  currency: 'USDC',
  network: 'POLYGON',
  address: '0x...',
  isDefault: false
});
 
// Different stablecoins
const usdtTron = await addExternalCryptoWallet(token, orgId, merchantId, {
  name: 'USDT - Tron',
  currency: 'USDT',
  network: 'TRON',
  address: 'T...',
  isDefault: false
});
 
console.log('Multi-network external wallets configured');

Next Steps