Handling webhook deliveries

Prerequisites for your webhook endpoint

Only qualified or void calls are sent to the webhook.

Webhook data is sent as JSON in the POST request body.

As a consequence, your endpoint must:

  • be publicly accessible (Allo-Media cannot send requests to localhost or closed networks)
  • be able to receive POST HTTP requests (Allo-Media will not send GET, PUT, PATCH, etc.)
  • Optional, but strongly recommended: Have TLS enabled (with a valid SSL certificate issued to your domain)

Delivery Headers

HTTP POST payloads that are delivered to your webhook's configured URL endpoint will contain a special X-Uhlive-Signature header. Its value is the HMAC hex digest of the request body, and it is generated using the SHA-256 hash function and the secret passphrase (if provided).

More information about how to validate this header's value is available below.

Request example

POST /payload HTTP/2
User-agent: Uhlive-Webhook/1.2.0
X-uhlive-delivery: 1701699087.8115606
X-uhlive-hook-id: hook_id_1
X-Uhlive-Signature: sha256=d57c68ca6f92289e6987922ff26938930f6e66a2d161ef06abdf1859230aa23c
Content-Type: application/json

{
"unique_id": "12345",
"timestamp": "2023-12-04T15:18:29Z",
"transcript_json": {
"callData": [
{
"from": "out",
"words": [
{
"score": 1.0,
"start": 1.24,
"value": "oui",
"length":0.33
},
{
"score": 1.0,
"start": 1.57,
"value": "bonjour",
"length": 0.87
}
],
"content": "oui bonjour",
"datetime": 1.24,
"duration": 1.2
}
],
"callUniqueId": "",
"meta": {
"ivr_end": 456,
"obfuscated": "false_or_true",
"lang": "fr",
"asr_model": "the.asr.model."
},
},
"duration": "123",
"ivr_end": 456,
"status": "qualified",
"campaign": "campaign_id_1",
"audio_state": "AVAILABLE",
"metadata": {
"ClientCaptureDate": "2023-12-04 10:03:10",
"ClientID": "ClientID-123456789",
"AudioFileLocation": "20231204_110310_CLME.wav",
"UDF_num_01": "5",
"ANI": "33123456789",
"UDF_text_02": "81",
"Agent": "John Doe",
"UDF_text_01": "john.doe@example.com",
"Direction": "IN",
"UDF_text_03": "51010",
"UDF_text_04": "12345",
"Dept": "The Dept Name"
},
"tags": [{
"unique_id": "25712b82-d578-4744-ac43-902433fefeb8",
"display_name": "One of many tags",
"confidence": 0.8,
"type": "bool",
"value": true
}],
"visit": null,
"tracking_info": null,
"out": null,
"in": {
"label": null,
"data": "+33123456789",
"type": "PHONE"
}
}

Response time

Your server needs to respond to indicate that the delivery was successfully received. Your server must respond with a 2XX response within 10 seconds of receiving a webhook delivery, otherwise we will retry the request one time before terminating the connection and considering the delivery a failure.

Validating webhook deliveries

About validating webhook deliveries

Once your server is configured to receive payloads, it will listen for any delivery that's sent to the endpoint you configured. To ensure that your server only processes webhook deliveries that were sent by Allo-Media and to ensure that the delivery was not tampered with, you should validate the webhook signature before processing the delivery further. This will help you avoid spending server time to process deliveries that are not from Allo-Media and will help avoid man-in-the-middle attacks.

To do this, you need to:

  1. Provide a secret token for a webhook.
  2. Store the token securely on your server.
  3. Validate incoming webhook payloads against the token, to verify that they are coming from Allo-Media and were not tampered with.

Securely storing the secret token

After creating a secret token, you should store it in a secure location that your server can access. Never hardcode a token into an application or push a token to any online repository.

Validating webhook deliveries

Allo-Media will use your secret token to create a hash signature that's sent to you with each payload. The hash signature will appear in each delivery as the value of the X-Uhlive-Signature header.

In your code that handles webhook deliveries, you should calculate a hash using your secret token. Then compare the hash that Allo-Media sent with the expected hash that you calculated, and ensure that they match.

There are a few important things to keep in mind when validating webhook payloads:

  • Allo-Media uses an HMAC hex digest to compute the hash.
  • The hash signature always starts with sha256=.
  • The hash signature is generated using your webhook's secret token and the payload contents.
  • If your language and server implementation specifies a character encoding, ensure that you handle the payload as UTF-8.
  • Never use a plain == operator. Instead, consider using a method like secure_compare or crypto.timingSafeEqual, which performs a "constant time" string comparison to help mitigate certain timing attacks against regular equality operators, or regular loops in JIT-optimized languages.

Testing the webhook payload validation

You can use the following secret and payload values to verify that your implementation is correct:

  • secret: This is the secret
  • payload: {"value": "Hello World!"}

If your implementation is correct, the signatures that you generate should match the following signature values:

  • signature: 8c09b2e2cb0b61582960ce6dc79fbf7e912b7700c23e326ef5ec81d582867d95
  • X-Uhlive-Signature: sha256=8c09b2e2cb0b61582960ce6dc79fbf7e912b7700c23e326ef5ec81d582867d95

Examples

You can use your programming language of choice to implement HMAC verification in your code. Following are some examples showing how an implementation might look in various programming languages.

For example, you can define the following verify_signature function and call it when you receive a webhook payload:

def verify_signature(payload_body)
signature = 'sha256=' + OpenSSL::HMAC.hexdigest(OpenSSL::Digest.new('sha256'), ENV['SECRET_TOKEN'], payload_body)
return halt 500, "Signatures didn't match!" unless Rack::Utils.secure_compare(signature, request.env['HTTP_X_UHLIVE_SIGNATURE_256'])
end
import hashlib
import hmac
def verify_signature(payload_body, secret_token, signature_header):
"""Verify that the payload was sent from Allo-Media by validating SHA256.

Raise and return 403 if not authorized.

Args:
payload_body: original request body to verify (request.body())
secret_token: secret provided to Allo-Media (WEBHOOK_SECRET)
signature_header: header received from Allo-Media (X-Uhlive-Signature)
"""

if not signature_header:
raise HTTPException(status_code=403, detail="X-Uhlive-Signature header is missing!")
hash_object = hmac.new(secret_token.encode('utf-8'), msg=payload_body, digestmod=hashlib.sha256)
expected_signature = "sha256=" + hash_object.hexdigest()
if not hmac.compare_digest(expected_signature, signature_header):
raise HTTPException(status_code=403, detail="Request signatures didn't match!")
let encoder = new TextEncoder();

async function verifySignature(secret, header, payload) {
let parts = header.split("=");
let sigHex = parts[1];

let algorithm = { name: "HMAC", hash: { name: 'SHA-256' } };

let keyBytes = encoder.encode(secret);
let extractable = false;
let key = await crypto.subtle.importKey(
"raw",
keyBytes,
algorithm,
extractable,
[ "sign", "verify" ],
);

let sigBytes = hexToBytes(sigHex);
let dataBytes = encoder.encode(payload);
let equal = await crypto.subtle.verify(
algorithm.name,
key,
sigBytes,
dataBytes,
);

return equal;
}

function hexToBytes(hex) {
let len = hex.length / 2;
let bytes = new Uint8Array(len);

let index = 0;
for (let i = 0; i < hex.length; i += 2) {
let c = hex.slice(i, i + 2);
let b = parseInt(c, 16);
bytes[index] = b;
index += 1;
}

return bytes;
}