Signature
Overview
This document provides guidance on webhook message signatures and how to validate the X-Caf-Signature
header that is included in all webhook requests made by Caf.io services.
Message signatures
Since webhook URLs are exposed to the internet, your application needs a secure mechanism to verify that requests are genuinely from Caf.io. We implement a signature validation header for this purpose, which we call the message signature.
Despite being an internal validation for your integration, rejecting requests with invalid signatures is part of the webhook validation process at Caf.io. We may randomly send events with invalid signatures to verify your integration continues to meet our validation criteria. In any case, this validation is in your interest to prevent fraud. We maintain audit trails of delivered events, delivery attempts, and discarded events.
Signature header
Each webhook request includes a X-Caf-Signature
header with the following format:
X-Caf-Signature: 5257a869e7ecebeda32affa62cdca3fa51cad7e77a0e56ff536d0ce8e108d8bd
The header contains the HMAC SHA-256 signature value generated from the raw request body using your client secret.
How to validate
Caf.io implements a keyed-hash message authentication code (HMAC) mechanism with SHA256 to generate a signature for each message sent, with the final encoding in hexadecimal.
This signature is generated using your application's client secret (the same one used to generate tokens) found in Trust, the platform that manages the configurations. The signature is sent through the X-Caf-Signature
header in each HTTP
request.
To validate this signature, your integration should:
Generate the HMAC of the received message using your secret (stored in a secure location)
Compare it with the received signature using a secure comparison algorithm
Important security considerations
Implementation examples
Many programming languages include secure HMAC implementations in their standard libraries:
Python: hmac module
Node.js: crypto.Hmac class
Ruby: OpenSSL::HMAC
Java: javax.crypto.Mac
Format variations
All these examples are valid for the same JSON but with different formatting and must be supported by your integration:
Without spaces or line breaks
{"id":"evt_123456789","source":"https://caf.io/transactions","specversion":"1.0","type":"transaction.updated","time":"2025-07-08T14:30:00Z","datacontenttype":"application/json","data":{"transactionId":"tx_abc123","status":"completed"}}
Signature: X-Caf-Signature: 6f9ed23a7b505a3b6907c5f6eb2ad1b056fbf35a643d365a9a072ed7aabca153
With spaces, no line breaks
{ "id":"evt_123456789", "source":"https://caf.io/transactions", "specversion":"1.0", "type":"transaction.updated", "time":"2025-07-08T14:30:00Z", "datacontenttype":"application/json", "data":{"transactionId":"tx_abc123", "status":"completed"} }
Signature: X-Caf-Signature: cf7e092c9148a48f5ee5f12b947f46b331eac6bf0745e1e1d0f3df722e219df3
With spaces and line breaks
{
"id": "evt_123456789",
"source": "https://caf.io/transactions",
"specversion": "1.0",
"type": "transaction.updated",
"time": "2025-07-08T14:30:00Z",
"datacontenttype": "application/json",
"data": {
"transactionId": "tx_abc123",
"status": "completed"
}
}
Signature: X-Caf-Signature: adf5446334f754e73588f3ae10b308306307f0c797f7f678912d740c6deddf6a
With properties in different order
{"time":"2025-07-08T14:30:00Z","type":"transaction.updated","source":"https://caf.io/transactions","specversion":"1.0","datacontenttype":"application/json","data":{"status":"completed","transactionId":"tx_abc123"},"id":"evt_123456789"}
Signature: X-Caf-Signature: e2d26f22f89932ff3d23a699031b22d6f30323501430dc08c3a971dd875e23b5
Code examples
Node.js
const crypto = require('crypto');
function verifyWebhookSignature(payload, sigHeader, secret) {
const signature = sigHeader;
const expectedSignature = crypto
.createHmac('sha256', secret)
.update(payload)
.digest('hex');
return crypto.timingSafeEqual(
Buffer.from(signature),
Buffer.from(expectedSignature)
);
}
Java
private String bytesToHexString(byte[] bytes) {
var sb = new StringBuilder();
for (var b : bytes) {
var hex = String.format("%02x", b);
sb.append(hex);
}
return sb.toString();
}
private boolean verifyHmacSHA256(String secret, String data, String expectedSignature) {
try {
var mac = Mac.getInstance("HmacSHA256");
var secretKeySpec = new SecretKeySpec(secret.getBytes(StandardCharsets.UTF_8), "HmacSHA256");
mac.init(secretKeySpec);
var hmacBytes = mac.doFinal(data.getBytes(StandardCharsets.UTF_8));
return bytesToHexString(hmacBytes).equals(expectedSignature);
} catch (NoSuchAlgorithmException | InvalidKeyException e) {
return false;
}
}
Go
http.HandleFunc("/webhook", func(w http.ResponseWriter, r *http.Request) {
body, err := io.ReadAll(r.Body)
if err != nil {
fmt.Printf("could not read body")
w.WriteHeader(400)
return
}
headerSignature := r.Header.Get("X-Caf-Signature")
signature, err := hex.DecodeString(headerSignature)
if err != nil {
fmt.Printf("invalid signature format")
w.WriteHeader(401)
return
}
hasher := hmac.New(sha256.New, []byte(SECRET))
hasher.Write(body)
expected := hasher.Sum(nil)
if !hmac.Equal(expected, signature) {
fmt.Printf("invalid signature")
w.WriteHeader(401)
return
}
// Message validated, process the webhook
})
Security best practices
Always verify signatures - Never trust webhook requests without verifying their signatures
Process raw body bytes - Use the raw body bytes for signature verification, not parsed JSON
Use constant-time comparison - To prevent timing attacks, use a constant-time string comparison function
Keep your webhook secret secure - Never expose your webhook secret in client-side code
Implement idempotency - Process each webhook event only once, even if received multiple times
Obtaining your webhook secret
You can find your webhook secret in Trust, the platform that manages configurations, under the webhook configuration settings. If you believe your secret has been compromised, you can generate a new one at any time.
Last updated