actualbudget/actual · error · Error

missing-key

missing-key

Error message

File ${activeFile.name} is encrypted. Please provide a password.

What it means

Thrown by api/download-budget in packages/loot-core/src/server/api.ts when the active budget file (remote or local) has an encryptKeyId — i.e. it is end-to-end encrypted — but downloadBudget() was called without a password. Tagged with code 'missing-key'. The API refuses to proceed because it cannot derive the decryption key.

Source

Thrown at packages/loot-core/src/server/api.ts:217

    const file = files.find(f => f.groupId === syncId);
    if (!file) {
      throw withErrorCode(
        new Error(
          `Budget "${syncId}" not found. Check the sync id of your budget in the Advanced section of the settings page.`,
        ),
        'budget-not-found',
      );
    }

    remoteBudget = file;
  }

  const activeFile = remoteBudget ? remoteBudget : localBudget;

  // Set the e2e encryption keys
  if (activeFile.encryptKeyId) {
    if (!password) {
      throw withErrorCode(
        new Error(
          `File ${activeFile.name} is encrypted. Please provide a password.`,
        ),
        'missing-key',
      );
    }

    const result = await handlers['key-test']({
      cloudFileId: remoteBudget ? remoteBudget.fileId : localBudget.cloudFileId,
      password,
    });
    if (result.error) {
      throw withErrorCode(
        new Error(getTestKeyError(result.error)),
        result.error.reason,
      );
    }
  }

View on GitHub (pinned to d4334cb6e6)

Solutions

  1. Pass the encryption password: downloadBudget(syncId, { password })
  2. Store/retrieve the password from your secrets manager in headless environments
  3. Remove end-to-end encryption on the budget if you no longer need it (via the settings page)
  4. If password unknown, the data cannot be decrypted — restore from an unencrypted backup

Example fix

// before
await api.downloadBudget(syncId);
// after
await api.downloadBudget(syncId, { password: process.env.BUDGET_ENCRYPTION_PASSWORD });
Defensive patterns

Strategy: validation

Validate before calling

const file = (await getBudgetFiles()).find(f => f.groupId === syncId);
if (file?.encryptKeyId && !password) throw new Error('Password required: budget is E2E encrypted');

Type guard

function requiresPassword(f?: { encryptKeyId?: string | null }): boolean {
  return !!f?.encryptKeyId;
}

Try / catch

try {
  await api.downloadBudget(syncId, { password });
} catch (e) {
  if (e.code === 'missing-key') throw new Error('Supply a password for the encrypted budget');
  throw e;
}

Prevention

When it happens

Trigger: Calling downloadBudget(syncId) without the { password } option on a budget that was encrypted via Settings > Advanced > End-to-end encryption; also triggered when the local file references an encryptKeyId but the caller supplied no password to key-test/key setup.

Common situations: Automated scripts written before the budget was encrypted later gaining a key; users forgetting the file was E2E encrypted; headless jobs with credentials stored but password omitted from the downloadBudget call.

Related errors


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