Verify Webhooks

When receiving webhooks from us, you have the option of authenticating those webhooks to ensure it's us sending them.

Chata.ai signs webhooks by including encrypted headers. This allows you to verify that we've sent the webhooks, not a third party. Before you verify a webhook you'll need the webhook secret. See Webhooks to see how to obtain the webhook secret.

This is not mandatory, but we strongly suggest implementing verification for your own security.

Webhook Headers

AutoQL-Signature
The AutoQL-Signature header will contain a hashed token, which is a Base64-encoded HMAC SHA 256 hash of the webhook secret, the AutoQL-Timestamp, and the request body.

AutoQL-Timestamp
The AutoQL-Timestamp header will contain the timestamp of the request in Epoch Milliseconds. This timestamp will also be hashed in the AutoQL-Signature (see details below).

Method

When the integrator adds a Webhook destination URL, for either a sandbox or production environment, a unique Webhook Secret will be generated for that destination. This Webhook Secret should be stored in a secure location.

To verify the webhook request, the AutoQL-Signature header can be recreated and compared to the value sent in the request.

This is accomplished by concatenating the the AutoQL-Timestamp value with the raw request body (separated by a period) and hashing it with the webhook secret.

The final hash is then base64 encoded.

Examples

hash = hmacSHA256Hash(webhook_secret, "1613603664000" + "." + "request body")
expected_signature = base64encode(hash)
"""The following example uses the Flask request object"""

import base64
import datetime
import hmac

from flask import request
from hashlib import sha256

WEBHOOK_SECRET = 'WH_abcdefg'
WEBHOOK_TIME_WINDOW_MILLIS = 300000  # 5 minutes

autoql_signature = request.headers.get('AutoQL-Signature')
autoql_timestamp = request.headers.get('AutoQL-Timestamp')

joined_payload = '{}.{}'.format(autoql_timestamp, request.data.decode('utf-8'))
hmac_hash = hmac.new(WEBHOOK_SECRET.encode('utf-8'), joined_payload.encode('utf-8'), digestmod=sha256)
expected_signature = base64.b64encode(hmac_hash.digest()).decode()

if expected_signature != autoql_signature:
    raise Exception('Signatures do not match!')
    
now = int(datetime.datetime.utcnow().timestamp() * 1000.0)
if (now - int(autoql_timestamp)) > WEBHOOK_TIME_WINDOW_MILLIS:
    raise Exception('Request is too old!')
import javax.crypto.Mac;
import javax.crypto.spec.SecretKeySpec;
import java.nio.charset.StandardCharsets;
import java.time.Instant;
import java.util.Base64;

...

String WEBHOOK_SECRET = "WH_abcdefg";
long WEBHOOK_TIME_WINDOW_MILLIS = 300000L;  // 5 minutes


public void checkSignature(byte[] requestBody) {

  String autoqlSignature = request.getHeader("AutoQL-Signature");
  String autoqlTimestamp = request.getHeader("AutoQL-Timestamp");

  Mac sha256Hmac = Mac.getInstance("HmacSHA256");
  byte[] secretBytes = WEBHOOK_SECRET.getBytes(StandardCharsets.UTF_8);
  SecretKeySpec keySpec = new SecretKeySpec(secretBytes, "HmacSHA256");
  sha256Hmac.init(keySpec);

  String joinedPayload = String.format("%s.%s", autoqlTimestamp, requestBody.toString());
  byte[] macData = sha256Hmac.doFinal(joinedPayload.getBytes(StandardCharsets.UTF_8));
  
  String expectedSignature = Base64.getEncoder().encodeToString(macData);
  
  if (!expectedSignature.equals(autoqlSignature)) {
    throw new Exception("Signatures do not match!");
  }
  
  if ( (Instant.now().toEpochMilli() - Long.parseLong(autoqlTimestamp)) > WEBHOOK_TIME_WINDOW_MILLIS ) {
    throw new Exception("Request is too old!");
  }
}