actualbudget/actual · error · TransactionError

Subtransaction amount is invalid, must be an integer: ${sub.

Error message

Subtransaction amount is invalid, must be an integer: ${sub.amount}

What it means

Actual Budget stores all money amounts as integer cents. During transaction normalization in the account sync pipeline, every subtransaction (split) amount is checked with Number.isInteger before being persisted. If a subtransaction has a non-null amount that is fractional or non-numeric (e.g. 10.5, "10.50", NaN), sync.ts throws this TransactionError to prevent corrupted ledger data.

Source

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

    // 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 === '') {
        payee_name = null;
      } else {
        payee_name = normalizePayeeName(trimmed, payeeNameNormalization);
      }
    }

    trans.imported_payee = trans.imported_payee || payee_name;
    if (trans.imported_payee) {

View on GitHub (pinned to d4334cb6e6)

Solutions

  1. Convert amounts to integer cents before calling the sync/import APIs: Math.round(amount * 100).
  2. If amounts arrive as strings ("10.50"), parse and convert: Math.round(parseFloat(s) * 100).
  3. Audit the bank-sync/import adapter producing the subtransactions and ensure it emits cents, not dollars.
  4. Catch TransactionError in the caller and surface which subtransaction failed so the source data can be corrected.

Example fix

// before
subtransactions: [
  { amount: 12.5, category: 'groceries' },
]
// after
subtransactions: [
  { amount: Math.round(12.5 * 100), category: 'groceries' }, // 1250 cents
]
Defensive patterns

Strategy: validation

Validate before calling

function hasValidSubamounts(t) {
  return (t.subtransactions ?? []).every(
    sub => sub.amount == null || (typeof sub.amount === 'number' && Number.isInteger(sub.amount)),
  );
}
if (!hasValidSubamounts(txn)) throw new Error('subtransaction amounts must be integer cents');

Type guard

function isCents(n) {
  return typeof n === 'number' && Number.isInteger(n);
}

Prevention

When it happens

Trigger: normalizeTransactions is called (directly or via bank sync / file import) with a transaction whose subtransactions array contains an entry where amount is a float (10.5), a numeric string ("10.50"), NaN, or another non-integer value while also being non-null.

Common situations: Bank importers or bank-sync adapters that compute subtransaction amounts from decimal currency values without multiplying by 100 and rounding; custom API scripts (actual-js) passing dollar floats as sub.amount; CSV import rules that split a transaction into percentage-based pieces producing fractional cents; floating-point arithmetic (0.1+0.2) producing amounts like 30.000000000000004.

Related errors


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