Webhook Signature Verification
Every Talonic webhook delivery includes an HMAC-SHA256 signature in the X-Talonic-Signature header. Verify this signature to ensure the request came from Talonic.
How it works
Talonic computes an HMAC-SHA256 hash of the raw request body using your webhook secret. The signature is sent as sha256=<hex-digest> in the X-Talonic-Signature header. Compare this against your own computed hash to verify authenticity.
Full handler examples
Python (Flask)
import hmac
import hashlib
from flask import Flask, request, jsonify
app = Flask(__name__)
WEBHOOK_SECRET = "whsec_your_secret_key"
def verify_signature(payload: bytes, signature: str, secret: str) -> bool:
"""Verify HMAC-SHA256 signature using constant-time comparison."""
expected = "sha256=" + hmac.new(
secret.encode(), payload, hashlib.sha256
).hexdigest()
return hmac.compare_digest(expected, signature)
@app.route("/webhooks/talonic", methods=["POST"])
def handle_webhook():
# Use get_data() to access the raw body before any parsing
raw_body = request.get_data()
signature = request.headers.get("X-Talonic-Signature", "")
if not verify_signature(raw_body, signature, WEBHOOK_SECRET):
return jsonify({"error": "Invalid signature"}), 401
event = request.get_json()
event_type = event.get("event")
if event_type == "extraction.completed":
extraction_id = event["data"]["extraction_id"]
# Process the completed extraction...
print(f"Extraction {extraction_id} completed")
return jsonify({"received": True}), 200Node.js (Express)
import express from "express";
import crypto from "crypto";
const app = express();
const WEBHOOK_SECRET = "whsec_your_secret_key";
function verifySignature(payload, signature, secret) {
const expected =
"sha256=" +
crypto.createHmac("sha256", secret).update(payload).digest("hex");
return crypto.timingSafeEqual(
Buffer.from(expected),
Buffer.from(signature)
);
}
// Use express.raw() to preserve the raw body for signature verification
app.post("/webhooks/talonic", express.raw({ type: "application/json" }), (req, res) => {
const signature = req.headers["x-talonic-signature"] || "";
if (!verifySignature(req.body, signature, WEBHOOK_SECRET)) {
return res.status(401).json({ error: "Invalid signature" });
}
const event = JSON.parse(req.body.toString());
const eventType = event.event;
if (eventType === "extraction.completed") {
const extractionId = event.data.extraction_id;
// Process the completed extraction...
console.log(`Extraction ${extractionId} completed`);
}
res.status(200).json({ received: true });
});
app.listen(3000);Go
func verifySignature(payload []byte, signature, secret string) bool {
mac := hmac.New(sha256.New, []byte(secret))
mac.Write(payload)
expected := "sha256=" + hex.EncodeToString(mac.Sum(nil))
return hmac.Equal([]byte(expected), []byte(signature))
}Testing verification locally
Generate a test signature and send a request to your local webhook handler to verify the flow end-to-end.
# Generate a signature for a test payload
SECRET="whsec_your_secret_key"
PAYLOAD='{"event":"extraction.completed","data":{"extraction_id":"ext_test123"}}'
SIGNATURE="sha256=$(echo -n "$PAYLOAD" | openssl dgst -sha256 -hmac "$SECRET" | awk '{print $2}')"
# Send the signed request to your local handler
curl -X POST http://localhost:3000/webhooks/talonic \
-H "Content-Type: application/json" \
-H "X-Talonic-Signature: $SIGNATURE" \
-H "X-Talonic-Event: extraction.completed" \
-H "X-Talonic-Delivery-Id: dlv_test001" \
-d "$PAYLOAD"Troubleshooting
Encoding mismatch
The HMAC is computed over the raw bytes of the request body. If your framework decodes the body to a string and re-encodes it, whitespace or character encoding differences will produce a different hash. Always use the raw, unmodified body bytes for verification.
Body parsing middleware stripping raw body
Express express.json() and similar middleware parse the body into an object and discard the raw bytes. Use express.raw({ type: "application/json" }) on your webhook route to preserve the raw buffer. In Flask, use request.get_data() before calling request.get_json().
Clock skew
Talonic includes an X-Talonic-Timestamp header with each delivery. For additional security, reject deliveries where the timestamp is more than 5 minutes old. This prevents replay attacks using captured signatures.
Security best practices
Always verify signatures before processing webhook payloads. Use constant-time comparison functions (hmac.compare_digest in Python, crypto.timingSafeEqual in Node.js) to prevent timing attacks that could leak signature information through response time differences.
Store your webhook secret in environment variables or a secrets manager — never hard-code it in source code or commit it to version control. Rotate secrets periodically via the Talonic platform: create a new secret, update your handler, then revoke the old secret.
Reject requests with missing or malformed signatures immediately with a 401 response. Log failed verification attempts for security monitoring. Validate the X-Talonic-Timestamp header to reject stale deliveries and prevent replay attacks.
Ensure your webhook endpoint uses HTTPS in production. HTTP endpoints expose the signature header and payload to interception. See delivery format for the full header set, retry policy for redelivery behavior, events for the catalog, and authentication for API key security.