actualbudget/actual · error

invalid-secret-name

invalid-secret-name

Error message

Unknown secret name

What it means

The POST /secrets endpoint validates that the requested secret name is one of the known entries in the SecretName enum before storing it. If the body's 'name' is absent, misspelled, or not part of the supported set, the server rejects the request with HTTP 400 and reason 'invalid-secret-name'. This prevents arbitrary keys from being written into the secret store.

Source

Thrown at packages/sync-server/src/app-secrets.js:42

// Per-budget-file secrets are managed by file owners
function isBudgetFileOwner(fileId, userId) {
  const { granted } = UserService.checkFilePermission(fileId, userId) || {
    granted: 0,
  };
  return granted > 0;
}

function canManagePerBudgetFileSecrets(fileId, userId) {
  return isAdmin(userId) || isBudgetFileOwner(fileId, userId);
}

app.post('/', async (req, res) => {
  const { name, value } = req.body || {};
  const fileId = req.get('X-Actual-File-Id');
  const perBudgetFile = fileId != null;

  if (!(name in SecretName)) {
    res.status(400).send({
      status: 'error',
      reason: 'invalid-secret-name',
      details: 'Unknown secret name',
    });
    return;
  }

  if (perBudgetFile) {
    if (!isValidFileId(fileId)) {
      res.status(400).send({
        status: 'error',
        reason: 'invalid-file-id',
        details: 'invalid fileId',
      });
      return;
    }

    if (!canManagePerBudgetFileSecrets(fileId, res.locals.user_id)) {

View on GitHub (pinned to d4334cb6e6)

Solutions

  1. Use an exact SecretName value from packages/sync-server/src/services/secrets-service.js (e.g. the gocardless/pluggy credential names) as the 'name' field.
  2. Print or fetch the current SecretName enum from the server source version you run and update your integration to match.
  3. Ensure the request body actually contains 'name' and Content-Type is application/json so express.json() parses it.
  4. Upgrade or align client and server versions so both agree on the secret name set.

Example fix

// before
await fetch('/secrets', { method: 'POST', body: JSON.stringify({ name: 'pluggy_key', value }) });
// after
await fetch('/secrets', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ name: 'pluggyai_client_secret', value }) });
Defensive patterns

Strategy: validation

Validate before calling

const KNOWN_SECRET_NAMES = ['gocardless_secret_id','gocardless_account_id','pluggyai_client_id','pluggyai_client_secret'];
function isValidSecretName(name) {
  return typeof name === 'string' && KNOWN_SECRET_NAMES.includes(name);
}

Type guard

function isSecretName(name) {
  return typeof name === 'string' && name in SecretName;
}

Try / catch

const res = await fetch('/secrets', { method: 'POST', body: JSON.stringify({ name, value }) });
if (res.status === 400) {
  const body = await res.json();
  if (body.reason === 'invalid-secret-name') throw new Error(`Unknown secret name: ${name}`);
}

Prevention

When it happens

Trigger: POST /secrets with a body whose 'name' is not a key of SecretName (e.g. typo, unknown provider name, name field missing entirely so 'undefined in SecretName' is false); API client using an outdated secret name after a rename in the SecretName enum.

Common situations: Hand-rolled scripts or integrations posting a secret name guessed from documentation of a different Actual version; provider credentials (e.g. gocardless/pluggy key names) changed between server versions; JSON body missing 'name' because of a content-type/parse issue.

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/b28d027d43cfdcb7. Report an issue: GitHub.