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
- Convert amounts to integer cents before submission: Math.round(amount * 100).
- Parse numeric values explicitly (Number(str)) and round to avoid float residue.
- Reject non-numeric or decimal amounts at the import-script level with a validation step.
- 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
- Always submit amounts in integer cents; convert once at the ingestion boundary.
- Use Math.round(amount * 100) and avoid raw float arithmetic on money.
- Parse CSV numerics explicitly with Number() and validate before use.
- Run a batch-wide pre-flight check for integer amounts before calling the API.
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
- `date` is required when adding a transaction
- Unknown payee name normalization: ${String(normalization)}
- Subtransaction amount is invalid, must be an integer: ${sub.
- Transaction ${id} does not belong to account ${accountId}
- There is already a filter named ${item.name}
AI-assisted analysis of actualbudget/actual@d4334cb6e6 (2026-08-29).
Data as JSON: /api/errors/a2aad84495b8c37a.
Report an issue: GitHub.