actualbudget/actual · error · TransactionError

Amount is invalid, must be an integer: ${trans.amount}

Error message

Amount is invalid, must be an integer: ${trans.amount}

What it means

Transaction amounts in Actual are stored as integer cents, so normalizeTransactions rejects any transaction whose amount is set but not an integer, throwing a TransactionError. This catches floating-point amounts (e.g. -25.99) or string amounts before they can corrupt the ledger. Amounts must be integer cents, not decimal currency units.

Source

Thrown at packages/loot-core/src/server/accounts/sync.ts:460

    payeeNameNormalization?: PayeeNameNormalization;
  } = {},
) {
  const payeesToCreate = new Map();

  const normalized = [];
  for (let trans of transactions) {
    // Validate the date because we do some stuff with it. The db
    // layer does better validation, but this will give nicer errors
    if (trans.date == null) {
      throw new Error('`date` is required when adding a transaction');
    }

    // Strip off the irregular properties
    const { payee_name: originalPayeeName, subtransactions, ...rest } = trans;
    trans = rest;

    if (trans.amount != null && !Number.isInteger(trans.amount)) {
      throw new TransactionError(
        `Amount is invalid, must be an integer: ${trans.amount}`,
      );
    }

    if (subtransactions) {
      for (const sub of subtransactions) {
        if (sub.amount != null && !Number.isInteger(sub.amount)) {
          throw new TransactionError(
            `Subtransaction amount is invalid, must be an integer: ${sub.amount}`,
          );
        }
      }
    }

    let payee_name = originalPayeeName;
    if (payee_name) {
      const trimmed = payee_name.trim();
      if (trimmed === '') {

View on GitHub (pinned to d4334cb6e6)

Solutions

  1. Convert amounts to integer cents before submission: Math.round(amount * 100).
  2. Parse numeric values explicitly (Number(str)) and round to avoid float residue.
  3. Reject non-numeric or decimal amounts at the import-script level with a validation step.
  4. Use integer math in cents throughout rather than float arithmetic on money.

Example fix

// before
{ date: '2024-05-01', amount: -25.99, payee_name: 'Kroger' }
// after
{ date: '2024-05-01', amount: Math.round(-25.99 * 100), payee_name: 'Kroger' } // -2599
Defensive patterns

Strategy: validation

Validate before calling

for (const t of txs) {
  if (t.amount != null && !Number.isInteger(t.amount)) {
    throw new Error(`amount must be integer cents, got: ${t.amount}`);
  }
}

Type guard

function isCentsAmount(a: unknown): a is number {
  return typeof a === 'number' && Number.isInteger(a);
}

Try / catch

try {
  await importTransactions(accountId, txs);
} catch (e) {
  if (e instanceof TransactionError && e.message.startsWith('Amount is invalid')) {
    console.error('Convert amounts to integer cents (Math.round(x * 100)) before importing');
    return;
  }
  throw e;
}

Prevention

When it happens

Trigger: Calling importTransactions/addTransactions with amount = -25.99 (decimal dollars), amount = '-25.99' (string), or NaN — anything where Number.isInteger(amount) is false while amount != null. Subtransactions are validated the same way.

Common situations: Integrations passing amounts straight from a bank API/CSV in decimal form without converting to cents; float arithmetic residue; JSON/CSV parsers yielding strings for numeric columns.

Related errors


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