Code Walkthrough

To follow this guide to set up your webhook, all you need is a computer with Node.js installed.

Create a new Node.js project

Run the following on the command line to create the needed files and dependencies:

mkdir Web1on1-webhooks // creates a directory
cd Web1on1-webhooks // open created directory
touch index.js // creates empty index.js file
npm init // creates package.json
npm install express body-parser crypto --save // install dependencies

Create a HTTP server

Add the following code to index.js and make your params.secret verification token and Web1on1 API key available as environment variables WEBHOOK_SECRET and API_KEY

Here's the code:

'use strict';

const express = require('express');
const bodyParser = require('body-parser');
const crypto = require('crypto');

const app = express();

app.listen(3000, () => console.log('[Web1on1] Webhook is listening'));

This code creates an HTTP server that listens for requests on port 3000. For this guide we are using Express.js, but you can do it using only Node.js or any other framework you love.

Return the challenge

Add the following code to index.js:

app.get('/webhook', (req, res) => {
    if (!req.query.type || req.query.type !== 'subscribe' || !req.query.challenge) {
        return res.status(403).end();
    }

    // return challenge
    return res.end(req.query.challenge);
});

This code is required to ensure your webhook is authentic and working: by calling this endpoint, we're checking that your response body is the same as the challenge sent as a query parameter.

Webhook endpoint

Add the following code to index.js

app.post('/webhook', (req, res) => {
    if (req.body.event === 'conversation.message' &&
        req.body.data.message.text === 'Ping'
    ) {
        return res.status(200).json({
            token: process.env.API_KEY,
            messages: [{ text: 'Pong' }]
        });
    }
    return res.status(200);
}

This code creates your webhook endpoint. This example replies to any message having text "Ping" by responding with a "Pong" text message in the conversation, and discards other messages.

Note that the endpoint returns a 200 OK response, which tells Web1on1 that the event has been received.

Secure the webhook

To validate incoming data, we can check if the signature hash in the X-Hub-Signature header matches the SHA1-HMAC signature of the raw request payload, signed by the webhook secret. If these tokens are identical, the request came from Web1on1.

Add the following code to index.js before the webhook endpoint block of step 4:

// Calculate the X-Hub-Signature header value.
function getSignature(buf) {
    const salt = process.env.WEBHOOK_SECRET;
    const hmac = crypto.createHmac('sha1', salt);
    hmac.update(buf, 'utf-8');
    return 'sha1=' + hmac.digest('hex');
}

// Verifies the SHA1 signature of the raw request payload
// before bodyParser parses it. Will abort parsing if
// signature is invalid, and pass a generic error to response
// Read more: https://github.com/expressjs/body-parser#verify
function verifyRequest(req, res, buf, encoding) {
    const expected = req.get('x-hub-signature');
    if (!expected) {
        throw new Error('Missing signature on incoming request');
    }
    const calculated = getSignature(buf);
    console.log("X-Hub-Signature:", expected, buf.toString('utf8'));
    if (expected !== calculated) {
        throw new Error('Invalid signature on incoming request');
    } else {
        console.log("Valid signature!");
    }
}

// Express error-handling middleware function.
// Read more: http://expressjs.com/en/guide/error-handling.html
function abortOnValidationError(err, req, res, next) {
    if (err) {
        console.log('** Invalid X-HUB signature!', err.toString());
        res.status(401).send({ error: 'Invalid signature.' });
    } else {
        next();
    }
}

// Load verify middleware just for post route on our receive webhook,
// and catch any errors it might throw to prevent the request from
// being parsed further.
app.post('/webhook', bodyParser.json({ verify: verifyRequest }));

// Add an error-handling Express middleware function to prevent
// returning sensitive information.
app.use(abortOnValidationError);

This makes sure any requests withouth a valid signature will be rejected.

Example Payloads

Currently, all webhooks configured for a conversation's organization and all webhooks of parent organizations are called for each message in the conversation.

message.create

Each webhook invocation of the message.create.* events will receive the full data.conversation the data.message object and a related data.links object as its payload data.

The data your webhook URL will receive on the message.create.contact.chat (a chat message in a contact conversation) will look somewhat like:

Request: POST https://your-webhook-url/path

{
    "id": "35e96d00-04b6-11e9-8b61-d9b53a664240",
    "event": "message.create.contact.chat",
    "organization": "59178cb1c63934459825dee3",
    "activity": {
        "@context": "https://www.w3.org/ns/activitystreams",
        "type": "Create",
        "actor": {
            "id": "https://api.web1on1.chat/v2/users/590bcf50458b716046347f36",
            "type": "Person"
        },
        "object": {
            "id": "https://api.web1on1.chat/v2/messages/5c1c31c8f8eb0973cded49d8",
            "type": "Object"
        },
        "published": "2018-12-21T00:20:24Z",
        "summary": "User Jenny Jones created a chat message"
    },
    "data": {
        "message": {
            "organization": "59178cb1c63934459825dee3",
            "orgPath": "563f8098396c50df77857b6d#59178cb1c63934459825dee3",
            "conversation": "5c1855788f6f790e0019aeab",
            "type": "chat",
            "isBackchannel": false,
            "role": "agent",
            "slug": "flaOGamcS",
            "url": "https://acme.shipper.chat/flaOGamcS",
            "contentType": "text/plain",
            "service": "5b2c38ae9f3c885eb7ad7b11",
            "services": [],
            "touchpoint": "web",
            "channels": [],
            "version": 1,
            "user": "590bcf50458b716046347f36",
            "text": "Your appointment is set for next Wednesday.",
            "items": [],
            "actions": [],
            "results": [],
            "meta": {
                "_slip": "lz9pf9"
            },
            "createdBy": "590bcf50458b716046347f36",
            "updatedAt": "2018-12-21T00:20:24.373Z",
            "createdAt": "2018-12-21T00:20:24.373Z",
            "path": "5c1c31c8f8eb0973cded49d8",
            "level": 1,
            "id": "5c1c31c8f8eb0973cded49d8"
        },
        "conversation": {
            "organization": "59178cb1c63934459825dee3",
            "orgPath": "563f8098396c50df77857b6d#59178cb1c63934459825dee3",
            "name": "Hinditank Cogbang",
            "type": "contact",
            "slug": "g2WkBOeu3J",
            "url": "https://acme.shipper.chat/c/g2WkBOeu3J",
            "status": "active",
            "category": "Used Car",
            "categoryIndex": 0,
            "contact": "5c1855788f6f790e0019aeac",
            "participants": [
                {
                    "updatedAt": "2018-12-20T16:44:39.615Z",
                    "createdAt": "2018-12-18T02:03:40.044Z",
                    "name": "Jenny Jones",
                    "avatar": "https://storage.googleapis.com/cs2-uploads/avatars/ca6e1762-988e-4cbc-becb-ad952b6df9a4.png",
                    "user": "590bcf50458b716046347f36",
                    "cursor": 23,
                    "unreadCount": 0,
                    "inbox": false,
                    "accepted": true,
                    "active": true,
                    "role": "agent"
                }
            ],
            "messages": [
                "5c1855798f6f790e0019aeae",
                "5c1855798f6f790e0019aeaf"
            ],
            "channels": [],
            "channelsOffline": [],
            "touchpoints": {
                "email": {
                    "selected": false,
                    "services": []
                },
                "web": {
                    "selected": true,
                    "services": [
                        "5b2c38ae9f3c885eb7ad7b11"
                    ]
                }
            },
            "forms": [
                {
                    "values": {
                        "day": "Wednesday"
                    },
                    "categoryIndex": 0,
                    "category": "Prospects",
                    "type": "topic",
                    "state": "active",
                    "form": "5b2bf9dd5da4952b63ea1a5b",
                    "name": "Appointment"
                }
            ],
            "results": [],
            "meta": {
                "_title": "Web1on1 | Smooch Widget",
                "_url": "https://api.web1on1.chat/smooch/5b2c38af8b1e5d00212b3149"
            },
            "updatedAt": "2018-12-21T00:20:24.593Z",
            "createdAt": "2018-12-18T02:03:36.992Z",
            "id": "5c1855788f6f790e0019aeab"
        },
        "links": {
            "organization": "https://api.web1on1.chat/v2/organizations/59178cb1c63934459825dee3",
            "conversation": "https://api.web1on1.chat/v2/conversations/5c1855788f6f790e0019aeab",
            "message": "https://api.web1on1.chat/v2/messages/5c1c31c8f8eb0973cded49d8",
            "contact": null,
            "user": "https://api.web1on1.chat/v2/users/590bcf50458b716046347f36"
        }
    },
    "timestamp": "2018-12-21T00:20:24.656Z"
}

user.login

A user.login event posts the corresponding data.user object in webhook payloads.

Request: POST https://your-webhook-url/path

{
    "id": "6bab65d0-04b4-11e9-8b61-d9b53a664240",
    "event": "user.login",
    "organization": "563f8098396c50df77857b6d",
    "activity": {
        "@context": "https://www.w3.org/ns/activitystreams",
        "type": "Join",
        "actor": {
            "id": "https://api.web1on1.chat/v2/users/590bcf50458b716046347f36",
            "type": "Person"
        },
        "object": {
            "id": "https://api.web1on1.chat/v2/users/590bcf50458b716046347f36",
            "type": "Object"
        },
        "published": "2018-12-21T00:07:35Z",
        "summary": "User Jenny Jones logged in"
    },
    "data": {
        "user": {
            "givenName": "Jenny",
            "familyName": "Jones",
            "displayName": "jennyjones",
            "email": "jenny@example.com",
            "phone": "+49 2048 9000125",
            "organizations": [
                {
                    "organization": "563f8098396c50df77857b6d",
                    "orgPath": "563f8098396c50df77857b6d",
                    "selected": true,
                    "role": "admin"
                }
            ],
            "settings": {
                "timezone": "Europe/Amsterdam",
                "timezoneSet": false,
                "localeSet": true,
                "locale": "nl"
            },
            "status": "active",
            "avatar": "https://storage.googleapis.com/cs2-uploads/avatars/ca6e1762-988e-4cbc-becb-ad952b6df9a4.png",
            "lastLogin": "2018-12-20T16:44:12.642Z",
            "updatedAt": "2018-12-20T16:44:12.680Z",
            "createdAt": "2018-06-21T19:17:48.795Z",
            "name": "Jenny Jones",
            "id": "590bcf50458b716046347f36"
        }
    },
    "timestamp": "2018-12-21T00:07:35.853Z"
}

user.error

A user.error event posts the corresponding data.user object in webhook payloads, as well as the data.error

Not all user errors are propagated as an event just yet.

Request: POST https://your-webhook-url/path

{
    "id": "1ff5c0b0-792d-11ea-88b9-511982ad081a",
    "event": "user.error",
    "organization": "563f8098396c50df77857b6d",
    "activity": {
        "@context": "https://www.w3.org/ns/activitystreams",
        "type": "Reject",
        "actor": {
            "id": "http://api.web1on1.chat/v2/users/5e6a55efa1906a6e192a72bb",
            "type": "Person"
        },
        "object": {
            "id": "http://api.web1on1.chat/v2/users/5e6a55efa1906a6e192a72bb",
            "type": "Object"
        },
        "published": "2020-04-08T08:08:50Z",
        "summary": "User Jenny Jones reject User Jenny Jones"
    },
    "data": {
        "user": { ...user object... },
        "error": {
            "type": "SpamComplaint",
            "name": "Spam complaint",
            "description": "The subscriber explicitly marked this message as spam.",
            "createdAt": "2020-04-08T08:08:48.9387616Z"
        }
    },
    "timestamp": "2020-04-08T00:08:50.491Z"
}

Similarly, other *.error events will feature the resource and an error object.

API Reference

The API Reference provides more details on the webhook data you'll receive.