> For the complete documentation index, see [llms.txt](https://docs.caf.io/llms.txt). Markdown versions of documentation pages are available by appending `.md` to page URLs; this page is available as [Markdown](https://docs.caf.io/caf-api/connect/webhook/signature.md).

# 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 Certta services.

## Message signatures

Since webhook URLs are exposed to the internet, your application needs a secure mechanism to verify that requests are genuinely from Certta. We implement a signature validation header for this purpose, which we call the message signature.

{% hint style="warning" %}
Despite being an internal validation for your integration, rejecting requests with invalid signatures is part of the webhook validation process at Certta. 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.
{% endhint %}

## 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

Certta 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

{% hint style="info" %}
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 `JSON`s, `{"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.
{% endhint %}

## Implementation examples

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

* Python: [hmac module](https://docs.python.org/3/library/hmac.html)
* Node.js: [crypto.Hmac class](https://nodejs.org/api/crypto.html#class-hmac)
* Ruby: [OpenSSL::HMAC](https://ruby-doc.org/stdlib-2.4.0/libdoc/openssl/rdoc/OpenSSL/HMAC.html)
* Go: [crypto/hmac package](https://pkg.go.dev/crypto/hmac)
* Java: [javax.crypto.Mac](https://docs.oracle.com/en/java/javase/11/docs/api/java.base/javax/crypto/Mac.html)

### 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

```json
{"id":"evt_123456789","source":"TRANSACTION","specversion":"1.0","type":"TRANSACTIONSTATUSUPDATEDEVENT","time":"2025-07-08T14:30:00Z","datacontenttype":"application/json","data":{"id":"tx_abc123","status":"APPROVED"}}
```

The `X-Caf-Signature` value is calculated from these exact bytes and your webhook secret.

#### With spaces, no line breaks

```json
{ "id":"evt_123456789", "source":"TRANSACTION", "specversion":"1.0", "type":"TRANSACTIONSTATUSUPDATEDEVENT", "time":"2025-07-08T14:30:00Z", "datacontenttype":"application/json", "data":{"id":"tx_abc123","status":"APPROVED"} }
```

Even though this JSON represents the same object, its signature differs because its raw bytes differ.

#### With spaces and line breaks

```json
{
    "id": "evt_123456789",
    "source": "TRANSACTION",
    "specversion": "1.0",
    "type": "TRANSACTIONSTATUSUPDATEDEVENT",
    "time": "2025-07-08T14:30:00Z",
    "datacontenttype": "application/json",
    "data": {
        "id": "tx_abc123",
        "status": "APPROVED"
    }
}
```

This formatted body also produces a different signature.

#### With properties in different order

```json
{"time":"2025-07-08T14:30:00Z","type":"TRANSACTIONSTATUSUPDATEDEVENT","source":"TRANSACTION","specversion":"1.0","datacontenttype":"application/json","data":{"status":"APPROVED","id":"tx_abc123"},"id":"evt_123456789"}
```

Changing property order also changes the signature.

### Code examples

#### Node.js

```javascript
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

```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

```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

{% hint style="danger" %}

1. **Always verify signatures** - Never trust webhook requests without verifying their signatures
2. **Process raw body bytes** - Use the raw body bytes for signature verification, not parsed JSON
3. **Use constant-time comparison** - To prevent timing attacks, use a constant-time string comparison function
4. **Keep your webhook secret secure** - Never expose your webhook secret in client-side code
5. **Implement idempotency** - Process each webhook event only once, even if received multiple times
   {% endhint %}

## 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.


---

# Agent Instructions
This documentation is published with GitBook. GitBook is the documentation platform designed so that both humans and AI agents can read, navigate, and reason over technical content effectively. Learn more at gitbook.com.

## Querying This Documentation
If you need additional information that is not directly available in this page, you can query the documentation dynamically by asking a question.

Perform an HTTP GET request on the current page URL with the `ask` query parameter, and the optional `goal` query parameter:

```
GET https://docs.caf.io/caf-api/connect/webhook/signature.md?ask=<question>&goal=<endgoal>
```

`ask` is the immediate question: it should be specific, self-contained, and written in natural language.
`goal` is optional and describes the broader end goal you are ultimately trying to accomplish on behalf of the user. GitBook uses it to tailor the answer towards what is most useful for that goal.

The response will contain a direct answer to the question and relevant excerpts and sources from the documentation.

Use this mechanism when the answer is not explicitly present in the current page, you need clarification or additional context, or you want to retrieve related documentation sections.
