actualbudget/actual · error
Unknown payee name normalization: ${String(normalization)}
Error message
Unknown payee name normalization: ${String(normalization)} What it means
normalizePayeeName applies one of a fixed set of normalizations ('original' or 'title-case') to payee names during transaction sync. A `satisfies never` guard makes the switch exhaustive at compile time; at runtime an unrecognized value falls through to this throw. It protects the sync pipeline from invalid normalization options coming from callers.
Source
Thrown at packages/loot-core/src/server/accounts/sync.ts:430
return trans.payee;
}
export const PAYEE_NAME_NORMALIZATIONS = ['original', 'title-case'] as const;
export type PayeeNameNormalization = (typeof PAYEE_NAME_NORMALIZATIONS)[number];
function normalizePayeeName(
payeeName: string,
normalization: PayeeNameNormalization,
): string {
switch (normalization) {
case 'original':
return payeeName;
case 'title-case':
return title(payeeName);
default:
normalization satisfies never;
throw new Error(
`Unknown payee name normalization: ${String(normalization)}`,
);
}
}
async function normalizeTransactions(
transactions,
acctId,
{
payeeNameNormalization = 'title-case',
}: {
payeeNameNormalization?: PayeeNameNormalization;
} = {},
) {
const payeesToCreate = new Map();
const normalized = [];
for (let trans of transactions) {View on GitHub (pinned to d4334cb6e6)
Solutions
- Change the normalization value to exactly 'original' or 'title-case'.
- Check the option's source (API payload, config) for typos and casing ('title-case', not 'titlecase').
- Pin your API client version to match the running Actual version.
- Log the incoming normalization value at the boundary to catch bad payloads early.
Example fix
// before
normalizeTransactions({ normalization: 'titlecase' });
// after
normalizeTransactions({ normalization: 'title-case' }); // 'original' | 'title-case' Defensive patterns
Strategy: type-guard
Validate before calling
const NORMALIZATIONS = ['original', 'title-case'] as const;
type Normalization = (typeof NORMALIZATIONS)[number];
if (!NORMALIZATIONS.includes(opts.normalization as Normalization)) {
throw new Error(`normalization must be one of ${NORMALIZATIONS.join(', ')}`);
} Type guard
function isNormalization(v: unknown): v is 'original' | 'title-case' {
return v === 'original' || v === 'title-case';
} Try / catch
try {
await importTransactions(accountId, txs, opts);
} catch (e) {
if (e instanceof Error && e.message.startsWith('Unknown payee name normalization')) {
console.error('Fix the normalization option; use "original" or "title-case"');
return;
}
throw e;
} Prevention
- Type the option as the literal union ('original' | 'title-case') so typos fail at compile time.
- Copy option names from the API type definitions, not from memory.
- Boundary-check any user/CLI input that maps to this option.
- Keep API client and server versions in sync.
When it happens
Trigger: Calling the transactions-sync path (normalizeTransactions, e.g. via importTransactions) with a payee-name normalization value other than 'original' or 'title-case' — e.g. 'titlecase', 'capitalize', or an option from a mismatched client version.
Common situations: API integrations passing free-form strings for the option; version drift where a caller uses an option added in a newer/older version than the running core; scripts copying option names with wrong spelling/casing.
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
- transactions-import: accountId must be an id
- `date` is required when adding a transaction
- Amount is invalid, must be an integer: ${trans.amount}
- There is already a filter named ${item.name}
- Filter name is required
AI-assisted analysis of actualbudget/actual@d4334cb6e6 (2026-08-29).
Data as JSON: /api/errors/101659129fa4f9ca.
Report an issue: GitHub.