actualbudget/actual · error

Unknown payee name 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.

Solutions

  1. Change the normalization value to exactly 'original' or 'title-case'.
  2. Check the option's source (API payload, config) for typos and casing ('title-case', not 'titlecase').
  3. Pin your API client version to match the running Actual version.
  4. 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

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


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

Appendix: 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)