Integrating event information from your application with Livelox

The information that an event organiser has to provide when setting up a event in Livelox includes event name, time span, classes, maps, courses, controls, and - optionally - course prints, to list a few. In this text, such a data set is referred to as event information. Traditionally, the information is exported as files from the software used for the course setting and/or copied from web-based event management application, and then imported and/or typed in to Livelox. Needless to say, this approach involves a few manual steps.

To help event organisers to get rid of these manual steps, Livelox provides a REST API that course setting software and web-based event management applications can utilize to import information for an event into Livelox. This documentation gives an overview of the workflow and the API endpoints involved.

Feel free to email us at info@livelox.com if you would like to integrate your event management application with Livelox, or if you have any questions related to this topic.

Terminology

  • Event management application - a web-based application that provides event calendar, registration, start lists and result lists, typically hosted by an orienteering federation
  • Course setting application - an application, typically platform-native, that facilitates computer-aided course setting, like OCAD or Purple Pen
  • Map - an image file georeferenced through e.g. a world file or coordinate mappings
  • Course - a start, a number of controls, and a finish making up a course
  • Control - a georeferenced control location
  • Course print - a vectorized image for the course overlay, i.e. the purple objects representing a course, any forbidden areas, etc.
  • Importable event - a JSON object combining all event information and other metadata for importing into Livelox

The workflow

The workflow will vary a bit depending on whether the event information contains course setting information or not. The general approach is similar in both cases, though.

  1. Your application provides a dialog for the Livelox export. The dialog typically informs the user (that is, the event organiser or course setter) about what Livelox is, and lets the user fill in any information required by Livelox that is not already handled in your application.
  2. The user fills in any information in the dialog and clicks an Import button.
  3. Your application then compiles all required information in a way that makes sense for the application and sends it to the Livelox API using a number of requests. The first request should be to the POST /importableEvents endpoint, providing an importable event object as the JSON payload. This object has a quite flexible data structure. It may contain event and course information in itself, but also hold references to files such as map images and course data files that will be uploaded later on.
  4. Livelox responds with an unique importable event ID (called {importableEventId} below) and a Livelox import event endpoint to redirect the user to further on. If possible, store this information in your application.
  5. If your application handles course setting information, upload all files referenced in the importable event object using the POST /importableEvents/{importableEventId}/files/{fileName} endpoint. Examples of files: map image files, world files, IOF XML CourseData files, KMZ files, SVG course print files, and your application's native course setting files (if supported by Livelox).
  6. When all files have been uploaded, you can optionally check whether the data is valid through the GET /importableEvents/{importableEventId}/validationErrors endpoint.
  7. Your application redirects the user to the liveloxImportEventUrl returned by the POST /importableEvents request made previously. If the application is not web-based, open a web browser pointing to the endpoint.
  8. The user completes the import process at the Livelox website, giving him or her the possibility to create a new event in Livelox or pick an existing event. Livelox will prompt the user to log in if not already logged in.
  9. If a webhook endpoint has been specified in the POST /importableEvents, this endpoint will be called to notify your application that an event has been created in Livelox.

Authorization

Authorization is needed to use the API.

  • Web-based applications use a server-side API key.
  • Non-web-based applications utilize user-delegated access with the events.import scope. Authorization using an API key is not allowed in this case.

For further details, please refer to the main API documentation page.

API endpoints for importable events

POST https://api.livelox.com/importableEvents

Makes an event available to import for a user in Livelox. The request payload should be an importable event object with Content-Type: application/json.

Response

200 OK on success, 400 Bad Request on validation failure, 403 Forbidden on authorization failure.

Response payload example

{
    "id": "3e2ff8867e48422ab80862901c8cf77c",
    "liveloxImportEventUrl": "https://www.livelox.com/Admin/Events/ImportEvent?importableEventIdentifier=YourApplicationName%33e2ff8867e48422ab80862901c8cf77c"
}

The id property is the reference to the importable event in Livelox, called {importableEventId} in this documentation. If you provided an id when creating the importable event, it will be reused. Otherwise, a new identifier will be created. Store this value in your application for further use.

POST https://api.livelox.com/importableEvents/{importableEventId}/files/{fileName}

Uploads files that are referenced in the importable event object that was POSTed to https://api.livelox.com/importableEvents. You can either place all files in a zip file and upload that file, or post the files one by one. The payload of the request should be the raw file content.

When posting a zip file, the {fileName} of the zip file itself doesn't matter. However, make sure to name the files in the archive to correspond with the file references in the importable event object.

When posting files one by one, make sure that each {fileName} corresponds with the file references in the importable event object.

Response

201 Created on success, 403 Forbidden on authorization failure, 404 Not Found on missing event.

GET https://api.livelox.com/importableEvents/{importableEventId}?includeImportedEvent={true|false}

Returns an importable event object. If includeImportedEvent is set to true, the Livelox event it has been imported to, if any, will be present in the importedEvent property of the response object. This is useful when you want to know whether there has been an event created in Livelox based on the importable event, and properties such as name and time for the event.

Response

200 OK on success, 403 Forbidden on authorization failure, 404 Not Found on missing event.

GET https://api.livelox.com/importableEvents/{importableEventId}/validationErrors

Returns any validation errors for the importable event object. If the errors array is empty, you could safely redirect the user to Livelox. Items in the warnings array correspond to non-critical issues that will be ignored during import.

Response

200 OK on success, 403 Forbidden on authorization failure, 404 Not Found on missing event.

Response payload example

{
    "errors": ["The course 'W21' must have at least two controls."],
    "warnings": ["Could not georeference course image file 'W21.svg'."],
}

DELETE https://api.livelox.com/importableEvents/{importableEventId}

Deletes an existing importable event object including all uploaded files for the event. This endpoint provides a means to clean up when a user cancels the dialog before importing anything. Any event in Livelox created based on the importable event will not be affected.

Response

204 No Content on success, 403 Forbidden on authorization failure, 404 Not Found on missing event.

DELETE https://api.livelox.com/importableEvents/{importableEventId}/files/{fileName}

Deletes a file that has been uploaded. This endpoint provides a means to clean up when a user cancels the dialog before importing anything. Any event in Livelox created based on the importable event will not be affected.

Response

204 No content on success, 403 Forbidden on authorization failure, 404 Not Found on missing event or missing file.

The importable event object

Below is an importable event object containing all available properties along with descriptive comments. In practice, the importable event objects that you construct will likely contain fewer properties. Most properties are optional; however, try to fill as many of them as yor application allows for.

{
    // your application's unique identifier for the event, if any
    // can be an integer or a string of up to 64 characters
    "id": 123456,

    // the name of the event
    "name": "My event",

    // the UTC time interval of the event
    // make sure to use the time format of the example below, including the "T" and "Z" letters
    "timeInterval": {
        "start": "2020-11-16T09:00:00Z",
        "end": "2020-11-16T12:00:00Z"
    },

    // the time zone where the event is held, expressed in TZ database format, see https://en.wikipedia.org/wiki/List_of_tz_database_time_zones
    "timeZone": "Europe/Stockholm",

    // an array of event organisers, typically clubs
    "organisers": [
        {
            // if you application synchronizes club information with Livelox, use array elements with the id property set
            // to the club ID in your system (integer or string), otherwise go for the name property
            // if both id and name is present, id will take precedence
            "id": 7890,
            "name": "My club"
        }
    ],

    // the country where the event is held, expressed as an ISO 3166-1 alpha-3 code, see https://en.wikipedia.org/wiki/ISO_3166-1_alpha-3
    "country": "SWE",

    // the type of event; individual (default) or relay
    "type": "individual",

    // the event level; valid values are club, local, regional, national, international
    "level": "regional",
    
    // the classes that the event contains
    "classes": [
        {
            // your application's unique identifier for the class, if any; can be an integer or a string of up to 64 characters
            "id": 13579,

            // the name of the class
            "name": "W21",

            // the number of relay legs for the class, if the type of event is relay
            // omit for individual events
            "numberOfRelayLegs": 3
        },
        // ... more classes ...
    ],
    
    // the EPSG code of the projection used when expressing coordinates for maps and controls, see https://spatialreference.org/ref/epsg/
    "projectionEpsgCode": 3006,
    
    // the maps for the event, usually just one
    "maps": [
        {
            // the name of the file that contains the map image
            // the file should be posted in a separate request
            // PNG (preferred), TIFF (preferred), JPEG, GIF and KMZ formats are supported
            "fileName": "my-map.png",

            // the name of the map; used for informational purposes only
            "name": "My map",

            // the scale of the map, given as the inverse value (e.g. 15000 for 1:15000); used to determine course setting symbol sizes
            // if omitted, and the map origins from a KMZ file, Livelox will try to extract the map scale from the KMZ file
            "mapScale": 15000,

            // georeferencing information for the map, if it is known
            // either worldFileName or coordinateMapping should be set, but not both
            // omit if you provided a KMZ file for fileName
            "georeference": {
                // the name of the file that contains the world file information, see https://en.wikipedia.org/wiki/World_file
                // the file should be posted in a separate request
                // coordinates in the world file should be expressed in the coordinate system given by projectionEpsgCode (see above)
                "worldFileName": "my-map.pgw",

                // mapping from geographical positions (WGS84 latitude/longitude) to map image positions
                // (pixels having integer coordinates, where (0, 0) is the map image's upper left corner, positive x leftwards, positive y downwards)
                // the first item in the positions array corresponds to the first item in the imagePositions array,
                // the second item in the positions array corresponds to the second item in the imagePositions array,
                // and so on
                // at least two items should be present in each array, and the arrays should have the same number of items
                "coordinateMapping": {
                    // a list of WGS84 latitude/longitude coordinates
                    "positions": [{ "latitude": 59.752649, "longitude": 17.715182 }, { "latitude": 59.759546, "longitude": 17.697759 }],
                    // a list of map image pixel coordinates
                    "imagePositions": [{ "x": 0, "y": 1112 }, { "x": 1451, "y": 0 }]
                }
            },

            // the position in meters of the map's bottom left corner in the map's "paper" coordinate system, positive x leftwards, positive y upwards
            // only to be used if the georeference property has not been set, and course data or map image files do not provide map corner positions
            // (e.g. /CourseData/RaceCourseData/Map/MapPositionTopLeft and CourseData/RaceCourseData/Map/MapPositionBottomRight in IOF XML 3)
            // the user will have to complete the georeferencing procedure using satellite imagery in the Livelox user interface after the import
            // if using this property, also make sure to set the mapPosition property for each control
            "bottomLeftCornerPosition": {
                "x": 0.387,
                "y": 0.415
            },
            // the position in meters of the map's top right corner in the map's "paper" coordinate system, positive x leftwards, positive y upwards
            // only to be used if the georeference property has not been set, and course data or map image files do not provide map corner positions
            // (e.g. /CourseData/RaceCourseData/Map/MapPositionTopLeft and CourseData/RaceCourseData/Map/MapPositionBottomRight in IOF XML 3)
            // the user will have to complete the georeferencing procedure using satellite imagery in the Livelox user interface after the import
            // if using this property, also make sure to set the mapPosition property for each control
            "topRightCornerPosition": {
                "x": 0.684,
                "y": 0.625
            }
        }
    ],

    // if the course information is stored in separate files, an array with the names of these files, usually just one
    // the files should be posted in separate requests
    // IOF XML 2, IOF XML 3, OCAD, Purple Pen and Condes KMZ formats are supported
    // if used, you don't have to set the courses and controls arrays below
    "courseDataFileNames": ["course-data.xml"],

    // if you're using the courseDataFileNames array above, you may optionally use this array to provide a course images for each course
    // course images are the vectorized images for the course prints, i.e. the purple objects representing a course, any forbidden areas, etc.
    // the map itself should not be included
    // if no course images are provided, Livelox will use the control positions to draw the course print
    // files should be named according to either course names or, if IOF XML course data is used, course IDs, plus any file name extension
    "courseImageFileNames": ["course-1.svg", "course-2.svg"],

    // the controls for the event, including starts and finishes
    // do not use if courseDataFileNames is used
    "controls": [
        {
            // the unique code for the control (required)
            "code": "S",

            // one of "Start", "Control", "Finish" (required)
            "type": "Start",

            // at least one of projectedPosition, position, mapPosition - listed in order of preference - is required

            // the position of the control given in the reference system given by projectionEpsgCode, if any (see above)
            "projectedPosition": {
                "x": 652117,
                "y": 6627663
            },

            // the position of the control in WGS84 coordinates
            "position": {
                "latitude": 59.758863,
                "longitude": 17.708028
            },

            // the position in meters of the control in the map's "paper" coordinate system,
            // where (0, 0) is the map's lower left corner, positive x leftwards, positive y upwards
            "mapPosition": {
                "x": 0.4411,
                "y": 0.5456
            },

            // gaps made in the control circle to reveal map details of importance (only applicable when type is "control")
            // omit if no gaps are present
            "circleGaps": [
                {
                    // the angle where the circle gap begins, in radians
                    // 0 corresponds to the point on the control circle right of the control center in the map's coordinate system
                    // positive values are counterclockwise from that point
                    "startAngle": 1.238,

                    // the angle distance that the circle gap continues for counted from the start angle, in radians
                    // a positive value represents a counterclockwise movement
                    "distance": 0.273
                },
                // ...
            ],

            // the size of the symbol, expressed as if drawn in the terrain
            // for a start: the circumference of the start triangle (default: 90 meters in ISOM2017; 28 meters in ISSprOM2019)
            // for a control: the radius of the control circle (default: 37.5 meters in ISOM2017; 12 meters in ISSprOM2019)
            // for a finish: the radius of the outer finish circle (default: 45 meters in ISOM2017; 14 meters in ISSprOM2019)
            // omit to use the default symbol size
            "symbolSize": 90,

            // the line width in meters used to draw the symbol, expressed as if drawn in the terrain
            // (default: 5.25 meters in ISOM2017; 1.4 meters in ISSprOM2019)
            "symbolLineWidth": 5.25
        },
        // ... more controls ...
    ],

    // the courses for the event
    // do not use if courseDataFileNames is used
    "courses": [
        {
            // the name of the course (required)
            "name": "Course 1",

            // the control for the course, including start and finish (required)
            "controls": [
                {
                    // the control code (required)
                    "code": "S",

                    // the connection line segments from this control to the next control, used to provide gaps in the continuous line to reveal map details of importance
                    // only one of projectedConnectionLines, connectionLines, mapConnectionLines - listed in order of preference - has to be provided
                    // omit to use a continuous line from this control to the next control

                    // a number of line segments given in the reference system given by projectionEpsgCode, if any (see above)
                    "projectedConnectionLines": [
                        { "start": { "x": 652070.85, "y": 6627655.65 }, "end": { "x": 652023.9, "y": 6627648 }},
                        { "start": { "x": 651996.3, "y": 6627643.5 }, "end": { "x": 651949.35, "y": 6627635.85 }}
                        // ... more lines ...
                    ],

                    // a number of line segments given in WGS84 coordinates
                    "connectionLines": [
                        { "start": { "latitude": 59.758811, "longitude": 17.707193 }, "end": { "latitude": 59.75876, "longitude": 17.706353 } },
                        { "start": { "latitude": 59.75873, "longitude": 17.705859 }, "end": { "latitude": 59.758678, "longitude": 17.705019 } }
                        // ... more lines ...
                    ],

                    // a number of line segments given in the map's "paper" coordinate system,
                    // where (0, 0) is the map's lower left corner, positive x leftwards, positive y upwards
                    "mapConnectionLines": [
                        { "start": { "x": 0.43799, "y": 0.19211 }, "end": { "x": 0.43486, "y": 0.1916 } },
                        { "start": { "x": 0.43302, "y": 0.1913 }, "end": { "x": 0.42989, "y": 0.19079 } }
                        // ... more lines ...
                    ]
                },
                {
                    // the control code (required)
                    "code": "31",

                    // the center of the control number (only applicable when type is "control")
                    // only one of controlNumberProjectedCoordinate, controlNumberPosition, mapControlNumberPosition - listed in order of preference - has to be provided
                    // omit to use a default placement of the control number according to the incoming and outgoing connection line

                    // the position given in the reference system given by projectionEpsgCode, if any (see above)
                    "controlNumberProjectedPosition": {
                        "x": 651810.525,
                        "y": 6627702.975
                    },

                    // the position in WGS84 coordinates
                    "controlNumberPosition": {
                        "latitude": 59.759331,
                        "longitude": 17.702599
                    },

                    // the position in meters of the control in the map's "paper" coordinate system,
                    // where (0, 0) is the map's lower left corner, positive x leftwards, positive y upwards
                    "mapControlNumberPosition": {
                        "x": 0.420635,
                        "y": 0.195265
                    },

                    "projectedConnectionLines": [
                        // projectedConnectionLines, if any (see above)
                    ],

                    "connectionLines": [
                        // connectionLines, if any (see above)
                    ],

                    "mapConnectionLines": [
                        // mapConnectionLines, if any (see above)
                    ]
                },
                // ... more controls for the course ...
                {
                    // the control code (required)
                    "code": "F"
                }
            ],

            // the shortest runnable length of the course in meters
            "length": 4870,

            // the total climb of the course along the optimal route choice
            "climb": 135,

            // the line width in meters used for connection lines between controls, expressed as if drawn in the terrain (default: 5.25 meters in ISOM2017; 1.4 meters in ISSprOM2019)
            // omit to use the default line width
            "controlConnectionLineWidth": 5.25,

            // the vectorized images for the course prints, i.e. the purple objects representing the course, any forbidden areas, etc.
            // the map itself should not be included
            // usually zero or one course image
            // if no course image is provided, Livelox will use the control positions to draw the course print
            "courseImages": [
                {
                    // the name of the file that contains the course image
                    // the file should be posted in a separate request
                    // SVG (preferred), SVGZ (preferred) and PDF formats are supported
                    "fileName": "course-1.svg",

                    // the scale of the course image, given as the inverse value (e.g. 15000 for 1:15000)
                    // omit to use the scale of the first map
                    "mapScale": 15000
                }
            ]
        },
        // ...
    ],

    // the relations between courses and classes
    // usually one course per class, but multiple courses per class occur when forking is in use
    // do not use if courseDataFileNames is used, and the course data files contain ClassCourseAssignment elements
    // if no course class connection information is provided, one class per course will be created
    "courseClassConnections": [
        {
            // the name of the class (required)
            "className": "W21",
            // the name of the course (required)
            "courseName": "Course 1"
        },
        // ... more relations ...
    ],

    // the event in Livelox that has been created from the importable event, if there exists such an event
    // only returned from GET /importableEvents/{importableEventId}?includeImportedEvent=true
    // should not be used for other requests
    "importedEvent": {
        // Livelox event properties; note that these differ from the importable event object structure
    },
    
    // an endpoint that Livelox should send a POST request to when an event has changed in Livelox
    // see the Webhooks section
    "webhookUrl": "https://www.your-application.com/path/to/webhook-endpoint",

    // information about various URLs in the Livelox website
    // only returned from GET /importableEvents/{importableEventId}
    // should not be used for other requests
    "link": {
        // the unique identifier of the importable event, known as importableEventId in this documentation
        "id": "3e2ff8867e48422ab80862901c8cf77c",
        // URL to redirect the user to to complete the import of the importable event; only provided when the importable event hasn't been imported yet
        "liveloxImportEventUrl": "https://www.livelox.com/Admin/Events/ImportEvent?externalIdentifier=YourClientId%3a3e2ff8867e48422ab80862901c8cf77c",
        // URL for the page that lists all classes and their corresponding replay links for the event; only provided when the importable event has been imported
        "liveloxShowEventUrl": "https://www.livelox.com/Events/Show/12345",
        // URL for the page where the event can be administered; only provided when the importable event has been imported
        "liveloxEditEventUrl": "https://www.livelox.com/Admin/Events/Overview/12345"
    }
}

API endpoints for events that have been imported to Livelox

If your application is an event management application, it should keep track of which events that are connected (i.e. have been imported) to Livelox. To keep the information in sync, certain types of updates to Livelox-connected events in your application should be relayed to Livelox, for example updates to classes, start lists and result lists. Use the API endpoints described in this section for these kinds of updates.

For performance and decoupling reasons, the update mechanism is asynchronous. Your application posts update commands to Livelox, and these commands will be processed as update operations at a later time, usually within a few seconds. If a webhookUrl has been defined for the event, that URL will be called when the update operation has been completed. More information about webhooks is available in the Webhooks section.

Response payload example (for all endpoints described in this section)

{
    "operationId": "89907ae6581d4e16917fad4ec9aa3f3b"
}

The operationId property holds a reference to the update operation in Livelox.

PUT https://api.livelox.com/events/external/{importableEventId}

Instructs Livelox to update an event that have previously been imported into Livelox. The request payload should be an importable event object with Content-Type: application/json. Only event name and classes will be updated. Other updates must be carried out using the user interface of the Livelox web application.

Response

200 OK on success, 403 Forbidden on authorization failure, 404 Not Found on missing event.

DELETE https://api.livelox.com/events/external/{importableEventId}

Instructs Livelox to delete an event that have previously been imported into Livelox.

Response

200 OK on success, 403 Forbidden on authorization failure, 404 Not Found on missing event.

POST https://api.livelox.com/events/external/{importableEventId}/startLists

Instructs Livelox to import a start list for an event that have previously been imported into Livelox. The request payload should be an IOF XML 3 start list with Content-Type: application/xml.

Response

200 OK on success, 403 Forbidden on authorization failure, 404 Not Found on missing event.

POST https://api.livelox.com/events/external/{importableEventId}/resultLists

Instructs Livelox to import a result list for an event that have previously been imported into Livelox. The request payload should be an IOF XML 3 result list with Content-Type: application/xml.

Response

200 OK on success, 403 Forbidden on authorization failure, 404 Not Found on missing event.

Webhooks

To get notified when an importable event is imported into Livelox, or when it is subsequently updated or deleted, use the webhook mechanism that Livelox provides.

  • In the POST /importableEvents request, include a property webhookUrl in the importable event object.
  • When the event is created or changed, or the event couldn't be changed due to invalid data posted by your application, Livelox will send an HTTP POST request to that URL. The request payload contains information about the event and the action that caused the change.
  • The HMAC approach is used to authenticate the request. Pay attention to the following HTTP headers.

    • WebhookTimestamp: the UTC time when the webhook request was sent by Livelox, in ISO 8601 format.
    • WebhookSignature: a hex-encoded string containing the HMAC-SHA256 hash of the WebhookTimestamp concatenated with a colon (:) and the request payload. The webhook secret provided to your application should be used as the hash key. Your application must calculate this hash using the same algorihtm and compare it with WebhookSignature. If the hashes differ, the request must be considered invalid and be ignored.
    • WebhookAttemptCount: the number of times Livelox has tried to send this webhook request. If your application fails to respond to the webhook request with HTTP 2xx, Livelox will retry up to 10 times with increasing delay between the attempts.
    • WebhookMessageCreatedTimestamp: the UTC time when the webhook message was created, in ISO 8601 format. This time is usually a few seconds prior to the first sending.
  • Contact support@livelox.com to obtain a webhook secret for your application.

Webhook request headers example

Content-Type: application/json
WebhookTimestamp: 2022-08-24T14:06:37.464Z
WebhookSignature: 72217089a8dcadd7457ce2edf86ac79ba0ff8430ab339daf8d93da09ba6ac878
WebhookAttemptCount: 1
WebhookMessageCreatedTimestamp: 2022-08-24T14:06:32.013Z

Webhook request payload example

{
    // the action that happened; EventCreated, EventUpdated, EventDeleted, StartListImported, ResultListImported
    "action": "EventCreated",
    
    // your application's unique identifier for the event, given as the id property in the importable event object
    // this property is always a string, even if it was given as an integer previously
    "importableEventId": "123456",
    
    // the event ID in Livelox
    "liveloxEventId": 67890,
    
    // the URL to use to get the event information from the Livelox API
    "liveloxApiGetEventUrl": "https://api.livelox.com/events/67890?includeOrganisers=true&includeClasses=true&includeRelayLegs=true&includeRelayLegGroups=true&includeMaps=true&includeProperties=true",
    
    // URL for the page that lists all classes and their corresponding replay links for the event
    "liveloxShowEventUrl": "https://www.livelox.com/Events/Show/67890",

    // URL for the page where the event can be administered
    "liveloxEditEventUrl": "https://www.livelox.com/Admin/Events/Overview/67890",

    // whether the operation succeeded; only provided for operations triggered by your application
    "success": true,

    // a human-readable error message; only provided for operations triggered by your application that failed when Livelox processed them
    "errorMessage": "The result list is of invalid format.",
    
    // the operation ID; only provided for operations triggered by your application
    "operationId": "89907ae6581d4e16917fad4ec9aa3f3b"
}

Your application will typically use this information to set up and store a connection between your application's event and the event in Livelox.