Working with webhooks

You can register an automation service that you have implemented as a webhook with the Automation Framework to receive events. Learn about event types sent, event payloads, and how to authenticate calls to your webhook using Basic Auth or HMAC

The Automation Framework allows you to register a webhook to receive events. We echo certain events to the endpoint registered for the externalId defined in the Registration payload.

👍

Tip

Be sure to read the Webhook Events section of Automation Framework best practices.

Processing events

The Automation Framework delivers all events to your bot at all times with the context to determine the current owner of the current author. This means that your bot will receive message events after the bot has handed a conversation to an agent. Your bot implementation is responsible for determining what to do with each event received.

For example, when a message event is delivered to you, it contains an owner property.

If the owner is BOT (with an appId), that bot is responsible for responding, handing the conversation to an agent, or whatever the appropriate action based on your defined workflows.

{
  "coordinate": { ... },
  "author": {...},
  "type": "message"
  "text": "Hello",
  "owner": {
    "type": "BOT",
    "appId": "myFirstBot"
  },
...
}

If the owner is AGENT (or a different bot), then your bot should know not to engage

{
  ...
  "owner": {
    "type": "AGENT"
  },
...
}

When a conversation is closed by, the owner is reset to BOT.

Event types

Event TypeEvents
Message EventsMessage from Author received
Control (or Ownership) EventsChange in ownership of a conversation
Conversation EventsCREATED - a conversation created
CLOSED - a conversation closed
AGENT_RESPONSE - an agent responded in a conversation

Event payloads

A Message from Author event is created upon receipt of the first message from an author to the bot.

Shortly after, when Care creates a conversation, the Automation Framework sends a Conversation Created event payload

{
  "coordinate": {
    "companyKey": "[COMPANY KEY]",
    "networkKey": "apple",
    "externalId": "117101b2-e66a-11e7-86ec-b1a405106b73",
    "messageId": "620fa7fb-7f39-482d-8089-df3b79afcdc8",
    "scope": "PRIVATE"
  },
  "author": {
    "id": "urn:mbid:AQAAY1tEzIV3HdVRnC8Y4Vejggvm/+WUwha4PWxibzIckHh4bILgMGfabpU13CNOvSNFSrVrCnI5KRzW3N40oY78ImdJNZ0nO8qB0UKC/84zvLyiJRGzp0CDA3YGay2BAfbb5aaKPFLK+d7aKebbjOHgMoejzGQ=",
    "fullName": "Anonymous Lion"
  },
  "type": "update",
  "operation": "CREATED",
  "conversation": {
    "displayId": 239939,
    "status": "ASSIGNED"
  }
}

When the conversation is closed, a Conversation Closed event is created and a payload is sent to the bot

{
  "coordinate": {
    "companyKey": "[COMPANY KEY]",
    "networkKey": "apple",
    "externalId": "117101b2-e66a-11e7-86ec-b1a405106b73",
    "botId": "apple-bot",
    "scope": "PRIVATE",
    "normalizedAuthorId": "urn:mbid:AQAAY1tEzIV3HdVRnC8Y4Vejggvm/+WUwha4PWxibzIckHh4bILgMGfabpU13CNOvSNFSrVrCnI5KRzW3N40oY78ImdJNZ0nO8qB0UKC/84zvLyiJRGzp0CDA3YGay2BAfbb5aaKPFLK+d7aKebbjOHgMoejzGQ="
  },
  "author": {
    "id": "urn:mbid:AQAAY1tEzIV3HdVRnC8Y4Vejggvm/+WUwha4PWxibzIckHh4bILgMGfabpU13CNOvSNFSrVrCnI5KRzW3N40oY78ImdJNZ0nO8qB0UKC/84zvLyiJRGzp0CDA3YGay2BAfbb5aaKPFLK+d7aKebbjOHgMoejzGQ=",
    "fullName": "Anonymous Lion"
  },
  "type": "update",
  "operation": "CLOSED",
  "conversation": {
    "displayId": 239939,
    "dispositionId": 5
  }
}

When an agent responds to a conversation, an Agent Response event is created and a payload is sent to the bot.

{
  "coordinate": {
	  "companyKey": "[COMPANY KEY]",
	  "networkKey": "smooch",
	  "externalId": "5ac4fbf000a58300217ac627",
	  "messageId": "5c5aeb123456789022a9c5fd",
	  "botId": "myFirstBot",
	  "scope": "PRIVATE"
    },
  "author": {
	  "id": "user1234",
	  "fullName": "Mr. Surname"
  },
  "operation": "AGENT_RESPONSE",
  "conversation": {
  	"displayId": 584322,
  	"dispositionId": 2
  },
  "agentResponse": {
    "publishDate": 1544632666002,
    "text": "well hello there"
  },
  "type": "update"
}

When the ownership of the conversation gets handed off from the bot to the agent following payload gets sent:
... describe payload as above ...

Webhook Event Delivery failures

When a webhook event sent to your bot is unable to be delivered or appears to fail, the Integration Framework automatically transfers control from BOT to AGENT. Any HTTP response code outside of the range [200..300) will be considered a failure, as will any timeouts or connection errors.

If a persistent pattern of failures is noted (more than 10 consecutive failures over the past 2 minutes), then the Integration Framework will temporarily suspend delivery attempts, and automatically transfer control from BOT to AGENT for any messages that arrive during the time deliveries are suspended.

Authenticating calls to your Webhook

The Automation Framework allows you to register a webhook to receive events. The webhook can authenticate with Basic Authentication or HMAC. Again, this is for the endpoint you expose to receive events, not to authenticate calls to the Bot API v3.

Basic Authentication (webhook callbacks)

If a bot is registered with BASIC_AUTH as the authentication method for the callback URL, the Automation Framework will make the callback to the bot using standard Basic Authentication. In basic HTTP authentication, a request contains a header field of the form Authorization: Basic <credentials>, where credentials is the base64 encoding of id and password joined by a colon. The ID and password used are the identity and secret values passed with the bot registration.

HMAC (webhook callbacks)

HMAC authentication (Hash-based Message Authentication Code) provides a secure method for both authorizing and authenticating HTTP requests. This document provides an overview of how Khoros constructs our HMAC signature and steps to perform validation.

To use HMAC authentication, you'll need to:

  • Understand relevant headers used with the request
  • Generate a fingerprint of the request received on your Bot server
  • Create and sign the fingerprint
    We provide the general steps to validate the HMAC signature in About Signature Validation. We provide a code example later in this guide.

HMAC headers

Before you begin, review this section to understand important headers that are included in the request from Khoros to your Bot server. You will use these to create your fingerprint and during validation. We'll be referencing these headers throughout this guide.

HeaderDescription
x-auth-apikeyServers can support multiple client keys. This value is used to look up the corresponding secret for a given key and should match the hmacKey value provided with your bot registration.
x-auth-timestampThe timestamp on the client (in epoch milliseconds) at which the request was generated.
x-auth-signature-v2The computed signature created by signing the fingerprint with the secret.

About signature validation

This section describes the basic tasks required to validate the HMAC signature. Later sections go into deeper detail. Refer to the headers descriptions provided earlier when performing the tasks described.

  1. Ensure that the x-auth-apikey header is valid. It should match the hmacKey you provided with your bot registration.
  2. If the API key is valid, construct a fingerprint. (See the next section to learn about the fingerprint format.)
  3. Sign the fingerprint using your secret (the secret value provided with the bot registration)
  4. Compare the computed signature with the x-auth-signature-v2 value
  5. If the signatures match, check the that the x-auth-timestamp value has not drifted too far. Ensuring that it is within a minute of the current time should be sufficient. Make sure that you check the absolute value difference between the current time and the x-auth-timestamp time to allow for some clock skew between servers
  6. If the time difference is acceptable, you have authenticated the request.

Constructing a fingerprint

Upon receiving a request on your Bot server, generate a fingerprint of the request. The fingerprint is a collection of fields provided by the request, separated by the pipe ( | ) character.

Fingerprint format

ts|httpMethod|hostPathQuery|requestBody|smmheaders

where each field is as follows:

  • ts: request time in epoch millis. This is the value of the x-auth-timestamp header
  • httpMethod: GET, POST, etc
  • hostPathQuery: a concatenation of the hostname, the path, and any query parameters. Hostname does not include any ports or scheme
    • Example: A request to this URL: https://gjesse.foo.bar:999/look/at/this?thing=here
    • would create this value: gjesse.foo.bar/look/at/this?thing=here
  • requestBody: The entire body of the request, read as a UTF8-encoded string
  • smmheaders: a concatenated list of any headers sent and prefixed with "x-smm-",
    • list is concatenated with a colon (:) between each value.
    • all header keys are lowercase
    • if any header values are present, the first character in the list is a colon
    • field is blank if no x-smm- headers are present
    • Fields are first combined as key-value pairs, then sorted alphabetically before final concatenation.
      • Example: If a request has multiple “x-smm-example” headers, x-smm-example:abc would appear before x-smm-example:bcd
    • At this time, we are not including x-smm- headers in outbound requests.
# sending a simple payload to a local server example
echo '{"coordinate":{"companyKey":"gjesse"}}' | curl http://gjesse.aws.lcloud.com:3000/botkit/receive?query=param \
    -d @- \
    -H "Content-type: application/json; charset=utf-8" \
    -H "x-auth-timestamp: `date +%s000`" \
    -H 'x-auth-signature-v2: asdfasdf' \
    -H 'x-auth-apikey: user' \
    -H 'x-smm-example: abc' \
    -H 'x-smm-example: def' \
    -H 'x-smm-otherexample: foo'
 
# would result in this fingerprint being constructed on the receiving server:
  
1540407343000|POST|gjesse.aws.lcloud.com/botkit/receive?query=param|{"coordinate":{"companyKey":"gjesse"}}|:x-smm-example:abc:x-smm-example:def:x-smm-otherexample:foo

HMAC example

This example creates and signs a fingerprint. The fingerprint is signed using the HMAC-SHA-256 standard, and then base64-encoded. See Github for the source code of the example.

// get the crypto-js lib for hashing
var crypto = require('crypto-js');

// get and check the validity of the api key
var apiKey = req.headers['x-auth-apikey'];
if (apiKey !== "<your expected api key>") {
  console.log("invalid api key provided")
  res.status(401).end();
  return;
}

// regex for removing the port section from the host header
var portTrim = /:\d+$/;
// get the host from headers without the port
var host = req.headers['host'].replace(portTrim, "")
// get the provided timestamp header
var ts = req.headers['x-auth-timestamp'];
// get the expected signature
var sig = req.headers['x-auth-signature-v2'];
// calculate header portion of the fingerprint
var headerFingerprint = getHeaderFingerprint(req.headers);

// construct the fingerprint
var fingerprint = [ts, req.method, host + req.url, req.rawBody, headerSig].join('|');

// hash our fingerprint with our secret
var hahs = crypto.HmacSHA256(fingerprint, secret);
// convert to a base64 encoded string
var calculatedSig = crypto.enc.Base64.stringify(hash);

// make sure our signatures match
if (sig !== calculatedSig) {
  console.log("provided sig: %s. calculated sig: %s", sig, calculatedSig);
  console.log("signatures do not match!")
  res.status(401).end();
  return;
}

// finally check the timestamp
var now = Date.now()
if (Math.abs(now - ts) > 60000) {
   console.log("time drift is too much - rejecting")
   res.status(401).end();
   return;
}

// extract relevant headers and construct a fingerprint header string
function getHeaderFingerprint(headers) {
  // holder array for matching headers
  var smmHeaders = [];
   // loop through all the provided headers
  for (var key in headers) {
      var element = headers[key];
      // filter in only x-smm-* keys
      if (key.startsWith("x-smm-")) {
          // your framework may represent multiple header values as a list - if so this will be a little different
          // loop over each element for the header
          element.split(",").forEach(part => {
              // push any found elements on to the array
              smmHeaders.push(":" + key + ":" + part.trim());
          })
      }
  }
  // sort all headers alphabetically
  smmHeaders.sort();
  // return a : separated string
  return smmHeaders.join("")
};