Skip to main content

Overview

Bank accounts represent business accounts used for transfer payments in cash adjustments and expenses. They are configured per branch and referenced when tender_type is TRANSFER.
Store only masked account information in the database. Full account numbers and CLABE keys should be stored in secure credential storage.

Account Fields

From BankAccount.php:14-22:
FieldTypeRequiredDescription
branch_idForeign KeyYesParent branch
aliasStringYesFriendly name (e.g., “Main Checking”)
bank_nameStringYesFinancial institution name
account_number_maskedStringNoMasked account number (e.g., “****1234”)
clabe_maskedStringNoMasked CLABE interbank key (e.g., “************4321”)
is_activeBooleanYesEnable/disable account (default: true)
metaJSONNoAdditional metadata

Create a Bank Account

Endpoint

POST /api/v1/bank-accounts

Request Example

{
  "branch_id": 1,
  "alias": "Main Checking",
  "bank_name": "BBVA",
  "account_number_masked": "****5678",
  "clabe_masked": "************4321",
  "is_active": true,
  "meta": {
    "currency": "MXN",
    "account_type": "checking",
    "routing_number": "****9876"
  }
}

Response

{
  "message": "Bank account created successfully",
  "data": {
    "id": 1,
    "branch_id": 1,
    "alias": "Main Checking",
    "bank_name": "BBVA",
    "account_number_masked": "****5678",
    "clabe_masked": "************4321",
    "is_active": true,
    "meta": {
      "currency": "MXN",
      "account_type": "checking"
    },
    "created_at": "2026-03-06T10:00:00Z",
    "updated_at": "2026-03-06T10:00:00Z"
  }
}

Query Scopes

From BankAccount.php:55-67:
// Filter active accounts
BankAccount::active()->get();

// Filter by branch
BankAccount::byBranch($branchId)->get();

Example Queries

// Get all active accounts for a branch
const accounts = await api.get('/bank-accounts', {
  params: {
    branch_id: 1,
    is_active: true
  }
});

// Get all accounts across all branches
const allAccounts = await api.get('/bank-accounts');

Relationships

From BankAccount.php:32-51:
  • branch: Parent branch
  • adjustmentLines: All adjustment lines using this account
  • expenses: All expenses paid from or to this account

Usage in Transactions

Linking to Transactions

In Cash Adjustments

When recording a transfer income:
{
  "cash_session_id": 1,
  "type": "EXTERNAL_IMPORT",
  "direction": "INFLOW",
  "lines": [
    {
      "tender_type": "TRANSFER",
      "amount": 1500.00,
      "currency": "MXN",
      "bank_account_id": 1,
      "reference": "TXN-ABC123"
    }
  ]
}

In Cash Expenses

When paying a bill via transfer:
{
  "cash_session_id": 1,
  "tender_type": "TRANSFER",
  "amount": 650.00,
  "category": "Utilities",
  "vendor": "Electric Company",
  "bank_account_id": 1,
  "reference": "BILL-MARCH-2026"
}
The bank_account_id field is required when tender_type is TRANSFER in either adjustments or expenses.

Account Types

Common account configurations:

Checking Account

{
  "alias": "Operations Checking",
  "bank_name": "BBVA",
  "account_number_masked": "****1234",
  "meta": {
    "account_type": "checking",
    "currency": "MXN",
    "monthly_fee": 0
  }
}

Savings Account

{
  "alias": "Emergency Fund",
  "bank_name": "Santander",
  "account_number_masked": "****5678",
  "meta": {
    "account_type": "savings",
    "currency": "MXN",
    "interest_rate": 2.5
  }
}

Payroll Account

{
  "alias": "Payroll Account",
  "bank_name": "Banorte",
  "account_number_masked": "****9012",
  "meta": {
    "account_type": "checking",
    "currency": "MXN",
    "purpose": "payroll_only"
  }
}

Security Best Practices

Always mask account numbers and CLABE keys. Store full credentials in a secure vault (e.g., AWS Secrets Manager, HashiCorp Vault).
// ❌ WRONG - Never do this
const account = {
  account_number_masked: "123456789012"
};

// ✅ CORRECT - Always mask
const account = {
  account_number_masked: "****9012"
};
Don’t return sensitive fields in API responses unless absolutely necessary. Use separate endpoints for admin-only credential access.
Log all reads and updates to bank account records for security auditing.
If storing any sensitive data in meta, ensure database-level encryption is enabled for JSON columns.

Masking Format

Standard masking formats:
FieldFormatExample
Account NumberLast 4 digits****5678
CLABE (18 digits)Last 4 digits**************4321
Routing NumberLast 4 digits****9876
function maskAccountNumber(fullNumber) {
  if (!fullNumber || fullNumber.length < 4) return '****';
  const lastFour = fullNumber.slice(-4);
  const masked = '*'.repeat(fullNumber.length - 4) + lastFour;
  return masked;
}

// Examples
maskAccountNumber('123456789012'); // "********9012"
maskAccountNumber('012345678901234567'); // "**************4567" (CLABE)

Active/Inactive Management

Deactivate an Account

Instead of deleting, mark accounts as inactive to preserve transaction history:
PATCH /api/v1/bank-accounts/{id}
{
  "is_active": false
}

Query Active Accounts Only

const activeAccounts = await api.get('/bank-accounts', {
  params: { is_active: true }
});
Transactions with bank_account_id referencing an inactive account should still display the account details but prevent new transactions from using it.

Multi-Branch Setup

Each branch should have its own set of accounts:
// Branch 1 - Downtown
await api.post('/bank-accounts', {
  branch_id: 1,
  alias: 'Downtown Checking',
  bank_name: 'BBVA',
  account_number_masked: '****1111'
});

await api.post('/bank-accounts', {
  branch_id: 1,
  alias: 'Downtown Savings',
  bank_name: 'BBVA',
  account_number_masked: '****2222'
});

// Branch 2 - Airport
await api.post('/bank-accounts', {
  branch_id: 2,
  alias: 'Airport Checking',
  bank_name: 'Santander',
  account_number_masked: '****3333'
});

Transaction Tracking

Inflows via Transfer

Track customer transfers or deposits:
// Customer made a bank transfer payment
await api.post('/cash-adjustments', {
  cash_session_id: sessionId,
  type: 'EXTERNAL_IMPORT',
  direction: 'INFLOW',
  lines: [
    {
      tender_type: 'TRANSFER',
      amount: 2500.00,
      bank_account_id: 1,
      reference: 'CUSTOMER-TXN-789',
      meta: {
        customer_id: 42,
        order_reference: 'ORD-2026-123'
      }
    }
  ]
});

Outflows via Transfer

Record vendor payments or bill settlements:
// Paid utility bill from business account
await api.post('/cash-expenses', {
  cash_session_id: sessionId,
  tender_type: 'TRANSFER',
  amount: 850.00,
  category: 'Utilities',
  vendor: 'Gas Company',
  bank_account_id: 1,
  reference: 'BILL-GAS-MARCH',
  incurred_at: new Date().toISOString()
});

Reporting Queries

Transfers by Account

SELECT 
  ba.alias,
  ba.bank_name,
  COUNT(cal.id) as adjustment_line_count,
  SUM(cal.amount) as total_adjustment_amount,
  COUNT(ce.id) as expense_count,
  SUM(ce.amount) as total_expense_amount
FROM bank_accounts ba
LEFT JOIN cash_adjustment_lines cal ON ba.id = cal.bank_account_id
LEFT JOIN cash_expenses ce ON ba.id = ce.bank_account_id
WHERE ba.is_active = true
GROUP BY ba.id, ba.alias, ba.bank_name
ORDER BY total_adjustment_amount DESC;

Monthly Transfer Activity

SELECT 
  ba.alias,
  DATE_TRUNC('month', cs.operating_date) as month,
  SUM(cal.amount) as inflows,
  SUM(ce.amount) as outflows
FROM bank_accounts ba
LEFT JOIN cash_adjustment_lines cal ON ba.id = cal.bank_account_id
LEFT JOIN cash_adjustments ca ON cal.cash_adjustment_id = ca.id
LEFT JOIN cash_sessions cs ON ca.cash_session_id = cs.id
LEFT JOIN cash_expenses ce ON ba.id = ce.bank_account_id
WHERE ba.is_active = true
  AND cs.operating_date >= '2026-01-01'
GROUP BY ba.id, ba.alias, DATE_TRUNC('month', cs.operating_date)
ORDER BY month DESC, ba.alias;

Metadata Examples

Store additional context in the meta field:

Basic Metadata

{
  "meta": {
    "currency": "MXN",
    "account_type": "checking",
    "opened_date": "2025-01-15"
  }
}

Advanced Metadata

{
  "meta": {
    "currency": "MXN",
    "account_type": "checking",
    "interest_rate": 0.5,
    "monthly_fee": 150.00,
    "minimum_balance": 5000.00,
    "account_manager": "John Doe",
    "account_manager_phone": "+52-555-1234567",
    "online_banking_url": "https://bbva.mx/online",
    "external_sync_enabled": true,
    "last_sync_at": "2026-03-06T08:00:00Z"
  }
}

Best Practices

Use clear, descriptive aliases that indicate account purpose: “Main Operations”, “Payroll Account”, “Emergency Fund”
Periodically review and update account information, especially after bank changes or account migrations
Mark accounts as inactive instead of deleting to preserve historical transaction references
Ensure each branch has at least one active account configured for transfer operations
Document your metadata schema so all developers know which fields are expected

Example: Complete Branch Account Setup

// Setup accounts for a new branch
async function setupBranchAccounts(branchId) {
  // Main checking account
  const checking = await api.post('/bank-accounts', {
    branch_id: branchId,
    alias: 'Main Checking',
    bank_name: 'BBVA',
    account_number_masked: '****1234',
    clabe_masked: '**************5678',
    is_active: true,
    meta: {
      account_type: 'checking',
      currency: 'MXN',
      monthly_fee: 150.00
    }
  });

  // Savings account for excess funds
  const savings = await api.post('/bank-accounts', {
    branch_id: branchId,
    alias: 'Savings Account',
    bank_name: 'BBVA',
    account_number_masked: '****9012',
    clabe_masked: '**************3456',
    is_active: true,
    meta: {
      account_type: 'savings',
      currency: 'MXN',
      interest_rate: 2.5
    }
  });

  // Payroll account
  const payroll = await api.post('/bank-accounts', {
    branch_id: branchId,
    alias: 'Payroll Account',
    bank_name: 'Santander',
    account_number_masked: '****7890',
    clabe_masked: '**************1111',
    is_active: true,
    meta: {
      account_type: 'checking',
      currency: 'MXN',
      purpose: 'payroll_only'
    }
  });

  return { checking, savings, payroll };
}

Next Steps

Cash Adjustments

Use bank accounts in transfer adjustment lines

Cash Expenses

Pay expenses via bank transfer

Build docs developers (and LLMs) love