we-promise/sure · error · Provider::Mercury::MercuryError

access_forbidden

access_forbidden

Error message

Access forbidden - check your API token permissions

What it means

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.

Source

Thrown at app/models/provider/mercury.rb:122

        "Authorization" => "Bearer #{token}",
        "Content-Type" => "application/json",
        "Accept" => "application/json"
      }
    end

    def handle_response(response)
      case response.code
      when 200
        JSON.parse(response.body, symbolize_names: true)
      when 400
        Rails.logger.error "Mercury API: Bad request - #{response.body}"
        raise MercuryError.new("Bad request to Mercury API: #{response.body}", :bad_request)
      when 401
        # Parse the error response for more specific messages
        error_message = parse_error_message(response.body)
        raise MercuryError.new(error_message, :unauthorized)
      when 403
        raise MercuryError.new("Access forbidden - check your API token permissions", :access_forbidden)
      when 404
        raise MercuryError.new("Resource not found", :not_found)
      when 429
        raise MercuryError.new("Rate limit exceeded. Please try again later.", :rate_limited)
      else
        Rails.logger.error "Mercury API: Unexpected response - Code: #{response.code}, Body: #{response.body}"
        raise MercuryError.new("Failed to fetch data: #{response.code} #{response.message} - #{response.body}", :fetch_failed)
      end
    end

    def parse_error_message(body)
      parsed = JSON.parse(body, symbolize_names: true)
      errors = parsed[:errors] || {}

      case errors[:errorCode]
      when "ipNotWhitelisted"
        ip = errors[:ip] || "unknown"
        "IP address not whitelisted (#{ip}). Add your IP to the API token's whitelist in Mercury dashboard."

View on GitHub (pinned to e69894adb9)

Solutions

  1. 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.
  2. In the Mercury dashboard, check which organization the API token was created under and recreate it under the organization that owns the account.
  3. 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.
  4. If the token has a configurable scope, widen it to cover accounts and transactions.

Example fix

# before
accounts = provider.get_accounts
first = accounts[:accounts].first
provider.get_account(other_org_account_id) # 403

# after
accounts = provider.get_accounts
owned_ids = accounts[:accounts].map { |a| a[:id] }
raise ArgumentError, "account #{id} not visible to this token" unless owned_ids.include?(id)
provider.get_account(id)
Defensive patterns

Strategy: try-catch

Validate before calling

# only query account IDs the token can actually see
visible = provider.get_accounts[:accounts].map { |a| a[:id] }
raise ArgumentError, "#{account_id} not owned by this token" unless visible.include?(account_id)

Try / catch

begin
  provider.get_account(account_id)
rescue Provider::Mercury::MercuryError => e
  case e.error_type
  when :access_forbidden then flag_wrong_org(item) # token vs account mismatch
  else raise
  end
end

Prevention

When it happens

Trigger: 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.

Common situations: 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.

Understand the failure class

Related errors


AI-assisted analysis of we-promise/sure@e69894adb9 (2026-08-21). Data as JSON: /api/errors/abdad91411b6e003. Report an issue: GitHub.