actualbudget/actual · error
token-not-found
token-not-found
Error message
token-not-found
What it means
The sync-server rejects the request because the session token presented by the client does not exist in the account database. validateSession reads the token from the request body or the x-actual-token header, looks it up via getSession, and returns a 401 with details 'token-not-found' when no matching row is found. This means the server has no record of ever issuing that token (or it was deleted, e.g. by a logout that clears sessions).
Source
Thrown at packages/sync-server/src/util/validate-user.ts:20
import ipaddr from 'ipaddr.js';
import { getSession } from '#account-db';
import { config } from '#load-config';
export const TOKEN_EXPIRATION_NEVER = -1;
const MS_PER_SECOND = 1000;
export function validateSession(req: Request, res: Response) {
let { token } = req.body || {};
if (!token) {
token = req.headers['x-actual-token'];
}
const session = getSession(token);
if (!session) {
res.status(401);
res.send({
status: 'error',
reason: 'unauthorized',
details: 'token-not-found',
});
return null;
}
if (
session.expires_at !== TOKEN_EXPIRATION_NEVER &&
session.expires_at * MS_PER_SECOND <= Date.now()
) {
res.status(401);
res.send({
status: 'error',
reason: 'token-expired',
});
return null;View on GitHub (pinned to d4334cb6e6)
Solutions
- Re-authenticate the client: log in again via /login (or the app's sign-in screen) to obtain a fresh session token.
- Verify the client is pointed at the correct syncServerURL and that the server's account.db still contains the session (SELECT * FROM sessions).
- Ensure the x-actual-token header is forwarded by any reverse proxy in front of the server.
- If the server database was reset, users must log in again; restore account.db from backup if old tokens must keep working.
Example fix
// before: request without token
await fetch(url + '/sync', { method: 'POST', body });
// after: attach token
const headers = { 'x-actual-token': token, 'Content-Type': 'application/json' };
await fetch(url + '/sync', { method: 'POST', headers, body }); Defensive patterns
Strategy: try-catch
Validate before calling
const token = body.token || headers['x-actual-token'];
if (!token) throw new Error('x-actual-token header or body token is required before calling the sync server'); Type guard
function hasToken(req): req is Request & { token: string } {
return typeof (req.body?.token ?? req.headers['x-actual-token']) === 'string' &&
(req.body?.token ?? req.headers['x-actual-token']).length > 0;
} Try / catch
try {
const res = await fetch(url + '/sync', { headers: { 'x-actual-token': token } });
const data = await res.json();
if (res.status === 401 && data.details === 'token-not-found') {
await reauthenticate(); // obtain fresh token and retry once
}
} catch (e) {
logger.error('sync request failed', e);
} Prevention
- Always re-login after a server database reset or migration.
- Store the token centrally and refresh it on any 401 instead of caching indefinitely.
- Confirm reverse proxies forward the x-actual-token header.
- Pin the syncServerURL per environment to avoid pointing clients at the wrong server.
When it happens
Trigger: Calling any sync-server endpoint authenticated by validateSession (e.g. /sync, /download, /list-user-files) with: (1) no x-actual-token header and no token in the body, (2) a token string that was never issued by this server (wrong server URL, server wiped its account.db), or (3) a token invalidated by a password change / session cleanup on the server.
Common situations: Client pointing at the wrong sync-server instance (self-hosted vs hosted); server database re-created or migrated while clients kept old tokens; reverse-proxy stripping the x-actual-token header; hardcoding a token from a dev environment into production config.
Related errors
- token-expired
- login: User token not set
- User ID is required for file creation
- Authentication required. Set --password/--session-token, ACT
- Authentication required. Provide --password or --session-tok
AI-assisted analysis of actualbudget/actual@d4334cb6e6 (2026-08-29).
Data as JSON: /api/errors/4aef2ff5ae4c0120.
Report an issue: GitHub.