{"record":{"id":"3f579db6405f167d","repo":"actualbudget/actual","slug":"forbidden","errorCode":null,"errorMessage":"Forbidden","messagePattern":"Forbidden","errorType":"exception","errorClass":null,"httpStatus":null,"severity":"error","filePath":"packages/sync-server/src/app-simplefin/app-simplefin.js","lineNumber":469,"sourceCode":"    });\n\n    const location = response.headers.get('location');\n    if (response.status < 300 || response.status >= 400 || !location) {\n      break;\n    }\n    if (hop >= MAX_REDIRECTS) {\n      throw new Error('Too many redirects');\n    }\n\n    const nextUrl = new URL(location, currentUrl);\n    if (nextUrl.origin !== new URL(currentUrl).origin) {\n      delete headers.Authorization;\n    }\n    currentUrl = nextUrl.toString();\n  }\n\n  if (response.status === 403) {\n    throw new Error('Forbidden');\n  }\n\n  const text = await response.text();\n  try {\n    const results = JSON.parse(text);\n    results.sferrors = results.errors;\n    results.hasError = false;\n    results.errors = {};\n    return results;\n  } catch (e) {\n    console.log(`Error parsing JSON response: ${text}`);\n    throw e;\n  }\n}\n","sourceCodeStart":451,"sourceCodeEnd":484,"githubUrl":"https://github.com/actualbudget/actual/blob/d4334cb6e6123f4d3bcea1ad6166608884c7e658/packages/sync-server/src/app-simplefin/app-simplefin.js#L451-L484","documentation":"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.","triggerScenarios":"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.","commonSituations":"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.","solutions":["Generate a new access key at https://bridge.simplefin.org/auth/login and update it in the SimpleFIN bank-sync settings.","Log in to the SimpleFIN Bridge dashboard and re-authorize access for the integration.","Confirm the stored access key is current (not an older revoked one) and re-run setup.","If access was intentionally revoked, remove the SimpleFIN account from Actual instead of retrying."],"exampleFix":"// before\n// syncing with stale key\nawait getAccounts(oldAccessKey, startDate, endDate); // 403\n\n// after\n// re-setup with fresh key from bridge.simplefin.org\nconst newKey = await claimAccessKey(newToken);\nawait getAccounts(newKey, startDate, endDate);","handlingStrategy":"try-catch","validationCode":null,"typeGuard":null,"tryCatchPattern":"try {\n  const accounts = await getAccounts(accessKey, startDate, endDate);\n} catch (e) {\n  if (e.message === 'Forbidden') {\n    // access revoked — prompt user to re-authenticate via bridge.simplefin.org\n    promptSimplefinReauth();\n    return;\n  }\n  throw e;\n}","preventionTips":["On 403, always re-run the setup/claim flow rather than retrying the same key.","Don't revoke access in the SimpleFIN Bridge while integrations still use it.","Surface auth errors to users distinctly from network errors."],"tags":["simplefin","auth","http-error","credentials"],"backgroundTag":"http-403-forbidden","analyzedSha":"d4334cb6e6123f4d3bcea1ad6166608884c7e658","analyzedAt":"2026-08-29T01:02:11.213Z","schemaVersion":2},"datasetVersion":"2026-08-29T02:17:18.158Z"}