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

  1. Re-authenticate the client: log in again via /login (or the app's sign-in screen) to obtain a fresh session token.
  2. Verify the client is pointed at the correct syncServerURL and that the server's account.db still contains the session (SELECT * FROM sessions).
  3. Ensure the x-actual-token header is forwarded by any reverse proxy in front of the server.
  4. 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

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


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