actualbudget/actual · error · Error

token-expired

token-expired

Error message

Authentication failed: invalid or expired session token

What it means

During init(), the previously stored session token is validated via 'subscribe-get-user'. If the user is missing or tokenExpired is true, the stale token is cleared and this error is thrown with code 'token-expired'. It means the server explicitly reported the token as expired or invalid, so the client must re-authenticate.

Source

Thrown at packages/loot-core/src/server/main.ts:304

  await sqlite.init();
  asyncStorage.init({ persist: false });
  await fs.init();
  fs._setDocumentDir(dataDir || process.cwd());

  if (serverURL) {
    setServer(serverURL);

    if ('sessionToken' in config && config.sessionToken) {
      // Session token authentication
      await runHandler(handlers['subscribe-set-token'], {
        token: config.sessionToken,
      });
      // Validate the token
      const user = await runHandler(handlers['subscribe-get-user'], undefined);
      if (!user || user.tokenExpired === true) {
        // Clear invalid token
        await runHandler(handlers['subscribe-set-token'], { token: '' });
        throw withErrorCode(
          new Error('Authentication failed: invalid or expired session token'),
          'token-expired',
        );
      }
      if (user.offline === true) {
        // Clear token since we can't validate
        await runHandler(handlers['subscribe-set-token'], { token: '' });
        throw withErrorCode(
          new Error('Authentication failed: server offline or unreachable'),
          'network-failure',
        );
      }
    } else if ('password' in config && config.password) {
      const result = await runHandler(handlers['subscribe-sign-in'], {
        password: config.password,
      });
      if (result?.error) {
        // `result.error` is already a machine-readable slug (e.g.

View on GitHub (pinned to d4334cb6e6)

Solutions

  1. Re-authenticate: clear the stored token (the code already clears it) and call init again with the server password
  2. Verify the client points at the correct sync server URL for the token it holds
  3. If you run the server, confirm it wasn't reset and its signing secret is stable across restarts
  4. Log the user in again through the UI to obtain a fresh token

Example fix

// before
await init({ URL: serverUrl, TOKEN: staleToken });
// after
await init({ URL: serverUrl, password }); // password auth re-derives a fresh token
Defensive patterns

Strategy: try-catch

Validate before calling

// Check token freshness before init
const token = getStoredToken();
if (!token || isTokenPayloadExpired(token)) {
  await reauthenticate(); // get fresh token via password sign-in
}

Type guard

function isTokenExpiredError(e: unknown): e is Error & { code: 'token-expired' } {
  return e instanceof Error && (e as { code?: string }).code === 'token-expired';
}

Try / catch

try {
  await init({ URL: serverUrl, TOKEN: storedToken });
} catch (e) {
  if (e instanceof Error && (e as { code?: string }).code === 'token-expired') {
    await init({ URL: serverUrl, password }); // fall back to password sign-in
  } else {
    throw e;
  }
}

Prevention

When it happens

Trigger: Calling init() (directly or via initApp) with SERVER_TOKEN/auth config set to a token the server no longer recognizes: token expired server-side, server restarted with different secret, or the budget/user was deleted.

Common situations: Long-lived desktop session reconnecting after token TTL passed; pointing a client at a different sync server than the one that issued the token; server database reset or user removed.

Understand the failure class

Related errors


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