Verify Webhook Signatures

Use webhook signature verification for protection from a variety of attacks.

It’s important to ensure that a request truly came from the expected sender. To help secure incoming webhooks, Zai can sign all webhook events sent to your endpoints by including a signature in each Webhooks-signature header.

This should be a part of the standard process for setting up webhooks and this guide provides the details you’ll need to get started. Also, refer to our Webhooks guide if you need more information on using webhooks.

Signature replay attacks

To prevent an attacker from intercepting and re-transmitting valid data transmissions, Zai includes a timestamp in the Webhooks-signature header. The timestamp is also verified by the signature, so an attacker won’t be able to change the timestamp without invalidating the signature.

The timestamp and signature are generated each time Zai sends an event to your endpoint. If Zai retries an event, a new signature and timestamp is generated.

How to verify Zai is the sender

Before you can start verifying signatures, Zai will need to obtain your signature secret key information using the Create a Secret Key API. The value must be ASCII characters and 32 bytes in size, otherwise, we’ll let you know through validation checks if it isn’t.

Verification of signatures should be able to be performed in any language using the shared signature key. Example implementations are included at the bottom of this guide.

1. Extract the timestamp and signatures

Split the header, at the , character, to get both the t (timestamp) and v (signature) values. Then split each element, using the = character as the separator, to get a prefix and value pair.

2. Create the signed_payload string

The signed_payload string is created by linking:

  • The timestamp (as a string) from the previous step

  • The dot character .

  • The request body (this is JSON in a string format)

3. Generate the expected signature

Use the secret_key that you previously set using the webhook/secret_key API endpoint. Each encryption uses hash-based message authentication code (HMAC) with SHA256 and the value will be base64 encoded format (raw URL encoding). Use the signed_payload string from step 2 as the message.

4. Compare signatures

Compare the signature(s) in the header to the expected signature. If they match, compute the difference between the current timestamp and the received timestamp. Verify that the timestamp is within your allowed tolerance.

When comparing signatures, you should use a constant_time string comparison to protect against timing attacks. We recommend that you use HMAC's library functions to compare signatures.

Example implementations

package main
import (

func createSignatureToken(payload string, secretKey string, timeToInclude int64) (string, error) {
	hash := hmac.New(sha256.New, []byte(secretKey))
	signedPayload := strconv.Itoa(int(timeToInclude)) + "." + string(payload)
	_, err := io.WriteString(hash, signedPayload)
	if err != nil {
		return "", err
	return base64.RawURLEncoding.EncodeToString(hash.Sum(nil)), nil

func main() {
	s := "xPpcHHoAOM"
	t := time.Now().Unix()
	payload := "{\"event\": \"status_updated\"}"
	result, _ := createSignatureToken(payload, s, t)
require "openssl";
require "uri";
require "base64";
require 'cgi'

def createSignatureToken(payload, secret, time) 
  t = [ time, payload].join(".")
  digest ='sha256')
  hash = OpenSSL::HMAC.digest(digest, secret, t)
  result = Base64.urlsafe_encode64(hash, padding: false)
  return result

time = "1257894000"
payload = "{\"event\": \"status_updated\"}"
secret = "xPpcHHoAOM"
result = createSignatureToken(payload, secret, time)
puts result
var crypto = require('crypto');
const fs = require('fs');

secret = "xPpcHHoAOM"
time = "1257894000"
payload = "{\"event\": \"status_updated\"}"
const {createHash} = require('crypto');

createSignatureToken(payload, secret, time)

function createSignatureToken(payload, secret, time) {
    message = time + '.' + payload
    result = crypto.createHmac('sha256', secret).update(message).digest("base64url").toString('utf8');
      return result

import javax.crypto.Mac;
import javax.crypto.spec.SecretKeySpec;
import java.util.Base64;

public class ApiSecurityExample {
  public static void main(String[] args) {
    try {
    String secret = "xPpcHHoAOM";
    String time = "1257894000";
    String payload = "{\"event\": \"status_updated\"}";
    String message = time + "." + payload;

    Mac sha256_HMAC = Mac.getInstance("HmacSHA256");
    SecretKeySpec secret_key = new SecretKeySpec(secret.getBytes(), "HmacSHA256");

    byte[] hash = sha256_HMAC.doFinal(message.getBytes());
    String encoded = Base64.getUrlEncoder().withoutPadding().encodeToString(hash);
    catch (Exception e){
import hmac
import hashlib
import base64

payload = "{\"event\": \"status_updated\"}"
time = "1257894000"
secretKey = "xPpcHHoAOM"

secret_key_bytes = bytes(secretKey, 'utf-8')
signed_payload = time + '.' + payload
signed_payload_bytes = bytes(signed_payload, 'utf-8')
signature =, signed_payload_bytes, digestmod=hashlib.sha256).digest()
result = str(base64.urlsafe_b64encode(signature), 'utf-8').rstrip('=')
<!DOCTYPE html>

$payload = "{\"event\": \"status_updated\"}";
$time = "1257894000";
$secretKey = "xPpcHHoAOM";

$arr = array($time,'.',$payload);
$signed_payload = join('', $arr);
$hash = hash_hmac('sha256', $signed_payload, $secretKey, true);
$output = rtrim( strtr( base64_encode( $hash ), '+/', '-_'), '=');
echo $output;

using System;
using System.Security.Cryptography;

public class Program
	public static void Main()
		string secret = "xPpcHHoAOM";
		string payload = "{\"event\": \"status_updated\"}";
		string time = "1257894000";
		string signed_payload = time + "." + payload;
      	var encoding = new System.Text.UTF8Encoding();
      	byte[] keyByte = encoding.GetBytes(secret);
      	byte[] messageBytes = encoding.GetBytes(signed_payload);
      using (var hmacsha256 = new HMACSHA256(keyByte))
        byte[] hashmessage = hmacsha256.ComputeHash(messageBytes);
		string signature = Convert.ToBase64String(hashmessage).Replace("/", "-").Replace("+", "_").Replace("=", "");