Getting started
Webhooks allow you to build or set up internal applications which subscribe to
certain events from Guuru. When one of those events is triggered,
we will send a HTTP POST
payload to the webhook's configured URL.
Initial configuration
To set up a webhook, visit the Partner Portal Developer page under Settings. Each supported event (e.g. Chat Rated) is shown there and at a minimum you should define a URL under your control for each event you are interested in receiving.
When the event occurs, if you have the corresponding webhook set as active and
a non empty URL configured, we will send a HTTP POST
request to your URL with
a Content-Type: application/json
encoded body.
Events
The request contains a X-Guuru-Event
header which identifies the kind of event that
triggered the request.
Event | Description |
---|---|
chat-assigned | chat has been assigned to experts so that they accept it |
chat-opened | an expert is handling the chat |
chat-closed | chat has been closed and is considered resolved |
chat-reopened | expert or customer reopened the chat to handle a better resolution |
chat-transferred | guuru transferred the chat to a company representative |
chat-rated | customer has finished chating and rated the chat |
message-created | a new message has been sent to a chat |
Note that a chat-transferred
or chat-closed
event will eventually be followed by a
chat-rated
event as the customer can rate chats after their resolution.
Idempotency
Webhook endpoints might occasionally receive the same event more than once. We advise you to guard against duplicated event receipts by making your event processing idempotent.
One way of doing this is logging the events you’ve processed, and then not processing already-logged events. You can handle this by looking at the header Idempotency-Key
Payload structure
The request body is sent in JSON
and follows the structures described below (by event).
Fields are omitted when their value is not available.
{
"id": String,
"requestedExperts": [String],
"category": {
"id": String,
"title": String,
},
"user": {
"id": String,
"name": String,
"email": String,
"locale": String // ISO 3166 2 letter country code
},
"question": String,
"languageCode": String, // ISO 639-1 2 letter language code
"createdAt": Number, // in milliseconds since epoch
"referer": String,
"customMeta": { // any custom keys defined in the chatloader
"customKey": String
// ...
},
"assignedAt": Number, // in milliseconds since epoch
}
{
"chat": {
"id": String,
},
"id": String,
"user": {
"id": String,
},
"expert": {
"id": String,
},
"isUser": Boolean,
"text": String,
"type": String,
"hideForUser": Boolean,
"hideForExpert": Boolean,
"createdAt": Number, // in milliseconds since epoch
"attachment": {
"url": String,
"filename": String,
"mimetype": String,
},
}
{
"id": String,
"user": {
"id": String,
"name": String,
"email": String,
"locale": String // ISO 3166 2 letter country code
},
"expert": {
"id": String,
"name": String,
"email": String,
},
"category": {
"id": String,
"title": String,
},
"question": String,
"languageCode": String, // ISO 639-1 2 letter language code
"status": String,
"rating": Number, // 0, 0.5, or 1
"transcriptURL": String,
"createdAt": Number, // in milliseconds since epoch
"acceptedAt": Number, // in milliseconds since epoch
"closedAt": Number, // in milliseconds since epoch
"ratedAt": Number, // in milliseconds since epoch
"lastOpenedAt": Number, // in milliseconds since epoch
"referer": String,
"customMeta": { // any custom keys defined in the chatloader
"customKey": String
// ...
},
}
Securing your webhooks
Once your server is configured to receive payloads, it will listen for any request sent to the URL you configured. For security reasons, you probably want to limit requests to those coming from Guuru.
You need to set up a secret token (pre-shared key) in two places: Partner Portal and your server.
The pre-shared key shouldn't be used for anything else, i.e., it should not be a password or token that you use for another service. We recommend that you generate a strong random key using
openssl rand -hex 20
but any sequence will do.
Once configured, we will start sending a X-Guuru-Hmac-Sha256
header in our
HTTP POST
requests. This is a signature computed using hash-based message
authentication code (HMAC
) with SHA-256
from the payload and the secret
you provided.
To verify that the request was not tampered with (integrity) and was sent by us (authenticity) use the following steps:
Extract the signature from the header: The signature is sent as the value of a request header with the key
X-Guuru-Hmac-Sha256
. Note that your programming language or library may expose the header key as a lower case string, i.e.,x-guuru-hmac-sha256
.Determine the expected signature: Compute an
HMAC
with theSHA256
hash function. Use the secret you configured in the Partner Portal as the key, and use the request body as the message and convert to anhex
string representation.Compare signatures: Compare the signature in the header to the expected signature. If they match you can process the request, if they don't reject the request.
In the examples below, we could have used a simple comparison
==
to check if the computed signature matches the received signature and it would work, however, to protect against timing attacks we recommend using a constant-time string comparison, when available.
The following examples illustrate this.
const crypto = require('crypto');
const digest = crypto.createHmac('SHA256', SECRET)
.update(Buffer.from(req.body, 'utf8'))
.digest('hex');
if (crypto.timingSafeEqual(
Buffer.from(digest),
Buffer.from(req.headers['x-guuru-hmac-sha256']),
)) {
// ...
}
import hashlib
import hmac
hash = hmac.new(SECRET, request.data, hashlib.sha256)
digest = hash.hexdigest()
if hmac.compare_digest(digest, request.headers['X-Guuru-Hmac-Sha256']):
# ...
require 'openssl'
hash = OpenSSL::Digest.new('sha256')
digest = OpenSSL::HMAC.digest(hash, SECRET, req.data)
if digest == request.header['x-guuru-hmac-sha256'].join
# ...
import (
"crypto/hmac"
"crypto/sha256"
"encoding/hex"
)
hash := hmac.New(sha256.New, []byte("secr3t"))
hash.Write([]byte(body))
digest := hex.EncodeToString(hash.Sum(nil))
if hmac.Equal([]byte(digest), []byte(r.Header.Get("X-Guuru-Hmac-Sha256"))) {
// ...
}
public class Example {
public static String getSignature(String secret, String body) {
String digest;
try {
Mac hmac = Mac.getInstance("HmacSHA256");
SecretKeySpec secretKey = new SecretKeySpec(
secret.getBytes(),
"HmacSHA256"
);
hmac.init(secretKey);
// hex digest of the hmac signature
digest = String.format(
"%x",
new BigInteger(1, hmac.doFinal(body.getBytes()))
);
} catch (Exception e) {
throw new RuntimeException(e);
}
return digest;
}
// ...
public void example(/* ... */) {
String digest = getSignature("secr3t", body);
if (!MessageDigest.isEqual(
digest.getBytes(),
t.getRequestHeaders().getFirst("x-guuru-hmac-sha256").getBytes()
)) {
// ...
}
}
}
Making a test request
The command below sends an example payload corresponding to a Chat Rated event, that would be sent to the configured URL when the user rates a chat.
You can use it to test your implementation before configuring the webhook.
In the example below the
X-Guuru-Hmac-Sha256
is computed with the secretsecr3t
as an example. Never use this or any other weak secret in production.
curl -X POST -H 'Content-Type: application/json' \
-H 'X-Guuru-Event: chat-rated' \
-H 'X-Guuru-Hmac-Sha256: 661dc72784376f80296f93790146a60d6b703b0faca466ebfaaf783787a47114' \
-d '{
"user": {
"email": "john.doe@example.com",
"locale": "de",
"name": "John"
},
"question": "I did not receive last month bill, how can I pay?",
"status": "rated",
"language": "de",
"transcriptURL": "https://chat.guuru.com/apple/transcripts/-L98hdjh8Ehuhd",
"rating": 1,
"category": "general",
"createdAt": 1533795300000,
"acceptedAt": 1533795320000,
"closedAt": 1533795480000,
"ratedAt": 1533795480000,
"referer": "https://support.apple.com/en-us/HT204247"
}' <URL>
Retry window
If there is any problem delivering the request to your endpoint, e.g., network problems or your endpoint is down for maintenance, deploys or any other reason, it will not be lost as we will keep retrying.
For any given chat that we are unable to send, we will retry every 10 minutes (600 seconds) for up to 7 days. However, if for any given webhook there are more than 500 failed delivery attempts in any given hour that webhook will be disabled and will have to be re-enabled manually.
We use At-Least-Once
delivery semantics, meaning that in some circumstances we
may send the same event twice.
You can handle this by using the Idempotency-Key
header that is unique per
event.