actualbudget/actual · error
token-expired
token-expired
Error message
token-expired
What it means
The sync server validates the session's expiry: sessions carry an expires_at timestamp (unix seconds), and TOKEN_EXPIRATION_NEVER (-1) marks never-expiring tokens. If expires_at is not -1 and expires_at * 1000 is at or before the current time, validateSession returns 401 with reason 'token-expired'. The token is known to the server but its lifetime has elapsed.
Source
Thrown at packages/sync-server/src/util/validate-user.ts:33
}
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;
}
return session;
}
export function validateAuthHeader(req: Request) {
// fallback to trustedProxies when trustedAuthProxies not set
const trustedAuthProxies: string[] =
config.get('trustedAuthProxies') ?? config.get('trustedProxies');
// ensure the first hop from our server is trusted
const peer = req.socket.remoteAddress;
if (peer === undefined) {
console.error(`Header Auth Login attempted but there was no defined peer.`);View on GitHub (pinned to d4334cb6e6)
Solutions
- Re-authenticate to get a fresh token (log in again via /login or /openid/login); the client should handle 401 reason 'token-expired' by refreshing automatically.
- Increase the server's token expiration setting (token_expiration_int in the config / TOKEN_EXPIRATION env) if sessions expire too quickly.
- Verify server clock (NTP) — a fast clock invalidates tokens early.
- For long-lived automation/API usage, configure never-expiring behavior where appropriate (expires_at = -1) or implement token refresh.
Example fix
// before: blindly reusing stored token
const res = await fetch(url + '/sync', { headers: { 'x-actual-token': storedToken } });
// after: handle expiry by re-authenticating
let res = await fetch(url + '/sync', { headers: { 'x-actual-token': storedToken } });
if (res.status === 401) {
storedToken = await login(password); // refresh and retry
res = await fetch(url + '/sync', { headers: { 'x-actual-token': storedToken } });
} Defensive patterns
Strategy: retry
Validate before calling
const expiresAtSec = session?.expires_at;
if (expiresAtSec !== -1 && expiresAtSec * 1000 <= Date.now()) {
throw new Error('token expired; re-authenticate before calling the sync server');
} Type guard
function isSessionValid(s: { expires_at: number } | null): boolean {
return s != null && (s.expires_at === -1 || s.expires_at * 1000 > Date.now());
} Try / catch
try {
const res = await fetch(url + '/sync', { headers: { 'x-actual-token': token } });
if (res.status === 401 && (await res.json()).reason === 'token-expired') {
token = await login(credentials); // refresh token, then retry
return await syncRequest(token);
}
} catch (e) {
logger.error('sync failed after token refresh', e);
} Prevention
- Check expires_at locally before each request and refresh proactively.
- Sync client clocks with NTP to avoid premature expiry.
- Set an appropriate token_expiration_int on the server for your usage pattern.
- Handle 401 reason 'token-expired' with automatic re-login in your HTTP client wrapper.
When it happens
Trigger: Any authenticated sync-server request where the stored session's expires_at (unix seconds) is <= now and not -1. Happens when a client keeps a token longer than the server's configured session expiration (openId/header-auth token lifetimes or the server's token expiration setting) and reuses it after expiry.
Common situations: Desktop or mobile app left open across a long period with an expired token; clocks skewed between client and server making a valid token look expired; server-side token_expiration_int set to a short duration (e.g. 3600s) while the app expects long-lived sessions.
Related errors
- token-not-found
- 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/fb90b8f24f4d5fe7.
Report an issue: GitHub.