actualbudget/actual · error · APIError
transactions-import: payeeNameNormalization must be one of $
Error message
transactions-import: payeeNameNormalization must be one of ${bankSync.PAYEE_NAME_NORMALIZATIONS.join(', ')}, got '${String(payeeNameNormalization)}' What it means
The transactions-import API accepts an optional opts.payeeNameNormalization that controls how payee names are normalized during reconciliation. The value must be one of PAYEE_NAME_NORMALIZATIONS = ['original', 'title-case'] (sync.ts:416); anything else, including typos or differently-cased values, throws this APIError before any import runs. It defaults to 'title-case' when omitted.
Source
Thrown at packages/loot-core/src/server/accounts/app.ts:1651
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,
reimportDeleted: opts?.reimportDeleted,
payeeNameNormalization,
},
);
return {View on GitHub (pinned to d4334cb6e6)
Solutions
- Set opts.payeeNameNormalization to exactly 'title-case' or 'original' (lowercase).
- Omit the option entirely to accept the 'title-case' default.
- Validate the value against PAYEE_NAME_NORMALIZATIONS before calling (the array is exported from loot-core's bankSync module).
- If migrating from an older version, update stale option values to the current enum.
Example fix
// before
await importTransactions({ accountId, transactions, isPreview: false, opts: { payeeNameNormalization: 'Title Case' } });
// after
await importTransactions({ accountId, transactions, isPreview: false, opts: { payeeNameNormalization: 'title-case' } }); Defensive patterns
Strategy: validation
Validate before calling
const ALLOWED = ['original', 'title-case'];
if (opts?.payeeNameNormalization && !ALLOWED.includes(opts.payeeNameNormalization)) {
throw new Error(`payeeNameNormalization must be one of ${ALLOWED.join(', ')}`);
} Type guard
const PAYEE_NAME_NORMALIZATIONS = ['original', 'title-case'] as const;
type PayeeNameNormalization = (typeof PAYEE_NAME_NORMALIZATIONS)[number];
function isPayeeNameNormalization(v: unknown): v is PayeeNameNormalization {
return typeof v === 'string' && (PAYEE_NAME_NORMALIZATIONS as readonly string[]).includes(v);
} Try / catch
try {
await api.transactionsImport(accountId, txns, opts);
} catch (e) {
if (e instanceof APIError && e.message.includes('payeeNameNormalization')) {
// fall back to the default
await api.transactionsImport(accountId, txns, { ...opts, payeeNameNormalization: 'title-case' });
} else throw e;
} Prevention
- Use the exported PAYEE_NAME_NORMALIZATIONS const / PayeeNameNormalization type instead of raw strings.
- Rely on the 'title-case' default unless normalization is explicitly needed.
- Build config UIs as a constrained select of the two allowed values.
- Grep for the option name in release notes when upgrading — the enum can grow.
When it happens
Trigger: Calling importTransactions with opts.payeeNameNormalization set to anything other than 'original' or 'title-case', e.g. 'Title-Case', 'lowercase', 'none', or an outdated value from an older API version.
Common situations: Copy-pasting option names from old docs or blog posts; case-mismatched enum values from loosely typed JS callers; a config UI passing user-entered text instead of a constrained select value.
Understand the failure class
Background: Invalid enum value errors: "Unknown type", "Invalid scope", "must be one of" — when a string is not on the library's allowed list — this error's family across 23 libraries.
Related errors
- Unknown payee name normalization: ${String(normalization)}
- `date` is required when adding a transaction
- Amount is invalid, must be an integer: ${trans.amount}
- `payeeName` is required when adding a transaction
- There is already a filter named ${item.name}
AI-assisted analysis of actualbudget/actual@d4334cb6e6 (2026-08-29).
Data as JSON: /api/errors/00c01394be28b7a8.
Report an issue: GitHub.