The World’s Marathons webhook API makes it possible for you as an event organizer to fully automate the data transfer from World’s Marathons to your own registration system. You can subscribe to webhook events which are sent for every successful order placed on the World’s Marathons platform. The order.success webhook is sent with a 24 hour delay from the completion of the order. Our webhooks follow the industry standard used by services such as Stripe, Slack etc.


World’s Marathons will sign the webhook events it sends to your endpoints. We do so by including a signature in each event’s WM-Signature header. This allows you to verify that the events were sent by World’s Marathons, not by a third party. Below is a description on how you verify the signature.

WM-Signature: t=1492774577,v1=5257a869e7ecebeda32affa62cdca3fa51cad7e77a0e56ff536d0ce8e108d8bd

Step 1: Extract the timestamp and signatures from the header

Split the header, using the , character as the separator, to get a list of elements. Then split each element, using the = character as the separator, to get a prefix and value pair.

The value for the prefix t corresponds to the timestamp, and v1 corresponds to the signature(s).

Step 2: Prepare the signed_payload string

You achieve this by concatenating:

  • The timestamp (as a string)
  • A dot (The character .)
  • And the actual JSON payload (i.e., the request’s body)

Step 3: Determine the expected signature

Compute an HMAC with the SHA256 hash function. Use the endpoint’s signing secret as the key, and use the signed_payload string as the message.

Step 4: Compare signatures

Compare the signature(s) in the header to the expected signature. If a signature matches, compute the difference between the current timestamp and the received timestamp, then decide if the difference is within your tolerance.

To protect against timing attacks, use a constant-time string comparison to compare the expected signature to each of the received signatures.

Example Code .NET

public async Task<IHttpActionResult> Webhook()
    var signature = System.Web.HttpContext.Current.Request.Headers["WM-Signature"];
    string jsonBody = await Request.Content.ReadAsStringAsync();

    if (IsSignedPayloadOk(signature, jsonBody))
        // order is coming from us.
        // do stuff

    return Ok();

private bool IsSignedPayloadOk(string signature, string jsonBody)
    var parts = signature.Split(',');

    var receivedTimeStamp = parts[0].Split('=')[1];
    var receivedSecureSignedPayload = parts[1].Split('=')[1];

    var signedPayload = $"{receivedTimeStamp}.{jsonBody}";
    var secureSignedPayload = "";

    using (HMACSHA256 hmac = new HMACSHA256(Encoding.UTF8.GetBytes("your_secret_goes_here")))
        var hash = hmac.ComputeHash(Encoding.UTF8.GetBytes(signedPayload));
        secureSignedPayload = BitConverter.ToString(hash)
                                .Replace("-", "").ToLower();

    return secureSignedPayload == receivedSecureSignedPayload;


To subscribe to the World’s Marathon order webhooks navigate into the Settings menu in the Race Office. From there you’ll be able to activate the Webhooks and add your endpoint in the text box. From here you’ll also be able to get your Secret.

Respond to Webhooks

To acknowledge receipt of an event, your endpoint must return a 2xx HTTP status code. We will retry the webhook 4 times if we get anything else than a 2xx response back from your server. If the event has not been successfully received you will always be able to find all the order details by logging into the Race Office. From here you can also resend the webhook manually.

Resend Webhooks for Successful Orders

As long as you have an active webhook endpoint configured you'll always be able to resend a successful orders webhook from the Order Details page.

Webhook Event Body


HTTP POST (SSL mandatory)

We will only accept endpoints using https as the information transferred can be sensitive.


The body will be in JSON format.

Click to view full specification here

Custom ID Mappings
You have the possibility to add custom Ids for products and information. You can use that to match to you current registration system. Please read more here.

Example Json Body

  "id": "762a379d-1dee-4637-a387-077d1a84d280",
  "created": 1558345762,
  "type": "order.successful",
  "data": {
    "event_id": "your-event-id",
    "edition_id": "20190020",
    "order_reference": "2019-00000001",
    "order_date": 1558345762,
    "amount": 130.00,
    "currency": "EUR",
    "coupon": {},
    "team_name": "The Avengers",
    "participants": [
        "id": "1",
        "first_name": "Steve",
        "last_name": "Rogers",
        "email": "[email protected]",
        "gender": "M",
        "nationality": "US",
        "birth_date": "1956-02-25",
        "club": "Cap",
        "tickets": [
            "product_id": "SK-20190020-1",
            "product_name": "Half Marathon",
            "product_type": "ticket",
            "vat": 20.0,
            "price": 90.0,
            "product_discount": null,
            "external_product_id": "custom-id-1"
        "add_ons": [
            "product_id": "SK-20190020-4",
            "product_name": "T-Shirt",
            "product_type": "add-on",
            "vat": 20.0,
            "price": 30.0,
            "product_discount": null,
            "external_product_id": "custom-id-2"
            "options" [
                "label": "Size",
                "value": "XL",
                "external_option_id": "option-id-1",
                "external_value_id": "value-id-1"
            "product_id": "SK-20190020-8",
            "product_name": "Medal Engraving",
            "product_type": "add-on",
            "vat": 20.0,
            "price": 10.0,
            "product_discount": null,
            "external_product_id": "custom-id-3"
            "options" [
                "label": "Name to be engraved",
                "value": "Captain America",
                "external_option_id": "option-id-2",
                "external_value_id": "value-id-2"
        "info": [
            "label": "Is this your first half marathon?",
            "value": "Yes",
            "external_option_id": "option-id-3",
            "external_value_id": "value-id-3"
        "team_leader": true,
        "address": {
          "address_line_1": "Rd 1",
          "address_line_2": "c/o B",
          "city": "City",
          "state": "State",
          "postal_code": "111 11",
          "country": "US"
        "phone": {
          "code": "+1",
          "phone": "555 01 01 01"
        "ice": {
          "name": "Bruce Banner"
          "code": "+1",
          "phone": "555 01 01 02"

Test the endpoint locally

Once you’ve added an endpoint to your server, start an instance locally and use a tool like ngrok to make your endpoint available for receiving events.

Start ngrok in a command prompt with the same port number that you have configured for your server (e.g., ./ngrok http 8000). You should see information about your tunnel session such as status, expiration, and version. Take note of the Forwarding addresses (e.g., -> localhost:8000) as this is required for the following step.

Did this answer your question?