{"record":{"id":"c93da57cc17b4058","repo":"actualbudget/actual","slug":"payeename-is-required-when-adding-a-transaction","errorCode":null,"errorMessage":"`payeeName` is required when adding a transaction","messagePattern":"`payeeName` is required when adding a transaction","errorType":"validation","errorClass":null,"httpStatus":null,"severity":"error","filePath":"packages/loot-core/src/server/accounts/sync.ts","lineNumber":559,"sourceCode":"\n    if (!trans.amount) {\n      trans.amount = trans.transactionAmount.amount;\n    }\n\n    const mapping = mappings.get(trans.amount <= 0 ? 'payment' : 'deposit');\n\n    const date = trans[mapping.get('date')] ?? trans.date;\n    const payeeName = trans[mapping.get('payee')] ?? trans.payeeName;\n    const notes = trans[mapping.get('notes')];\n\n    // Validate the date because we do some stuff with it. The db\n    // layer does better validation, but this will give nicer errors\n    if (date == null) {\n      throw new Error('`date` is required when adding a transaction');\n    }\n\n    if (payeeName == null) {\n      throw new Error('`payeeName` is required when adding a transaction');\n    }\n\n    trans.imported_payee = trans.imported_payee || payeeName;\n    if (trans.imported_payee) {\n      trans.imported_payee = trans.imported_payee.trim();\n    }\n\n    let imported_id = trans.transactionId;\n    if (trans.cleared && !trans.transactionId && trans.internalTransactionId) {\n      imported_id = `${trans.account}-${trans.internalTransactionId}`;\n    }\n\n    // It's important to resolve both the account and payee early so\n    // when rules are run, they have the right data. Resolving payees\n    // also simplifies the payee creation process\n    trans.account = acctId;\n    trans.payee = await resolvePayee(trans, payeeName, payeesToCreate);\n","sourceCodeStart":541,"sourceCodeEnd":577,"githubUrl":"https://github.com/actualbudget/actual/blob/d4334cb6e6123f4d3bcea1ad6166608884c7e658/packages/loot-core/src/server/accounts/sync.ts#L541-L577","documentation":"Actual derives the payee for bank-sync transactions from payeeName when no mapped payee id exists; without it the transaction cannot be attributed or deduplicated. normalizeBankSyncTransactions validates payeeName right after date and throws this Error if it is null/undefined. (The accompanying imported_payee is also seeded from payeeName and trimmed.)","triggerScenarios":"normalizeBankSyncTransactions processes a downloaded transaction where both trans[mapping.get('payee')] and trans.payeeName are null/undefined — typically a provider payload with an empty/unset counterparty name or a wrong payee mapping key.","commonSituations":"Bank feeds with unnamed card transactions or ATM withdrawals lacking creditor data; GoCardless/SimpleFIN/enableBanking payloads where the merchant name sits under a different key than the mapping expects; hand-built transactions in custom sync integrations omitting payeeName; provider API version changes renaming the debtorName/creditorName fields.","solutions":["Set payeeName on every transaction in the download handler, falling back to a placeholder like 'Unknown payee' when the provider omits it.","Fix the field mapping so the provider's counterparty name field (e.g. creditorName, debtorName, remitterInformation) is mapped to payeeName.","Pre-process the download to filter out transactions with no counterparty information if they are not wanted.","Catch the error around the sync call, inspect the raw payload, and add a mapping/fallback for the missing field."],"exampleFix":"// before\nfor (const t of bankTx.transactions) {\n  normalized.push({ amount: ..., date: t.bookingDate });\n}\n// after\nfor (const t of bankTx.transactions) {\n  normalized.push({\n    amount: ...,\n    date: t.bookingDate,\n    payeeName: t.creditorName || t.debtorName || 'Unknown payee',\n  });\n}","handlingStrategy":"validation","validationCode":"function hasPayee(t) {\n  return t.payeeName != null || t.payee_id != null;\n}\nconst ready = txns.map(t => ({ ...t, payeeName: t.payeeName ?? 'Unknown payee' }));","typeGuard":"function hasPayeeName(t) {\n  return typeof (t && t.payeeName) === 'string' && t.payeeName.trim().length > 0;\n}","tryCatchPattern":"try {\n  await bankSync.syncAccount(accountId);\n} catch (e) {\n  if (e.message.includes('`payeeName` is required')) {\n    logger.warn({ accountId }, 'transaction missing payeeName; add provider fallback');\n  } else throw e;\n}","preventionTips":["Always provide a fallback payee name ('Unknown payee') for transactions lacking counterparty data.","Keep the payee field mapping per provider (GoCardless vs SimpleFIN vs enableBanking) in one tested place.","Re-verify mappings whenever a bank provider API version changes."],"tags":["bank-sync","validation","missing-field","payee"],"backgroundTag":"missing-required-field","analyzedSha":"d4334cb6e6123f4d3bcea1ad6166608884c7e658","analyzedAt":"2026-08-29T01:02:11.213Z","schemaVersion":2},"datasetVersion":"2026-08-29T02:17:18.158Z"}