actualbudget/actual · error

Forbidden

Error message

Forbidden

What it means

After following redirects, getAccounts checks for an HTTP 403 and throws 'Forbidden'. SimpleFIN returns 403 when the access key's credentials are rejected — most commonly after the user revoked access via the SimpleFIN Bridge, or the key/password was invalidated. The server surfaces this instead of a parse error so callers know it's an authorization problem.

Source

Thrown at packages/sync-server/src/app-simplefin/app-simplefin.js:469

    });

    const location = response.headers.get('location');
    if (response.status < 300 || response.status >= 400 || !location) {
      break;
    }
    if (hop >= MAX_REDIRECTS) {
      throw new Error('Too many redirects');
    }

    const nextUrl = new URL(location, currentUrl);
    if (nextUrl.origin !== new URL(currentUrl).origin) {
      delete headers.Authorization;
    }
    currentUrl = nextUrl.toString();
  }

  if (response.status === 403) {
    throw new Error('Forbidden');
  }

  const text = await response.text();
  try {
    const results = JSON.parse(text);
    results.sferrors = results.errors;
    results.hasError = false;
    results.errors = {};
    return results;
  } catch (e) {
    console.log(`Error parsing JSON response: ${text}`);
    throw e;
  }
}

View on GitHub (pinned to d4334cb6e6)

Solutions

  1. Generate a new access key at https://bridge.simplefin.org/auth/login and update it in the SimpleFIN bank-sync settings.
  2. Log in to the SimpleFIN Bridge dashboard and re-authorize access for the integration.
  3. Confirm the stored access key is current (not an older revoked one) and re-run setup.
  4. If access was intentionally revoked, remove the SimpleFIN account from Actual instead of retrying.

Example fix

// before
// syncing with stale key
await getAccounts(oldAccessKey, startDate, endDate); // 403

// after
// re-setup with fresh key from bridge.simplefin.org
const newKey = await claimAccessKey(newToken);
await getAccounts(newKey, startDate, endDate);
Defensive patterns

Strategy: try-catch

Try / catch

try {
  const accounts = await getAccounts(accessKey, startDate, endDate);
} catch (e) {
  if (e.message === 'Forbidden') {
    // access revoked — prompt user to re-authenticate via bridge.simplefin.org
    promptSimplefinReauth();
    return;
  }
  throw e;
}

Prevention

When it happens

Trigger: GET to the SimpleFIN accounts/transactions URL (after redirects) returns 403: revoked access key, deleted SimpleFIN account, expired credentials, or the key no longer matching the server.

Common situations: User clicked 'revoke' in the SimpleFIN Bridge dashboard while Actual still holds the old key; account password rotated at SimpleFIN; key regenerated and the old one invalidated.

Understand the failure class

Related errors


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