def fetch(self, endpoint=None, page=1, key=None, filter='default', **kwargs):
        """Returns the results of an API call.

        This is the main work horse of the class. It builds the API query
        string and sends the request to Stack Exchange. If there are multiple
        pages of results, and we've configured `max_pages` to be greater than
        1, it will automatically paginate through the results and return a
        single object.

        Returned data will appear in the `items` key of the resulting
        dictionary.

        :param endpoint: (string) The API end point being called. Available endpoints are listed on
            the official API documentation: http://api.stackexchange.com/docs

            This can be as simple as ``fetch('answers')``, to call the answers
            end point

            If calling an end point that takes additional parameter, such as `id`s
            pass the ids as a list to the `ids` key:

                .. code-block:: python

                    fetch('answers/{}', ids=[1,2,3])

            This will attempt to retrieve the answers for the three listed ids.

            If no end point is passed, a ``ValueError`` will be raised
        :param page: (int) The page in the results to start at. By default, it will start on
            the first page and automatically paginate until the result set
            reached ``max_pages``.
        :param key: (string) The site you are issuing queries to.
        :param filter: (string) The filter to utilize when calling an endpoint. Different filters
            will return different keys. The default is ``default`` and this will
            still vary depending on what the API returns as default for a
            particular endpoint
        :param kwargs: Parameters accepted by individual endpoints. These parameters
            **must** be named the same as described in the endpoint documentation
        :rtype: (dictionary) A dictionary containing wrapper data regarding the API call
            and the results of the call in the `items` key. If multiple
            pages were received, all of the results will appear in the
            ``items`` tag.
        """
        if not endpoint:
            raise ValueError('No end point provided.')

        self._endpoint = endpoint

        params = {
            "pagesize": self.page_size,
            "page": page,
            "filter": filter
        }

        if self.key:
            params['key'] = self.key
        if self.access_token:
            params['access_token'] = self.access_token

        # This block will replace {ids} placeholds in end points
        # converting .fetch('badges/{ids}', ids=[222, 1306, 99999]) to
        #   badges/222;1306;99999
        for k, value in list(kwargs.items()):
            if "{" + k + "}" in endpoint:
                endpoint = endpoint.replace("{"+k+"}", ';'.join(requests.compat.quote_plus(str(x)) for x in value))
                kwargs.pop(k, None)

        date_time_keys = ['fromdate', 'todate', 'since', 'min', 'max']
        for k in date_time_keys:
            if k in kwargs:
                if isinstance(kwargs[k], datetime.datetime):
                    kwargs[k] = int(calendar.timegm(kwargs[k].utctimetuple()))

        # This block will see if there there are ids remaining
        # This would occur if the developer passed `badges` instead of `badges/{ids}` to `fetch`
        # If this is the case, then convert to a string and assume this goes at the end of the endpoint

        if 'ids' in kwargs:
            ids = ';'.join(str(x) for x in kwargs['ids'])
            kwargs.pop('ids', None)
            endpoint += "/{}".format(ids)

        params.update(kwargs)
        if self._api_key:
            params['site'] = self._api_key

        data = []
        run_cnt = 1
        backoff = 0
        total = 0
        while run_cnt <= self.max_pages:
            run_cnt += 1

            base_url = "{}{}/".format(self._base_url, endpoint)

            try:
                response = requests.get(base_url, params=params, proxies=self.proxy)
            except requests.exceptions.ConnectionError as e:
                raise StackAPIError(self._previous_call, str(e), str(e), str(e))

            self._previous_call = response.url
            try:
                response.encoding = 'utf-8-sig'
                response = response.json()
            except ValueError as e:
                raise StackAPIError(self._previous_call, str(e), str(e), str(e))

            try:
                error = response["error_id"]
                code = response["error_name"]
                message = response["error_message"]
                raise StackAPIError(self._previous_call, error, code, message)
            except KeyError:
                pass  # This means there is no error

            if key:
                data.append(response[key])
            else:
                data.append(response)

            if len(data) < 1:
                break

            backoff = 0
            total = 0
            page = 1
            if 'backoff' in response:
                backoff = int(response['backoff'])
                sleep(backoff+1)        # Sleep an extra second to ensure no timing issues
            if 'total' in response:
                total = response['total']
            if 'has_more' in response and response['has_more'] and run_cnt <= self.max_pages:
                params["page"] += 1
            else:
                break

        r = []
        for d in data:
            if 'items' in d:
                r.extend(d['items'])
        result = {'backoff': backoff,
                  'has_more': False if 'has_more' not in data[-1] else data[-1]['has_more'],
                  'page': params['page'],
                  'quota_max': -1 if 'quota_max' not in data[-1] else data[-1]['quota_max'],
                  'quota_remaining': -1 if 'quota_remaining' not in data[-1] else data[-1]['quota_max'],
                  'total': total,
                  'items': list(chain(r))}

        return result