we-promise/sure · error · Provider::Questrade::AuthenticationError
access_forbidden
access_forbidden
Error message
Access forbidden - check your permissions
What it means
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.
Source
Thrown at app/models/provider/questrade.rb:256
level: "error",
message: "Questrade API #{reason} (#{response.code})",
source: self.class.name,
provider_key: "questrade",
metadata: { status: response.code, body: response.body.to_s.first(1000) }
)
end
def handle_response(response)
case response.code
when 200, 201
JSON.parse(response.body, symbolize_names: true)
when 400
capture_response_error("bad_request", response)
raise Error.new("Questrade bad request (#{response.code})", :bad_request)
when 401
raise AuthenticationError.new("Invalid or expired Questrade credentials", :unauthorized)
when 403
raise AuthenticationError.new("Access forbidden - check your permissions", :access_forbidden)
when 404
raise Error.new("Resource not found", :not_found)
when 429
raise RetryableResponseError.new("Questrade rate limit exceeded. Please try again later.", :rate_limited)
when 500..599
raise RetryableResponseError.new("Questrade server error (#{response.code}). Please try again later.", :server_error)
else
capture_response_error("unexpected_response", response)
raise Error.new("Questrade unexpected response (#{response.code})", :unknown)
end
end
end
View on GitHub (pinned to e69894adb9)
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.
Example fix
# before
provider.get_holdings(account_id: stored_account_id) # stale/foreign id
# after
account_ids = provider.list_accounts[:accounts].map { |a| a[:number] }
if account_ids.include?(stored_account_id)
provider.get_holdings(account_id: stored_account_id)
else
ItemAccount.where(external_id: stored_account_id).update_all(active: false)
end Defensive patterns
Strategy: try-catch
Validate before calling
owned = provider.list_accounts[:accounts].map { |a| a[:number] }
raise ArgumentError, "account #{id} not accessible" unless owned.include?(id) Try / catch
begin provider.get_holdings(account_id: id) rescue Provider::Questrade::AuthenticationError => e raise unless e.error_type == :access_forbidden item_account.update!(active: false, last_error: "403 from Questrade") end
Prevention
- Refresh the account list each sync and only iterate IDs it returns.
- Keep practice vs production tokens and their accounts in separate items/configs.
When it happens
Trigger: 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.
Common situations: 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.
Understand the failure class
- Authentication and authorization failures — expired tokens, bad credentials, and missing scopes.
Related errors
AI-assisted analysis of we-promise/sure@e69894adb9 (2026-08-21).
Data as JSON: /api/errors/e1910dd606871bb0.
Report an issue: GitHub.