actualbudget/actual · error

file-access-denied

file-access-denied

Error message

You don't have permissions over this file

What it means

POST /status passed the fileId format check but canAccessFile(fileId, res.locals.user_id) returned false, so the server responds HTTP 403 with reason 'file-access-denied'. The authenticated user token does not own or have access to the referenced budget file. This is an authorization failure, not a formatting problem.

Source

Thrown at packages/sync-server/src/app-pluggyai/app-pluggyai.js:40

  return isAdmin(userId) || UserService.countUserAccess(fileId, userId) > 0;
}

app.post(
  '/status',
  handleError(async (req, res) => {
    const fileId = req.get('X-Actual-File-Id');
    if (!!fileId) {
      if (!isValidFileId(fileId)) {
        res.status(400).send({
          status: 'error',
          reason: 'invalid-file-id',
          details: 'invalid fileId',
        });
        return;
      }

      if (!canAccessFile(fileId, res.locals.user_id)) {
        res.status(403).send({
          status: 'error',
          reason: 'file-access-denied',
          details: "You don't have permissions over this file",
        });
        return;
      }
    }

    const source = pluggyaiService.getCredentialSource(fileId);

    res.send({
      status: 'ok',
      data: {
        configured: !!source,
        source,
      },
    });
  }),

View on GitHub (pinned to d4334cb6e6)

Solutions

  1. Re-authenticate with a token for the user that owns the budget, or log in to the correct account in the client
  2. Use the fileId of a budget the current user actually owns
  3. Re-upload/sync the budget for this user so a matching files.users row exists
  4. Check the files and users_access tables (or equivalent) to confirm ownership mapping

Example fix

// before: token for user A, fileId of user B's budget
// after: log in as user B or use a fileId owned by user A
Defensive patterns

Strategy: validation

Validate before calling

// verify ownership client-side before calling:
const files = await fetch('/files/list', { headers: authHeaders }).then(r => r.json());
const owned = files.data?.some(f => f.fileId === fileId);
if (!owned) throw new Error('Current user cannot access this fileId');

Type guard

function canUserAccessFile(fileId, ownedFiles) {
  return Array.isArray(ownedFiles) && ownedFiles.some(f => f?.fileId === fileId);
}

Try / catch

const res = await fetch('/pluggyai/status', { headers: { 'X-Actual-File-Id': fileId, ...authHeaders } });
if (res.status === 403 && (await res.json()).reason === 'file-access-denied') {
  await reauthenticateAsOwner(); // or switch to the owning user's budget
}

Prevention

When it happens

Trigger: POST /status with a well-formed X-Actual-File-Id belonging to a different user's budget than the one implied by the request's auth token.

Common situations: Using an API token issued for a different Actual account; switching budgets and reusing a stale fileId; multi-user server where the file belongs to another user; token/user mismatch after re-creating the budget (new id).

Related errors


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