{"record":{"id":"73624728c3f572ca","repo":"actualbudget/actual","slug":"debug-category-id-is-not-an-expense-categ","errorCode":null,"errorMessage":"${debug}: category \"${id}\" is not an expense category","messagePattern":"(.+?): category \"(.+?)\" is not an expense category","errorType":"validation","errorClass":"APIError","httpStatus":null,"severity":"error","filePath":"packages/loot-core/src/server/api.ts","lineNumber":119,"sourceCode":"  }\n}\n\nasync function validateExpenseCategory(debug, id) {\n  if (id == null) {\n    throw APIError(`${debug}: category id is required`);\n  }\n\n  const row = await db.first<Pick<db.DbCategory, 'is_income'>>(\n    'SELECT is_income FROM categories WHERE id = ?',\n    [id],\n  );\n\n  if (!row) {\n    throw APIError(`${debug}: category \"${id}\" does not exist`);\n  }\n\n  if (row.is_income !== 0) {\n    throw APIError(`${debug}: category \"${id}\" is not an expense category`);\n  }\n}\n\nfunction checkFileOpen() {\n  if (!(prefs.getPrefs() || {}).id) {\n    throw APIError('No budget file is open');\n  }\n}\n\nlet batchPromise = null;\n\nhandlers['api/batch-budget-start'] = async function () {\n  if (batchPromise) {\n    throw APIError('Cannot start a batch process: batch already started');\n  }\n\n  // If we are importing, all we need to do is start a raw database\n  // transaction. Updating spreadsheet cells doesn't go through the","sourceCodeStart":101,"sourceCodeEnd":137,"githubUrl":"https://github.com/actualbudget/actual/blob/d4334cb6e6123f4d3bcea1ad6166608884c7e658/packages/loot-core/src/server/api.ts#L101-L137","documentation":"validateExpenseCategory throws APIError when the category exists but is marked as an income category (is_income !== 0). Only expense categories are accepted by methods that categorize spending; income categories (like 'Income') live in a separate group and must not be used there.","triggerScenarios":"Passing an income category id (e.g. the 'Income' category or a user-created income group member) to transaction/expense APIs that call validateExpenseCategory, such as createTransaction with an income category or budget-amount methods restricted to expense categories.","commonSituations":"Picking categories[0] assuming it's an expense category when it's actually the income group; a category picker UI that doesn't filter by group type; scripts importing refund/income transactions with the Income category via the expense path.","solutions":["Filter q.getCategories() results by checking the category's group is_income flag (or use category groups) and select only expense categories","For income transactions, use the API path designed for income categorization instead","Validate the chosen category is in an expense (non-income) group before calling the API","In UIs, hide income-group categories from expense pickers"],"exampleFix":"// before\nconst categories = await q.getCategories();\nawait q.createTransaction({ accountId, date, amount, category: categories[0].id });\n// APIError: ...is not an expense category\n// after\nconst categories = await q.getCategories();\nconst expense = categories.find(c => !c.is_income);\nawait q.createTransaction({ accountId, date, amount, category: expense.id });","handlingStrategy":"validation","validationCode":"const categories = await q.getCategories();\nconst cat = categories.find(c => c.id === categoryId);\nif (cat && cat.is_income) {\n  throw new Error(`category ${categoryId} is an income category`);\n}","typeGuard":"function isExpenseCategory(cat) {\n  return cat != null && cat.is_income === 0;\n}","tryCatchPattern":null,"preventionTips":["Filter category pickers to non-income groups for expense flows","Never use categories[0] blindly — income groups often sort first","Check the is_income flag before assigning a category in scripts","Route income transactions through the appropriate income-handling API path"],"tags":["validation","api","category","type-mismatch"],"backgroundTag":"invalid-category-type","analyzedSha":"d4334cb6e6123f4d3bcea1ad6166608884c7e658","analyzedAt":"2026-08-29T01:02:11.213Z","schemaVersion":2},"datasetVersion":"2026-08-29T02:17:18.158Z"}