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.

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:

  1. Generate the HMAC of the received message using your secret (stored in a secure location)

  2. Compare it with the received signature using a secure comparison algorithm

Important security considerations

As fields can be added to any event at any time without being a breaking change, message validation should be done before the content is transformed into a language object. This means using the body's byte array "as-is" to generate the comparison signature, without any transformation.

This is also important because when dealing with JSONs, {"prop1": "value1", "prop2": "value2"} is equivalent to {"prop2": "value2", "prop1": "value1"} for parsers/encoders since property ordering is not part of a JSON definition, but the byte arrays formed by the two objects are different. Also, some characters may be encoded differently depending on the library or language being used.

Implementation examples

Many programming languages include secure HMAC implementations in their standard libraries:

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

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