diff --git a/patchwork/api/bundle.py b/patchwork/api/bundle.py
new file mode 100644
index 0000000..5fa79b8
--- /dev/null
+++ b/patchwork/api/bundle.py
@@ -0,0 +1,80 @@
+# Patchwork - automated patch tracking system
+# Copyright (C) 2017 Stephen Finucane <stephen@that.guru>
+#
+# This file is part of the Patchwork package.
+#
+# Patchwork is free software; you can redistribute it and/or modify
+# it under the terms of the GNU General Public License as published by
+# the Free Software Foundation; either version 2 of the License, or
+# (at your option) any later version.
+#
+# Patchwork is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with Patchwork; if not, write to the Free Software
+# Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
+
+from django.db.models import Q
+from rest_framework.generics import ListAPIView
+from rest_framework.generics import RetrieveAPIView
+from rest_framework.serializers import HyperlinkedModelSerializer
+from rest_framework.serializers import SerializerMethodField
+
+from patchwork.api.base import PatchworkPermission
+from patchwork.api.filters import BundleFilter
+from patchwork.models import Bundle
+
+
+class BundleSerializer(HyperlinkedModelSerializer):
+
+    mbox = SerializerMethodField()
+
+    def get_mbox(self, instance):
+        request = self.context.get('request')
+        return request.build_absolute_uri(instance.get_mbox_url())
+
+    class Meta:
+        model = Bundle
+        fields = ('id', 'url', 'project', 'name', 'owner', 'patches',
+                  'public', 'mbox')
+        read_only_fields = ('owner', 'patches', 'mbox')
+        extra_kwargs = {
+            'url': {'view_name': 'api-bundle-detail'},
+            'project': {'view_name': 'api-project-detail'},
+            'owner': {'view_name': 'api-user-detail'},
+            'patches': {'view_name': 'api-patch-detail'},
+        }
+
+
+class BundleMixin(object):
+
+    permission_classes = (PatchworkPermission,)
+    serializer_class = BundleSerializer
+
+    def get_queryset(self):
+        if not self.request.user.is_anonymous():
+            bundle_filter = Q(owner=self.request.user) | Q(public=True)
+        else:
+            bundle_filter = Q(public=True)
+
+        return Bundle.objects\
+            .filter(bundle_filter)\
+            .prefetch_related('patches',)\
+            .select_related('owner', 'project')
+
+
+class BundleList(BundleMixin, ListAPIView):
+    """List bundles."""
+
+    filter_class = BundleFilter
+    search_fields = ('name',)
+    ordering_fields = ('id', 'name', 'owner')
+
+
+class BundleDetail(BundleMixin, RetrieveAPIView):
+    """Show a bundle."""
+
+    pass
diff --git a/patchwork/api/filters.py b/patchwork/api/filters.py
index 0f2e6e9..eff7ceb 100644
--- a/patchwork/api/filters.py
+++ b/patchwork/api/filters.py
@@ -20,6 +20,7 @@
 from django_filters import FilterSet
 from django_filters import IsoDateTimeFilter
 
+from patchwork.models import Bundle
 from patchwork.models import Check
 from patchwork.models import CoverLetter
 from patchwork.models import Event
@@ -68,3 +69,10 @@ class EventFilter(FilterSet):
     class Meta:
         model = Event
         fields = ('project', 'series', 'patch', 'cover')
+
+
+class BundleFilter(FilterSet):
+
+    class Meta:
+        model = Bundle
+        fields = ('project', 'owner', 'public')
diff --git a/patchwork/api/index.py b/patchwork/api/index.py
index 210c32e..513e8b6 100644
--- a/patchwork/api/index.py
+++ b/patchwork/api/index.py
@@ -34,4 +34,5 @@ class IndexView(APIView):
             'covers': request.build_absolute_uri(reverse('api-cover-list')),
             'series': request.build_absolute_uri(reverse('api-series-list')),
             'events': request.build_absolute_uri(reverse('api-event-list')),
+            'bundles': request.build_absolute_uri(reverse('api-bundle-list')),
         })
diff --git a/patchwork/tests/test_rest_api.py b/patchwork/tests/test_rest_api.py
index b6e6144..3a84f0f 100644
--- a/patchwork/tests/test_rest_api.py
+++ b/patchwork/tests/test_rest_api.py
@@ -26,6 +26,7 @@ from django.core.urlresolvers import reverse
 from patchwork.models import Check
 from patchwork.models import Patch
 from patchwork.models import Project
+from patchwork.tests.utils import create_bundle
 from patchwork.tests.utils import create_check
 from patchwork.tests.utils import create_cover
 from patchwork.tests.utils import create_maintainer
@@ -667,3 +668,79 @@ class TestCheckAPI(APITestCase):
 
         resp = self.client.delete(self.api_url(check))
         self.assertEqual(status.HTTP_405_METHOD_NOT_ALLOWED, resp.status_code)
+
+
+@unittest.skipUnless(settings.ENABLE_REST_API, 'requires ENABLE_REST_API')
+class TestBundleAPI(APITestCase):
+    fixtures = ['default_tags']
+
+    @staticmethod
+    def api_url(item=None):
+        if item is None:
+            return reverse('api-bundle-list')
+        return reverse('api-bundle-detail', args=[item])
+
+    def assertSerialized(self, bundle_obj, bundle_json):
+        self.assertEqual(bundle_obj.id, bundle_json['id'])
+        self.assertEqual(bundle_obj.name, bundle_json['name'])
+        self.assertEqual(bundle_obj.public, bundle_json['public'])
+        self.assertIn(bundle_obj.get_mbox_url(), bundle_json['mbox'])
+        self.assertEqual(bundle_obj.patches.count(),
+                         len(bundle_json['patches']))
+        self.assertIn(TestUserAPI.api_url(bundle_obj.owner.id),
+                      bundle_json['owner'])
+        self.assertIn(TestProjectAPI.api_url(bundle_obj.project.id),
+                      bundle_json['project'])
+
+    def test_list(self):
+        """Validate we can list bundles."""
+        resp = self.client.get(self.api_url())
+        self.assertEqual(status.HTTP_200_OK, resp.status_code)
+        self.assertEqual(0, len(resp.data))
+
+        user = create_user()
+        bundle_public = create_bundle(public=True, owner=user)
+        bundle_private = create_bundle(public=False, owner=user)
+
+        # anonymous users
+        # should only see the public bundle
+        resp = self.client.get(self.api_url())
+        self.assertEqual(status.HTTP_200_OK, resp.status_code)
+        self.assertEqual(1, len(resp.data))
+        bundle_rsp = resp.data[0]
+        self.assertSerialized(bundle_public, bundle_rsp)
+
+        # authenticated user
+        # should see the public and private bundle
+        self.client.force_authenticate(user=user)
+        resp = self.client.get(self.api_url())
+        self.assertEqual(status.HTTP_200_OK, resp.status_code)
+        self.assertEqual(2, len(resp.data))
+        for bundle_rsp, bundle_obj in zip(
+                resp.data, [bundle_public, bundle_private]):
+            self.assertSerialized(bundle_obj, bundle_rsp)
+
+    def test_detail(self):
+        """Validate we can get a specific bundle."""
+        user = create_user()
+        bundle = create_bundle(public=True)
+
+        resp = self.client.get(self.api_url(bundle.id))
+        self.assertEqual(status.HTTP_200_OK, resp.status_code)
+        self.assertSerialized(bundle, resp.data)
+
+    def test_create_update_delete(self):
+        """Ensure creates, updates and deletes aren't allowed"""
+        user = create_maintainer()
+        user.is_superuser = True
+        user.save()
+        self.client.force_authenticate(user=user)
+
+        resp = self.client.post(self.api_url(), {'email': 'foo@f.com'})
+        self.assertEqual(status.HTTP_405_METHOD_NOT_ALLOWED, resp.status_code)
+
+        resp = self.client.patch(self.api_url(user.id), {'email': 'foo@f.com'})
+        self.assertEqual(status.HTTP_405_METHOD_NOT_ALLOWED, resp.status_code)
+
+        resp = self.client.delete(self.api_url(1))
+        self.assertEqual(status.HTTP_405_METHOD_NOT_ALLOWED, resp.status_code)
diff --git a/patchwork/urls.py b/patchwork/urls.py
index 09b8b31..57d5cd7 100644
--- a/patchwork/urls.py
+++ b/patchwork/urls.py
@@ -158,6 +158,7 @@ if settings.ENABLE_REST_API:
         raise RuntimeError(
             'djangorestframework must be installed to enable the REST API.')
 
+    from patchwork.api import bundle as api_bundle_views
     from patchwork.api import check as api_check_views
     from patchwork.api import cover as api_cover_views
     from patchwork.api import event as api_event_views
@@ -208,6 +209,12 @@ if settings.ENABLE_REST_API:
         url(r'^series/(?P<pk>[^/]+)/$',
             api_series_views.SeriesDetail.as_view(),
             name='api-series-detail'),
+        url(r'^bundles/$',
+            api_bundle_views.BundleList.as_view(),
+            name='api-bundle-list'),
+        url(r'^bundles/(?P<pk>[^/]+)/$',
+            api_bundle_views.BundleDetail.as_view(),
+            name='api-bundle-detail'),
         url(r'^projects/$',
             api_project_views.ProjectList.as_view(),
             name='api-project-list'),
