{"record":{"id":"e1910dd606871bb0","repo":"we-promise/sure","slug":"access-forbidden-e1910d","errorCode":"access_forbidden","errorMessage":"Access forbidden - check your permissions","messagePattern":"Access forbidden - check your permissions","errorType":"exception","errorClass":"Provider::Questrade::AuthenticationError","httpStatus":403,"severity":"error","filePath":"app/models/provider/questrade.rb","lineNumber":256,"sourceCode":"        level: \"error\",\n        message: \"Questrade API #{reason} (#{response.code})\",\n        source: self.class.name,\n        provider_key: \"questrade\",\n        metadata: { status: response.code, body: response.body.to_s.first(1000) }\n      )\n    end\n\n    def handle_response(response)\n      case response.code\n      when 200, 201\n        JSON.parse(response.body, symbolize_names: true)\n      when 400\n        capture_response_error(\"bad_request\", response)\n        raise Error.new(\"Questrade bad request (#{response.code})\", :bad_request)\n      when 401\n        raise AuthenticationError.new(\"Invalid or expired Questrade credentials\", :unauthorized)\n      when 403\n        raise AuthenticationError.new(\"Access forbidden - check your permissions\", :access_forbidden)\n      when 404\n        raise Error.new(\"Resource not found\", :not_found)\n      when 429\n        raise RetryableResponseError.new(\"Questrade rate limit exceeded. Please try again later.\", :rate_limited)\n      when 500..599\n        raise RetryableResponseError.new(\"Questrade server error (#{response.code}). Please try again later.\", :server_error)\n      else\n        capture_response_error(\"unexpected_response\", response)\n        raise Error.new(\"Questrade unexpected response (#{response.code})\", :unknown)\n      end\n    end\nend\n","sourceCodeStart":238,"sourceCodeEnd":269,"githubUrl":"https://github.com/we-promise/sure/blob/e69894adb92547273377398c15f45c979cd9416a/app/models/provider/questrade.rb#L238-L269","documentation":"Raised by Provider::Questrade#handle_response on HTTP 403: the Bearer token authenticated, but the authenticated principal is not permitted for the request. Typically the Questrade account in the URL is not one the token's user/grants can access, or the practice account vs real account boundary is crossed.","triggerScenarios":"get_holdings/get_balances/get_activities for an account_id belonging to another Questrade user or to a different entitlement tier; querying a real trading account with a token issued for a practice (sandbox) login; an account shared with you but without API delegation enabled.","commonSituations":"Hardcoding or caching an account ID from list_accounts of a different user; supporting both practice and production Questrade realms and mixing their tokens/accounts; account access revoked by the owner after initial sync.","solutions":["Restrict all account IDs to those returned by list_accounts in the same session, refreshing the list each sync.","Confirm the realm: a practice-queue token only reaches practice accounts (and vice versa) — re-issue the token from the matching Questrade login.","If accessing a jointly held account, ensure the primary holder granted API access; otherwise sync only accounts you own.","Drop cached account records that consistently 403 and log them via the debug channel."],"exampleFix":"# before\nprovider.get_holdings(account_id: stored_account_id) # stale/foreign id\n\n# after\naccount_ids = provider.list_accounts[:accounts].map { |a| a[:number] }\nif account_ids.include?(stored_account_id)\n  provider.get_holdings(account_id: stored_account_id)\nelse\n  ItemAccount.where(external_id: stored_account_id).update_all(active: false)\nend","handlingStrategy":"try-catch","validationCode":"owned = provider.list_accounts[:accounts].map { |a| a[:number] }\nraise ArgumentError, \"account #{id} not accessible\" unless owned.include?(id)","typeGuard":null,"tryCatchPattern":"begin\n  provider.get_holdings(account_id: id)\nrescue Provider::Questrade::AuthenticationError => e\n  raise unless e.error_type == :access_forbidden\n  item_account.update!(active: false, last_error: \"403 from Questrade\")\nend","preventionTips":["Refresh the account list each sync and only iterate IDs it returns.","Keep practice vs production tokens and their accounts in separate items/configs."],"tags":["questrade","http-403","permissions","account-access","brokerage-api"],"backgroundTag":"http-403-forbidden","analyzedSha":"e69894adb92547273377398c15f45c979cd9416a","analyzedAt":"2026-08-21T18:22:41.165Z","schemaVersion":2},"datasetVersion":"2026-08-21T23:17:16.201Z"}