diff --git a/gracedb/api.py b/gracedb/api.py
index 0b9bc7877a9b8e735faf5c2c46a0c0d5ae11ca53..2386e71e67aac381e10d25995cc50ec56556d307 100644
--- a/gracedb/api.py
+++ b/gracedb/api.py
@@ -14,12 +14,13 @@ from django.contrib.auth.models import Group as AuthGroup
 from django.contrib.contenttypes.models import ContentType
 from gracedb.models import Event, Group, Search, Pipeline, EventLog, Tag
 from gracedb.models import EMGroup, EMBBEventLog, EMSPECTRUM
+from gracedb.models import VOEvent
 from view_logic import create_label, get_performance_info
 from view_logic import _createEventFromForm
 from view_logic import create_eel
 from view_utils import fix_old_creation_request
 from view_utils import eventToDict, eventLogToDict, labelToDict
-from view_utils import embbEventLogToDict
+from view_utils import embbEventLogToDict, voeventToDict
 from view_utils import reverse
 
 from translator import handle_uploaded_data
@@ -343,32 +344,6 @@ class TSVRenderer(BaseRenderer):
        
         return outTable
 
-# XXX this doesn't work because you don't have the request here. You could
-# try stuffing it into the renderer context, but that's really ugly. 
-#class VOEventRenderer(BaseRenderer):
-#    media_type = 'application/xml'
-#    format = 'xml'
-#
-#    def render(self, data, media_type=None, renderer_context=None):
-#        if 'error' in data.keys():
-#            return data['error']
-#
-#        outDoc = ''
-#        for e in data['events']:
-#            graceid = e['graceid']
-#
-#            try:
-#                # XXX If any part of this fails, the VOEvent will be empty.
-#                event = Event.getByGraceid(graceid)
-#                if not event.far or not event.gpstime:
-#                    raise Exception
-#                voevent = buildVOEvent(event, request)
-#            except:
-#                voevent = ''
-#            outDoc += voevent + '\n'
-#       
-#        return outDoc
-
 #==================================================================
 # Events
 
@@ -650,33 +625,6 @@ class EventDetail(APIView):
                     status=status.HTTP_400_BAD_REQUEST)
         return Response(status=status.HTTP_202_ACCEPTED)
 
-# FIXME or something.
-# This should really be a renderer and not a view. But the problem 
-# is that the renderer needs the request in order to build up URLs.
-# There must be a better way of doing this.
-class EventVODetail(APIView):
-    authentication_classes = (LigoAuthentication,)
-    #parser_classes = (LigoLwParser, RawdataParser)
-    parser_classes = (parsers.MultiPartParser,)
-    #serializer_class = EventSerializer
-    permission_classes = (IsAuthenticated,IsAuthorizedForEvent,)
-    renderer_classes = (JSONRenderer, BrowsableAPIRenderer,)
-
-    @event_and_auth_required
-    def get(self, request, event):
-        voevent_type = request.QUERY_PARAMS.get('voevent_type', 'preliminary')
-        try:
-            voevent = buildVOEvent(event, request, voevent_type=voevent_type)
-        except VOEventBuilderException, e:
-            return Response(str(e), status=status.HTTP_400_BAD_REQUEST)
-        except Exception, e:
-            return Response("Problem building VOEvent: %s" % str(e),
-                    status=status.HTTP_500_INTERNAL_SERVER_ERROR)
-
-        response = Response(voevent)
-        response["Cache-Control"] = "no-cache"
-        return response
-
 #==================================================================
 # Neighbors
 
@@ -1336,10 +1284,10 @@ class GracedbRoot(APIView):
         # Is there better?
         detail = reverse("event-detail", args=["G1200"], request=request)
         detail = detail.replace("G1200", "{graceid}")
-        vo_detail = reverse("event-vo-detail", args=["G1200"], request=request)
-        vo_detail = vo_detail.replace("G1200", "{graceid}")
         log = reverse("eventlog-list", args=["G1200"], request=request)
         log = log.replace("G1200", "{graceid}")
+        voevent = reverse("voevent-list", args=["G1200"], request=request)
+        voevent = voevent.replace("G1200", "{graceid}")
         embb = reverse("embbeventlog-list", args=["G1200"], request=request)
         embb = embb.replace("G1200", "{graceid}")
 
@@ -1368,7 +1316,7 @@ class GracedbRoot(APIView):
 
         templates = {
                 "event-detail-template" : detail,
-                "event-vo-detail-template" : vo_detail,
+                "voevent-list-template" : voevent,
                 "event-log-template" : log,
                 "embb-event-log-template" : embb,
                 "event-label-template" : labels,
@@ -1408,6 +1356,7 @@ class GracedbRoot(APIView):
             "wavebands"      : dict(EMSPECTRUM),
             "eel-statuses"   : dict(EMBBEventLog.EEL_STATUS_CHOICES),
             "obs-statuses"   : dict(EMBBEventLog.OBS_STATUS_CHOICES),
+            "voevent-types"  : dict(VOEvent.VOEVENT_TYPE_CHOICES),
            })
 
 ##################################################################
@@ -1612,3 +1561,122 @@ class PerformanceInfo(APIView):
 
         return Response(performance_info,status=status.HTTP_200_OK)
 
+
+#==================================================================
+# VOEvent Resources
+
+class VOEventList(APIView):
+    """VOEvent List Resource
+    """
+    authentication_classes = (LigoAuthentication,)
+    permission_classes = (IsAuthenticated,IsAuthorizedForEvent,)
+
+    @event_and_auth_required
+    def get(self, request, event):
+        voeventset = event.voevent_set.order_by("created","N")
+        count = voeventset.count()
+
+        voevents = [ voeventToDict(voevent, request)
+                for voevent in voeventset.iterator() ]
+
+        rv = {
+                'start': 0,
+                'numRows' : count,
+                'links' : {
+                    'self' : request.build_absolute_uri(),
+                    'first' : request.build_absolute_uri(),
+                    'last' : request.build_absolute_uri(),
+                    },
+                'voevents' : voevents,
+             }
+        return Response(rv)
+
+    @event_and_auth_required
+    def post(self, request, event):
+        voevent_type = request.DATA.get('voevent_type', None)
+        if not voevent_type:
+            msg = "You must provide a valid voevent_type."
+            return Response({'error': msg}, status = status.HTTP_400_BAD_REQUEST)
+            
+        skymap_type = request.DATA.get('skymap_type', None)
+        skymap_filename = request.DATA.get('skymap_filename', None)
+        skymap_image_filename = request.DATA.get('skymap_image_filename', None)
+
+        if (skymap_filename and not skymap_type) or (skymap_type and not skymap_filename):
+            msg = "Both or neither of skymap_time and skymap_filename must be specified."
+            return Response({'error': msg}, status = status.HTTP_400_BAD_REQUEST)
+
+        # Instantiate the voevent and save in order to get the serial number
+        voevent = VOEvent(voevent_type=voevent_type, event=event, issuer=request.user)
+
+        try:
+            voevent.save()
+        except Exception as e:
+            return Response("Failed to create VOEvent: %s" % str(e),
+                    status=status.HTTP_503_SERVICE_UNAVAILABLE)
+
+        # Now, you need to actually build the VOEvent.
+        try:
+            voevent_text, ivorn = buildVOEvent(event, voevent.N, voevent_type, request,
+                skymap_filename = skymap_filename, skymap_type = skymap_type,
+                skymap_image_filename = skymap_image_filename)
+        except VOEventBuilderException, e:
+            msg = "Problem building VOEvent: %s" % str(e)
+            return Response({'error': msg}, status = status.HTTP_400_BAD_REQUEST)
+
+        voevent_display_type = dict(VOEvent.VOEVENT_TYPE_CHOICES)[voevent_type].capitalize()
+        filename = "%s-%d-%s.xml" % (event.graceid(), voevent.N, voevent_display_type)
+        filepath = os.path.join(event.datadir(), filename)
+        fdest = VersionedFile(filepath, 'w')
+        fdest.write(voevent_text)
+        fdest.close()
+        file_version = fdest.version
+
+        voevent.filename = filename
+        voevent.file_version = file_version
+        voevent.ivorn = ivorn
+        voevent.save()
+
+        rv = voeventToDict(voevent, request=request)
+
+        # Create LogEntry to document the new VOEvent.
+        logentry = EventLog(event=event,
+                             issuer=request.user,
+                             comment='',
+                             filename=filename,
+                             file_version=file_version)
+        try:
+            logentry.save()
+        except:
+            rv['warnings'] = 'Problem saving log entry for VOEvent %s of %s' % (voevent.N, 
+                event.graceid()) 
+
+        # Tag log entry as 'sky_loc'
+        tmp = EventLogTagDetail()
+        retval = tmp.put(request, event.graceid(), logentry.N, 'em_follow') 
+        # XXX This seems like a bizarre way of getting an error message out.
+        if retval.status_code != 201:
+            rv['tagWarning'] = 'Error tagging VOEvent log message as em_follow.'
+
+        # Issue alert.
+        description = "VOEVENT: %s" % filename
+        issueAlertForUpdate(event, description, doxmpp=True, 
+            filename=filename, serialized_object=rv)
+
+        response = Response(rv, status=status.HTTP_201_CREATED)
+        response['Location'] = rv['self']
+        return response
+
+class VOEventDetail(APIView):
+    authentication_classes = (LigoAuthentication,)
+    permission_classes = (IsAuthenticated,IsAuthorizedForEvent,)
+
+    @event_and_auth_required
+    def get(self, request, event, n):
+        try:
+            voevent = event.voevent_set.filter(N=n)[0]
+        except:
+            return Response("VOEvent does not exist.",
+                    status=status.HTTP_404_NOT_FOUND)
+        return Response(voeventToDict(voevent, request=request))
+
diff --git a/gracedb/buildVOEvent.py b/gracedb/buildVOEvent.py
index 27b363037480b4402e03846116cf0f356f3b014b..899a2d37d39d157bf470b283d08f7df819c1a2b4 100755
--- a/gracedb/buildVOEvent.py
+++ b/gracedb/buildVOEvent.py
@@ -8,7 +8,9 @@ See the VOEvent specification for details
 http://www.ivoa.net/Documents/latest/VOEvent.html
 """
 
-from VOEventLib.VOEvent import VOEvent, Who, Author, Param, How, Why, What, Group
+from VOEventLib.VOEvent import VOEvent, Who, Author, Param, How, What, Group
+from VOEventLib.VOEvent import Citations, EventIVORN
+#from VOEventLib.VOEvent import Why
 from VOEventLib.Vutil import makeWhereWhen, stringVOEvent
 
 # XXX ER2.utils.  utils is in project directory.  ugh.
@@ -17,6 +19,7 @@ from datetime import datetime
 from django.conf import settings
 from django.core.urlresolvers import reverse
 from models import CoincInspiralEvent, MultiBurstEvent
+from models import VOEvent as GraceDBVOEvent
 
 import os
 
@@ -30,37 +33,10 @@ def get_url(request, graceid, view_name, file_name=None):
     rel_url = reverse(view_name, args=args)
     return request.build_absolute_uri(rel_url)
 
-#
-# Types of VOEvents:
-#   preliminary:    no skymap
-#   initial:        BAYESTAR skymap
-#   update:         PE skymap
-#
-#   If the type of skymap doesn't exist, then we need to fail in such
-#   a way as to get the attention of the requestor. We don't want to
-#   forward a bad VOEvent with now skymap.
-#
-#   For each skymap, we demand that there be at least one of: 1) an image file,
-#   2) a data file. The image (data) file name should conform to the pattern:
-#   stem + '.png' ('.fits.gz').
-#   This is obviously very fragile. A 'Skymap' data model would help this 
-#   situation considerably, especially if additional skymap types arise.
-#
-SKYMAP_INFO = {
-    'initial' : {
-        'name' : 'BAYESTAR',
-        'stem' : 'bayestar',
-    },
-    'update'  : {
-        'name' : 'LALINFERENCE_MCMC',
-        'stem' : 'lalinference_nest',
-    }
-}
-
-VOEVENT_TYPES = ['preliminary', 'initial', 'update',]
-
-def buildVOEvent(event, request=None, description=None, role=None, 
-    voevent_type='preliminary'):
+VOEVENT_TYPE_DICT = dict(GraceDBVOEvent.VOEVENT_TYPE_CHOICES)
+
+def buildVOEvent(event, serial_number, voevent_type, request=None, skymap_filename=None,
+    skymap_type=None, skymap_image_filename = None):
 
     if not event.far:
         raise VOEventBuilderException("Cannot build a VOEvent because event has no FAR.")
@@ -68,17 +44,24 @@ def buildVOEvent(event, request=None, description=None, role=None,
     if not event.gpstime:
         raise VOEventBuilderException("Cannot build a VOEvent because event has no gpstime.")
 
-    if not voevent_type in VOEVENT_TYPES:
-        # Do something real here XXX
-        raise VOEventBuilderException("voevent_type must be preliminary, initial, or update")
+    if not voevent_type in VOEVENT_TYPE_DICT.keys():
+        raise VOEventBuilderException("voevent_type must be preliminary, initial, update, or retraction")
+
+    # Let's convert that voevent_type to something nicer looking
+    voevent_type = VOEVENT_TYPE_DICT[voevent_type]
 
     objid = event.graceid()
 
+    # Now build the IVORN. 
+    event_id = "%s-%d-%s" % (objid, serial_number, voevent_type.capitalize())
+    ivorn = settings.SKYALERT_IVORN_PATTERN % event_id
+
     ############ VOEvent header ############################
     v = VOEvent(version="2.0")
-    v.set_ivorn(settings.SKYALERT_IVORN_PATTERN % objid)
-    v.set_role(role or settings.SKYALERT_ROLE)
-    v.set_Description(description or settings.SKYALERT_DESCRIPTION)
+    v.set_ivorn(ivorn)
+    v.set_role(settings.SKYALERT_ROLE)
+    if voevent_type != 'retraction':
+        v.set_Description(settings.SKYALERT_DESCRIPTION)
 
     ############ Who ############################
     w = Who()
@@ -90,21 +73,25 @@ def buildVOEvent(event, request=None, description=None, role=None,
     v.set_Who(w)
 
     ############ Why ############################
-    y = Why()
-    y.add_Description("Candidate gravitational wave event identified by low-latency analysis")
-    v.set_Why(y)
+    # Moving this information into the 'How' section.
+    #if voevent_type != 'retraction':
+    #    y = Why()
+    #    y.add_Description("Candidate gravitational wave event identified by low-latency analysis")
+    #    v.set_Why(y)
 
     ############ How ############################
 
-    h = How()
-    instruments = event.instruments.split(',')
-    if 'H1' in instruments:
-        h.add_Description("H1: LIGO Hanford 4 km gravitational wave detector")
-    if 'L1' in instruments:
-        h.add_Description("L1: LIGO Livingston 4 km gravitational wave detector")
-    if 'V1' in instruments:
-        h.add_Description("V1: Virgo 3 km gravitational wave detector")
-    v.set_How(h)
+    if voevent_type != 'retraction':
+        h = How()
+        h.add_Description("Candidate gravitational wave event identified by low-latency analysis")
+        instruments = event.instruments.split(',')
+        if 'H1' in instruments:
+            h.add_Description("H1: LIGO Hanford 4 km gravitational wave detector")
+        if 'L1' in instruments:
+            h.add_Description("L1: LIGO Livingston 4 km gravitational wave detector")
+        if 'V1' in instruments:
+            h.add_Description("V1: Virgo 3 km gravitational wave detector")
+        v.set_How(h)
 
     ############ What ############################
     w = What()
@@ -123,6 +110,9 @@ def buildVOEvent(event, request=None, description=None, role=None,
     # [25] http://vizier.u-strasbg.fr/doc/catstd-3.2.htx
     #
     # basically, a string that makes sense to humans about what units a value is. eg. "m/s"
+    
+    # The serial number
+    w.add_Param(Param(name="Pkt_Ser_Num", value=serial_number))
 
     # The GraceID
     w.add_Param(Param(name="GraceID", 
@@ -139,65 +129,86 @@ def buildVOEvent(event, request=None, description=None, role=None,
         value = voevent_type.capitalize(),
         Description=["VOEvent alert type"]))
 
-    # False alarm rate
-    w.add_Param(Param(name="FAR", 
-        dataType="float", 
-        ucd="arith.rate;stat.falsealarm", 
-        unit="Hz", 
-        value=float(event.far), 
-        Description=["False alarm rate for GW candidates with this strength or greater"]))
-
     # Shib protected event page
     w.add_Param(Param(name="EventPage",
         ucd="meta.ref.url",
         value=get_url(request, objid, "view2"),
         Description=["Web page for evolving status of this candidate event"]))
 
-    # Pipeline
-    w.add_Param(Param(name="Pipeline", 
-        dataType="string", 
-        ucd="meta.code",
-        unit="",
-        value=event.pipeline.name,
-        Description=["Low-latency data analysis pipeline"]))
+    if voevent_type != 'retraction':
+        # Instruments
+        w.add_Param(Param(name="Instruments", 
+            dataType="string",
+            ucd="meta.code", 
+            value=event.instruments, 
+            Description=["List of instruments used in analysis to identify this event"]))
 
-    # Search
-    if event.search:
-        w.add_Param(Param(name="Search", 
+        # False alarm rate
+        w.add_Param(Param(name="FAR", 
+            dataType="float", 
+            ucd="arith.rate;stat.falsealarm", 
+            unit="Hz", 
+            value=float(event.far), 
+            Description=["False alarm rate for GW candidates with this strength or greater"]))
+
+        # Pipeline
+        w.add_Param(Param(name="Pipeline", 
+            dataType="string", 
             ucd="meta.code",
             unit="",
-            dataType="string", 
-            value=event.search.name,
-            Description=["Specific low-latency search"]))
+            value=event.pipeline.name,
+            Description=["Low-latency data analysis pipeline"]))
+
+        # Search
+        if event.search:
+            w.add_Param(Param(name="Search", 
+                ucd="meta.code",
+                unit="",
+                dataType="string", 
+                value=event.search.name,
+                Description=["Specific low-latency search"]))
 
     if voevent_type in ["initial", "update"]:
-        # Skymaps. Create group and set particular fits and image file names
-        g = Group('GW_SKYMAP', SKYMAP_INFO[voevent_type]['name'])
-        fits_name = SKYMAP_INFO[voevent_type]['stem'] + '.fits.gz'
-        img_name  = SKYMAP_INFO[voevent_type]['stem'] + '.png'
 
-        # Check for the existence of the files.
-        for filename in [fits_name, img_name]:
-            filepath = os.path.join(event.datadir(), filename)
-            if not os.path.exists(filepath):
-                raise VOEventBuilderException("Skymap file %s not found" % filename) 
+        if not skymap_filename:
+            raise VOEventBuilderException("Skymap filename not provided.")
+
+        fits_name = skymap_filename
+        fits_path = os.path.join(event.datadir(), fits_name)
+        if not os.path.exists(fits_path):
+            raise VOEventBuilderException("Skymap file does not exist: %s" % skymap_filename)
+
+        # Let's try to get an image.
+        if not skymap_image_filename:
+            stem = '.'.join(fits_name.split('.')[:-1])
+            img_name = stem + '.png'
+            img_path = os.path.join(event.datadir(), img_name)
+            if not os.path.exists(img_path):
+                img_name = None
+
+        if not skymap_type:
+            raise VOEventBuilderException("Skymap type must be provided.")
+
+        # Skymaps. Create group and set particular fits and image file names
+        g = Group('GW_SKYMAP', skymap_type)
 
         # shib urls.
         shib_fits_skymap_url = get_url(request, objid, "file", fits_name)
-        shib_png_skymap_url  = get_url(request, objid, "file", img_name)
+        if img_name:
+            shib_png_skymap_url  = get_url(request, objid, "file", img_name)
 
         # x509 urls. Hafta specify the api namespace.
         x509_fits_skymap_url = get_url(request, objid, "x509:files", fits_name)
-        x509_png_skymap_url  = get_url(request, objid, "x509:files", img_name)
+        if img_name:
+            x509_png_skymap_url  = get_url(request, objid, "x509:files", img_name)
 
         # Add parameters to the skymap group
-        g.add_Param(Param(name="skymap_png_x509", 
+        g.add_Param(Param(name="skymap_fits_shib", 
             dataType="string",
             ucd="meta.ref.url", 
             unit="",
-            value=x509_png_skymap_url,
-            Description=["Sky Map image X509 protected"]))
-
+            value=shib_fits_skymap_url,
+            Description=["Sky Map FITS Shibboleth protected"]))
         g.add_Param(Param(name="skymap_fits_x509", 
             dataType="string",
             ucd="meta.ref.url", 
@@ -205,140 +216,156 @@ def buildVOEvent(event, request=None, description=None, role=None,
             value=x509_fits_skymap_url,
             Description=["Sky Map FITS X509 protected"]))
 
-        g.add_Param(Param(name="skymap_png_shib", 
-            dataType="string",
-            ucd="meta.ref.url", 
-            unit="",
-            value=shib_png_skymap_url,
-            Description=["Sky Map image Shibboleth protected"]))
-
-        g.add_Param(Param(name="skymap_fits_shib", 
-            dataType="string",
-            ucd="meta.ref.url", 
-            unit="",
-            value=shib_fits_skymap_url,
-            Description=["Sky Map FITS Shibboleth protected"]))
+        if img_name:
+            g.add_Param(Param(name="skymap_png_shib", 
+                dataType="string",
+                ucd="meta.ref.url", 
+                unit="",
+                value=shib_png_skymap_url,
+                Description=["Sky Map image Shibboleth protected"]))
+            g.add_Param(Param(name="skymap_png_x509", 
+                dataType="string",
+                ucd="meta.ref.url", 
+                unit="",
+                value=x509_png_skymap_url,
+                Description=["Sky Map image X509 protected"]))
 
         w.add_Group(g)
 
     # Analysis specific attributes
-    if isinstance(event,CoincInspiralEvent):
-        # get mchirp and mass
-        mchirp = float(event.mchirp)
-        mass = float(event.mass)
-        # calculate eta = (mchirp/total_mass)**(5/3)
-        eta = pow((mchirp/mass),5.0/3.0)
-        w.add_Param(Param(name="ChirpMass", 
-            dataType="float", 
-            ucd="phys.mass", 
-            unit="solar mass",
-            value=mchirp,
-            Description=["Estimated CBC chirp mass"]))
+    if voevent_type != 'retraction':
+        if isinstance(event,CoincInspiralEvent) and voevent_type != 'retraction':
+            # get mchirp and mass
+            mchirp = float(event.mchirp)
+            mass = float(event.mass)
+            # calculate eta = (mchirp/total_mass)**(5/3)
+            eta = pow((mchirp/mass),5.0/3.0)
+            w.add_Param(Param(name="ChirpMass", 
+                dataType="float", 
+                ucd="phys.mass", 
+                unit="solar mass",
+                value=mchirp,
+                Description=["Estimated CBC chirp mass"]))
 
-        w.add_Param(Param(name="Eta", 
-            dataType="float", 
-            ucd="phys.mass;arith.factor", 
-            unit="",
-            value=eta,
-            Description=["Estimated ratio of reduced mass to total mass"]))
-
-        # build up MaxDistance. event.singleinspiral_set.all()?
-        # Each detector calculates an effective distance assuming the inspiral is 
-        # optimally oriented. It is the maximum distance at which a source of the 
-        # given parameters would've been seen by that particular detector. To get
-        # an effective 'maximum distance', we just find the minumum over detectors
-        max_distance = float('inf')
-        for obj in event.singleinspiral_set.all():
-            if obj.eff_distance < max_distance:
-                max_distance = obj.eff_distance
-        if max_distance < float('inf'):
-            w.add_Param(Param(name="MaxDistance", 
+            w.add_Param(Param(name="Eta", 
                 dataType="float", 
-                ucd="pos.distance", 
-                unit="Mpc",
-                value=max_distance, 
-                Description=["Estimated maximum distance for CBC event"]))
-            
-    elif isinstance(event,MultiBurstEvent):
-        w.add_Param(Param(name="CentralFreq", 
-            dataType="float", 
-            ucd="gw.frequency", 
-            unit="Hz", 
-            value=float(event.central_freq),
-            Description=["Central frequency of GW burst signal"]))
-        w.add_Param(Param(name="Duration", 
-            dataType="float", 
-            ucd="time.duration", 
-            unit="s", 
-            value=float(event.duration),
-            Description=["Measured duration of GW burst signal"]))
-
-        # XXX Calculate the fluence. Unfortunately, this requires parsing the trigger.txt
-        # file for hrss values.  These should probably be pulled into the database.
-        # But there is no consensus on whether hrss or fluence is meaningful. So I will
-        # put off changing the schema for now.
-        try:
-            # Go find the data file.
-            log = event.eventlog_set.filter(comment__startswith="Original Data").all()[0]
-            filename = log.filename
-            filepath = os.path.join(event.datadir(),filename)
-            if os.path.isfile(filepath):
-                datafile = open(filepath,"r")
-            else:
-                raise Exception("No file found.")
-            # Now parse the datafile.
-            # The line we want looks like:
-            # hrss: 1.752741e-23 2.101590e-23 6.418900e-23
-            for line in datafile:
-                if line.startswith('hrss:'):
-                    hrss_values = [float(hrss) for hrss in line.split()[1:]]
-            max_hrss = max(hrss_values)
-            # From Min-A Cho: fluence = pi*(c**3)*(freq**2)*(hrss_max**2)*(10**3)/(4*G)
-            # Note that hrss here actually has units of s^(-1/2)
-            pi = 3.14152
-            c = 2.99792E10
-            G = 6.674E-8
-            fluence = pi * pow(c,3) * pow(event.central_freq,2) * 1000.0
-            fluence = fluence * pow(max_hrss,2)
-            fluence = fluence / (4.0*G)
-
-            w.add_Param(Param(name="Fluence", 
+                ucd="phys.mass;arith.factor", 
+                unit="",
+                value=eta,
+                Description=["Estimated ratio of reduced mass to total mass"]))
+
+            # build up MaxDistance. event.singleinspiral_set.all()?
+            # Each detector calculates an effective distance assuming the inspiral is 
+            # optimally oriented. It is the maximum distance at which a source of the 
+            # given parameters would've been seen by that particular detector. To get
+            # an effective 'maximum distance', we just find the minumum over detectors
+            max_distance = float('inf')
+            for obj in event.singleinspiral_set.all():
+                if obj.eff_distance < max_distance:
+                    max_distance = obj.eff_distance
+            if max_distance < float('inf'):
+                w.add_Param(Param(name="MaxDistance", 
+                    dataType="float", 
+                    ucd="pos.distance", 
+                    unit="Mpc",
+                    value=max_distance, 
+                    Description=["Estimated maximum distance for CBC event"]))
+                
+        elif isinstance(event,MultiBurstEvent):
+            w.add_Param(Param(name="CentralFreq", 
+                dataType="float", 
+                ucd="gw.frequency", 
+                unit="Hz", 
+                value=float(event.central_freq),
+                Description=["Central frequency of GW burst signal"]))
+            w.add_Param(Param(name="Duration", 
                 dataType="float", 
-                ucd="gw.fluence", 
-                unit="erg/cm^2", 
-                value=fluence,
-                Description=["Estimated fluence of GW burst signal"]))
-        except Exception: 
+                ucd="time.duration", 
+                unit="s", 
+                value=float(event.duration),
+                Description=["Measured duration of GW burst signal"]))
+
+            # XXX Calculate the fluence. Unfortunately, this requires parsing the trigger.txt
+            # file for hrss values.  These should probably be pulled into the database.
+            # But there is no consensus on whether hrss or fluence is meaningful. So I will
+            # put off changing the schema for now.
+            try:
+                # Go find the data file.
+                log = event.eventlog_set.filter(comment__startswith="Original Data").all()[0]
+                filename = log.filename
+                filepath = os.path.join(event.datadir(),filename)
+                if os.path.isfile(filepath):
+                    datafile = open(filepath,"r")
+                else:
+                    raise Exception("No file found.")
+                # Now parse the datafile.
+                # The line we want looks like:
+                # hrss: 1.752741e-23 2.101590e-23 6.418900e-23
+                for line in datafile:
+                    if line.startswith('hrss:'):
+                        hrss_values = [float(hrss) for hrss in line.split()[1:]]
+                max_hrss = max(hrss_values)
+                # From Min-A Cho: fluence = pi*(c**3)*(freq**2)*(hrss_max**2)*(10**3)/(4*G)
+                # Note that hrss here actually has units of s^(-1/2)
+                pi = 3.14152
+                c = 2.99792E10
+                G = 6.674E-8
+                fluence = pi * pow(c,3) * pow(event.central_freq,2) * 1000.0
+                fluence = fluence * pow(max_hrss,2)
+                fluence = fluence / (4.0*G)
+
+                w.add_Param(Param(name="Fluence", 
+                    dataType="float", 
+                    ucd="gw.fluence", 
+                    unit="erg/cm^2", 
+                    value=fluence,
+                    Description=["Estimated fluence of GW burst signal"]))
+            except Exception: 
+                pass
+        else:
             pass
-    else:
-        pass
 
     v.set_What(w)
 
     ############ Wherewhen ############################
-    wwd = {'observatory':     'LIGO Virgo',
-           'coord_system':    'UTC-FK5-GEO',
-           # XXX time format
-           'time':            str(gpsToUtc(event.gpstime).isoformat())[:-6],   #'1918-11-11T11:11:11',
-           #'timeError':       1.0,
-           'longitude':       0.0,
-           'latitude':        0.0,
-           'positionalError': 180.0,
-    }
-
-    ww = makeWhereWhen(wwd)
-    if ww: v.set_WhereWhen(ww)
+    if voevent_type != 'retraction':
+        wwd = {'observatory':     'LIGO Virgo',
+               'coord_system':    'UTC-FK5-GEO',
+               # XXX time format
+               'time':            str(gpsToUtc(event.gpstime).isoformat())[:-6],   #'1918-11-11T11:11:11',
+               #'timeError':       1.0,
+               'longitude':       0.0,
+               'latitude':        0.0,
+               'positionalError': 180.0,
+        }
+
+        ww = makeWhereWhen(wwd)
+        if ww: v.set_WhereWhen(ww)
 
     ############ Citation ############################
-    #c = Citations()
-    #c.add_EventIVORN(EventIVORN(cite="followup", valueOf_="ivo:silly/billy#89474"))
-    #c.add_EventIVORN(EventIVORN(cite="followup", valueOf_="ivo:silly/billy#89475"))
-    #v.set_Citations(c)
+    if voevent_type != 'preliminary':
+        c = Citations()
+        for ve in event.voevent_set.all():
+            # Oh, actually we need to exclude *this* voevent.
+            if serial_number == ve.N:
+                continue
+            if voevent_type == 'initial':
+                ei = EventIVORN('supersedes', ve.ivorn)
+                c.set_Description('Initial localization is now available')
+            elif voevent_type == 'update':            
+                ei = EventIVORN('supersedes', ve.ivorn)
+                c.set_Description('Updated localization is now available')
+            elif voevent_type == 'retraction':
+                ei = EventIVORN('retraction', ve.ivorn)
+                c.set_Description('Determined to not be a viable GW event candidate')
+            c.add_EventIVORN(ei)
+
+        v.set_Citations(c)
 
     ############ output the event ############################
     xml = stringVOEvent(v) 
         #schemaURL = "http://www.ivoa.net/xml/VOEvent/VOEvent-v2.0.xsd")
-    return xml
+    return xml, ivorn
 
 def submitToSkyalert(event, validate_only=False):
     ## Python stub code for validating and authoring VOEvents to Skyalert
diff --git a/gracedb/migrations/0038_auto__add_voevent__add_unique_voevent_event_N.py b/gracedb/migrations/0038_auto__add_voevent__add_unique_voevent_event_N.py
new file mode 100644
index 0000000000000000000000000000000000000000..38a7fbedd4426b71069eadcf247a30d516c0db7b
--- /dev/null
+++ b/gracedb/migrations/0038_auto__add_voevent__add_unique_voevent_event_N.py
@@ -0,0 +1,307 @@
+# -*- coding: utf-8 -*-
+from south.utils import datetime_utils as datetime
+from south.db import db
+from south.v2 import SchemaMigration
+from django.db import models
+
+
+class Migration(SchemaMigration):
+
+    def forwards(self, orm):
+        # Adding model 'VOEvent'
+        db.create_table(u'gracedb_voevent', (
+            (u'id', self.gf('django.db.models.fields.AutoField')(primary_key=True)),
+            ('event', self.gf('django.db.models.fields.related.ForeignKey')(to=orm['gracedb.Event'])),
+            ('created', self.gf('django.db.models.fields.DateTimeField')(auto_now_add=True, blank=True)),
+            ('issuer', self.gf('django.db.models.fields.related.ForeignKey')(to=orm['auth.User'])),
+            ('ivorn', self.gf('django.db.models.fields.CharField')(default='', max_length=200)),
+            ('filename', self.gf('django.db.models.fields.CharField')(default='', max_length=100)),
+            ('file_version', self.gf('django.db.models.fields.IntegerField')(null=True)),
+            ('N', self.gf('django.db.models.fields.IntegerField')()),
+            ('voevent_type', self.gf('django.db.models.fields.CharField')(max_length=2)),
+        ))
+        db.send_create_signal(u'gracedb', ['VOEvent'])
+
+        # Adding unique constraint on 'VOEvent', fields ['event', 'N']
+        db.create_unique(u'gracedb_voevent', ['event_id', 'N'])
+
+
+    def backwards(self, orm):
+        # Removing unique constraint on 'VOEvent', fields ['event', 'N']
+        db.delete_unique(u'gracedb_voevent', ['event_id', 'N'])
+
+        # Deleting model 'VOEvent'
+        db.delete_table(u'gracedb_voevent')
+
+
+    models = {
+        u'auth.group': {
+            'Meta': {'object_name': 'Group'},
+            u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
+            'name': ('django.db.models.fields.CharField', [], {'unique': 'True', 'max_length': '80'}),
+            'permissions': ('django.db.models.fields.related.ManyToManyField', [], {'to': u"orm['auth.Permission']", 'symmetrical': 'False', 'blank': 'True'})
+        },
+        u'auth.permission': {
+            'Meta': {'ordering': "(u'content_type__app_label', u'content_type__model', u'codename')", 'unique_together': "((u'content_type', u'codename'),)", 'object_name': 'Permission'},
+            'codename': ('django.db.models.fields.CharField', [], {'max_length': '100'}),
+            'content_type': ('django.db.models.fields.related.ForeignKey', [], {'to': u"orm['contenttypes.ContentType']"}),
+            u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
+            'name': ('django.db.models.fields.CharField', [], {'max_length': '50'})
+        },
+        u'auth.user': {
+            'Meta': {'object_name': 'User'},
+            'date_joined': ('django.db.models.fields.DateTimeField', [], {'default': 'datetime.datetime.now'}),
+            'email': ('django.db.models.fields.EmailField', [], {'max_length': '75', 'blank': 'True'}),
+            'first_name': ('django.db.models.fields.CharField', [], {'max_length': '30', 'blank': 'True'}),
+            'groups': ('django.db.models.fields.related.ManyToManyField', [], {'symmetrical': 'False', 'related_name': "u'user_set'", 'blank': 'True', 'to': u"orm['auth.Group']"}),
+            u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
+            'is_active': ('django.db.models.fields.BooleanField', [], {'default': 'True'}),
+            'is_staff': ('django.db.models.fields.BooleanField', [], {'default': 'False'}),
+            'is_superuser': ('django.db.models.fields.BooleanField', [], {'default': 'False'}),
+            'last_login': ('django.db.models.fields.DateTimeField', [], {'default': 'datetime.datetime.now'}),
+            'last_name': ('django.db.models.fields.CharField', [], {'max_length': '30', 'blank': 'True'}),
+            'password': ('django.db.models.fields.CharField', [], {'max_length': '128'}),
+            'user_permissions': ('django.db.models.fields.related.ManyToManyField', [], {'symmetrical': 'False', 'related_name': "u'user_set'", 'blank': 'True', 'to': u"orm['auth.Permission']"}),
+            'username': ('django.db.models.fields.CharField', [], {'unique': 'True', 'max_length': '30'})
+        },
+        u'contenttypes.contenttype': {
+            'Meta': {'ordering': "('name',)", 'unique_together': "(('app_label', 'model'),)", 'object_name': 'ContentType', 'db_table': "'django_content_type'"},
+            'app_label': ('django.db.models.fields.CharField', [], {'max_length': '100'}),
+            u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
+            'model': ('django.db.models.fields.CharField', [], {'max_length': '100'}),
+            'name': ('django.db.models.fields.CharField', [], {'max_length': '100'})
+        },
+        u'gracedb.approval': {
+            'Meta': {'object_name': 'Approval'},
+            'approvedEvent': ('django.db.models.fields.related.ForeignKey', [], {'to': u"orm['gracedb.Event']"}),
+            'approver': ('django.db.models.fields.related.ForeignKey', [], {'to': u"orm['auth.User']"}),
+            'approvingCollaboration': ('django.db.models.fields.CharField', [], {'max_length': '1'}),
+            'created': ('django.db.models.fields.DateTimeField', [], {'auto_now_add': 'True', 'blank': 'True'}),
+            u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'})
+        },
+        u'gracedb.coincinspiralevent': {
+            'Meta': {'ordering': "['-id']", 'object_name': 'CoincInspiralEvent', '_ormbases': [u'gracedb.Event']},
+            'combined_far': ('django.db.models.fields.FloatField', [], {'null': 'True'}),
+            'end_time': ('django.db.models.fields.PositiveIntegerField', [], {'null': 'True'}),
+            'end_time_ns': ('django.db.models.fields.PositiveIntegerField', [], {'null': 'True'}),
+            u'event_ptr': ('django.db.models.fields.related.OneToOneField', [], {'to': u"orm['gracedb.Event']", 'unique': 'True', 'primary_key': 'True'}),
+            'false_alarm_rate': ('django.db.models.fields.FloatField', [], {'null': 'True'}),
+            'ifos': ('django.db.models.fields.CharField', [], {'default': "''", 'max_length': '20'}),
+            'mass': ('django.db.models.fields.FloatField', [], {'null': 'True'}),
+            'mchirp': ('django.db.models.fields.FloatField', [], {'null': 'True'}),
+            'minimum_duration': ('django.db.models.fields.FloatField', [], {'null': 'True'}),
+            'snr': ('django.db.models.fields.FloatField', [], {'null': 'True'})
+        },
+        u'gracedb.embbeventlog': {
+            'Meta': {'ordering': "['-created', '-N']", 'unique_together': "(('event', 'N'),)", 'object_name': 'EMBBEventLog'},
+            'N': ('django.db.models.fields.IntegerField', [], {}),
+            'comment': ('django.db.models.fields.TextField', [], {'blank': 'True'}),
+            'created': ('django.db.models.fields.DateTimeField', [], {'auto_now_add': 'True', 'blank': 'True'}),
+            'dec': ('django.db.models.fields.FloatField', [], {'null': 'True'}),
+            'decList': ('django.db.models.fields.TextField', [], {'blank': 'True'}),
+            'decWidth': ('django.db.models.fields.FloatField', [], {'null': 'True'}),
+            'decWidthList': ('django.db.models.fields.TextField', [], {'blank': 'True'}),
+            'duration': ('django.db.models.fields.PositiveIntegerField', [], {'null': 'True'}),
+            'durationList': ('django.db.models.fields.TextField', [], {'blank': 'True'}),
+            'eel_status': ('django.db.models.fields.CharField', [], {'max_length': '2'}),
+            'event': ('django.db.models.fields.related.ForeignKey', [], {'to': u"orm['gracedb.Event']"}),
+            'extra_info_dict': ('django.db.models.fields.TextField', [], {'blank': 'True'}),
+            'footprintID': ('django.db.models.fields.TextField', [], {'blank': 'True'}),
+            'gpstime': ('django.db.models.fields.PositiveIntegerField', [], {'null': 'True'}),
+            'gpstimeList': ('django.db.models.fields.TextField', [], {'blank': 'True'}),
+            'group': ('django.db.models.fields.related.ForeignKey', [], {'to': u"orm['gracedb.EMGroup']"}),
+            u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
+            'instrument': ('django.db.models.fields.CharField', [], {'max_length': '200', 'blank': 'True'}),
+            'obs_status': ('django.db.models.fields.CharField', [], {'max_length': '2'}),
+            'ra': ('django.db.models.fields.FloatField', [], {'null': 'True'}),
+            'raList': ('django.db.models.fields.TextField', [], {'blank': 'True'}),
+            'raWidth': ('django.db.models.fields.FloatField', [], {'null': 'True'}),
+            'raWidthList': ('django.db.models.fields.TextField', [], {'blank': 'True'}),
+            'submitter': ('django.db.models.fields.related.ForeignKey', [], {'to': u"orm['auth.User']"}),
+            'waveband': ('django.db.models.fields.CharField', [], {'max_length': '25'})
+        },
+        u'gracedb.emgroup': {
+            'Meta': {'object_name': 'EMGroup'},
+            u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
+            'name': ('django.db.models.fields.CharField', [], {'unique': 'True', 'max_length': '20'})
+        },
+        u'gracedb.event': {
+            'Meta': {'ordering': "['-id']", 'object_name': 'Event'},
+            'created': ('django.db.models.fields.DateTimeField', [], {'auto_now_add': 'True', 'blank': 'True'}),
+            'far': ('django.db.models.fields.FloatField', [], {'null': 'True'}),
+            'gpstime': ('django.db.models.fields.DecimalField', [], {'null': 'True', 'max_digits': '16', 'decimal_places': '6'}),
+            'group': ('django.db.models.fields.related.ForeignKey', [], {'to': u"orm['gracedb.Group']"}),
+            u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
+            'instruments': ('django.db.models.fields.CharField', [], {'default': "''", 'max_length': '20'}),
+            'labels': ('django.db.models.fields.related.ManyToManyField', [], {'to': u"orm['gracedb.Label']", 'through': u"orm['gracedb.Labelling']", 'symmetrical': 'False'}),
+            'likelihood': ('django.db.models.fields.FloatField', [], {'null': 'True'}),
+            'nevents': ('django.db.models.fields.PositiveIntegerField', [], {'null': 'True'}),
+            'perms': ('django.db.models.fields.TextField', [], {'null': 'True'}),
+            'pipeline': ('django.db.models.fields.related.ForeignKey', [], {'to': u"orm['gracedb.Pipeline']"}),
+            'search': ('django.db.models.fields.related.ForeignKey', [], {'to': u"orm['gracedb.Search']", 'null': 'True'}),
+            'submitter': ('django.db.models.fields.related.ForeignKey', [], {'to': u"orm['auth.User']"})
+        },
+        u'gracedb.eventlog': {
+            'Meta': {'ordering': "['-created', '-N']", 'unique_together': "(('event', 'N'),)", 'object_name': 'EventLog'},
+            'N': ('django.db.models.fields.IntegerField', [], {}),
+            'comment': ('django.db.models.fields.TextField', [], {}),
+            'created': ('django.db.models.fields.DateTimeField', [], {'auto_now_add': 'True', 'blank': 'True'}),
+            'event': ('django.db.models.fields.related.ForeignKey', [], {'to': u"orm['gracedb.Event']"}),
+            'file_version': ('django.db.models.fields.IntegerField', [], {'null': 'True'}),
+            'filename': ('django.db.models.fields.CharField', [], {'default': "''", 'max_length': '100'}),
+            u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
+            'issuer': ('django.db.models.fields.related.ForeignKey', [], {'to': u"orm['auth.User']"})
+        },
+        u'gracedb.grbevent': {
+            'Meta': {'ordering': "['-id']", 'object_name': 'GrbEvent', '_ormbases': [u'gracedb.Event']},
+            'author_ivorn': ('django.db.models.fields.CharField', [], {'max_length': '200', 'null': 'True'}),
+            'author_shortname': ('django.db.models.fields.CharField', [], {'max_length': '200', 'null': 'True'}),
+            'coord_system': ('django.db.models.fields.CharField', [], {'max_length': '200', 'null': 'True'}),
+            'dec': ('django.db.models.fields.FloatField', [], {'null': 'True'}),
+            'error_radius': ('django.db.models.fields.FloatField', [], {'null': 'True'}),
+            u'event_ptr': ('django.db.models.fields.related.OneToOneField', [], {'to': u"orm['gracedb.Event']", 'unique': 'True', 'primary_key': 'True'}),
+            'how_description': ('django.db.models.fields.CharField', [], {'max_length': '200', 'null': 'True'}),
+            'how_reference_url': ('django.db.models.fields.URLField', [], {'max_length': '200', 'null': 'True'}),
+            'ivorn': ('django.db.models.fields.CharField', [], {'max_length': '200', 'null': 'True'}),
+            'observatory_location_id': ('django.db.models.fields.CharField', [], {'max_length': '200', 'null': 'True'}),
+            'ra': ('django.db.models.fields.FloatField', [], {'null': 'True'})
+        },
+        u'gracedb.group': {
+            'Meta': {'object_name': 'Group'},
+            u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
+            'name': ('django.db.models.fields.CharField', [], {'max_length': '20'})
+        },
+        u'gracedb.label': {
+            'Meta': {'object_name': 'Label'},
+            'defaultColor': ('django.db.models.fields.CharField', [], {'default': "'black'", 'max_length': '20'}),
+            u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
+            'name': ('django.db.models.fields.CharField', [], {'unique': 'True', 'max_length': '20'})
+        },
+        u'gracedb.labelling': {
+            'Meta': {'object_name': 'Labelling'},
+            'created': ('django.db.models.fields.DateTimeField', [], {'auto_now_add': 'True', 'blank': 'True'}),
+            'creator': ('django.db.models.fields.related.ForeignKey', [], {'to': u"orm['auth.User']"}),
+            'event': ('django.db.models.fields.related.ForeignKey', [], {'to': u"orm['gracedb.Event']"}),
+            u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
+            'label': ('django.db.models.fields.related.ForeignKey', [], {'to': u"orm['gracedb.Label']"})
+        },
+        u'gracedb.multiburstevent': {
+            'Meta': {'ordering': "['-id']", 'object_name': 'MultiBurstEvent', '_ormbases': [u'gracedb.Event']},
+            'amplitude': ('django.db.models.fields.FloatField', [], {'null': 'True'}),
+            'bandwidth': ('django.db.models.fields.FloatField', [], {'null': 'True'}),
+            'central_freq': ('django.db.models.fields.FloatField', [], {'null': 'True'}),
+            'confidence': ('django.db.models.fields.FloatField', [], {'null': 'True'}),
+            'duration': ('django.db.models.fields.FloatField', [], {'null': 'True'}),
+            u'event_ptr': ('django.db.models.fields.related.OneToOneField', [], {'to': u"orm['gracedb.Event']", 'unique': 'True', 'primary_key': 'True'}),
+            'false_alarm_rate': ('django.db.models.fields.FloatField', [], {'null': 'True'}),
+            'ifos': ('django.db.models.fields.CharField', [], {'default': "''", 'max_length': '20'}),
+            'ligo_angle': ('django.db.models.fields.FloatField', [], {'null': 'True'}),
+            'ligo_angle_sig': ('django.db.models.fields.FloatField', [], {'null': 'True'}),
+            'ligo_axis_dec': ('django.db.models.fields.FloatField', [], {'null': 'True'}),
+            'ligo_axis_ra': ('django.db.models.fields.FloatField', [], {'null': 'True'}),
+            'peak_time': ('django.db.models.fields.PositiveIntegerField', [], {'null': 'True'}),
+            'peak_time_ns': ('django.db.models.fields.PositiveIntegerField', [], {'null': 'True'}),
+            'snr': ('django.db.models.fields.FloatField', [], {'null': 'True'}),
+            'start_time': ('django.db.models.fields.PositiveIntegerField', [], {'null': 'True'}),
+            'start_time_ns': ('django.db.models.fields.PositiveIntegerField', [], {'null': 'True'})
+        },
+        u'gracedb.pipeline': {
+            'Meta': {'object_name': 'Pipeline'},
+            u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
+            'name': ('django.db.models.fields.CharField', [], {'max_length': '100'})
+        },
+        u'gracedb.search': {
+            'Meta': {'object_name': 'Search'},
+            'description': ('django.db.models.fields.TextField', [], {'blank': 'True'}),
+            u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
+            'name': ('django.db.models.fields.CharField', [], {'max_length': '100'})
+        },
+        u'gracedb.singleinspiral': {
+            'Gamma0': ('django.db.models.fields.FloatField', [], {'null': 'True'}),
+            'Gamma1': ('django.db.models.fields.FloatField', [], {'null': 'True'}),
+            'Gamma2': ('django.db.models.fields.FloatField', [], {'null': 'True'}),
+            'Gamma3': ('django.db.models.fields.FloatField', [], {'null': 'True'}),
+            'Gamma4': ('django.db.models.fields.FloatField', [], {'null': 'True'}),
+            'Gamma5': ('django.db.models.fields.FloatField', [], {'null': 'True'}),
+            'Gamma6': ('django.db.models.fields.FloatField', [], {'null': 'True'}),
+            'Gamma7': ('django.db.models.fields.FloatField', [], {'null': 'True'}),
+            'Gamma8': ('django.db.models.fields.FloatField', [], {'null': 'True'}),
+            'Gamma9': ('django.db.models.fields.FloatField', [], {'null': 'True'}),
+            'Meta': {'object_name': 'SingleInspiral'},
+            'alpha': ('django.db.models.fields.FloatField', [], {'null': 'True'}),
+            'alpha1': ('django.db.models.fields.FloatField', [], {'null': 'True'}),
+            'alpha2': ('django.db.models.fields.FloatField', [], {'null': 'True'}),
+            'alpha3': ('django.db.models.fields.FloatField', [], {'null': 'True'}),
+            'alpha4': ('django.db.models.fields.FloatField', [], {'null': 'True'}),
+            'alpha5': ('django.db.models.fields.FloatField', [], {'null': 'True'}),
+            'alpha6': ('django.db.models.fields.FloatField', [], {'null': 'True'}),
+            'amplitude': ('django.db.models.fields.FloatField', [], {'null': 'True'}),
+            'bank_chisq': ('django.db.models.fields.FloatField', [], {'null': 'True'}),
+            'bank_chisq_dof': ('django.db.models.fields.IntegerField', [], {'null': 'True'}),
+            'beta': ('django.db.models.fields.FloatField', [], {'null': 'True'}),
+            'channel': ('django.db.models.fields.CharField', [], {'max_length': '20'}),
+            'chi': ('django.db.models.fields.FloatField', [], {'null': 'True'}),
+            'chisq': ('django.db.models.fields.FloatField', [], {'null': 'True'}),
+            'chisq_dof': ('django.db.models.fields.IntegerField', [], {'null': 'True'}),
+            'coa_phase': ('django.db.models.fields.FloatField', [], {'null': 'True'}),
+            'cont_chisq': ('django.db.models.fields.FloatField', [], {'null': 'True'}),
+            'cont_chisq_dof': ('django.db.models.fields.IntegerField', [], {'null': 'True'}),
+            'eff_distance': ('django.db.models.fields.FloatField', [], {'null': 'True'}),
+            'end_time': ('django.db.models.fields.IntegerField', [], {'null': 'True'}),
+            'end_time_gmst': ('django.db.models.fields.FloatField', [], {'null': 'True'}),
+            'end_time_ns': ('django.db.models.fields.IntegerField', [], {'null': 'True'}),
+            'eta': ('django.db.models.fields.FloatField', [], {'null': 'True'}),
+            'event': ('django.db.models.fields.related.ForeignKey', [], {'to': u"orm['gracedb.Event']"}),
+            'event_duration': ('django.db.models.fields.FloatField', [], {'null': 'True'}),
+            'f_final': ('django.db.models.fields.FloatField', [], {'null': 'True'}),
+            u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
+            'ifo': ('django.db.models.fields.CharField', [], {'max_length': '20', 'null': 'True'}),
+            'impulse_time': ('django.db.models.fields.IntegerField', [], {'null': 'True'}),
+            'impulse_time_ns': ('django.db.models.fields.IntegerField', [], {'null': 'True'}),
+            'kappa': ('django.db.models.fields.FloatField', [], {'null': 'True'}),
+            'mass1': ('django.db.models.fields.FloatField', [], {'null': 'True'}),
+            'mass2': ('django.db.models.fields.FloatField', [], {'null': 'True'}),
+            'mchirp': ('django.db.models.fields.FloatField', [], {'null': 'True'}),
+            'mtotal': ('django.db.models.fields.FloatField', [], {'null': 'True'}),
+            'psi0': ('django.db.models.fields.FloatField', [], {'null': 'True'}),
+            'psi3': ('django.db.models.fields.FloatField', [], {'null': 'True'}),
+            'rsqveto_duration': ('django.db.models.fields.FloatField', [], {'null': 'True'}),
+            'search': ('django.db.models.fields.CharField', [], {'max_length': '20', 'null': 'True'}),
+            'sigmasq': ('django.db.models.fields.FloatField', [], {'null': 'True'}),
+            'snr': ('django.db.models.fields.FloatField', [], {'null': 'True'}),
+            'spin1x': ('django.db.models.fields.FloatField', [], {'null': 'True'}),
+            'spin1y': ('django.db.models.fields.FloatField', [], {'null': 'True'}),
+            'spin1z': ('django.db.models.fields.FloatField', [], {'null': 'True'}),
+            'spin2x': ('django.db.models.fields.FloatField', [], {'null': 'True'}),
+            'spin2y': ('django.db.models.fields.FloatField', [], {'null': 'True'}),
+            'spin2z': ('django.db.models.fields.FloatField', [], {'null': 'True'}),
+            'tau0': ('django.db.models.fields.FloatField', [], {'null': 'True'}),
+            'tau2': ('django.db.models.fields.FloatField', [], {'null': 'True'}),
+            'tau3': ('django.db.models.fields.FloatField', [], {'null': 'True'}),
+            'tau4': ('django.db.models.fields.FloatField', [], {'null': 'True'}),
+            'tau5': ('django.db.models.fields.FloatField', [], {'null': 'True'}),
+            'template_duration': ('django.db.models.fields.FloatField', [], {'null': 'True'}),
+            'ttotal': ('django.db.models.fields.FloatField', [], {'null': 'True'})
+        },
+        u'gracedb.tag': {
+            'Meta': {'object_name': 'Tag'},
+            'displayName': ('django.db.models.fields.CharField', [], {'max_length': '200', 'null': 'True'}),
+            'eventlogs': ('django.db.models.fields.related.ManyToManyField', [], {'to': u"orm['gracedb.EventLog']", 'symmetrical': 'False'}),
+            u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
+            'name': ('django.db.models.fields.CharField', [], {'max_length': '100'})
+        },
+        u'gracedb.voevent': {
+            'Meta': {'ordering': "['-created', '-N']", 'unique_together': "(('event', 'N'),)", 'object_name': 'VOEvent'},
+            'N': ('django.db.models.fields.IntegerField', [], {}),
+            'created': ('django.db.models.fields.DateTimeField', [], {'auto_now_add': 'True', 'blank': 'True'}),
+            'event': ('django.db.models.fields.related.ForeignKey', [], {'to': u"orm['gracedb.Event']"}),
+            'file_version': ('django.db.models.fields.IntegerField', [], {'null': 'True'}),
+            'filename': ('django.db.models.fields.CharField', [], {'default': "''", 'max_length': '100'}),
+            u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
+            'issuer': ('django.db.models.fields.related.ForeignKey', [], {'to': u"orm['auth.User']"}),
+            'ivorn': ('django.db.models.fields.CharField', [], {'default': "''", 'max_length': '200'}),
+            'voevent_type': ('django.db.models.fields.CharField', [], {'max_length': '2'})
+        }
+    }
+
+    complete_apps = ['gracedb']
\ No newline at end of file
diff --git a/gracedb/models.py b/gracedb/models.py
index 16770c922f26a2c038ac1bc19529d8cc4bbd96c8..210caea14783dfe60c9013bcff999d893470d506 100644
--- a/gracedb/models.py
+++ b/gracedb/models.py
@@ -822,3 +822,54 @@ class Tag(models.Model):
 #         # messages in the tag.
 #         eventlist = [log.event for log in self.eventlogs.all()]
 
+class VOEvent(models.Model):
+    class Meta:
+        ordering = ['-created','-N']
+        unique_together = ("event","N")
+    # Now N will be the serial number.
+    event = models.ForeignKey(Event, null=False)
+    created = models.DateTimeField(auto_now_add=True)
+    issuer = models.ForeignKey(DjangoUser)
+    ivorn = models.CharField(max_length=200, default="")
+    filename = models.CharField(max_length=100, default="")
+    file_version = models.IntegerField(null=True)
+    N = models.IntegerField(null=False)
+    VOEVENT_TYPE_CHOICES = (('PR','preliminary'), ('IN','initial'), ('UP','update'), ('RE', 'retraction'),)
+    voevent_type = models.CharField(max_length=2, choices=VOEVENT_TYPE_CHOICES)
+
+    def fileurl(self):
+        if self.filename:
+            actual_filename = self.filename
+            if self.file_version >= 0:
+                actual_filename += ',%d' % self.file_version
+            return reverse('file', args=[self.event.graceid(), actual_filename])
+        else:
+            return None
+
+    def save(self, *args, **kwargs):
+        success = False
+        # XXX filename must not be 'None' because null=False for the filename
+        # field above.
+        self.filename = self.filename or ""
+        attempts = 0
+        while (not success and attempts < 5):
+            attempts = attempts + 1
+            if not self.N:
+                if self.event.voevent_set.count():
+                    self.N = int(self.event.voevent_set.aggregate(models.Max('N'))['N__max']) + 1
+                else:
+                    self.N = 1
+            try:
+                super(VOEvent, self).save(*args, **kwargs)
+                success = True
+            except IntegrityError:
+                # IntegrityError means an attempt to insert a duplicate
+                # key or to violate a foreignkey constraint.
+                # We are under race conditions.  Let's try again.
+                pass
+
+        if not success:
+            # XXX Should this be a custom exception?  That way we could catch it
+            # in the views that use it and give an informative error message.
+            raise Exception("Too many attempts to save log message. Something is wrong.")
+
diff --git a/gracedb/urls_rest.py b/gracedb/urls_rest.py
index 65b32c934d5488b0bae997b005b9fb11e9236ef8..1f12d8f5da1e6b2b3624cd9bde148caee45cff0c 100644
--- a/gracedb/urls_rest.py
+++ b/gracedb/urls_rest.py
@@ -5,7 +5,7 @@ from django.conf.urls import patterns, url
 
 # rest_framework
 from gracedb.api import GracedbRoot
-from gracedb.api import EventList, EventDetail, EventVODetail
+from gracedb.api import EventList, EventDetail
 from gracedb.api import EventLogList, EventLogDetail
 from gracedb.api import EMBBEventLogList, EMBBEventLogDetail
 from gracedb.api import TagList
@@ -18,6 +18,7 @@ from gracedb.api import PerformanceInfo
 from gracedb.api import EventPermissionList
 from gracedb.api import GroupEventPermissionList
 from gracedb.api import GroupEventPermissionDetail
+from gracedb.api import VOEventList, VOEventDetail
 
 
 urlpatterns = patterns('gracedb.api',
@@ -28,8 +29,6 @@ urlpatterns = patterns('gracedb.api',
     # events/[{graceid}[/{version}]]
     url (r'events/$',
         EventList.as_view(), name='event-list'),
-    url (r'events/voevent/(?P<graceid>[GEHMT]\d+)$',
-        EventVODetail.as_view(), name='event-vo-detail'),
     url (r'events/(?P<graceid>[GEHMT]\d+)$',
         EventDetail.as_view(), name='event-detail'),
 
@@ -40,6 +39,13 @@ urlpatterns = patterns('gracedb.api',
     url (r'events/(?P<graceid>[GEHMT]\d+)/log/(?P<n>\d+)$',
         EventLogDetail.as_view(), name='eventlog-detail'),
 
+    # VOEvent Resources
+    # events/{graceid}/voevent/[{serial_number}]
+    url (r'events/(?P<graceid>[GEHMT]\d+)/voevent/$',
+        VOEventList.as_view(), name='voevent-list'),
+    url (r'events/(?P<graceid>[GEHMT]\d+)/voevent/(?P<n>\d+)$',
+        VOEventDetail.as_view(), name='voevent-detail'),
+
     # EMBB Event Log Resources
     # events/{graceid}/logs/[{logid}]
     url (r'events/(?P<graceid>[GEHMT]\d+)/embb/$',
diff --git a/gracedb/view_utils.py b/gracedb/view_utils.py
index f286a86aed3cd3191900f80aaa703973d99f2930..e56ede5f77efbc679aaebd16304f4936eeff1ed6 100644
--- a/gracedb/view_utils.py
+++ b/gracedb/view_utils.py
@@ -284,6 +284,45 @@ def embbEventLogToDict(eel, request=None):
                   "extra_info_dict" : eel.extra_info_dict,
              }
   
+# VOEvent serializer
+def voeventToDict(voevent, request=None):
+    filename = urlquote('%s,%d' % (voevent.filename, voevent.file_version))
+
+    uri = None
+    file_uri = None
+    if request:
+        uri = reverse("voevent-detail",
+                args=[voevent.event.graceid(), voevent.N],
+                request=request)
+        file_uri = reverse("files",
+            args=[voevent.event.graceid(), filename],
+            request=request)
+
+    issuer_info = {
+        "username": voevent.issuer.username,
+        "display_name": "%s %s" % (voevent.issuer.first_name, voevent.issuer.last_name),
+    }
+
+    # Read in the filecontents
+    filepath = os.path.join(voevent.event.datadir(), voevent.filename)
+    text = None
+    try: 
+        text = open(filepath, 'r').read()
+    except:
+        pass
+
+    return {
+                "self"         : uri,
+                "text"         : text,
+                "file"         : file_uri,
+                "N"            : voevent.N,
+                "issuer"       : issuer_info,
+                "ivorn"        : voevent.ivorn,
+                "filename"     : voevent.filename,
+                "file_version" : voevent.file_version,
+                "voevent_type" : voevent.voevent_type,
+                "created"      : voevent.created.isoformat(),
+           }
 
 
 #---------------------------------------------------------------------------------------