Sending Outbound SMS messages

A computer program (bot) sends an SMS to either a new or an existing contact (of any organization), with a link to a Conversational Landing Page (CLP).

The CLP is a regular web page, with our web messenger included either as a web widget or fully embedded in the page (for example in a customer portal).

When the contact received the SMS and clicks on the link in the SMS, the browser will be opened and loads the CLP, which in turn will load the widget.

The widget then sees that it is a returning contact (via the URL parameters) and will load the contact conversation and continue talking.

When the contact replies, the agent will see the full contact info and history available.

Prerequisites

Implementation

Step 1 - Verify if the contact exists

Find the contact record in the contacts collection:

GET /v2/contacts?organization=ORGID&profile.telephone=PHONENUMBER

Example

GET /v2/contacts?profile.telephone=%2B31646280993&organization=5c7574c78ae07962b12e264c

Step 2 - Create the contact if it's new

This is explained here

You can create channel transfer links (SMS to Web) by sending an Action Button with the URL of your CLP.

The web widget bundle uses the 'id' and 'token' query parameters to link the contact, and our backend fills in these {authuser} and {authtoken} variables.

You can find a complete example here

You can use the meta.SenderID to make it a one-way notification message - instead of a phone number, the SenderID is displayed, and the contact can't reply, only click the link.

On clicking, he/she lands on the mobile-optimized CLP to start/resume the conversation. A bot or agent can accept the conversation on incoming consumer messages.

Example code

const Api = require('chipchat');
const debug = require('debug');
const packageJSON = require('../package.json');

const log = debug(`${packageJSON.name}(${packageJSON.version})`);
const bot = new Api({ token: process.env.TOKEN });

// fill in the clp url where the widget is running
const host = '';
// fill in the sender id you would like to use
const SenderID = '';
//fill in the first message (a welcome message before the link is send)
const welcomeMessage = 'hi there, it is time to make an appoinment again...';

// the country code to use
const countrycode = '+31';
// the locale of the contacts
const locale = 'en';

// in real life you would get these contacts from CS itself
const users = [
    { telephone: '+31612345678', email: 'barack.obama@usa.com', givenName: 'Barack', familyName: 'Obama', address: { 'Zip Code': '', 'City': '' } }
];

const sendInvitePerSMS = async (organization, ctx) => {
    //eslint-disable-next-line
    for await (const user of users) {
        log('inviting user via sms: %j', user);

        let phone = user.telephone;
        if (phone.toString().trim().length === 9) {
            phone = countrycode + phone;
        }

        const welcomeMess = {
            text: welcomeMessage,
            role: 'agent',
            delay: 1000,
            touchpoint: 'sms'
        };
        if (SenderID !== '') {
            welcomeMess.meta = { SenderID };
        }
        const messages = [
            {
                text: '/accept',
                type: 'command'
            },
            welcomeMess,
            {
                role: 'agent',
                text: 'Please click on the link to plan your meeting with us:',
                meta: { SenderID },
                touchpoint: 'sms',
                // delay is needed to give sms party time to create service and link to this account
                delay: 10000,
                actions: [{
                    text: 'link:',
                    type: 'link',
                    //make sure you pass all extra params as well
                    uri: `${host}${host.includes('?') ? '&' : '?'}`
                        + `id={authuser}&token={authtoken}&ts=${new Date().getTime()}`
                }]
            },
            {
                text: '/leave',
                type: 'command',
                delay: 11000
            }
        ];
        // eslint-disable-next-line
        await bot.contacts.list({
            'profile.telephone': phone,
            organization
        }).then(async (contacts) => {
            log('contactList: %j, %j', contacts);
            if (typeof contacts === 'object' && contacts.length > 0) {
                log('contact already exists, reinviting %j', contacts.length);
                log('sending invitation messages: %j', messages);
                await bot.conversation(contacts[0].conversation).then((conv) => {
                    messages.forEach(async (msg) => {
                        await conv.say(msg);
                    });
                });
            } else {
                log(`creating contact ${user.givenName} ${user.familyName} with phone ${phone} in org ${organization}`);
                const profile = {
                    ...user,
                    locale,
                    telephone: phone
                };
                const conv = {
                    organization,
                    type: 'contact',
                    //"tags": ["appoinmentreason", "spring tBot-3.1.1 batch-2"],
                    contact: { profile },
                    messages,
                    meta: {}
                };

                log('creating conversation', JSON.stringify(conv, null, 4));
                await bot.conversations.create(conv).then((conversation) => {
                    log('created!: %j', conversation || 'no conversation created');
                }).catch(async (e) => {
                    log('creation failed!: ', e);
                    await ctx.say({
                        text: `sending to ${profile.givenName} ${profile.familyName} (${phone}) failed: ${e && e.message}`,
                        isBackchannel: true
                    });
                });
            }
        });
        log('done sending to users');
    }
};

bot.on('error', (err) => {
    log('error occured in SMSBot', err);
});

// the bot will listen to >smsbot, so configure smsbot as a command in the bot configuration.
bot.on('message.create.system.command', { text: '>smsbot' }, async (m, c) => {
    const org = m.orgPath.split('#').slice(-1)[0];
    const conversationDetails = await bot.conversations.get(m.conversation, { select: 'participants' });
    const activeAdmin = conversationDetails.participants.find(p => p.active === true && p.role === 'admin');
    const organization = await bot.organizations.get(org, { select: 'commands,displayName' });
    const hasSMS = await bot.services.list({ organization: org, type: 'twilio' });
    if (activeAdmin && m.orgPath.includes(activeAdmin.organization)) {
        if (hasSMS && hasSMS.length > 0) {
            try {
                const confirmationMessage = {
                    text: `Weet je zeker dat je een SMS wilt sturen naar ${users.length} klant(en)?`,
                    isBackchannel: true,
                    actions: [
                        'Ja', 'Nee'
                    ].map(action => ({ type: 'reply', text: action, payload: action }))
                };
                c.ask(confirmationMessage, async (msgConfirmation, ctxConfirmation) => {
                    if (msgConfirmation.text === 'Ja') {
                        sendInvitePerSMS(users, org, ctxConfirmation).then(async () => {
                            await ctxConfirmation.say({
                                text: 'Verstuurd! Als je me weer nodig hebt start dan weer een gesprek met mij via >smsbot',
                                isBackchannel: true
                            });
                            await ctxConfirmation.say({ type: 'command', text: '/leave' });
                            /* this did not work, leave would not arrive at CS
                                    ctxConfirmation.say([
                                        { text: 'send!, bye!', isBackchannel: true },
                                        { type: 'command', text: '/leave', delay: 1000 }
                                    ]); */
                        }).catch(e => {
                            log('error sendInvitePerSMS-------->', e);
                            ctxConfirmation.say({ type: 'command', text: '/leave', delay: 2000 });
                        });
                    } else {
                        await ctxConfirmation.say({ text: 'ok, bye!', isBackchannel: true });
                        await ctxConfirmation.say({ type: 'command', text: '/leave', delay: 1000 });
                    }
                });
            } catch (e) {
                log('error', e);
                await c.say({ text: 'sorry, error occured. check smsbot logs. bye!', isBackchannel: true });
                await c.say({ type: 'command', text: '/leave', delay: 1000 });
            }
        } else {
            await c.say({ text: `${organization.displayName} does not have SMS integration active`, isBackchannel: true });
            await c.say({ type: 'command', text: '/leave', delay: 1000 });
        }
    } else {
        await c.say({ text: `Sorry ${activeAdmin.name}, it seems you do have enought rights`
            + `to send sms messages in behalve of organization ${organization.displayName}`,
        isBackchannel: true });
        await c.say({ type: 'command', text: '/leave', delay: 1000 });
    }
});

// Start Express.js webhook server to start listening
// when on localhost
if (process.env.NODE_ENV === 'development') {
    log('starting localhost bot');
    bot.start();
}
exports.cloudfunction = bot.router({ async: true });