# Validate Deliveries

This guide explains how to validate Clearout **webhook deliveries using HMAC signature** verification. Validating webhook requests properly guarantees their authenticity and prevents your application from receiving malicious requests.

### Why Validate Webhooks&#x20;

Webhook validation is crucial for security and includes protection against:

* **Replay Attacks** - Timestamps prevent old requests from being replayed
* **Request Tampering** - HMAC signatures ensure data integrity
* **Unauthorized Requests** - Only requests with valid signatures are processed
* **Data Spoofing** - Verification confirms requests originate from Clearout

> **Important**
>
> Always validate webhook signatures in production. Never process webhook data without proper verification, as this could lead to security vulnerabilities and data corruption.

### Signature Format&#x20;

Clearout sends webhook signatures in the `x-co-webhook-signature` header with the following format:

```
t=,v1=
```

#### Signature Components <a href="#signature-components" id="signature-components"></a>

* **t** - Unix timestamp (UTC) when the webhook was created
* **v1** - HMAC-SHA256 signature of the payload

#### How Signatures Are Generated <a href="#signature-generation" id="signature-generation"></a>

Clearout generates signatures using the following process:

* Create a string by concatenating the timestamp and raw JSON payload: `timestamp.payload`
* Generate HMAC-SHA256 hash using your webhook secret token
* Encode the hash as a hexadecimal string
* Include both timestamp and signature in the header

### General Instructions for Any Language&#x20;

When implementing webhook verification in any programming language, follow these core steps:

{% stepper %}
{% step %}
**Parse Signature Header**

Extract the timestamp and signature from the `x-co-webhook-signature` header:

```
Header format: t=,v1=
Example: t=1691234567,v1=abc123def456...
```

{% endstep %}

{% step %}
**Construct Data String**

Create the data string for HMAC calculation using this exact format:

```
data = timestamp + "." + raw_webhook_body
```

> **Critical: Use Raw Body**
>
> Always use the raw request body as received, before any parsing or modifications:
>
> * **Before JSON parsing** - Use the raw string before converting to JSON objects
> * **Before any processing** - Don't modify, trim, or reformat the body
> * **Exact match required** - The body must match exactly what was sent by Clearout
> * **Preserve formatting** - Maintain original whitespace, indentation, and field ordering
>   {% endstep %}

{% step %}
**Calculate HMAC**

Generate HMAC SHA256 signature using your webhook secret:

```
expected_signature = HMAC_SHA256(data_string, webhook_secret)
```

{% endstep %}

{% step %}
**Verify Signature**

Compare signatures using constant-time comparison to prevent timing attacks:

```
if (constant_time_compare(expected_signature, received_signature)) {
    // Signature is valid
}
```

{% endstep %}

{% step %}
**Validate Timestamp (Optional)**

Check that the timestamp is recent to prevent replay attacks (optional but recommended):

```
current_time = get_current_unix_timestamp()
age = current_time - webhook_timestamp

if (age > 120) { // >2 minutes old (recommended)
    // Reject webhook
}
```

> **Recommended Time Window**
>
> We recommend validating timestamps within a 2-minute window, but you can choose any grace period between 2-5 minutes based on your security requirements and system performance needs. This validation is optional but helps prevent replay attacks.
> {% endstep %}

{% step %}
**Return Response**

Always return HTTP 200 status for successful webhook processing:

```
HTTP/1.1 200 OK
Content-Type: application/json

{"status": "success"}
```

> **Important: HTTP Status Codes**
>
> Your webhook endpoint must return HTTP 200 for successful processing. Any other status code will cause Clearout to retry the webhook delivery according to the retry schedule.
> {% endstep %}
> {% endstepper %}

### **JavaScript Example**&#x20;

{% code expandable="true" %}

```javascript
import express, { Request, Response } from "express";
import crypto from "crypto";

const app = express();
const port = 3000;

// Parse JSON body
app.use(express.json());

// Webhook signature verification function
function verifySignature(secret, body, signatureHeader) {
  try {
    const [tPart, v1Part] = signatureHeader.split(",");

    if (!tPart || !v1Part) {
      console.log("Invalid signature header format");
      return false;
    }

    const timestamp = tPart.split("=")[1];
    const receivedSig = v1Part.split("=")[1];

    if (!timestamp || !receivedSig) {
      console.log("Missing timestamp or signature in header");
      return false;
    }

    const data = `${timestamp}.${JSON.stringify(body)}`;
    const expectedSig = crypto
      .createHmac("sha256", secret)
      .update(data, "utf8")
      .digest("hex");

    if (expectedSig !== receivedSig) {
      console.log("Signature verification failed");
      return false;
    }

    console.log("Signature verified");
    return true;
  } catch (error) {
    console.log("Error verifying signature:", error);
    return false;
  }
}

// Timestamp validation function
function verifyTimestamp(signatureHeader, maxAgeSeconds = 120) {
  try {
    const [tPart] = signatureHeader.split(",");

    if (!tPart) {
      console.log("Invalid signature header format");
      return false;
    }

    const timestamp = tPart.split("=")[1];

    if (!timestamp) {
      console.log("Missing timestamp in header");
      return false;
    }

    const age = Math.floor(Date.now() / 1000) - parseInt(timestamp, 10);

    if (age <  0) {
      console.log("Timestamp is in the future");
      return false;
    }

    if (age > maxAgeSeconds) {
      console.log(`Timestamp too old: ${age} seconds (max: ${maxAgeSeconds})`);
      return false;
    }

    console.log("Timestamp verified");
    return true;
  } catch (error) {
    console.log("Error verifying timestamp:", error);
    return false;
  }
}

// Webhook endpoint
app.post("/webhook", (req, res) => {
  const secret = process.env.WEBHOOK_SECRET || "your-webhook-secret";

  // Extract signature header safely
  const signatureHeader = req.headers["x-co-webhook-signature"];

  if (typeof signatureHeader !== "string") {
    return res
      .status(400)
      .json({ status: "error", message: "Missing signature header" });
  }

  // Verify signature and timestamp
  const isValidSignature = verifySignature(secret, req.body, signatureHeader);
  const isValidTimestamp = verifyTimestamp(signatureHeader, 120); // 2 minutes

  if (!isValidSignature) {
    return res
      .status(401)
      .json({ status: "error", message: "Invalid signature" });
  }

  if (!isValidTimestamp) {
    return res
      .status(401)
      .json({ status: "error", message: "Timestamp too old" });
  }

  // Process webhook data here
  console.log("Webhook processed successfully:", req.body);

  // Return 200 OK to confirm successful processing
  res.json({
    status: "OK",
    message: "Webhook received and processed",
    signatureValid: isValidSignature,
    timestampValid: isValidTimestamp,
  });
});

app.listen(port, () => {
  console.log(`Webhook server listening on port ${port}`);
});
```

{% endcode %}

> **Key Features**
>
> * **Signature Verification** - Validates HMAC-SHA256 signature
> * **Timestamp Validation** - Prevents replay attacks (2-minute window)
> * **Error Handling** - Proper error responses for different failure scenarios
> * **Security** - Uses environment variables for secrets
> * **Logging** - Detailed console output for debugging

### Python Example&#x20;

{% code expandable="true" %}

```python
from flask import Flask, request, jsonify
import hmac
import hashlib
import time
import os

app = Flask(__name__)

def verify_signature(secret, body, signature_header):
    """
    Verify HMAC SHA256 signature using raw body (exact string sent in request).
    Header format: "t=,v1="
    """
    try:
        t_part, v1_part = signature_header.split(",")
        timestamp = t_part.split("=")[1]
        received_sig = v1_part.split("=")[1]

        # Must use raw request body (string), not re-dumped JSON
        data = f"{timestamp}.{body}"
        expected_sig = hmac.new(
            secret.encode("utf-8"),
            data.encode("utf-8"),
            hashlib.sha256,
        ).hexdigest()

        print("body:", body)
        print("secret:", secret)
        print("timestamp:", timestamp)
        print("receivedSig:", received_sig)
        print("expectedSig:", expected_sig)

        # Verify signature match
        if not hmac.compare_digest(expected_sig, received_sig):
            return False
        print("Signature verified")

        return True
    except Exception as e:
        print("Error in verify_signature:", e)
        return False

def verify_timestamp(signature_header):
    """
    Verify timestamp freshness (reject older than 2 minutes).
    """
    try:
        t_part, _ = signature_header.split(",")
        timestamp = int(t_part.split("=")[1])
        print("timestamp:", timestamp)

        age = int(time.time()) - timestamp
        print("age:", age)
        if age > 120:  # >2 minutes old (recommended)
            return False
        print("Timestamp verified")

        return True
    except Exception as e:
        print("Error in verify_timestamp:", e)
        return False

@app.route("/")
def hello_world():
    return "Hello, World!"

@app.route("/webhook", methods=["POST"])
def webhook():
    signature_header = request.headers.get("x-co-webhook-signature")
    print("signatureHeader:", signature_header)
    raw_body = request.get_data(as_text=True)
    secret = os.environ.get("WEBHOOK_SECRET", "your-secret-here")

    if not signature_header:
        return jsonify({"status": "error", "message": "Missing signature header"}), 400

    # Verify signature and timestamp
    is_valid_signature = verify_signature(secret, raw_body, signature_header)
    is_valid_timestamp = verify_timestamp(signature_header)

    print("isValidSignature:", is_valid_signature)
    print("isValidTimestamp:", is_valid_timestamp)

    if not (is_valid_signature and is_valid_timestamp):
        return jsonify({
            "status": "error",
            "message": "Invalid signature or timestamp",
            "signatureValid": is_valid_signature,
            "timestampValid": is_valid_timestamp,
        }), 400

    return jsonify({
        "status": "OK",
        "message": "Webhook received",
        "signatureValid": is_valid_signature,
        "timestampValid": is_valid_timestamp,
    })

if __name__ == "__main__":
    app.run()
```

{% endcode %}

> **Important Notes for Python**
>
> * **Raw Body** - Use `request.get_data(as_text=True)` to get the exact raw body string
> * **JSON Stringify** - The raw body must match exactly what was sent, including whitespace and formatting
> * **HMAC Compare** - Use `hmac.compare_digest()` for secure signature comparison
> * **Error Handling** - Always wrap signature verification in try-catch blocks
> * **Environment Variables** - Store your webhook secret in environment variables for security

### PHP Example&#x20;

{% code expandable="true" %}

```php
<?php
function verify_signature($secret, $body, $signature_header) {
    // Parse the signature header
    $parts = explode(',', $signature_header);
    $timestamp = explode('=', $parts[0])[1];
    $received_sig = explode('=', $parts[1])[1];

    // Create the expected signature
    $data = $timestamp . '.' . json_encode($body);
    $expected_sig = hash_hmac('sha256', $data, $secret);

    // Verify signature match
    if (!hash_equals($expected_sig, $received_sig)) {
        return false;
    }

    return true;
}

function verify_timestamp($signature_header) {
    // Parse the signature header
    $parts = explode(',', $signature_header);
    $timestamp = intval(explode('=', $parts[0])[1]);

    // Verify timestamp freshness (reject older than 2 minutes)
    $age = time() - $timestamp;
    if ($age > 120) { // >2 minutes old (recommended)
        return false;
    }

    return true;
}

// Usage example
$signature = $_SERVER['HTTP_X_CO_WEBHOOK_SIGNATURE'] ?? '';
$secret = $_ENV['WEBHOOK_SECRET'] ?? 'your-secret-here';

if (!$signature) {
    http_response_code(400);
    echo json_encode(['error' => 'Missing signature header']);
    exit;
}

$body = json_decode(file_get_contents('php://input'), true);

if (!verify_signature($secret, $body, $signature) || !verify_timestamp($signature)) {
    http_response_code(400);
    echo json_encode(['error' => 'Invalid signature or timestamp']);
    exit;
}

// Process the webhook
echo json_encode(['status' => 'success']);
?>
```

{% endcode %}

> **Important Notes for PHP**
>
> * **Error Handling** - Always validate signature header exists before processing
> * **Timestamp Validation** - Validates timestamps within 2 minutes (recommended range: 2-5 minutes)
> * **Hash Equals** - Use `hash_equals()` for secure signature comparison
> * **Environment Variables** - Store your webhook secret in environment variables for security

### Security Best Practices&#x20;

#### Secret Management <a href="#secret-management" id="secret-management"></a>

* **Store securely** - Never hardcode secrets in your application code
* **Use environment variables** - Store secrets in environment variables or secure key management systems
* **Rotate regularly** - Update your webhook secret periodically for enhanced security
* **Access control** - Limit access to webhook secrets to authorized personnel only

#### Validation Requirements <a href="#validation-requirements" id="validation-requirements"></a>

* **Always validate** - Never skip signature validation, even in development
* **Check timestamps** - Reject requests older than 5 minutes to prevent replay attacks
* **Use constant-time comparison** - Use secure comparison functions to prevent timing attacks
* **Return appropriate status codes** - Return HTTP 200 for successful processing

#### Timestamp Validation (Optional) <a href="#timestamp-validation" id="timestamp-validation"></a>

Verify that the webhook timestamp is recent to prevent replay attacks:

* **Extract timestamp** from the signature header
* **Calculate age** by comparing with current time
* **Reject old requests** beyond your chosen time window
* **UTC timezone** - All timestamps are in UTC

> **Recommended Time Window**
>
> We recommend validating timestamps within a 2-minute window, but you can choose any grace period between 2-5 minutes based on your security requirements and system performance needs.

### HTTP Status Codes&#x20;

Your webhook endpoint must return appropriate HTTP status codes to indicate successful processing

#### Successful Response <a href="#successful-response" id="successful-response"></a>

* **200 OK** - Webhook processed successfully
* **Response Time** - Must respond within 30 seconds

> **Important**
>
> You **must** return a 200 status code for the webhook delivery to be considered successful. Any other status code (4xx or 5xx) will cause the webhook to be retried after a certain time interval. Only a 200 response will mark the webhook as delivered and stop the retry process.

#### What Triggers Retries <a href="#retry-triggers" id="retry-triggers"></a>

* **4xx Client Errors** - Bad request, unauthorized, forbidden, not found, etc.
* **5xx Server Errors** - Internal server error, service unavailable, etc.
* **Timeout** - No response within 30 seconds
* **Connection Errors** - Network issues, DNS failures, etc.

> **Retry Behavior**
>
> When your endpoint returns a non-200 status code, Clearout will automatically retry the webhook delivery using exponential backoff. The retry schedule depends on your account type. See our [Redelivering Webhooks](https://docs.clearout.io/webhooks/redelivering-webhooks.html) guide for detailed retry timing information.

**Next Steps**

Now that you can validate webhook deliveries, learn how to [test your webhook integration](https://docs.clearout.io/webhooks/test-webhooks.html) and understand [webhook retry behavior](https://docs.clearout.io/webhooks/redelivering-webhooks.html).


---

# Agent Instructions: 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:

```
GET https://docs.clearout.io/developers/webhooks/validate-deliveries.md?ask=<question>
```

The question should be specific, self-contained, and written in natural language.
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.
