Documentation of registered Webhook calls

The swagger documentation for the APIs neccessary to register can be found on Mapcat Mapmatch WebAPI Development, and it can also be tried out there. (API key required)

Webhook allows the client service to sign up for the vehicle tracking events. When an event occurs, the system sends a HTTP POST call to the URL given at registration. The call's content will be based on the schemas provided below. The registered Webhooks are application-level, therefore assigned to AppIds.

Basic operation, error handling

We expect for every Webhook request such a 200 OK HTTP response, whose body contains the request identifier of the outgoing Webhook request (see: Schemas - Response Schema), timeout is set to 25 seconds. If the response is not 200 OK or the body does not contain the appropriate identifier or the request times out, the system tries sending the events again with a new request identifier. If a request times out, the system waits 10 seconds before repeating it with a new request identifier. If that request times out as well, the waiting time is extended to 15 minutes. Webhook error handling is based on MatchingSesson (on a per vehicle basis), therefore an error scenario for a vehicle does not affect event notifications for other vehicles. The only exception to the rules above is the Ping request, where we always send exactly one request, and any kind of error is considered a failed request.

Webhook events are always sent FIFO, until an event is successfully accepted by the client, no further events are sent. The system guarantees at least one successful sending for every ticketing and jump event. In case of a system error, an event could be repeated. If an event type has multiple Webhooks registered, the system attempts sending requests until at least one sending is successful.

It is important to acknowledge possible doubled events, otherwise more recent events could congest because of the FIFO guarantee.

Operation without Webhook

The client service is expected to only send track data if there is an active Webhook registered and available. If there is no Webhook, we collect the events, but instead of waiting 10 seconds, the system checks for an active Webhook every minute. If this state persists, the system can block receiving further vehicle data until a correct Webhook is registered.

Limitations

  • A maximum of 10 Webhooks can be registed to an AppId

Events

  • ping, a test event sent at registration to check the url
  • jump, jump event
  • ticket, section ticket purchase event
  • alert, alert event

Http Headers

  • name: X-MapMatch-Webhook-Signature
  • content: sha256=[signature]

This header contains the contents of the HTTP POST request hashed with HMAC SHA256 using the private key. Verifying this hash can confirm that the request was sent by our system. This is optional, but it is important that the private key is not compromised.

We send further HTTP Headers, which were given as key-value pairs when the Webhook was registered.

Schemas

Response Schema

All Webhook requests must be acknowledged with this response, otherwise the system repeats the request. This is an example for the Ping request:

{
  "requestId": "4c266097-7db4-4980-bcc3-828f7c3469a6" // the "Id" field of the request body
}

Ping Schema

{
  "Id": "4c266097-7db4-4980-bcc3-828f7c3469a6",
  "AppId":"8b9ff992-869a-4713-8a98-f71fa0489bb7",
  "WebHookId":"ad62e09c-65bd-4713-ba3b-68b25bd25e75",
  "SessionId":"00000000-0000-0000-0000-000000000000",
  "Attempt": 1,
  "Notifications": [
    {
      "Type": "Ping",
      "SendTime": "2017-08-07T12:21:07.4698681+00:00"
    }
  ],
  "Properties": {
    "hookId": "ad62e09c-65bd-4713-ba3b-68b25bd25e75",
    "appId": "8b9ff992-869a-4713-8a98-f71fa0489bb7"
  }
}

Event Schema

{
  "Id": "b9588ae9-69cb-43cc-ab22-7de2ad72002c", // Unique identifier for every event
  "Attempt": 1, // Deprecated: The number of attempts in a sending cycle, but error handling was changed since then.
  "AppId":"8b9ff992-869a-4713-8a98-f71fa0489bb7",
  "WebHookId":"ad62e09c-65bd-4713-ba3b-68b25bd25e75",
  "SessionId":"8cb02405-f0df-4f3a-b983-0f862458ff32",
  "Notifications": [ // event list ordered chronologically
    {
      "Type": "Jump",
      "SendTime": "2017-08-07T12:21:07.4698681+00:00"
      // ...Full schema below
    },
    {
      "Type": "Ticket",
      "SendTime": "2017-08-07T12:21:07.4698681+00:00"
      // ...Full schema below
    },
  ],
  "Properties": {
    "hookId": "ad62e09c-65bd-4713-ba3b-68b25bd25e75",
    "appId": "8b9ff992-869a-4713-8a98-f71fa0489bb7"
  }
}

Properties.hookId and Properties.appId will be removed, as it was moved to Body.

Jump Notification Schema

{
  "From": { // Jump starting position
    "Longitude": 16.5850887298584,
    "Latitude": 46.50639724731445
  },
  "To": { // Jump arrival position
    "Longitude": 16.52459907531738,
    "Latitude": 46.51979064941406
  },
  "FromTime": 1488770954, // jump starting time in unix timestamp format
  "ToTime": 1488985198, // jump arrival time in unix timestamp format
  "LastEdid": "M70u10k380m", // EDID of last section ticket
  "LastEdidiDirection": "Forward", // direction of last section ticket
  "JumpType": 3, // 0 - Normal, 1 - Tracking contained too many EDID segments 2 - Border jump into Hungary, 3 - Border jump out from Hungary
  "Time": 1488770954, // Event time to order events, should be equal to FromTime
  "Type": "Jump", // Notification type
  "SendTime": "2017-08-10T13:20:33.9905933Z" // Internal system time of the notification's creation
}

Ticket Notification Schema

{
    "Edid": "M7u206k50m", // ticket EDID
    "EdidDirection": "Backward", // section ticket direction
    "Fact": 1, // 0 - map matched, 1 - normal ticket
    "Position": { // position of purchase, or, in case of map matched ticket, start position of the section
        "Latitude": 46.47738265991211,
        "Longitude": 16.97397041320801
    },
    "Price": 277.7799987792969, // Price in HUF
    "SendTime": "2017-08-10T13:21:09.1537981Z", // Internal system time of the notification's creation
    "Time": 1488990656, // Ticket timestamp
    "Type": "Ticket" // Notification type
}

Alert Notification Schema

{
    "Message": "Too many unprocessed track points (56)", // Information about the possible error
    "SendTime": "2017-08-10T13:21:09.1537981Z", // Internal system time of the notification's creation
    "Time": 1488990656, // Error detection time in unix timestamp format
    "Type": "Alert" // Notification type
}

Example code

Client

const https = require('https');
const fs = require('fs');
const path = require('path');

const appId = '' // Your application ID
const apiKey = '' // Your application key
const webhookServer = '' // Your web server address

if (appId == '' || apiKey == '' || webhookServer == '') {
    console.log('Required fields: appId, apiKey, webHookId');
    return;
}

const commonHeaders = {
    'Content-Type': 'application/json',
    'X-MapMatch-ApiKey': apiKey,
    'X-MapMatch-AppId': appId
}

const postRequest = {
    host: 'dev-mapmatch.mapcat.com',
    method: 'POST',
    headers: commonHeaders
}

let matchingSessionId = '';
let webHookId = '';
let equipmentId = '';

function generateEquipmentId() {
    let text = '';
    let possible = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789';
    for (let i = 0; i < 10; i++)
        text += possible.charAt(Math.floor(Math.random() * possible.length));
    return text;
}

function post(postData, path) {
    return new Promise(function (resolve, reject) {
        postRequest.path = path;
        let req = https.request(postRequest, function (res) {
            let buffer = '';
            res.on('data', function (data) {
                buffer = buffer + data;
            });
            res.on('end', function () {
                if (this.statusCode == 200 || this.statusCode == 201) {
                    resolve(buffer);
                } else {
                    reject(buffer);
                }
            });
        });
        req.on('error', function (e) {
            reject(e.message);
        });
        req.write(JSON.stringify(postData));
        req.end();
    })
}

function registerMatchingSession() {
    return new Promise(function (resolve, reject) {
        equipmentId = generateEquipmentId();
        let postData = {
            'equipmentId': equipmentId,
            'registrationTimeStamp': 1488927610,
            'baseVehicleCategory': 3,
            'euroCategory': 5
        }
        post(postData, '/api/MatchingSession').then(function (data) {
            matchingSessionId = JSON.parse(data).id;
            resolve(matchingSessionId);
        }).catch(function (err) {
            reject(err);
        });
    })
}

function registerWebhook() {
    return new Promise(function (resolve, reject) {
        let postData = {
            'url': webhookServer,
            'secret': 'SigningSecret',

            'events': [
                'Jump',
                'Ticket',
                'Alert'
            ],
            'active': true
        }
        post(postData, '/api/Webhook').then(function (data) {
            webHookId = JSON.parse(data).id;
            resolve(webHookId);
        }).catch(function (err) {
            reject(err);
        });
    })
}

function checkWebhook() {
    return new Promise(function (resolve, reject) {
        const getRequest = {
            host: postRequest.host,
            path: '/api/Webhook/',
            method: 'GET',
            headers: commonHeaders
        };
        let req = https.get(getRequest, function (res) {
            let buffer = '';
            res.on('data', function (data) {
                buffer = buffer + data;
            });
            res.on('end', function () {
                if (this.statusCode == 200 || this.statusCode == 201) {
                    let hooks = JSON.parse(buffer);
                    for (i in hooks) {
                        if (hooks[i].url == webhookServer) {
                            webHookId = hooks[i].id;
                            resolve(webHookId);
                        }
                    }
                    if (webHookId == '') {
                        registerWebhook().then(function (data) {
                            webHookId = data.id;
                            resolve(webHookId);
                        }).catch(function (err) {
                            reject(err);
                        });
                    }
                } else {
                    reject(buffer);
                }
            });
        });
        req.on('error', function (e) {
            reject(e.message);
        });
        req.end();
    })
}

function matchingTrack(matchingSessionId, track) {
    return new Promise(function (resolve, reject) {
        let rawdata = fs.readFileSync(track);
        let tracklog = JSON.parse(rawdata);
        let postData = {
            'trackings': [
                {
                    'equipmentId': equipmentId,
                    'matchingSessionId': matchingSessionId,
                    'path': tracklog
                }
            ]
        }
        post(postData, '/api/Track').then(function (data) {
            resolve(data);
        }).catch(function (err) {
            reject(err);
        });
    })
}

// Sample session
function runTestSession() {
    // check whether webhook exists
    checkWebhook().then(function () {
        fs.readdir('./tracks', function (err, files) {
            if (err) {
                console.error('Could not list the directory.', err);
                process.exit(1);
            }
            files.forEach(function (file, index) {
                let filepath = path.join(__dirname, 'tracks', file);
                // Register matching session
                registerMatchingSession().then(function (id) {
                    // Send track data
                    matchingTrack(id, filepath).then(function (data) {
                        console.log(data);
                    }).catch(function (err) {
                        console.log(err);
                    });
                }).catch(function (err) {
                    console.log(err);
                });
            })
        })
    }).catch(function (err) {
        console.log(err);
    });
}
runTestSession();

Webhook server


let http = require('http');
let port = -1;
if (port == -1) {
    console.log('Required field: port');
    return;
}
console.log('\033c')
console.log("Server port: " + port);
let server = http.createServer().listen(port);

server.on('request', function (req, res) {
  let body = ''

  req.on('data', function (data) {
    body += data;
  });

  req.on('end', function () {
    res.setHeader('Access-Control-Allow-Origin', '*');
    res.setHeader('Access-Control-Allow-Methods', 'POST');
    res.setHeader('Access-Control-Allow-Headers', 'X-Requested-With,content-type');
    res.setHeader('Access-Control-Allow-Credentials', true);

    if (!body) {
      res.setHeader('Content-Type', 'text/html');
      res.end("");
      return;
    }

    res.setHeader('Content-Type', 'application/json');
    let data = JSON.parse(body);
    if (data) {
      let id = { RequestId: data.Id };
      res.end(JSON.stringify(id));
      return;
    }
    res.end("Error");
  });
});