> ## Documentation Index
> Fetch the complete documentation index at: https://old-docs.kie.ai/llms.txt
> Use this file to discover all available pages before exploring further.

# Webhook Security Verification

> Understand Webhook security verification and how to handle them

<Warning>
  To ensure the security of callback requests, it is strongly recommended to enable Webhook HMAC signature verification in production environments to prevent forged requests and replay attacks.
</Warning>

### Algorithm Overview

Kie AI uses the **HMAC-SHA256** algorithm to generate signatures, ensuring the integrity and authenticity of webhook callbacks.

**Signature Generation Process:**

1. **Concatenate the data to sign**: `taskId + "." + timestampSeconds`
   * `taskId`: Task ID from the request body
   * `timestampSeconds`: Unix timestamp in seconds from the `X-Webhook-Timestamp` header

2. **Calculate HMAC-SHA256 signature**:
   ```
   signature = HMAC-SHA256(dataToSign, webhookHmacKey)
   ```

3. **Base64 encode the signature**:
   ```
   finalSignature = Base64.encode(signature)
   ```

### Obtain Webhook HMAC Key

You can generate and view your `webhookHmacKey` on the [Kie AI Settings Page](https://kie.ai/settings).

<Info>
  The `webhookHmacKey` is used to verify that callback requests originate from Kie AI's official servers. Keep this key secure and never expose it or commit it to code repositories.
</Info>

### Webhook Header Description

When you enable the `webhookHmacKey` feature in the settings page, all callback requests will include the following fields in the HTTP headers:

<ParamField header="X-Webhook-Timestamp" type="integer" required>
  Unix timestamp (in seconds) when the callback request was sent.
</ParamField>

<ParamField header="X-Webhook-Signature" type="string" required>
  Signature generated using the HMAC-SHA256 algorithm with Base64 encoding.

  Signature generation rule:

  ```
  base64(HMAC-SHA256(taskId + "." + timestamp, webhookHmacKey))
  ```

  Where:

  * `taskId` is the task ID from the callback body
  * `timestamp` is the value of `X-Webhook-Timestamp`
  * `webhookHmacKey` is the key you generated in the console
</ParamField>

### Webhook Verification Process

Follow these steps to verify the legitimacy of webhook requests:

<Steps>
  <Step title="Read Header Fields">
    Extract the `X-Webhook-Timestamp` and `X-Webhook-Signature` fields from the HTTP headers.

    ```javascript theme={null}
    const timestamp = req.headers['x-webhook-timestamp'];
    const receivedSignature = req.headers['x-webhook-signature'];
    ```
  </Step>

  <Step title="Generate Signature">
    Using your locally stored `webhookHmacKey`, generate the HMAC-SHA256 signature following these rules:

    1. Extract `task_id` from the request body
    2. Concatenate the string: `taskId + "." + timestamp`
    3. Generate signature using HMAC-SHA256 algorithm with `webhookHmacKey`
    4. Base64 encode the signature result

    ```javascript theme={null}
    const crypto = require('crypto');

    const taskId = req.body.data.task_id;
    const message = `${taskId}.${timestamp}`;

    const computedSignature = crypto
      .createHmac('sha256', webhookHmacKey)
      .update(message)
      .digest('base64');
    ```
  </Step>

  <Step title="Compare Signatures">
    Compare the computed signature with `X-Webhook-Signature` using a constant-time comparison algorithm to prevent timing attacks.

    ```javascript theme={null}
    // Use crypto.timingSafeEqual for constant-time comparison
    if (computedSignature.length !== receivedSignature.length) {
      return res.status(401).json({ error: 'Invalid signature' });
    }

    const isValid = crypto.timingSafeEqual(
      Buffer.from(computedSignature),
      Buffer.from(receivedSignature)
    );

    if (isValid) {
      // Signature verified, request is legitimate
      console.log('Webhook signature verified');
    } else {
      // Signature verification failed, reject request
      return res.status(401).json({ error: 'Invalid signature' });
    }
    ```

    <Check>
      If the signatures match, the webhook request is confirmed to be from Kie AI's official servers and can be safely processed.
    </Check>
  </Step>
</Steps>

### Complete Example Code

Here are complete examples of implementing webhook signature verification in popular programming languages:

<Tabs>
  <Tab title="Node.js">
    ```javascript theme={null}
    const express = require('express');
    const crypto = require('crypto');
    const app = express();

    app.use(express.json());

    // Read webhookHmacKey from environment variables or configuration
    const WEBHOOK_HMAC_KEY = process.env.WEBHOOK_HMAC_KEY;

    function generateSignature(taskId, timestampSeconds, secret) {
      // 1. Concatenate the data to sign
      const dataToSign = `${taskId}.${timestampSeconds}`;
      
      // 2. Calculate HMAC-SHA256 signature
      const hmac = crypto.createHmac('sha256', secret);
      hmac.update(dataToSign);
      
      // 3. Base64 encode
      return hmac.digest('base64');
    }

    function verifySignature(taskId, timestampSeconds, receivedSignature, secret) {
      // Regenerate signature
      const expectedSignature = generateSignature(taskId, timestampSeconds, secret);
      
      // Use secure string comparison
      if (expectedSignature.length !== receivedSignature.length) {
        return false;
      }
      
      return crypto.timingSafeEqual(
        Buffer.from(expectedSignature),
        Buffer.from(receivedSignature)
      );
    }

    function verifyWebhookSignature(req, res, next) {
      // 1. Read header fields
      const timestamp = req.headers['x-webhook-timestamp'];
      const receivedSignature = req.headers['x-webhook-signature'];

      if (!timestamp || !receivedSignature) {
        return res.status(401).json({ error: 'Missing signature headers' });
      }

      // 2. Verify signature
      const taskId = req.body.data?.task_id;
      if (!taskId) {
        return res.status(400).json({ error: 'Missing task_id' });
      }

      const isValid = verifySignature(taskId, timestamp, receivedSignature, WEBHOOK_HMAC_KEY);
      
      if (!isValid) {
        return res.status(401).json({ error: 'Invalid signature' });
      }

      // Signature verified
      next();
    }

    // Apply middleware
    app.post('/webhook-callback', verifyWebhookSignature, (req, res) => {
      const { code, msg, data } = req.body;
      
      console.log('Received legitimate webhook request:', {
        taskId: data.task_id,
        status: code,
        callbackType: data.callbackType
      });
      
      // Process callback data...
      
      res.status(200).json({ status: 'received' });
    });

    app.listen(3000, () => {
      console.log('Webhook server running on port 3000');
    });
    ```
  </Tab>

  <Tab title="Python">
    ```python theme={null}
    from flask import Flask, request, jsonify
    import hmac
    import hashlib
    import base64
    import os

    app = Flask(__name__)

    # Read webhookHmacKey from environment variables or configuration
    WEBHOOK_HMAC_KEY = os.getenv('WEBHOOK_HMAC_KEY', '')

    def generate_signature(task_id, timestamp_seconds, secret):
        """Generate Webhook signature"""
        # 1. Concatenate the data to sign
        data_to_sign = f"{task_id}.{timestamp_seconds}"
        
        # 2. Calculate HMAC-SHA256 signature
        signature = hmac.new(
            secret.encode('utf-8'),
            data_to_sign.encode('utf-8'),
            hashlib.sha256
        ).digest()
        
        # 3. Base64 encode
        return base64.b64encode(signature).decode('utf-8')

    def verify_signature(task_id, timestamp_seconds, received_signature, secret):
        """Verify Webhook signature"""
        # Regenerate signature
        expected_signature = generate_signature(task_id, timestamp_seconds, secret)
        
        # Use secure string comparison
        return hmac.compare_digest(expected_signature, received_signature)

    def verify_webhook_signature():
        # 1. Read header fields
        timestamp = request.headers.get('X-Webhook-Timestamp')
        received_signature = request.headers.get('X-Webhook-Signature')
        
        if not timestamp or not received_signature:
            return False, 'Missing signature headers'
        
        # 2. Verify signature
        data = request.json
        task_id = data.get('data', {}).get('task_id')
        
        if not task_id:
            return False, 'Missing task_id'
        
        is_valid = verify_signature(task_id, timestamp, received_signature, WEBHOOK_HMAC_KEY)
        
        if not is_valid:
            return False, 'Invalid signature'
        
        return True, 'Verified'

    @app.route('/webhook-callback', methods=['POST'])
    def handle_webhook():
        # Verify signature
        is_valid, message = verify_webhook_signature()
        
        if not is_valid:
            return jsonify({'error': message}), 401
        
        # Signature verified, process callback data
        data = request.json
        code = data.get('code')
        msg = data.get('msg')
        callback_data = data.get('data', {})
        task_id = callback_data.get('task_id')
        callback_type = callback_data.get('callbackType')
        
        print(f"Received legitimate webhook request: {task_id}, status: {code}, type: {callback_type}")
        
        # Process callback data...
        
        return jsonify({'status': 'received'}), 200

    if __name__ == '__main__':
        app.run(host='0.0.0.0', port=3000)
    ```
  </Tab>

  <Tab title="PHP">
    ```php theme={null}
    <?php
    header('Content-Type: application/json');

    // Read webhookHmacKey from environment variables or configuration
    $webhookHmacKey = getenv('WEBHOOK_HMAC_KEY');

    function generateSignature($taskId, $timestampSeconds, $secret) {
        // 1. Concatenate the data to sign
        $dataToSign = $taskId . '.' . $timestampSeconds;
        
        // 2. Calculate HMAC-SHA256 signature
        $signature = hash_hmac('sha256', $dataToSign, $secret, true);
        
        // 3. Base64 encode
        return base64_encode($signature);
    }

    function verifySignature($taskId, $timestampSeconds, $receivedSignature, $secret) {
        // Regenerate signature
        $expectedSignature = generateSignature($taskId, $timestampSeconds, $secret);
        
        // Use secure string comparison
        return hash_equals($expectedSignature, $receivedSignature);
    }

    function verifyWebhookSignature($webhookHmacKey) {
        // 1. Read header fields
        $timestamp = $_SERVER['HTTP_X_WEBHOOK_TIMESTAMP'] ?? null;
        $receivedSignature = $_SERVER['HTTP_X_WEBHOOK_SIGNATURE'] ?? null;
        
        if (!$timestamp || !$receivedSignature) {
            return ['valid' => false, 'error' => 'Missing signature headers'];
        }
        
        // 2. Verify signature
        $input = file_get_contents('php://input');
        $data = json_decode($input, true);
        $taskId = $data['data']['task_id'] ?? null;
        
        if (!$taskId) {
            return ['valid' => false, 'error' => 'Missing task_id'];
        }
        
        $isValid = verifySignature($taskId, $timestamp, $receivedSignature, $webhookHmacKey);
        
        if (!$isValid) {
            return ['valid' => false, 'error' => 'Invalid signature'];
        }
        
        return ['valid' => true, 'data' => $data];
    }

    // Verify signature
    $result = verifyWebhookSignature($webhookHmacKey);

    if (!$result['valid']) {
        http_response_code(401);
        echo json_encode(['error' => $result['error']]);
        exit;
    }

    // Signature verified, process callback data
    $data = $result['data'];
    $code = $data['code'] ?? null;
    $msg = $data['msg'] ?? '';
    $callbackData = $data['data'] ?? [];
    $taskId = $callbackData['task_id'] ?? '';
    $callbackType = $callbackData['callbackType'] ?? '';

    error_log("Received legitimate webhook request: $taskId, status: $code, type: $callbackType");

    // Process callback data...

    // Return success response
    http_response_code(200);
    echo json_encode(['status' => 'received']);
    ?>
    ```
  </Tab>

  <Tab title="Java">
    ```java theme={null}
    import javax.crypto.Mac;
    import javax.crypto.spec.SecretKeySpec;
    import java.nio.charset.StandardCharsets;
    import java.security.InvalidKeyException;
    import java.security.NoSuchAlgorithmException;
    import java.util.Base64;
    import java.util.Objects;

    public class WebhookVerifier {
        
        public static boolean verifySignature(String taskId, long timestampSeconds, String receivedSignature, String secret) {
            // Regenerate signature
            String expectedSignature = generateSignature(taskId, timestampSeconds, secret);

            // Use secure string comparison
            return constantTimeEquals(expectedSignature, receivedSignature);
        }

        public static String generateSignature(String taskId, long timestampSeconds, String secret) {
            try {
                // 1. Concatenate the data to sign
                String dataToSign = taskId + "." + timestampSeconds;

                // 2. Calculate HMAC-SHA256 signature
                Mac mac = Mac.getInstance("HmacSHA256");
                SecretKeySpec keySpec = new SecretKeySpec(
                        secret.getBytes(StandardCharsets.UTF_8), "HmacSHA256");
                mac.init(keySpec);
                byte[] hash = mac.doFinal(dataToSign.getBytes(StandardCharsets.UTF_8));

                // 3. Base64 encode
                return Base64.getEncoder().encodeToString(hash);
            } catch (NoSuchAlgorithmException | InvalidKeyException e) {
                throw new RuntimeException("Failed to generate webhook signature", e);
            }
        }

        private static boolean constantTimeEquals(String a, String b) {
            if (a == null || b == null) {
                return Objects.equals(a, b);
            }
            if (a.length() != b.length()) {
                return false;
            }
            int result = 0;
            for (int i = 0; i < a.length(); i++) {
                result |= a.charAt(i) ^ b.charAt(i);
            }
            return result == 0;
        }
    }
    ```
  </Tab>
</Tabs>

### Example Webhook Request

Here's what a complete webhook request looks like:

```http theme={null}
POST /your-webhook-endpoint HTTP/1.1
Host: your-server.com
Content-Type: application/json
X-Webhook-Timestamp: 1769670760
X-Webhook-Signature: KxDlpbbq0GDOKqm0+FuJpJWTzY8baHSjhEt4kwElqQI=

{
  "taskId": "ee9c2715375b7837f8bb51d641ff5863",
  "code": 200,
  "msg": "Success",
  "data": {
    "task_id": "ee9c2715375b7837f8bb51d641ff5863",
    "callbackType": "task_completed",
    ...
  }
}
```
