Summary

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 events which are sent for every successful order on the World’s Marathons platform. Our webhooks follow the industry standard used by services such as Stripe, Slack etc.

Security

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

[HttpPost]
[Route("webhook")]
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;
}

Subscribe

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.

Webhook Event Body

Method

HTTP POST (SSL mandatory)

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

Body

The body will be in JSON format.

Click to view full specification here

Example Json Body

{
  "id": "762a379d-1dee-4637-a387-077d1a84d280",
  "created": 1558345762,
  "type": "order.successful",
  "data": {
    "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
          }
        ],
        "add_ons": [
          {
            "product_id": "SK-20190020-4",
            "product_name": "T-Shirt",
            "product_type": "add-on",
            "vat": 20.0,
            "price": 30.0,
            "product_discount": null,
            "options" [
              {
                "label": "Size",
                "value": "XL"
              }
            ]
          },
          {
            "product_id": "SK-20190020-8",
            "product_name": "Medal Engraving",
            "product_type": "add-on",
            "vat": 20.0,
            "price": 10.0,
            "product_discount": null,
            "options" [
              {
                "label": "Name to be engraved",
                "value": "Captain America"
              }
            ]
          }
        ],
        "info": [
          {
            "label": "Is this your first half marathon?",
            "value": "Yes"
          }
        ],
        "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., https://xxxxxxxx.ngrok.io -> localhost:8000) as this is required for the following step.

Did this answer your question?