diff --git a/djblets/auth/ratelimit.py b/djblets/auth/ratelimit.py
index 93ceea8a9056032d882589f7c91431f59a40c402..e299a123974e32f0df254d7c93abb29d050a75ae 100644
--- a/djblets/auth/ratelimit.py
+++ b/djblets/auth/ratelimit.py
@@ -13,7 +13,8 @@ from django.utils import six
 from djblets.cache.backend import make_cache_key
 
 #: The default login rate limit key used for preparing cache keys.
-DEFAULT_KEY = 'ratelimit_user_or_ip'
+DEFAULT_LOGIN_KEY = 'login_ratelimit_user_or_ip'
+DEFAULT_API_KEY = 'api_ratelimit_user_or_ip'
 
 
 def get_user_id_or_ip(request):
@@ -94,19 +95,29 @@ def _prep_cache_key(limit, period, user_id_or_ip):
     return '%s%s%s' % (safe_rate, user_id_or_ip, window)
 
 
-def is_ratelimited(request, increment=False):
+def is_ratelimited(request, increment=False, rate_str=None, api_limit=None):
     """Check current user or IP address has exceeded the rate limit.
 
     The parameters are used to create a new key or fetch an existing key to
     save or update to the cache and to determine the amount of time period
-    left using the get_usage_count method.
+    left using the get_usage_count method. The key is customized based on
+    whether a failed login attempt or an API call has occurred.
 
     Args:
         request (django.http.HttpRequest):
             The HTTP request from the client.
 
         increment (bool):
-            Whether the number of login attempts should be incremented.
+            Whether the number of login or API request attempts
+            should be incremented.
+
+        rate_str (unicode):
+            The number of attempts allowed within a period of time
+            (can be seconds, minutes, hours, or days).
+
+        api_limit (bool):
+            Whether the rate limit should be applied based on a
+            failed login attempt or any API call.
 
     Returns:
         bool:
@@ -117,7 +128,7 @@ def is_ratelimited(request, increment=False):
     return usage['count'] >= usage['limit']
 
 
-def get_usage_count(request, increment=False):
+def get_usage_count(request, increment=False, rate_str=None, api_limit=False):
     """Return rate limit status for a given user or IP address.
 
     This method performs validation checks on the input parameters
@@ -134,6 +145,14 @@ def get_usage_count(request, increment=False):
         increment (bool):
             Whether the number of login attempts should be incremented.
 
+        rate_str (unicode):
+            The number of attempts allowed within a period of time
+            (can be seconds, minutes, hours, or days).
+
+        api_limit (bool):
+            Whether the rate limit should be applied based on a
+            failed login attempt or any API call.
+
     Returns:
         dict:
         A dictionary with the following keys:
@@ -148,8 +167,20 @@ def get_usage_count(request, increment=False):
             The time left before rate limit is over.
     """
     # Obtain specified time limit and period (e.g.: s, m, h, d).
-    rate_limit = getattr(settings, 'LOGIN_LIMIT_RATE', '5/m')
-    limit, period = Rate.parse(rate_limit)
+    default_key = ''
+
+    if api_limit == False or not api_limit:
+        if not rate_str:
+            rate_str = getattr(settings, 'LOGIN_LIMIT_RATE', '5/m')
+
+        default_key = DEFAULT_LOGIN_KEY
+    elif api_limit == True:
+        if not rate_str:
+            rate_str = getattr(settings, 'API_LIMIT_RATE', '1000/h')
+
+        default_key = DEFAULT_API_KEY
+
+    limit, period = Rate.parse(rate_str, api_limit)
 
     # Determine user ID or IP address from HTTP request.
     user_id_or_ip = get_user_id_or_ip(request)
@@ -157,7 +188,7 @@ def get_usage_count(request, increment=False):
     # Prepare cache key to add or update to cache
     # and determine remaining time period left.
     prepped_cache_key = _prep_cache_key(limit, period, user_id_or_ip)
-    cache_key = make_cache_key(DEFAULT_KEY + ':' + prepped_cache_key)
+    cache_key = make_cache_key(default_key + ':' + prepped_cache_key)
     time_left = _get_window(period) - int(time.time())
 
     if increment:
@@ -203,10 +234,9 @@ class Rate(object):
         'd': 24 * 60 * 60,
     }
     RATE_RE = re.compile(r'(\d+)/(\d*)([smhd])?')
-    DEFAULT_LIMIT = '5/m'
 
     @classmethod
-    def parse(cls, rate_str):
+    def parse(cls, rate_str, api_limit=False):
         """Return a Rate parsed from the given rate string.
 
         Converts the given rate string into a Rate object, which contains
@@ -214,26 +244,38 @@ class Rate(object):
         alotted for these attempts (seconds).
 
         Args:
-            rate (unicode):
+            rate_str (unicode):
                 The number of attempts allowed within a period
                 of time (can be seconds, minutes, hours, or days).
 
+            api_limit (bool):
+                Whether the rate limit should be applied based on a
+                failed login attempt or any API call.
+
         Returns:
             Rate:
             A Rate object that returns the number of login attempts
             allowed (count), and the total time period for these attempts
             (seconds).
         """
+        default_rate = '5/m'
+
+        if not api_limit:
+            default_rate = getattr(settings, 'LOGIN_LIMIT_RATE', '5/m')
+        else:
+            default_rate = getattr(settings, 'API_LIMIT_RATE', '1000/h')
+
         m = Rate.RATE_RE.match(rate_str)
 
         if m:
             count, multiplier, period = m.groups()
         else:
+            default_rate = '5/m'
             logging.warning('Could not parse given rate from settings: %r.'
                             'The default rate (%s) is being used.',
-                            rate_str, Rate.DEFAULT_LIMIT)
+                            rate_str, default_rate)
             count, multiplier, period = Rate.RATE_RE.match(
-                Rate.DEFAULT_LIMIT).groups()
+                default_rate).groups()
 
         if not period:
             period = 's'
diff --git a/djblets/settings.py b/djblets/settings.py
index e298735658ebf9206d190b56ebc8ebde44e1bc72..4b1d44c03c61b58b77f616964572bc29c3dbcd3e 100644
--- a/djblets/settings.py
+++ b/djblets/settings.py
@@ -22,6 +22,7 @@ HTDOCS_ROOT = os.path.join(DJBLETS_ROOT, 'htdocs')
 STATIC_ROOT = os.path.join(HTDOCS_ROOT, 'static')
 STATIC_URL = '/'
 LOGIN_LIMIT_RATE = '5/m'
+API_LIMIT_RATE = '1000/h'
 
 STATICFILES_DIRS = (
     os.path.join(DJBLETS_ROOT, 'static'),
diff --git a/djblets/webapi/resources/mixins/api_ratelimit.py b/djblets/webapi/resources/mixins/api_ratelimit.py
new file mode 100644
index 0000000000000000000000000000000000000000..5f86fda30cdc4829980be5569786e4c3e054994f
--- /dev/null
+++ b/djblets/webapi/resources/mixins/api_ratelimit.py
@@ -0,0 +1,132 @@
+"""Mixin for integrating rate limiting into any API call."""
+
+from __future__ import unicode_literals
+
+from django import forms
+from django.utils.translation import ugettext_lazy as _
+
+from djblets import settings as djblets_settings
+from djblets.auth.ratelimit import get_usage_count
+from djblets.webapi.resources.base import \
+    WebAPIResource as DjbletsWebAPIResource
+
+
+class WebAPIResourceRateLimitMixin(object):
+    """Mixin for integrating rate limiting into any API call.
+
+    For every request made by a user, the number of attempts is cached by the
+    ratelimiting feature implemented in get_usage_count() (see
+    auth/ratelimit.py).
+    """
+    #: The default number of API calls per time frame is used if the
+    #: LOGIN_LIMIT_RATE from djblets_settings cannot be obtained.
+    default_rate = '1000/h'
+    default_request_rate_limit = getattr(djblets_settings,
+                                        'LOGIN_LIMIT_RATE',
+                                        default_rate)
+
+    def get_request_rate_limit(self):
+        """Return the default rate limit.
+
+        Returns:
+            unicode:
+            The default rate limit.
+        """
+        return self.default_request_rate_limit
+
+    def set_request_rate_limit(self, rate_str):
+        """Set the default rate limit.
+
+        Args:
+            rate_str (unicode):
+                The rate limit that will be set as the new default rate limit.
+        """
+        self.default_request_rate_limit = rate_str
+
+    def get_ratelimit(self, request, increment=False):
+        """Return rate limit status for a given user or IP address.
+
+        This method performs validation checks on the input parameters
+        and creates the cache key to keep track of the
+        number of login attempts made by the user. It saves the new
+        cache key and initial number of attempts or updates the
+        existing cache key and number of attempts before returning
+        the count, limit, and time_left.
+
+        Args:
+            request (django.http.HttpRequest):
+                The HTTP request from the client.
+
+            increment (bool):
+                Whether the number of login attempts should be incremented.
+
+        Returns:
+            dict:
+            A dictionary with the following keys:
+
+            ``count`` (:py:class:`int`):
+                The number of login attempts made.
+
+            ``limit`` (:py:class:`int`):
+                The number of attempts allowed.
+
+            ``time_left`` (:py:class:`int`):
+                The time left before rate limit is over.
+        """
+        return get_usage_count(request,
+                               increment,
+                               self.get_request_rate_limit(),
+                               api_limit=True)
+
+    def should_check_ratelimit(self, request, user):
+        """Return whether the rate limit should be checked.
+
+        This method can be overridden to provide the option to check if a user
+        has exceeded the maximum number of API calls.
+
+        Args:
+            request (django.http.HttpRequest):
+                The HTTP request from the client.
+
+            user (django.contrib.auth.models.User):
+                The user who has made the request.
+
+        Returns:
+            boolean:
+                Return True if the rate limit should be checked, otherwise
+                False.
+        """
+        return True
+
+    def __call__(self, request, increment=False, api_format=None, *args, **kwargs):
+        """Check if rate limit of API calls has been reached.
+
+        This method provides the option to override Djblet's
+        WebAPIResource.__call__() method to check if the rate limit for the
+        number of API calls has been reached. If the rate limit has not been
+        reached, Then the WebAPIResource.__call__() method can invoke the
+        correct HTTP handler based on the type of request.
+
+        Args:
+            request (django.http.HttpRequest):
+                The HTTP request from the client.
+
+            api_format (unicode):
+                The api format.
+        """
+        if self.should_check_ratelimit(request, request.user):
+            ratelimit_result = self.get_ratelimit(request, increment)
+
+            if ratelimit_result['count'] >= ratelimit_result['limit']:
+                raise forms.ValidationError(
+                    _('Maximum number of API calls exceeded.'))
+
+            # Add header information to request before invoking the
+            # DjbletsWebAPIResource class.
+            request.META['RATE_LIMIT'] = self.get_request_rate_limit()
+            request.META['RATE_LIMIT_STATISTICS'] = ratelimit_result
+
+        return super(DjbletsWebAPIResource, self).__call__(request,
+                                                            api_format=None,
+                                                            *args,
+                                                            **kwargs)
diff --git a/djblets/webapi/tests/test_api_ratelimit.py b/djblets/webapi/tests/test_api_ratelimit.py
new file mode 100644
index 0000000000000000000000000000000000000000..3558b5bf73f5e5e8c8a9584eec199fd3b9339616
--- /dev/null
+++ b/djblets/webapi/tests/test_api_ratelimit.py
@@ -0,0 +1,45 @@
+from __future__ import print_function, unicode_literals
+
+from django.contrib.auth.models import User
+from django.core.exceptions import ValidationError
+from django.test.client import RequestFactory
+
+from djblets.testing.testcases import TestCase
+from djblets.webapi.resources.mixins.api_ratelimit import \
+    WebAPIResourceRateLimitMixin
+
+
+class WebAPIResourceTests(TestCase):
+    """Unit tests for djblets.webapi.resources.mixins.api_ratelimit."""
+
+    def setUp(self):
+        self.request = RequestFactory().request()
+        self.mixin = WebAPIResourceRateLimitMixin()
+
+    def test_rate_limit(self):
+        """Testing rate limit for API calls."""
+
+        rate_str = '4/m'
+        self.mixin.set_request_rate_limit(rate_str)
+        self.request.user = User()
+
+        self.assertEqual(rate_str, self.mixin.get_request_rate_limit())
+
+        usage = self.mixin.get_ratelimit(self.request, True)
+        self.assertEqual(usage['count'], 1)
+        self.assertEqual(usage['count'] >= usage['limit'], False)
+
+        usage = self.mixin.get_ratelimit(self.request, True)
+        self.assertEqual(usage['count'], 2)
+        self.assertEqual(usage['count'] >= usage['limit'], False)
+
+        usage = self.mixin.get_ratelimit(self.request, True)
+        self.assertEqual(usage['count'], 3)
+        self.assertEqual(usage['count'] >= usage['limit'], False)
+
+        usage = self.mixin.get_ratelimit(self.request, True)
+        self.assertEqual(usage['count'], 4)
+        self.assertEqual(usage['count'] >= usage['limit'], True)
+
+        with self.assertRaises(ValidationError):
+            self.mixin.__call__(self.request, increment=False, api_format=None)
diff --git a/tests/settings.py b/tests/settings.py
index b6c46d1f8c7fda0be004caf7bd30fd9bd436043d..85fd48a1ab731a246956c27f772fb258c1feadb9 100644
--- a/tests/settings.py
+++ b/tests/settings.py
@@ -119,3 +119,4 @@ for entry in os.listdir(base_path):
 INSTALLED_APPS += ['django_evolution']
 
 LOGIN_LIMIT_RATE = '5/m'
+API_LIMIT_RATE = '1000/h'
