{"record":{"id":"abdad91411b6e003","repo":"we-promise/sure","slug":"access-forbidden-abdad9","errorCode":"access_forbidden","errorMessage":"Access forbidden - check your API token permissions","messagePattern":"Access forbidden - check your API token permissions","errorType":"exception","errorClass":"Provider::Mercury::MercuryError","httpStatus":403,"severity":"error","filePath":"app/models/provider/mercury.rb","lineNumber":122,"sourceCode":"        \"Authorization\" => \"Bearer #{token}\",\n        \"Content-Type\" => \"application/json\",\n        \"Accept\" => \"application/json\"\n      }\n    end\n\n    def handle_response(response)\n      case response.code\n      when 200\n        JSON.parse(response.body, symbolize_names: true)\n      when 400\n        Rails.logger.error \"Mercury API: Bad request - #{response.body}\"\n        raise MercuryError.new(\"Bad request to Mercury API: #{response.body}\", :bad_request)\n      when 401\n        # Parse the error response for more specific messages\n        error_message = parse_error_message(response.body)\n        raise MercuryError.new(error_message, :unauthorized)\n      when 403\n        raise MercuryError.new(\"Access forbidden - check your API token permissions\", :access_forbidden)\n      when 404\n        raise MercuryError.new(\"Resource not found\", :not_found)\n      when 429\n        raise MercuryError.new(\"Rate limit exceeded. Please try again later.\", :rate_limited)\n      else\n        Rails.logger.error \"Mercury API: Unexpected response - Code: #{response.code}, Body: #{response.body}\"\n        raise MercuryError.new(\"Failed to fetch data: #{response.code} #{response.message} - #{response.body}\", :fetch_failed)\n      end\n    end\n\n    def parse_error_message(body)\n      parsed = JSON.parse(body, symbolize_names: true)\n      errors = parsed[:errors] || {}\n\n      case errors[:errorCode]\n      when \"ipNotWhitelisted\"\n        ip = errors[:ip] || \"unknown\"\n        \"IP address not whitelisted (#{ip}). Add your IP to the API token's whitelist in Mercury dashboard.\"","sourceCodeStart":104,"sourceCodeEnd":140,"githubUrl":"https://github.com/we-promise/sure/blob/e69894adb92547273377398c15f45c979cd9416a/app/models/provider/mercury.rb#L104-L140","documentation":"Raised by Provider::Mercury#handle_response when Mercury returns HTTP 403. Unlike 401, the token itself authenticated successfully, but it does not have permission for the requested resource — Mercury API tokens are created against a specific organization and can be scoped, so a valid token still gets 403 for resources outside its scope.","triggerScenarios":"get_account(account_id) or get_account_transactions(account_id, ...) where account_id belongs to a different Mercury organization than the token's; a token whose scope excludes the account being queried; accessing an endpoint the token type does not allow.","commonSituations":"Sandbox/dev token used against production accounts or vice versa; user has multiple Mercury orgs and connected a token from the wrong one; account ID copied from the wrong workspace; base_url overridden to another environment while keeping the original token.","solutions":["Confirm the account_id you pass (get_account/get_account_transactions) is returned by get_accounts for the same token — if it is not, the token belongs to a different organization.","In the Mercury dashboard, check which organization the API token was created under and recreate it under the organization that owns the account.","Verify base_url is the default https://api.mercury.com/api/v1 unless you intentionally target another environment, and that the token matches that environment.","If the token has a configurable scope, widen it to cover accounts and transactions."],"exampleFix":"# before\naccounts = provider.get_accounts\nfirst = accounts[:accounts].first\nprovider.get_account(other_org_account_id) # 403\n\n# after\naccounts = provider.get_accounts\nowned_ids = accounts[:accounts].map { |a| a[:id] }\nraise ArgumentError, \"account #{id} not visible to this token\" unless owned_ids.include?(id)\nprovider.get_account(id)","handlingStrategy":"try-catch","validationCode":"# only query account IDs the token can actually see\nvisible = provider.get_accounts[:accounts].map { |a| a[:id] }\nraise ArgumentError, \"#{account_id} not owned by this token\" unless visible.include?(account_id)","typeGuard":null,"tryCatchPattern":"begin\n  provider.get_account(account_id)\nrescue Provider::Mercury::MercuryError => e\n  case e.error_type\n  when :access_forbidden then flag_wrong_org(item) # token vs account mismatch\n  else raise\n  end\nend","preventionTips":["Treat the get_accounts list as the source of truth and never feed IDs from elsewhere.","Pair the token and base_url environment (prod vs sandbox) in one config object so they cannot drift."],"tags":["mercury","http-403","permissions","api-token-scope","banking-api"],"backgroundTag":"http-403-forbidden","analyzedSha":"e69894adb92547273377398c15f45c979cd9416a","analyzedAt":"2026-08-21T18:22:41.165Z","schemaVersion":2},"datasetVersion":"2026-08-21T23:17:16.201Z"}