PRUVAN SUPPORT CENTER

Follow

Webhooks

Pruvan has a Webhooks Add-On that enables users to send Pruvan Events to other systems.  In an "If This, than That" scenario we are providing the "If This" part (trigger).  To accept Pruvan's webhook requests in your system you will need to meet a few requirements.

Requirements

  • TLS 1.2 (https) - Your system must use TLS in order to use webhooks
    • Not needed for testing
  • JSON - Your system must accept Content-Type: application/json
  • Authorization - Only Basic Auth or Bearer Token can be used in the authorization header
    • Using Authorization is not required
  • Response - Your system should respond with an HTTP 200 on success or a valid HTTP Code on error.

Events

These are the events Pruvan currently supports:

Work Order Created

This is sent when a work order is created in the user's account.  It will contain all work order fields.

Work Order Updated

This is sent when ever a work order is updated.  It will contain the new work order fields.  It does not currently specify what has changed.

Item Published

This is sent whenever a photo, or other item such as a survey, has been published. Note that publishing is not the same as a photo merely being uploaded to Pruvan. This will contain metadata about the item, such as work order information.  There is also an option to send the file in the request.  Item Files will be sent in the JSON object in the file key as Base64 encoded binary data.  You will need to parse the Base64 string in your system to decode it in to a savable file.

Custom Payloads

Pruvan supports custom payloads through the use of a Handlebars-like system.  The outgoing request must still be valid JSON.  These are the methods for generating a custom payload

Field References

The handlebar references are based on our standard payload fields and are enclosed in double curly braces:

{{workorder.workOrderNumber}}
{{workorder.services[0].serviceName}}

Prevent HTML Encoding

Fields in custom payloads are normally HTML-encoded.  Some fields may contain HTML special characters that you want sent as-is, such as 'instructions'.  If you would like to prevent HTML encoding then use a triple curly brace:

{{{workorder.instructions}}}

With operator

The #with operator lets you change the namespace of the fields you reference in curly braces.  Instead of {{workorder.workOrderNumber}} you could use:

{{#with workorder}}
    {{workOrderNumber}}
{{/with}}

If operator

The #if operator only tests for truthy-ness, meaning you cannot do something like string comparison.  This is useful for sending JSON boolean values.  For example to print a true or false based on a fields truthfullness:

{{#if workorder.options.camera_icon_hidden}}true{{else}}false{{/if}}

Warning:  native boolean fields will not display in your JSON without the above #if statement, they will always be blank causing the JSON validation to fail.  Should you choose to wrap the boolean value in quotes then the string representations will be either "0" or "1".

Each Operator

Arrays can be looped over with the #each operator.  When inside of an #each statement, use {{this}} to reference the current indexes value.  {{@index}} will reference the current index number of an array and {{@key}} will reference the current key of an object.  {{@last}} and {{@first}} will be true if the current index is the last (or first) element of an array.  Caution: {{@last}} and {{@first}} will not work when looping on objects.  For an array of strings you would use:

{{#each workorder.history}}
    {{this}}
{{/each}}
    

For an array of objects the namespace is implied as the current object, such as services:

{{#each workorder.services}}
    {{serviceName}}
    {{quantity}}
    {{price}}
{{/each}}
    

Formatting JSON Output

It is important that after your template has been rendered with the data set that the output is valid JSON.  This will mean appropriate placement of quotation marks and commas.  The #if operator is useful inside of an #each statement for placing commas after all but the last item:

{{#each history}}
    "{{this}}"{{#if @last}}{{else}},{{/if}}
{{/each}}
    

A simplified sample custom template that outputs similar to our standard payload using all of the above operators:

    "event": "{{event}}",
    "workorder": {
        {{#with workorder}}
            "lastUpdateId":"{{lastUpdateId}}",
            "workOrderNumber":"{{workOrderNumber}}",
            "startDate":"{{startDate}}",
            "options": {
                {{#with options}}
                    "camera_icon_hidden": {{#if camera_icon_hidden}}true{{else}}false{{/if}},
                    "photo_size": "{{photo_size}}",
                    "evidenceTypes": [
                        {{#each evidenceTypes}}
                            "{{this}}"{{#if @last}}{{else}},{{/if}}
                        {{/each}}
                    ]
                {{/with}}
            },
            "assignedTo":"{{assignedTo}}",
            "history": [
                {{#each history}}
                    "{{this}}"{{#if @last}}{{else}},{{/if}}
                {{/each}}
            ],
            "completedDate":"{{completedDate}}",
            "services": [
                {{#each services}}
                    {
                        "lastUpdateId": "{{lastUpdateId}}",
                        "serviceName": "{{serviceName}}",
                        "survey": "{{survey}}",
                        "options": {
                            {{#with options}}
                                "evidenceTypes": [
                                    {{#each evidenceTypes}}
                                        "{{this}}"{{#if @last}}{{else}},{{/if}}
                                    {{/each}}
                                ],
                                "required": {{#if required}}true{{else}}false{{/if}}
                            {{/with}}
                        },
                        "quantity": "{{quantity}}",
                        "price": "{{price}}"
                    }{{#if @last}}{{else}},{{/if}}
                {{/each}}
            ]
        {{/with}}
    }
}

Testing

You can send test event calls to your system by creating a Webhook Add On in your Pruvan account and using the Test button. Instructions on setting up an Add On are in this article. Test calls are allowed against non-TLS URLs (http), though you will not be able to save such a URL.  Please be aware to not send production authentication credentials in the clear (over http).

Test calls will include a sample JSON object that contains all possible fields for the chosen event type, including a base64 encoded JPG when 'Include File' is selected on Item Publish.

NodeJS Example

// This is a test NodeJS script to spin up an HTTP server and print the response from Pruvan's Webhook request
// Use http://yourip:port/webhook/userUUID/eventType/event as your URL in the Add On setup

const http = require('http');
const URL = require('url');
const fs = require('fs');

const port = 8080;

function authenticated (user, authorization) {
    // validate user and authentication method

    var auth;
    if (authorization) {
        auth = authorization.split(' ');
        if (auth[0] == 'Basic') {
            auth[2] = Buffer.from(auth[1],'base64').toString('ascii');
        } else {
            auth[2] = auth[1];
        }
    }
    console.debug('Auth ' + auth[0] + ': ' + auth[2]);

    // This is just a placeholder
    if (user && auth[2]) {
        return true;
    } else {
        return false;
    }
};

function webhook (request, response) {
    console.log('/webhook was called');

    var pathname = URL.parse(request.url).pathname;
    var path = pathname.split('/'); // ['webhook','someUserTokenUUID','workorder','create']

    var payload;
    var temp = '';

    request.on('data', function (d) {
        temp +=d;
    });

    request.on('end', function () {
        var auth = request.headers.authorization;
        var user = path[2];

        if (authenticated(user, auth)) {
            var json = JSON.parse(temp);

            switch (path[3]) {
                case 'workorder':
                    process.stdout.write('workorder/');
                    switch (path[4]) {
                        case 'create':
                            // Do something with workorder created
                            process.stdout.write('create\n');
                            if (json.event == 'workorder.create') {
                                payload = {status: true, error: null};
                            } else {
                                payload = {status: false, error: 'Event is not workorder.create, it is ' + json.event};
                            }
                            break;
                        case 'update':
                                // Do something with workorder updated
                            process.stdout.write('update\n', true);
                            if (json.event == 'workorder.update') {
                                payload = {status: true, error: null};
                            } else {
                                payload = {status: false, error: 'Event is not workorder.update, it is ' + json.event};
                            }
                            break;
                        default:
                            console.error("No request handler found for " + path);
                            payload = {status: false, code: 404, error: "404 Not found"};
                    }
                break;
                case 'item':
                    process.stdout.write('item/');
                    switch (path[4]) {
                        case 'publish':
                                // Do something with item published
                            process.stdout.write('publish\n', true);
                            if (json.event == 'item.publish') {
                                payload = {status: true, error: null};
                            } else {
                                payload = {status: false, error: 'Event is not item.publish, it is ' + json.event};
                            }
                            if (json.file) {
                                // decode and save file if present
                                var file = Buffer.from(json.file, 'base64');
                                var filename = json.item.pictureId + '.' + json.item.fileExt;
                                fs.writeFile(__dirname + '/uploads/webhook/' + filename, file, function(err) {
                                    if(err) {
                                        console.error('webhook writeFile: ' + err);
                                    } else {
                                        console.info('webhook file saved to ' + filename);
                                    }
                                });
                            }
                            break;
                        default:
                            console.error("No request handler found for " + path);
                            payload = {status: false, code: 404, error: "404 Not found"};
                    }
                break;
                default:
                    console.error("No request handler found for " + path);
                    payload = {status: false, code: 404, error: "404 Not found"};
                break;
            }

            console.info('Method: ' + request.method);
            console.info('Headers: ' + JSON.stringify(request.headers));
            console.info('Body: ' + temp);

        } else {
            payload = {status: false, code: 403, error: 'Invalid credentials'};
        }

        if (payload.status) {
            response.writeHead(200);
        } else {
            var code = payload.code || 500;
            response.writeHead(code);
        }

        var error = payload.error || 'Success';
        response.write(error);
        response.end();
    });
};

http.createServer(function(request, response) {
    request.on('error', function (err) {
        console.error('request: ' + err);
        response.writeHead(500);
        response.end();
        return;
    });

    response.on('error', function (err) {
        console.error('response: ' + err);
        return;
    });

    webhook(request, response);
}).listen(port);


console.log("Server has started listening on port " + port);
    

 

Was this article helpful?
0 out of 0 found this helpful
Have more questions? Submit a request

Comments