diff --git a/gracedb/alerts/main.py b/gracedb/alerts/main.py
index c7ae382bc0ebbaebe5e6d63a4d1f64feadd5aafd..bcf3b62354617dcbec6a28408743df8614ba26bf 100644
--- a/gracedb/alerts/main.py
+++ b/gracedb/alerts/main.py
@@ -106,9 +106,9 @@ def issue_alerts(event_or_superevent, alert_type, url=None, file_name="",
     description="", label=None, serialized_object=None):
 
     # Check alert_type
-    if alert_type not in ["new", "label", "update"]:
+    if alert_type not in ["new", "label", "update", "signoff"]:
         raise ValueError(("alert_type is {0}, should be 'new', 'label', "
-            "or 'update'").format(alert_type))
+            "'update', or 'signoff'").format(alert_type))
 
     # Send XMPP alert
     if settings.SEND_XMPP_ALERTS:
@@ -121,8 +121,8 @@ def issue_alerts(event_or_superevent, alert_type, url=None, file_name="",
     if is_superevent(event_or_superevent):
         return
 
-    # We currently don't send phone or email alerts for updates
-    if alert_type == "update":
+    # We currently don't send phone or email alerts for updates or signoffs
+    if alert_type == "update" or alert_type == "signoff":
         return
 
     # Don't send phone or email alerts for MDC events or Test events
diff --git a/gracedb/alerts/superevent_utils.py b/gracedb/alerts/superevent_utils.py
index aa23f3c362ec1b09f8b339edd312d00a346fd76c..da8b9daf379cab41f2bf0293845694b7552aa809 100644
--- a/gracedb/alerts/superevent_utils.py
+++ b/gracedb/alerts/superevent_utils.py
@@ -5,7 +5,8 @@ from .main import issue_alerts
 from core.urls import build_absolute_uri
 from superevents.api.serializers import SupereventSerializer, \
     SupereventLogSerializer, SupereventLabelSerializer, \
-    SupereventEMObservationSerializer, SupereventVOEventSerializer
+    SupereventEMObservationSerializer, SupereventVOEventSerializer, \
+    SupereventSignoffSerializer
 from superevents.shortcuts import is_superevent
 
 import logging
@@ -140,3 +141,16 @@ def issue_alert_for_superevent_emobservation(emobservation, request=None):
     # Send alerts
     issue_alerts(emobservation.superevent, alert_type="update", url=url,
         description=description, serialized_object=serialized_object)
+
+
+def issue_alert_for_superevent_signoff(signoff, request=None):
+    # Get URL for superevent webview and serialized label
+    url, serialized_object = superevent_alert_helper(signoff,
+        SupereventSignoffSerializer, request=request)
+
+    # Description
+    description = signoff.status
+
+    # Send alerts
+    issue_alerts(signoff.superevent, alert_type="signoff", url=url,
+        description=description, serialized_object=serialized_object)
diff --git a/gracedb/events/models.py b/gracedb/events/models.py
index 97341779540588259a5242468f32b4a7c83cf9c7..771750211d16d264ecc03404e5e23dbf3e43763e 100644
--- a/gracedb/events/models.py
+++ b/gracedb/events/models.py
@@ -458,9 +458,6 @@ class EMObservation(EMObservationBase, AutoIncrementModel):
         return "{event_id} | {group} | {N}".format(
             event_id=self.event.graceid(), group=self.group.name, N=self.N)
 
-    def __unicode__(self):
-        return "%s-%s-%d" % (self.event.graceid(), self.group.name, self.N)
-
     def calculateCoveringRegion(self):
         footprints = self.emfootprint_set.all()
         super(EMObservation, self).calculateCoveringRegion(footprints)
@@ -514,6 +511,11 @@ class Labelling(m2mThroughBase):
     event = models.ForeignKey(Event)
     label = models.ForeignKey(Label)
 
+    def __unicode__(self):
+        return "{graceid} | {label}".format(graceid=self.event.graceid(),
+            label=self.label.name)
+
+
 # XXX Deprecated?  Is this used *anywhere*?
 # Appears to only be used in models.py.  Here and Event class as approval_set
 class Approval(models.Model):
@@ -923,7 +925,7 @@ class SignoffBase(models.Model):
         """Custom clean method for signoffs"""
 
         # Make sure instrument is non-blank if this is an operator signoff
-        if (signoff_type == self.SIGNOFF_TYPE_OPERATOR and
+        if (self.signoff_type == self.SIGNOFF_TYPE_OPERATOR and
             not self.instrument):
 
             raise ValidationError({'instrument':
@@ -931,6 +933,32 @@ class SignoffBase(models.Model):
 
         super(SignoffBase, self).clean(*args, **kwargs)
 
+    def get_req_label_name(self):
+        if self.signoff_type == 'OP':
+            return self.instrument + 'OPS'
+        elif self.signoff_type == 'ADV':
+            return 'ADVREQ'
+
+    def get_status_label_name(self):
+        if self.signoff_type == 'OP':
+            return self.instrument + self.status
+        elif self.signoff_type == 'ADV':
+            return 'ADV' + self.status
+
+    @property
+    def opposite_status(self):
+        if self.status == 'OK':
+            return 'NO'
+        elif self.status == 'NO':
+            return 'OK'
+
+    def get_opposite_status_label_name(self):
+        if self.signoff_type == 'OP':
+            return self.instrument + self.opposite_status
+        elif self.signoff_type == 'ADV':
+            return 'ADV' + self.opposite_status
+
+
 
 class Signoff(SignoffBase):
     """Class for Event signoffs"""
diff --git a/gracedb/superevents/api/serializers.py b/gracedb/superevents/api/serializers.py
index cc0f302749dc399806b4625a86d6c5a2d944f4dc..2283082c4eeafcbe7afac59c1ad177b48cba7052 100644
--- a/gracedb/superevents/api/serializers.py
+++ b/gracedb/superevents/api/serializers.py
@@ -3,7 +3,7 @@ from django.contrib.auth import get_user_model
 from django.utils.translation import ugettext_lazy as _
 from django.conf import settings
 from ..models import Superevent, Labelling, Log, VOEvent, EMObservation, \
-    EMFootprint
+    EMFootprint, Signoff
 
 from .fields import ParentObjectDefault, CommaSeparatedOrListField
 from .settings import SUPEREVENT_LOOKUP_FIELD
@@ -607,3 +607,12 @@ class SupereventEMObservationSerializer(serializers.ModelSerializer):
 
         return emo
 
+
+class SupereventSignoffSerializer(serializers.ModelSerializer):
+    submitter = serializers.SlugRelatedField(slug_field='username',
+        read_only=True)
+
+    class Meta:
+        model = Signoff
+        fields = ['submitter', 'instrument', 'status', 'comment',
+            'signoff_type']
diff --git a/gracedb/superevents/utils.py b/gracedb/superevents/utils.py
index cc0f320d7acc0320f7e04242a50aeb927a271e1e..5beeaf0012a6fd1eb54c429ae076aaece177eba8 100644
--- a/gracedb/superevents/utils.py
+++ b/gracedb/superevents/utils.py
@@ -14,7 +14,7 @@ from alerts.superevent_utils import issue_alert_for_superevent_creation, \
     issue_alert_for_superevent_label_creation, \
     issue_alert_for_superevent_label_removal, \
     issue_alert_for_superevent_emobservation, \
-    issue_alert_for_superevent_voevent
+    issue_alert_for_superevent_voevent, issue_alert_for_superevent_signoff
 from alerts.event_utils import issue_alert_for_event_log
 
 import os
@@ -505,3 +505,135 @@ def create_voevent_for_superevent(superevent, issuer, voevent_type,
         issue_alert_for_superevent_voevent(voevent)
 
     return voevent
+
+
+# TODO: wrap this function in a try-except block in the form
+def create_signoff_for_superevent(superevent, user, signoff_type,
+    signoff_instrument, signoff_status, signoff_comment, add_log_message=True,
+    issue_alert=True):
+
+    # Create signoff
+    signoff = Signoff.objects.create(superevent=superevent, submitter=user,
+        instrument=signoff_instrument, signoff_type=signoff_type,
+        status=signoff_status, comment=signoff_comment)
+
+    # Create log message to document the signoff
+    if add_log_message:
+        signoff_type_full = dict(Signoff.SIGNOFF_TYPE_CHOICES)[signoff_type]
+        comment = "{signoff_type} signoff certified status as {status}".format(
+            signoff_type=signoff_type_full.capitalize(), status=signoff_status)
+        if signoff_instrument:
+            comment += " for {inst}".format(inst=signoff_instrument)
+        em_follow = Tag.objects.get(name='em_follow')
+        signoff_log = create_log(user, comment, superevent, issue_alert=False,
+            tags=[em_follow])
+
+    # Remove label which requested signoff
+    labelling_to_remove = superevent.labelling_set.filter(label__name=
+        signoff.get_req_label_name()).first()
+    if labelling_to_remove is not None:
+        remove_label_from_superevent(labelling_to_remove, user)
+
+    # Add new label depending on signoff status
+    label_to_add = Label.objects.get(name=signoff.get_status_label_name())
+    add_label_to_superevent(superevent, label_to_add, user)
+
+    # Issue alert
+    if issue_alert:
+        issue_alert_for_superevent_signoff(signoff)
+
+    return signoff
+
+def update_signoff_for_superevent(signoff, user, changed_data,
+    add_log_message=True, issue_alert=True):
+    # changed_data is a dict which contains the fields of the signoff
+    # which have changed
+
+    # Get superevent
+    superevent = signoff.superevent
+
+    # Save signoff
+    signoff.save()
+
+    if 'status' in changed_data:
+        # If label for opposite status exists, remove it (i.e., the status has
+        # changed in this update, so we need to update the labels)
+        labelling_to_remove = superevent.labelling_set.filter(label__name=
+            signoff.get_opposite_status_label_name()).first()
+        if labelling_to_remove is not None:
+            remove_label_from_superevent(labelling_to_remove, user,
+                add_log_message=False, issue_alert=True)
+
+        # If label for current status doesn't exist, add it
+        label_to_add = Label.objects.get(name=signoff.get_status_label_name())
+        if label_to_add not in superevent.labels.all():
+            add_label_to_superevent(superevent, label_to_add, user,
+                add_log_message=False, issue_alert=True)
+
+    # Create log message to document the update
+    if add_log_message:
+        # Construct message
+        signoff_type_full = dict(Signoff.SIGNOFF_TYPE_CHOICES) \
+            [signoff.signoff_type]
+        comment = "{signoff_type} signoff updated".format(
+            signoff_type=signoff_type_full.capitalize())
+        if signoff.instrument:
+            comment += " for {inst}".format(inst=signoff.instrument)
+        if 'status' in changed_data:
+            comment += (": status {old_status} -> {new_status}, label "
+                "{r_label} removed, and label {a_label} applied").format(
+                old_status=signoff.opposite_status, new_status=signoff.status,
+                r_label=labelling_to_remove.label.name,
+                a_label=label_to_add.name)
+        em_follow = Tag.objects.get(name='em_follow')
+        signoff_log = create_log(user, comment, superevent, issue_alert=False,
+            tags=[em_follow])
+
+    # Issue alert
+    if issue_alert:
+        issue_alert_for_superevent_signoff(signoff)
+
+    return signoff
+
+
+def delete_signoff_for_superevent(signoff, user, add_log_message=True,
+    issue_alert=True):
+
+    # Get superevent
+    superevent = signoff.superevent
+
+    # Delete signoff
+    signoff.delete()
+
+    # Remove current OK or NO label: we don't add a log message here since
+    # we'll document this in the full log message about the signoff
+    labelling_to_remove = superevent.labelling_set.filter(label__name=
+        signoff.get_status_label_name()).first()
+    remove_label_from_superevent(labelling_to_remove, user,
+        add_log_message=False, issue_alert=True)
+
+    # Reapply initial "req" labe: we don't add a log message here since we'll
+    # document this in the full log message about the signoff
+    label_to_add = Label.objects.get(name=signoff.get_req_label_name())
+    add_label_to_superevent(superevent, label_to_add, user,
+        add_log_message=False, issue_alert=True)
+
+    # Log message
+    if add_log_message:
+        # Construct log message
+        signoff_type_full = dict(Signoff.SIGNOFF_TYPE_CHOICES) \
+            [signoff.signoff_type]
+        comment = "{signoff_type} signoff ".format(signoff_type=
+            signoff_type_full.capitalize())
+        if signoff.instrument:
+            comment += "for {inst} ".format(inst=signoff.instrument)
+        comment += "deleted: {r_label} removed and {a_label} reapplied".format(
+            r_label=labelling_to_remove.label.name, a_label=label_to_add.name)
+        em_follow = Tag.objects.get(name='em_follow')
+        signoff_log = create_log(user, comment, superevent, tags=[em_follow],
+            issue_alert=False)
+
+    # Alert
+    if issue_alert:
+        issue_alert_for_superevent_log(signoff_log)
+