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
- Re-authenticate with a token for the user that owns the budget, or log in to the correct account in the client
- Use the fileId of a budget the current user actually owns
- Re-upload/sync the budget for this user so a matching files.users row exists
- 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
- Re-login after switching Actual accounts
- Verify the token and fileId come from the same user/session
- After recreating a budget, use its new fileId
- On multi-user servers, confirm file ownership before API calls
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.