X-Webhook-Signature header. This enables your server to confirm that the payload has not been altered and originates from a trusted source.
Create a Webhook endpoint
The webhook secret is unique to each organization-webhook endpoint pair. You get a webhook secret after creating a webhook endpoint.
Signature validation snippets
Webhook.php
/**
* @param array<string, string> $headers // Incoming HTTP headers
* @param string $rawBody // Raw JSON request body
* @param string $secret // Shared secret key
* @return bool // Whether the signature is valid
*/
function validate_webhook(array $headers, string $rawBody, string $secret): bool {
$receivedSignature = $headers['X-Webhook-Signature'] ?? '';
$timestamp = $headers['X-Webhook-Timestamp'] ?? '';
$body = json_decode($rawBody, true);
$webhookPayload = [
"webhook_id" => $body["webhook_id"],
"url" => $body["url"],
"event" => $body["event"],
"resource_id" => $body["resource_id"],
"payload" => $body["payload"],
"timestamp" => $timestamp
];
$expectedSignature = hash_hmac(
'sha256',
json_encode($webhookPayload, JSON_UNESCAPED_SLASHES | JSON_UNESCAPED_UNICODE),
$secret
);
return hash_equals($expectedSignature, $receivedSignature);
}
/**
@param {import("express").Request} req - Express request object
@param {string} secret - Shared secret key
@returns {boolean} Whether the webhook signature is valid
*/
const crypto = require("crypto");
function validateWebhook(req, secret) {
const signature = req.header("X-Webhook-Signature");
const timestamp = req.header("X-Webhook-Timestamp");
const body = req.body;
const payload = {
webhook_id: body.webhook_id,
url: body.url,
event: body.event,
resource_id: body.resource_id,
payload: body.payload,
timestamp: timestamp
};
const json = JSON.stringify(payload);
const expectedSignature = crypto
.createHmac("sha256", secret)
.update(json)
.digest("hex");
return crypto.timingSafeEqual(Buffer.from(signature), Buffer.from(expectedSignature));
}
import crypto from "crypto";
/**
* Webhook payload structure as expected from the sender
*/
interface WebhookPayload {
webhook_id: string;
url: string;
event: string;
resource_id: string;
payload: unknown;
timestamp: string;
}
/**
* Headers relevant to the webhook signature
*/
interface WebhookHeaders {
'x-webhook-signature'?: string;
'x-webhook-timestamp'?: string;
}
/**
* Validates a webhook signature using HMAC SHA256
*
* @param body - The full parsed JSON request body
* @param headers - HTTP headers (lowercased keys)
* @param secret - Shared secret for HMAC
* @returns - true if the signature is valid, false otherwise
*/
function validateWebhook(
body: any,
headers: WebhookHeaders,
secret: string
): boolean {
const signature = headers["x-webhook-signature"];
const timestamp = headers["x-webhook-timestamp"];
if (!signature || !timestamp) return false;
const payload: WebhookPayload = {
webhook_id: body.webhook_id,
url: body.url,
event: body.event,
resource_id: body.resource_id,
payload: body.payload,
timestamp: timestamp,
};
const json = JSON.stringify(payload);
const expectedSignature = crypto
.createHmac("sha256", secret)
.update(json)
.digest("hex");
return crypto.timingSafeEqual(
Buffer.from(signature, "utf-8"),
Buffer.from(expectedSignature, "utf-8")
);
}
import hmac
import hashlib
import json
from typing import Any
from flask import Request
def validate_webhook(request: Request, secret: str) -> bool:
"""
Validates the webhook signature using HMAC SHA256.
Args:
request (flask.Request): The incoming HTTP request.
secret (str): The shared secret.
Returns:
bool: True if the signature is valid, False otherwise.
"""
received_signature: str = request.headers.get("X-Webhook-Signature", "")
timestamp: str = request.headers.get("X-Webhook-Timestamp", "")
body: dict[str, Any] = request.get_json()
webhook_payload = {
"webhook_id": body["webhook_id"],
"url": body["url"],
"event": body["event"],
"resource_id": body["resource_id"],
"payload": body["payload"],
"timestamp": timestamp
}
json_data: str = json.dumps(webhook_payload, ensure_ascii=False, separators=(',', ':'))
expected_signature: str = hmac.new(
secret.encode(), json_data.encode(), hashlib.sha256
).hexdigest()
return hmac.compare_digest(expected_signature, received_signature)
import javax.crypto.Mac;
import javax.crypto.spec.SecretKeySpec;
import java.nio.charset.StandardCharsets;
import java.util.Map;
import com.fasterxml.jackson.databind.ObjectMapper;
public class WebhookValidator {
public static boolean validate(
Map<String, Object> body,
Map<String, String> headers,
String secret
) throws Exception {
String signature = headers.get("X-Webhook-Signature");
String timestamp = headers.get("X-Webhook-Timestamp");
Map<String, Object> payload = Map.of(
"webhook_id", body.get("webhook_id"),
"url", body.get("url"),
"event", body.get("event"),
"resource_id", body.get("resource_id"),
"payload", body.get("payload"),
"timestamp", timestamp
);
ObjectMapper mapper = new ObjectMapper();
String json = mapper.writeValueAsString(payload);
Mac hmac = Mac.getInstance("HmacSHA256");
hmac.init(new SecretKeySpec(secret.getBytes(StandardCharsets.UTF_8), "HmacSHA256"));
byte[] hash = hmac.doFinal(json.getBytes(StandardCharsets.UTF_8));
StringBuilder hex = new StringBuilder();
for (byte b : hash) {
hex.append(String.format("%02x", b));
}
return hex.toString().equals(signature);
}
}
package main
import (
"crypto/hmac"
"crypto/sha256"
"encoding/hex"
"encoding/json"
"net/http"
"io"
)
func validateWebhook(r *http.Request, secret string) bool {
signature := r.Header.Get("X-Webhook-Signature")
timestamp := r.Header.Get("X-Webhook-Timestamp")
var body map[string]interface{}
raw, _ := io.ReadAll(r.Body)
json.Unmarshal(raw, &body)
payload := map[string]interface{}{
"webhook_id": body["webhook_id"],
"url": body["url"],
"event": body["event"],
"resource_id": body["resource_id"],
"payload": body["payload"],
"timestamp": timestamp,
}
payloadBytes, _ := json.Marshal(payload)
mac := hmac.New(sha256.New, []byte(secret))
mac.Write(payloadBytes)
expected := hex.EncodeToString(mac.Sum(nil))
return hmac.Equal([]byte(expected), []byte(signature))
}
use std::collections::HashMap;
use hmac::{Hmac, Mac};
use sha2::Sha256;
use serde_json::json;
type HmacSha256 = Hmac<Sha256>;
pub fn validate_webhook(
body: &HashMap<String, serde_json::Value>,
headers: &HashMap<String, String>,
secret: &str,
) -> bool {
let signature = headers.get("x-webhook-signature").unwrap_or(&"".to_string());
let timestamp = headers.get("x-webhook-timestamp").unwrap_or(&"".to_string());
let payload = json!({
"webhook_id": body.get("webhook_id"),
"url": body.get("url"),
"event": body.get("event"),
"resource_id": body.get("resource_id"),
"payload": body.get("payload"),
"timestamp": timestamp
});
let payload_str = payload.to_string();
let mut mac = HmacSha256::new_from_slice(secret.as_bytes()).expect("HMAC init failed");
mac.update(payload_str.as_bytes());
let expected_signature = hex::encode(mac.finalize().into_bytes());
expected_signature == *signature
}

