actualbudget/actual · error · APIError

transactions-import: accountId must be an id

Error message

transactions-import: accountId must be an id

What it means

Actual's transactions-import API validates its inputs up front and throws an APIError when accountId is not a string. Account ids in Actual are internal string identifiers; the check at app.ts:1645 runs before any reconciliation work, so callers passing null, undefined, a number, or an object are rejected immediately. It guards the rest of the import pipeline from operating on a malformed account reference.

Source

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

export type ImportTransactionsResult = bankSync.ReconcileTransactionsResult & {
  errors: Array<{
    message: string;
  }>;
};

async function importTransactions({
  accountId,
  transactions,
  isPreview,
  opts,
}: {
  accountId: AccountEntity['id'];
  transactions: ImportTransactionEntity[];
  isPreview: boolean;
  opts?: ImportTransactionsOpts;
}): Promise<ImportTransactionsResult> {
  if (typeof accountId !== 'string') {
    throw APIError('transactions-import: accountId must be an id');
  }

  const payeeNameNormalization = opts?.payeeNameNormalization ?? 'title-case';
  if (!bankSync.PAYEE_NAME_NORMALIZATIONS.includes(payeeNameNormalization)) {
    throw APIError(
      `transactions-import: payeeNameNormalization must be one of ${bankSync.PAYEE_NAME_NORMALIZATIONS.join(
        ', ',
      )}, got '${String(payeeNameNormalization)}'`,
    );
  }

  try {
    const reconciled = await bankSync.reconcileTransactions(
      accountId,
      transactions,
      {
        isPreview,
        defaultCleared: opts?.defaultCleared,

View on GitHub (pinned to d4334cb6e6)

Solutions

  1. Log and inspect the accountId value just before the call; ensure it comes from an Actual account entity's id field (a string).
  2. Resolve the account by name via the accounts/query API and use the returned id.
  3. Fix type mappings so numeric ids from external systems are converted to Actual string ids before calling.
  4. Wrap the call in try/catch on APIError and surface a clear validation message to the user.

Example fix

// before
await api.transactionsImport(bankAccount.rowId, txns);
// after
const acct = await api.qbi-query;
// resolve first:
const accounts = await api.getAccounts();
const acct = accounts.find(a => a.name === 'Checking');
if (!acct) throw new Error('Account not found');
await api.transactionsImport(acct.id, txns);
Defensive patterns

Strategy: validation

Validate before calling

if (typeof accountId !== 'string' || accountId === '') {
  throw new Error(`transactionsImport: expected string account id, got ${String(accountId)}`);
}

Type guard

function isAccountId(v: unknown): v is string {
  return typeof v === 'string' && v.length > 0;
}

Try / catch

try {
  await api.transactionsImport(accountId, transactions);
} catch (e) {
  if (e instanceof APIError && e.message.includes('accountId must be an id')) {
    // surface a validation message / re-resolve the account id
  } else throw e;
}

Prevention

When it happens

Trigger: Calling importTransactions / api.transactions-import with accountId === null, undefined, a numeric id, an object like {id: ...}, or a value read from the wrong field of an account entity.

Common situations: API consumers mapping external bank account objects to Actual accounts and passing the wrong property; using a numeric internal DB row id instead of Actual's string id; a failed lookup returning undefined before the import call.

Related errors


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