actualbudget/actual · error

Bank with ID ${bankId} not found.

Error message

Bank with ID ${bankId} not found.

What it means

Actual looks up the bank (GoCardless/Nordigen institution link) row by its ID before performing a bank-side operation such as creating a requisition. If no row in the `banks` table matches the given bankId, the app cannot proceed and throws this error. It means the caller passed a bank ID that does not exist in this budget's database.

Source

Thrown at packages/loot-core/src/server/accounts/app.ts:1741

    'SELECT COUNT(*) as count FROM accounts WHERE bank = ?',
    [bankId],
  );

  // No more accounts are associated with this bank. We can remove
  // it from GoCardless.
  const userToken = await asyncStorage.getItem('user-token');
  if (!userToken) {
    return 'ok';
  }

  if (!accountWithBankResult || accountWithBankResult.count === 0) {
    const bank = await db.first<Pick<db.DbBank, 'bank_id'>>(
      'SELECT bank_id FROM banks WHERE id = ?',
      [bankId],
    );

    if (!bank) {
      throw new Error(`Bank with ID ${bankId} not found.`);
    }

    const serverConfig = getServer();
    if (!serverConfig) {
      throw new Error('Failed to get server config.');
    }

    const requisitionId = bank.bank_id;

    try {
      await post(
        serverConfig.GOCARDLESS_SERVER + '/remove-account',
        {
          requisitionId,
        },
        {
          'X-ACTUAL-TOKEN': userToken,
        },

View on GitHub (pinned to d4334cb6e6)

Solutions

  1. Verify the bankId exists for this budget (SELECT id FROM banks) or re-list linked banks in the UI; use a current ID.
  2. Re-link the bank in Settings > Bank sync to create a fresh bank row, then retry.
  3. If migrating budgets, re-authenticate the bank in the new budget instead of copying IDs across.
  4. Remove or update stale client caches/references to deleted bank IDs.

Example fix

// before
await crinx.getOrCreateRequisition('9f3c-...deleted-bank-id...');
// after
const banks = await listUserBanks(); // confirm a valid id first
await crinx.getOrCreateRequisition(banks[0].bank_id);
Defensive patterns

Strategy: validation

Validate before calling

const bank = await db.first('SELECT bank_id FROM banks WHERE id = ?', [bankId]);
if (!bank) throw new Error(`Skipping: bank ${bankId} does not exist in this budget`);

Type guard

function isBank(v: { bank_id: string } | null | undefined): v is { bank_id: string } {
  return v != null && typeof v.bank_id === 'string';
}

Try / catch

try {
  await syncBank(bankId);
} catch (e) {
  if (e instanceof Error && e.message.startsWith('Bank with ID')) {
    // bank no longer exists: re-link the bank before retrying
    return;
  }
  throw e;
}

Prevention

When it happens

Trigger: Calling the bank-sync internals (via API or a stale UI client) with a bankId that was deleted, belongs to a different budget file, or was never created. Also happens with a migrated/restored budget where the `banks` row was removed but old references remain.

Common situations: Restoring an old budget file after re-linking the bank; switching sync-server instances or GoCardless accounts; using the API with an ID copied from another budget; the bank having been unlinked previously.

Related errors


AI-assisted analysis of actualbudget/actual@d4334cb6e6 (2026-08-29). Data as JSON: /api/errors/ad25cbe83200fb4f. Report an issue: GitHub.