diff --git a/docs/api/schemas/latest/patchwork.yaml b/docs/api/schemas/latest/patchwork.yaml
index 45a6118..faa0e4e 100644
--- a/docs/api/schemas/latest/patchwork.yaml
+++ b/docs/api/schemas/latest/patchwork.yaml
@@ -916,6 +916,112 @@ paths:
                 $ref: '#/components/schemas/Error'
       tags:
         - series
+  /api/relations/:
+    get:
+      description: List relations.
+      operationId: relations_list
+      parameters:
+        - $ref: '#/components/parameters/Page'
+        - $ref: '#/components/parameters/PageSize'
+        - $ref: '#/components/parameters/Order'
+      responses:
+        '200':
+          description: ''
+          headers:
+            Link:
+              $ref: '#/components/headers/Link'
+          content:
+            application/json:
+              schema:
+                type: array
+                items:
+                  $ref: '#/components/schemas/Relation'
+      tags:
+        - relations
+    put:
+      description: Update one or multiple relations.
+      operationId: relations_update
+      security:
+        - basicAuth: []
+        - apiKeyAuth: []
+      requestBody:
+        $ref: '#/components/requestBodies/Relation'
+      responses:
+        '200':
+          description: ''
+          content:
+            application/json:
+              schema:
+                type: array
+                items:
+                  $ref: '#/components/schemas/Relation'
+        '201':
+          description: Created
+        '400':
+          description: Bad request
+          content:
+            application/json:
+              schema:
+                $ref: '#/components/schemas/Error'
+        '403':
+          description: Forbidden
+          content:
+            application/json:
+              schema:
+                $ref: '#/components/schemas/Error'
+        '404':
+          description: Not found
+          content:
+            application/json:
+              schema:
+                $ref: '#/components/schemas/Error'
+      tags:
+        - relations
+    delete:
+      description: Delete all relations.
+      operationId: relations_delete
+      security:
+        - basicAuth: []
+        - apiKeyAuth: []
+      responses:
+        '204':
+          description: ''
+        '403':
+          description: Forbidden
+          content:
+            application/json:
+              schema:
+                $ref: '#/components/schemas/Error'
+      tags:
+        - relations
+
+  /api/relations/{id}/:
+    get:
+      description: Show a relation.
+      operationId: relation_read
+      parameters:
+        - in: path
+          name: id
+          description: A unique integer value identifying this relation.
+          required: true
+          schema:
+            title: ID
+            type: integer
+      responses:
+        '200':
+          description: ''
+          content:
+            application/json:
+              schema:
+                $ref: '#/components/schemas/Relation'
+        '404':
+          description: Not found
+          content:
+            application/json:
+              schema:
+                $ref: '#/components/schemas/Error'
+      tags:
+        - relations
   /api/users/:
     get:
       description: List users.
@@ -1179,6 +1285,22 @@ components:
         application/x-www-form-urlencoded:
           schema:
             $ref: '#/components/schemas/User'
+    Relation:
+      required: true
+      content:
+        application/json:
+          schema:
+            oneOf:
+              - type: array
+                items:
+                  $ref: '#/components/schemas/RelationUpdate'
+              - $ref: '#/components/schemas/RelationUpdate'
+        multipart/form-data:
+          schema:
+            $ref: '#/components/schemas/RelationUpdate'
+        application/x-www-form-urlencoded:
+          schema:
+            $ref: '#/components/schemas/RelationUpdate'
   schemas:
     Index:
       type: object
@@ -1777,6 +1899,17 @@ components:
           title: Delegate
           type: integer
           nullable: true
+    RelationUpdate:
+      type: object
+      properties:
+        id:
+          title: ID
+          type: integer
+        patches:
+          title: Patch IDs
+          type: array
+          items:
+            type: integer
     Person:
       type: object
       properties:
@@ -1966,6 +2099,22 @@ components:
             $ref: '#/components/schemas/PatchEmbedded'
           readOnly: true
           uniqueItems: true
+    Relation:
+      type: object
+      properties:
+        id:
+          title: ID
+          type: integer
+        url:
+          title: URL
+          type: string
+          format: uri
+          readOnly: true
+        patches:
+          type: array
+          items:
+            $ref: '#/components/schemas/PatchEmbedded'
+          uniqueItems: true
     User:
       type: object
       properties:
diff --git a/docs/api/schemas/patchwork.j2 b/docs/api/schemas/patchwork.j2
index 16d85a3..dd85ca6 100644
--- a/docs/api/schemas/patchwork.j2
+++ b/docs/api/schemas/patchwork.j2
@@ -917,6 +917,114 @@ paths:
                 $ref: '#/components/schemas/Error'
       tags:
         - series
+{% if version >= (1, 2) %}
+  /api/{{ version_url }}relations/:
+    get:
+      description: List relations.
+      operationId: relations_list
+      parameters:
+        - $ref: '#/components/parameters/Page'
+        - $ref: '#/components/parameters/PageSize'
+        - $ref: '#/components/parameters/Order'
+      responses:
+        '200':
+          description: ''
+          headers:
+            Link:
+              $ref: '#/components/headers/Link'
+          content:
+            application/json:
+              schema:
+                type: array
+                items:
+                  $ref: '#/components/schemas/Relation'
+      tags:
+        - relations
+    put:
+      description: Update one or multiple relations.
+      operationId: relations_update
+      security:
+        - basicAuth: []
+        - apiKeyAuth: []
+      requestBody:
+        $ref: '#/components/requestBodies/Relation'
+      responses:
+        '200':
+          description: ''
+          content:
+            application/json:
+              schema:
+                type: array
+                items:
+                  $ref: '#/components/schemas/Relation'
+        '201':
+          description: Created
+        '400':
+          description: Bad request
+          content:
+            application/json:
+              schema:
+                $ref: '#/components/schemas/Error'
+        '403':
+          description: Forbidden
+          content:
+            application/json:
+              schema:
+                $ref: '#/components/schemas/Error'
+        '404':
+          description: Not found
+          content:
+            application/json:
+              schema:
+                $ref: '#/components/schemas/Error'
+      tags:
+        - relations
+    delete:
+      description: Delete all relations.
+      operationId: relations_delete
+      security:
+        - basicAuth: []
+        - apiKeyAuth: []
+      responses:
+        '204':
+          description: ''
+        '403':
+          description: Forbidden
+          content:
+            application/json:
+              schema:
+                $ref: '#/components/schemas/Error'
+      tags:
+        - relations
+
+  /api/{{ version_url }}relations/{id}/:
+    get:
+      description: Show a relation.
+      operationId: relation_read
+      parameters:
+        - in: path
+          name: id
+          description: A unique integer value identifying this relation.
+          required: true
+          schema:
+            title: ID
+            type: integer
+      responses:
+        '200':
+          description: ''
+          content:
+            application/json:
+              schema:
+                $ref: '#/components/schemas/Relation'
+        '404':
+          description: Not found
+          content:
+            application/json:
+              schema:
+                $ref: '#/components/schemas/Error'
+      tags:
+        - relations
+{% endif %}
   /api/{{ version_url }}users/:
     get:
       description: List users.
@@ -1180,6 +1288,24 @@ components:
         application/x-www-form-urlencoded:
           schema:
             $ref: '#/components/schemas/User'
+{% if version >= (1, 2) %}
+    Relation:
+      required: true
+      content:
+        application/json:
+          schema:
+            oneOf:
+              - type: array
+                items:
+                  $ref: '#/components/schemas/RelationUpdate'
+              - $ref: '#/components/schemas/RelationUpdate'
+        multipart/form-data:
+          schema:
+            $ref: '#/components/schemas/RelationUpdate'
+        application/x-www-form-urlencoded:
+          schema:
+            $ref: '#/components/schemas/RelationUpdate'
+{% endif %}
   schemas:
     Index:
       type: object
@@ -1798,6 +1924,19 @@ components:
           title: Delegate
           type: integer
           nullable: true
+{% if version >= (1, 2) %}
+    RelationUpdate:
+      type: object
+      properties:
+        id:
+          title: ID
+          type: integer
+        patches:
+          title: Patch IDs
+          type: array
+          items:
+            type: integer
+{% endif %}
     Person:
       type: object
       properties:
@@ -1993,6 +2132,24 @@ components:
             $ref: '#/components/schemas/PatchEmbedded'
           readOnly: true
           uniqueItems: true
+{% if version >= (1, 2) %}
+    Relation:
+      type: object
+      properties:
+        id:
+          title: ID
+          type: integer
+        url:
+          title: URL
+          type: string
+          format: uri
+          readOnly: true
+        patches:
+          type: array
+          items:
+            $ref: '#/components/schemas/PatchEmbedded'
+          uniqueItems: true
+{% endif %}
     User:
       type: object
       properties:
diff --git a/docs/api/schemas/v1.2/patchwork.yaml b/docs/api/schemas/v1.2/patchwork.yaml
index 3a96aa3..17412d2 100644
--- a/docs/api/schemas/v1.2/patchwork.yaml
+++ b/docs/api/schemas/v1.2/patchwork.yaml
@@ -916,6 +916,112 @@ paths:
                 $ref: '#/components/schemas/Error'
       tags:
         - series
+  /api/1.2/relations/:
+    get:
+      description: List relations.
+      operationId: relations_list
+      parameters:
+        - $ref: '#/components/parameters/Page'
+        - $ref: '#/components/parameters/PageSize'
+        - $ref: '#/components/parameters/Order'
+      responses:
+        '200':
+          description: ''
+          headers:
+            Link:
+              $ref: '#/components/headers/Link'
+          content:
+            application/json:
+              schema:
+                type: array
+                items:
+                  $ref: '#/components/schemas/Relation'
+      tags:
+        - relations
+    put:
+      description: Update one or multiple relations.
+      operationId: relations_update
+      security:
+        - basicAuth: []
+        - apiKeyAuth: []
+      requestBody:
+        $ref: '#/components/requestBodies/Relation'
+      responses:
+        '200':
+          description: ''
+          content:
+            application/json:
+              schema:
+                type: array
+                items:
+                  $ref: '#/components/schemas/Relation'
+        '201':
+          description: Created
+        '400':
+          description: Bad request
+          content:
+            application/json:
+              schema:
+                $ref: '#/components/schemas/Error'
+        '403':
+          description: Forbidden
+          content:
+            application/json:
+              schema:
+                $ref: '#/components/schemas/Error'
+        '404':
+          description: Not found
+          content:
+            application/json:
+              schema:
+                $ref: '#/components/schemas/Error'
+      tags:
+        - relations
+    delete:
+      description: Delete all relations.
+      operationId: relations_delete
+      security:
+        - basicAuth: []
+        - apiKeyAuth: []
+      responses:
+        '204':
+          description: ''
+        '403':
+          description: Forbidden
+          content:
+            application/json:
+              schema:
+                $ref: '#/components/schemas/Error'
+      tags:
+        - relations
+
+  /api/1.2/relations/{id}/:
+    get:
+      description: Show a relation.
+      operationId: relation_read
+      parameters:
+        - in: path
+          name: id
+          description: A unique integer value identifying this relation.
+          required: true
+          schema:
+            title: ID
+            type: integer
+      responses:
+        '200':
+          description: ''
+          content:
+            application/json:
+              schema:
+                $ref: '#/components/schemas/Relation'
+        '404':
+          description: Not found
+          content:
+            application/json:
+              schema:
+                $ref: '#/components/schemas/Error'
+      tags:
+        - relations
   /api/1.2/users/:
     get:
       description: List users.
@@ -1179,6 +1285,22 @@ components:
         application/x-www-form-urlencoded:
           schema:
             $ref: '#/components/schemas/User'
+    Relation:
+      required: true
+      content:
+        application/json:
+          schema:
+            oneOf:
+              - type: array
+                items:
+                  $ref: '#/components/schemas/RelationUpdate'
+              - $ref: '#/components/schemas/RelationUpdate'
+        multipart/form-data:
+          schema:
+            $ref: '#/components/schemas/RelationUpdate'
+        application/x-www-form-urlencoded:
+          schema:
+            $ref: '#/components/schemas/RelationUpdate'
   schemas:
     Index:
       type: object
@@ -1777,6 +1899,17 @@ components:
           title: Delegate
           type: integer
           nullable: true
+    RelationUpdate:
+      type: object
+      properties:
+        id:
+          title: ID
+          type: integer
+        patches:
+          title: Patch IDs
+          type: array
+          items:
+            type: integer
     Person:
       type: object
       properties:
@@ -1966,6 +2099,22 @@ components:
             $ref: '#/components/schemas/PatchEmbedded'
           readOnly: true
           uniqueItems: true
+    Relation:
+      type: object
+      properties:
+        id:
+          title: ID
+          type: integer
+        url:
+          title: URL
+          type: string
+          format: uri
+          readOnly: true
+        patches:
+          type: array
+          items:
+            $ref: '#/components/schemas/PatchEmbedded'
+          uniqueItems: true
     User:
       type: object
       properties:
diff --git a/patchwork/api/index.py b/patchwork/api/index.py
index 45485c9..cf18453 100644
--- a/patchwork/api/index.py
+++ b/patchwork/api/index.py
@@ -21,4 +21,5 @@ class IndexView(APIView):
             'series': reverse('api-series-list', request=request),
             'events': reverse('api-event-list', request=request),
             'bundles': reverse('api-bundle-list', request=request),
+            'relations': reverse('api-relation-list', request=request),
         })
diff --git a/patchwork/api/relation.py b/patchwork/api/relation.py
new file mode 100644
index 0000000..1eff61e
--- /dev/null
+++ b/patchwork/api/relation.py
@@ -0,0 +1,95 @@
+# Patchwork - automated patch tracking system
+# Copyright (C) 2019, Bayerische Motoren Werke Aktiengesellschaft (BMW AG)
+#
+# SPDX-License-Identifier: GPL-2.0-or-later
+
+from django.db import transaction
+from django.db.models import Count
+from rest_framework import status
+from rest_framework.generics import GenericAPIView
+from rest_framework.generics import RetrieveDestroyAPIView
+from rest_framework.mixins import ListModelMixin
+from rest_framework.permissions import DjangoModelPermissionsOrAnonReadOnly
+from rest_framework.response import Response
+from rest_framework_bulk import BulkCreateModelMixin
+from rest_framework_bulk import BulkDestroyModelMixin
+from rest_framework_bulk import BulkListSerializer
+from rest_framework_bulk import BulkUpdateModelMixin
+
+from patchwork.api.base import BaseHyperlinkedModelSerializer
+from patchwork.api.embedded import PatchSerializer
+from patchwork.models import PatchRelation
+
+
+class PatchRelationSerializer(BaseHyperlinkedModelSerializer):
+    patches = PatchSerializer(many=True)
+
+    class Meta:
+        model = PatchRelation
+        list_serializer_class = BulkListSerializer
+        fields = ('id', 'url', 'patches',)
+        read_only_fields = ('url',)
+        extra_kwargs = {
+            'id': {'read_only': False},
+            'url': {'view_name': 'api-relation-detail'},
+        }
+
+
+class PatchRelationMixin:
+    serializer_class = PatchRelationSerializer
+    permission_classes = [DjangoModelPermissionsOrAnonReadOnly]
+    queryset = PatchRelation.objects.all()\
+        .prefetch_related('patches')\
+        .annotate(patches_count=Count('patch'))
+
+
+class PatchRelationList(PatchRelationMixin, ListModelMixin,
+                        BulkCreateModelMixin, BulkUpdateModelMixin,
+                        BulkDestroyModelMixin, GenericAPIView):
+    ordering = 'id'
+    ordering_fields = ['id', 'patches_count']
+
+    def get(self, request, *args, **kwargs):
+        return self.list(request, *args, **kwargs)
+
+    @transaction.atomic(savepoint=False)
+    def delete(self, request, *args, **kwargs):
+        return self.bulk_destroy(request, *args, **kwargs)
+
+    @transaction.atomic(savepoint=False)
+    def put(self, request, *args, **kwargs):
+        """Put single or multiple relation objects, whether or not their ids
+        already exist"""
+        queryset = self.filter_queryset(self.get_queryset())
+        create = []
+        update = []
+
+        data = request.data if isinstance(request.data, list) \
+            else [request.data]
+
+        for d in data:
+            if queryset.filter(pk=d['id']).exists():
+                update.append(d)
+            else:
+                create.append(d)
+
+        response1 = None
+        if len(update) > 0:
+            request._full_data = update
+            response1 = self.bulk_update(request, *args, **kwargs)
+            if len(create) == 0:
+                return response1
+
+        response2 = None
+        if len(create) > 0:
+            request._full_data = create
+            response2 = self.create(request, *args, **kwargs)
+            if len(update) == 0:
+                return response2
+
+        data = getattr(response1, 'data', []) + getattr(response2, 'data', [])
+        return Response(data, status=status.HTTP_200_OK)
+
+
+class PatchRelationDetail(PatchRelationMixin, RetrieveDestroyAPIView):
+    pass
diff --git a/patchwork/tests/api/test_relation.py b/patchwork/tests/api/test_relation.py
new file mode 100644
index 0000000..8c02291
--- /dev/null
+++ b/patchwork/tests/api/test_relation.py
@@ -0,0 +1,154 @@
+# Patchwork - automated patch tracking system
+# Copyright (C) 2019, Bayerische Motoren Werke Aktiengesellschaft (BMW AG)
+#
+# SPDX-License-Identifier: GPL-2.0-or-later
+
+import unittest
+
+from django.conf import settings
+from django.urls import reverse
+
+from patchwork.tests.api import utils
+from patchwork.tests.utils import create_patch
+from patchwork.tests.utils import create_patches
+from patchwork.tests.utils import create_relation
+from patchwork.tests.utils import create_relations
+from patchwork.tests.utils import create_user
+
+if settings.ENABLE_REST_API:
+    from rest_framework import status
+
+
+@unittest.skipUnless(settings.ENABLE_REST_API, 'requires ENABLE_REST_API')
+class TestRelationAPI(utils.APITestCase):
+    fixtures = ['default_tags']
+
+    @staticmethod
+    def api_url(item=None):
+        kwargs = {}
+        if item is None:
+            return reverse('api-relation-list', kwargs=kwargs)
+        kwargs['pk'] = item
+        return reverse('api-relation-detail', kwargs=kwargs)
+
+    @staticmethod
+    def create_relation_obj(pk=1, patch_count=2):
+        class Relation:
+            def __init__(self, id, patches):
+                self.id = id
+                self.patches = patches
+
+        return Relation(pk, [p.id for p in create_patches(patch_count)])
+
+    def assertSerialized(self, relation_obj, relation_json):
+        self.failIf(isinstance(relation_obj, list) !=
+                    isinstance(relation_json, list))
+
+        self.assertEqual(relation_obj.id, relation_json['id'])
+
+        patch_ids_obj = relation_obj.patches
+        if not isinstance(patch_ids_obj, list):
+            # patch_ids_obj is a queryset
+            patch_ids_obj = [patch.id for patch in patch_ids_obj.all()]
+        patch_ids_json = [patch['id'] for patch in relation_json['patches']]
+        self.assertCountEqual(patch_ids_obj, patch_ids_json)
+
+    def test_list_empty(self):
+        """List relation when none are present."""
+        resp = self.client.get(self.api_url())
+        self.assertEqual(status.HTTP_200_OK, resp.status_code)
+        self.assertEqual(0, len(resp.data))
+
+    @utils.store_samples('relation-list')
+    def test_list(self):
+        """List relations."""
+        relation = create_relation()
+
+        resp = self.client.get(self.api_url())
+        self.assertEqual(status.HTTP_200_OK, resp.status_code)
+        self.assertEqual(1, len(resp.data))
+        relation_rsp = resp.data[0]
+        self.assertSerialized(relation, relation_rsp)
+
+    @utils.store_samples('relation-bulk-delete')
+    def test_bulk_delete(self):
+        """Delete all relations."""
+        create_relations(count=3)
+
+        resp = self.client.delete(self.api_url())
+        self.assertEqual(status.HTTP_403_FORBIDDEN, resp.status_code)
+
+        user = create_user(permissions=['delete_patchrelation'])
+        self.client.force_login(user)
+
+        resp = self.client.delete(self.api_url())
+        self.assertEqual(status.HTTP_204_NO_CONTENT, resp.status_code)
+        self.assertIsNone(resp.data)
+
+        resp = self.client.get(self.api_url())
+        self.assertEqual(0, len(resp.data))
+
+    @utils.store_samples('relation-bulk-put-single')
+    def test_bulk_put_single(self):
+        """Add and update a new relation."""
+
+        def send_and_check(expected_response_code):
+            resp = self.client.put(self.api_url(), relation.__dict__)
+            self.assertEqual(expected_response_code, resp.status_code)
+            if expected_response_code == status.HTTP_403_FORBIDDEN:
+                return
+            self.assertEqual(1, len(resp.data))
+            relation_rsp = resp.data[0]
+            self.assertSerialized(relation, relation_rsp)
+
+        user = create_user(permissions=['change_patchrelation'])
+        self.client.force_login(user)
+
+        relation = self.create_relation_obj()
+        send_and_check(status.HTTP_201_CREATED)
+
+        # update the relation
+        relation.patches.append(create_patch().id)
+        send_and_check(status.HTTP_200_OK)
+
+        # Forbid put action for anonymous user
+        self.client.logout()
+        send_and_check(status.HTTP_403_FORBIDDEN)
+
+    @utils.store_samples('relation-bulk-put-multiple')
+    def test_bulk_put_multiple(self):
+        """Add and update multiple relations."""
+
+        def send_and_check(expected_response_code):
+            data = [relation.__dict__ for relation in relations]
+            resp = self.client.put(self.api_url(), data)
+            self.assertEqual(expected_response_code, resp.status_code)
+            if expected_response_code == status.HTTP_403_FORBIDDEN:
+                return
+            self.assertEqual(len(relations), len(resp.data))
+
+            for i in range(len(relations)):
+                self.assertSerialized(relations[i], resp.data[i])
+
+        user = create_user(permissions=['change_patchrelation'])
+        self.client.force_login(user)
+
+        relations = [self.create_relation_obj(pk=i) for i in range(1, 3)]
+        send_and_check(status.HTTP_201_CREATED)
+
+        # update one relation and create another one
+        relations[0].patches.append(create_patch().id)
+        relations.append(self.create_relation_obj(pk=len(relations) + 1))
+        send_and_check(status.HTTP_200_OK)
+
+        # Forbid put action for anonymous user
+        self.client.logout()
+        send_and_check(status.HTTP_403_FORBIDDEN)
+
+    def test_detail(self):
+        """Show relation."""
+        relation = create_relation()
+
+        resp = self.client.get(self.api_url(relation.id))
+        self.assertEqual(status.HTTP_200_OK, resp.status_code)
+        self.assertSerialized(relation, resp.data)
diff --git a/patchwork/tests/utils.py b/patchwork/tests/utils.py
index 4ac9afe..290e410 100644
--- a/patchwork/tests/utils.py
+++ b/patchwork/tests/utils.py
@@ -9,6 +9,7 @@ from datetime import timedelta
 from email.utils import make_msgid
 import os
 
+from django.contrib.auth.models import Permission
 from django.contrib.auth.models import User
 
 from patchwork.models import Bundle
@@ -16,6 +17,7 @@ from patchwork.models import Check
 from patchwork.models import Comment
 from patchwork.models import CoverLetter
 from patchwork.models import Patch
+from patchwork.models import PatchRelation
 from patchwork.models import Person
 from patchwork.models import Project
 from patchwork.models import Series
@@ -79,11 +81,12 @@ def create_person(**kwargs):
     return Person.objects.create(**values)
 
 
-def create_user(link_person=True, **kwargs):
+def create_user(link_person=True, permissions=None, **kwargs):
     """Create a 'User' object.
 
     Args:
         link_person (bool): If true, create a linked Person object.
+        permissions (list): Permissions to add to user by codename.
     """
     num = User.objects.count()
 
@@ -101,6 +104,12 @@ def create_user(link_person=True, **kwargs):
                                     first_name=values['first_name'],
                                     last_name=values['last_name'])
 
+    if permissions is not None:
+        for codename in permissions:
+            perm = Permission.objects.get(codename=codename)
+            user.user_permissions.add(perm)
+    user.save()
+
     if link_person:
         # unfortunately we don't split on these
         values['name'] = ' '.join([values.pop('first_name'),
@@ -347,3 +356,16 @@ def create_covers(count=1, **kwargs):
         kwargs (dict): Overrides for various cover letter fields
     """
     return _create_submissions(create_cover, count, **kwargs)
+
+
+def create_relation(pk=1, count_patches=2):
+    relation = PatchRelation.objects.create(id=pk)
+    for _ in range(count_patches):
+        # relations can span over multiple projects
+        create_patch(project=create_project(), related=relation)
+    return relation
+
+
+def create_relations(count=2):
+    for i in range(1, count + 1):
+        create_relation(pk=i)
diff --git a/patchwork/urls.py b/patchwork/urls.py
index c24bf55..69717ad 100644
--- a/patchwork/urls.py
+++ b/patchwork/urls.py
@@ -164,6 +164,7 @@ if settings.ENABLE_REST_API:
     from patchwork.api import patch as api_patch_views  # noqa
     from patchwork.api import person as api_person_views  # noqa
     from patchwork.api import project as api_project_views  # noqa
+    from patchwork.api import relation as api_relation_views  # noqa
     from patchwork.api import series as api_series_views  # noqa
     from patchwork.api import user as api_user_views  # noqa
 
@@ -233,9 +234,19 @@ if settings.ENABLE_REST_API:
             name='api-cover-comment-list'),
     ]
 
+    api_1_2_patterns = [
+        url(r'^relations/$',
+            api_relation_views.PatchRelationList.as_view(),
+            name='api-relation-list'),
+        url(r'^relations/(?P<pk>[^/]+)/$',
+            api_relation_views.PatchRelationDetail.as_view(),
+            name='api-relation-detail'),
+    ]
+
     urlpatterns += [
         url(r'^api/(?:(?P<version>(1.0|1.1|1.2))/)?', include(api_patterns)),
         url(r'^api/(?:(?P<version>(1.1|1.2))/)?', include(api_1_1_patterns)),
+        url(r'^api/(?:(?P<version>1.2)/)?', include(api_1_2_patterns)),
 
         # token change
         url(r'^user/generate-token/$', user_views.generate_token,
diff --git a/releasenotes/notes/add-patch-relations-c96bb6c567b416d8.yaml b/releasenotes/notes/add-patch-relations-c96bb6c567b416d8.yaml
new file mode 100644
index 0000000..6cc6ce8
--- /dev/null
+++ b/releasenotes/notes/add-patch-relations-c96bb6c567b416d8.yaml
@@ -0,0 +1,9 @@
+---
+features:
+  - |
+    A patch can now be related to other ones (e.g. revisions). Patch relations
+    can be set via the REST API for automating this task.
+api:
+  - |
+    Patch relations are available via ``/relations/`` and
+    ``/relations/{relationID}/`` endpoints and support bulk operations.
diff --git a/requirements-dev.txt b/requirements-dev.txt
index 60eb8a6..8a5a0fe 100644
--- a/requirements-dev.txt
+++ b/requirements-dev.txt
@@ -2,6 +2,7 @@ Django~=2.2.0; python_version >= '3.5'
 Django~=1.11.0; python_version < '3.0'  # pyup: ignore
 djangorestframework~=3.10.0; python_version >= '3.5'
 djangorestframework~=3.9.0; python_version < '3.0'  # pyup: ignore
+djangorestframework-bulk~=0.2.1
 django-filter~=2.2.0; python_version >= '3.5'
 django-filter~=1.1.0; python_version < '3.0'  # pyup: ignore
 django-debug-toolbar~=2.0.0; python_version >= '3.5'  # pyup: ignore
diff --git a/requirements-prod.txt b/requirements-prod.txt
index 797d30b..9164dc4 100644
--- a/requirements-prod.txt
+++ b/requirements-prod.txt
@@ -2,6 +2,7 @@ Django~=2.2.0; python_version >= '3.5'
 Django~=1.11.0; python_version < '3.0'  # pyup: ignore
 djangorestframework~=3.10.0; python_version >= '3.5'
 djangorestframework~=3.9.0; python_version < '3.0'  # pyup: ignore
+djangorestframework-bulk~=0.2.1
 django-filter~=2.2.0; python_version >= '3.5'
 django-filter~=1.1.0; python_version < '3.0'  # pyup: ignore
 psycopg2-binary~=2.8.0
diff --git a/tox.ini b/tox.ini
index 0c03857..1751f85 100644
--- a/tox.ini
+++ b/tox.ini
@@ -17,7 +17,8 @@ deps =
     django{20,21}: django-filter>=2.0,<3.0
     django22: django>=2.2,<2.3
     django22: djangorestframework>=3.10,<3.11
-    django22: django-filter>=2.1,<3.0
+    django{111,20,21,22}: djangorestframework-bulk>=0.2,<0.3
+
 setenv =
     DJANGO_SETTINGS_MODULE = patchwork.settings.dev
     PYTHONDONTWRITEBYTECODE = 1
