Webhook requests can include an HMAC-SHA256 signature to allow receivers to verify that the request body was sent by Cuebly and was not modified in transit.
The signature is sent in the following HTTP header:
X-Signature-Sha256: <signature>
The value is a lowercase hexadecimal HMAC-SHA256 hash.
Each webhook Scenario Action has its own secret.
The secret can be specified in the webhook-action configuration. If you do not specify this secret, Cuebly will generate one for you automatically.
The secret must be kept private and should never be exposed publicly.
The signature is calculated over the exact raw HTTP request body.
signature = HMAC_SHA256(raw_request_body, webhook_secret)
Important: the signature is calculated over the exact bytes of the body. Any change in whitespace, JSON formatting, encoding, or line endings will result in a different signature.
The HMAC signature is calculated over the raw HTTP request body only. Custom HTTP headers configured for a webhook are not included in the signature and therefore are not protected against modification by intermediaries.
Do not place security-sensitive information, authentication data, or values that require integrity guarantees in custom headers. If such data must be protected, include it in the request body instead, as the body is covered by the HMAC signature.
Secret:
5d9d4b7f3f9f1f0d0d6bdf93f7c95a7e8c9a0d5c3f8a6b1e7d2f4c9b8a1e6d3
Request body:
{"test":false,"type":"SOS"}
Generated signature:
1b459ce8e78db160eeaa10aef40a29053f5ef5b9cf5c9f6f9def9d6de642558f
HTTP request:
POST /webhook HTTP/1.1
Content-Type: application/json
X-Signature-Sha256: 1b459ce8e78db160eeaa10aef40a29053f5ef5b9cf5c9f6f9def9d6de642558f
{"test":false,"type":"SOS"}
To validate the webhook:
X-Signature-Sha256 header.Use a timing-safe comparison when possible.
package main
import (
"crypto/hmac"
"crypto/sha256"
"encoding/hex"
"io"
"net/http"
)
func verifySignature(body []byte, secret string, receivedSignature string) bool {
h := hmac.New(sha256.New, []byte(secret))
h.Write(body)
expectedSignature := hex.EncodeToString(h.Sum(nil))
return hmac.Equal(
[]byte(expectedSignature),
[]byte(receivedSignature),
)
}
func webhookHandler(w http.ResponseWriter, r *http.Request) {
secret := "5d9d4b7f3f9f1f0d0d6bdf93f7c95a7e8c9a0d5c3f8a6b1e7d2f4c9b8a1e6d3"
body, err := io.ReadAll(r.Body)
if err != nil {
http.Error(w, "failed to read body", http.StatusBadRequest)
return
}
receivedSignature := r.Header.Get("X-Signature-Sha256")
if !verifySignature(body, secret, receivedSignature) {
http.Error(w, "invalid signature", http.StatusUnauthorized)
return
}
w.WriteHeader(http.StatusOK)
w.Write([]byte("webhook accepted"))
}
const crypto = require("crypto");
function verifySignature(body, secret, receivedSignature) {
const expectedSignature = crypto
.createHmac("sha256", secret)
.update(body)
.digest("hex");
if (expectedSignature.length !== receivedSignature.length) {
return false;
}
return crypto.timingSafeEqual(
Buffer.from(expectedSignature),
Buffer.from(receivedSignature)
);
}
Example Express handler:
const express = require("express");
const crypto = require("crypto");
const app = express();
app.use(express.raw({ type: "application/json" }));
app.post("/webhook", (req, res) => {
const secret =
"5d9d4b7f3f9f1f0d0d6bdf93f7c95a7e8c9a0d5c3f8a6b1e7d2f4c9b8a1e6d3";
const receivedSignature = req.header("X-Signature-Sha256");
const expectedSignature = crypto
.createHmac("sha256", secret)
.update(req.body)
.digest("hex");
if (
!receivedSignature ||
expectedSignature.length !== receivedSignature.length ||
!crypto.timingSafeEqual(
Buffer.from(expectedSignature),
Buffer.from(receivedSignature)
)
) {
return res.status(401).send("invalid signature");
}
res.status(200).send("webhook accepted");
});
import hmac
import hashlib
def verify_signature(body: bytes, secret: str, received_signature: str) -> bool:
expected_signature = hmac.new(
secret.encode("utf-8"),
body,
hashlib.sha256
).hexdigest()
return hmac.compare_digest(expected_signature, received_signature)
Example Flask handler:
from flask import Flask, request, abort
import hmac
import hashlib
app = Flask(__name__)
@app.post("/webhook")
def webhook():
secret = "5d9d4b7f3f9f1f0d0d6bdf93f7c95a7e8c9a0d5c3f8a6b1e7d2f4c9b8a1e6d3"
body = request.get_data()
received_signature = request.headers.get("X-Signature-Sha256", "")
expected_signature = hmac.new(
secret.encode("utf-8"),
body,
hashlib.sha256
).hexdigest()
if not hmac.compare_digest(expected_signature, received_signature):
abort(401)
return "webhook accepted", 200
Create a body file:
printf '{"test":false,"type":"SOS"}' > body.json
Calculate the signature:
openssl dgst -sha256 \
-hmac "5d9d4b7f3f9f1f0d0d6bdf93f7c95a7e8c9a0d5c3f8a6b1e7d2f4c9b8a1e6d3" \
-hex body.json
Output:
HMAC-SHA2-256(body.json)= 1b459ce8e78db160eeaa10aef40a29053f5ef5b9cf5c9f6f9def9d6de642558f
Send a test request:
SIGNATURE=$(openssl dgst -sha256 \
-hmac "5d9d4b7f3f9f1f0d0d6bdf93f7c95a7e8c9a0d5c3f8a6b1e7d2f4c9b8a1e6d3" \
-hex body.json | awk '{print $2}')
curl -X POST https://example.com/webhook \
-H "Content-Type: application/json" \
-H "X-Signature-Sha256: $SIGNATURE" \
--data-binary @body.json
Use --data-binary to preserve the body exactly as it is stored in the file.