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

# Callback Protocol

> Source: https://kling.ai/document-api/api/get-started/callbacks
> Locale: en
> Current Tab: Callback Protocol
> This content is optimized for LLMs. In-page tabs are expanded and UI-only controls are omitted.

---

> **Introduction**
>
> 
>
> **This document covers the Callback Protocol and Webhook Signature.**
>
> 
>
> If you have configured a `callback_url`, start with **Callback** to understand the callback payload. Then continue to **Webhook Signature** to configure and verify callback requests.

## Callback Protocol

As for the Async task（image generation / video generation / virtual try-on），if you actively set the `callback_url` when you Create Task, the server will actively notify you when the task status changes, and the protocol is as follows:

### New Callback Function

Applicable to APIs based on new design standards (How to distinguish between new and legacy API design standards? The model version information located in the path is the new version, while the value set as the model_name parameter is the legacy version.)

```JSON
{
  "id": "string",                       // The task ID being queried
  "status": "string",                   // Task status, Enum values: submitted, processing, succeeded, failed
  "message": "string",                  // Task status information, displaying the failure reason when the task fails (such as triggering the content risk control of the platform, etc.)
  "create_time": 1722769557708,         // Task creation time, Unix timestamp, unit ms
  "update_time": 1722769557708,         // Task update time, Unix timestamp, unit ms
  "external_id": "string",              // The custom task ID for this task (if any)
  "outputs": [
    {
      "type": "video",                  // When the generated result is "video", the return value and related fields will vary depending on the type of generated content; List values for each content type: image, video, audio, element, voice
      "id": "string",                   // Video ID, generated by the system
      "url": "string",                  // URL for generating result, hotlink protection format (To ensure information security, generated results will be cleared after 30 days. Please make sure to save them promptly.)
      "watermark_url": "string",        // URL for generating result with watermark, hotlink protection format
      "duration": "string"              // The duration of the generated video, in seconds
    },
    {
      "type": "image",                  // When the generated result is "image", the return value and related fields will vary depending on the type of generated content; List values for each content type: image, video, audio, element, voice
      "url": "string",                  // URL for generating result, hotlink protection format (To ensure information security, generated results will be cleared after 30 days. Please make sure to save them promptly.)
      "watermark_url": "string",        // Image download URL with watermark, anti-theft link format
      "group_id": "string"              // Only appears when generating group images, used to mark grouping relationships
    },
    {
      "type": "audio",                  // When the generated result is "audio", the return value and related fields will vary depending on the type of generated content; List values for each content type: image, video, audio, element, voice
      "id": "string",                   // Audio ID, generated by the system
      "mp3_url": "string",              // URL for generating result, MP3 & hotlink protection format (To ensure information security, generated results will be cleared after 30 days. Please make sure to save them promptly.)
      "wav_url": "string",              // URL for generating result, WAV & hotlink protection format (To ensure information security, generated results will be cleared after 30 days. Please make sure to save them promptly.)
      "mp3_duration": "string",         // The duration of the generated MP3 format audio, in seconds
      "wav_duration": "string"          // The duration of the generated WAV format audio, in seconds
    },
    {
      "type": "voice",                  // When the generated result is "voice", the return value and related fields will vary depending on the type of generated content; List values for each content type: image, video, audio, element, voice
      "id": "string",                   // Voice ID, generated by the system
      "name": "string",                 // Audio name
      "url": "string",                  // Preview audio download link
      "owned_by": "string",             // Voice source, kling is the official voice library, and the number is the creator ID
      "status": "succeeded"             // The voice status can be divided into normal status and deleted status. The enumeration values are: succeeded, deleted
    },
    {
      "type": "element",                // When the generated result is "element", the return value and related fields will be different for different generated content types; List values for each content type: image, video, audio, element, voice
      "id": "string",                   // Element ID, generated by the system
      "name": "string",                 // Element name
      "description": "string",          // Element description
      "element_type": "string",         // Element type, divided into video character element and multi-image element, enumeration values are: video_character_elements and multi_image_elements
      "materials": [                    // Related materials of element
        {
          "type": "image",              // When returning "image" materials, the enumeration values for each content type are: image, video, voice
          "role": "string",             // Image reference material attributes are divided into frontal reference images and other reference images, with enumeration values of frontal and refer, respectively
          "url": "string"               // Material download link
        },
        {
          "type": "video",              // When returning "video" materials, the enumeration values for each content type are: image, video, voice
          "role": "refer",              // Video reference material attribute, fixed value: refer
          "url": "string"               // Material download link
        },
        {
          "type": "voice",              // When "voice" material is returned, enumeration values of each content type: image, video, voice
          "role": "refer",              // Voice reference material attribute, fixed value: refer
          "url": "string",              // Material download link
          "id": "string",               // Voice ID
          "name": "string",             // Voice name
          "owned_by": "string"          // Voice source, kling is the official voice library, and the number is the creator ID
        }
      ],
      "owned_by": "string",             // Element source, kling is the official voice library, and the number is the creator ID
      "status": "string",               // The status of the element can be divided into normal status and deleted status. The enumeration values are: succeeded, deleted
      "tags": [                         // Element label related information
        {
          "id": 1,                       // Tag ID
          "name": "string",             // Tag name
          "description": "string"       // Tag description
        }
      ]
    }
  ],
  "billing": [                           // Task consumption information
    {
      "charge_type": "string",          // Consumption account type: If the consumption is of balance, the parameter value is "cash"; if it is of resource package, the parameter value is "unit"
      "cash_type": "string",            // Balance type, only exists in the consumption limit scenario (charge_type=cash). If the consumption is the official quota, the parameter value is balance. If the consumption is the test quota, the parameter value is test_balance
      "amount": "string",               // Deduction amount; In the balance deduction situation (charge_type=cash), it represents the discount price deducted from the balance, and in the resource package deduction situation (charge_type=unit), it represents the deduction amount of units; decimal system
      "currency": "string",             // Consumption unit, which only exists in the consumption balance scenario (charge_type=cash), with a fixed parameter value of CNY/USD
      "package_type": "string",         // Consumable resource bundle type, which only exists in the consumable resource bundle scenario (charge_type=unit), with fixed enumeration values: video, image, audio
      "list_price": "string"            // Balance deduction list price only exists in the consumption limit scenario (charge_type=cash)
    }
  ]
}
```

### Legacy Callback Function

Applicable to Kling 3.0 Omni and earlier models.

```JSON
{
  "task_id": "string",               // Task ID, generated by the system
  "task_status": "string",           // Task status, Enum values: submitted, processing, succeed, failed
  "task_status_msg": "string",       // Task status information, displaying the failure reason when the task fails (such as triggering the content risk control of the platform, etc.)
  "created_at": 1722769557708,       // Task creation time, Unix timestamp, unit ms
  "updated_at": 1722769557708,       // Task update time, Unix timestamp, unit ms
  "final_unit_deduction": "string",   // The deduction units of task
  "final_balance_deduction": { // Balance deduction information
    "quota": "string", // Balance deduction discount price
    "list_price": "string" // Balance deduction list price
  },
  "task_info": {                     // Task creation parameters. Detailed information provided by the user during task creation.
    "parent_video": {
      "id": "string",                // Generated video ID; globally unique
      "url": "string",               // URL for generating images (To ensure information security, generated images/videos will be cleared after 30 days. Please make sure to save them promptly.)
      "duration": "string"           // Total duration of the video before continuation, in s
    },
    "external_task_id": "string"     // Customer-defined task ID
  },
  "task_result": {
    "images": [                      // The result of image-related tasks
      {
        "index": int,                // Image Number
        "url": "string"              // URL for generating images, such as: https://h1.inkwai.com/bs2/upload-ylab-stunt/xxx.png (To ensure information security, generated images/videos will be cleared after 30 days. Please make sure to save them promptly.)
      }
    ],
    "videos": [                      // The result of video-related tasks
      {
        "id": "string",              // Generated video ID; globally unique
        "url": "string",             // URL for generating videos (To ensure information security, generated images/videos will be cleared after 30 days. Please make sure to save them promptly.)
        "duration": "string"         // Total video duration, unit: s (seconds)
      }
    ]
  }
}
```

## Webhook Signature

**Webhook Signature** is a callback signature verification mechanism provided by the Kling API. It allows you verify callback requests using your Webhook Secret. When **Webhook Signature is enabled**, each callback request includes signature-related headers for verification. You can use them to ensure:

- The callback request was sent by Kling.
- The request payload has not been modified.
- The request timestamp is within the valid verification window.

> **Webhook Secret** is **independent** from API Key.
>
> 
>
> **Webhook Signature** does not change the existing callback flow. It only **adds signature-related headers** to callback requests.

### Prerequisites

Before using Webhook Signature, make sure:

- You have configured `options.callback_url` when creating an API request.
- Your server provides an HTTPS endpoint to receive callback requests.

---

## Configuration

### Create Webhook Secret

For security purposes, store the Webhook Secret as an environment variable and **do not expose it in source code repositories**.

| Step-1 Navigate to **「Basic Settings」** - **[「Webhook」](https://kling.ai/dev/account-info?tab=webhooks)** in **Kling API Console**, click **「Create Webhook Secret」**. | Step-2 **Copy** the generated Webhook Secret, and **paste** it into your server configuration.                |
| ------------------------------------------------------------------------------------------------------------------------------------------------------------ | ------------------------------------------------------------------------------------------------------------- |
| ![](<https://s15-kling.klingai.com/kos/s101/nlav112918/api-doc/images/loadimage%20(15).47e8757d532d9541.png>)                                                | ![](<https://s15-kling.klingai.com/kos/s101/nlav112918/api-doc/images/loadimage%20(16).a644a0189777e7ed.png>) |

### Rotate Webhook Secret

If your Webhook Secret is **compromised** or needs to be rotated, use **「Rotate」** to create a new one.

#### Standard Rotation

Both the old and new Webhook Secrets can be used to verify webhook signatures during the **7-day grace period**. You may delete the old Secret at any time. If it is not manually deleted, the old Webhook Secret will automatically **expire after 7 days**.

For more information, see **「Signature Verification During Rotation」** below.

| Step-1 Navigate **[「Webhook」](https://kling.ai/dev/account-info?tab=webhooks)**, and click **「Rotate」**.                  | Step-2 Select **「Standard Rotation」** to generate a new Secret.                                             | Step-3 Click **「Copy」** and update your server configuration as soon as possible.                           |
| ------------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------- |
| ![](<https://s15-kling.klingai.com/kos/s101/nlav112918/api-doc/images/loadimage%20(17).6493102fd81c4da2.png>) | ![](<https://s15-kling.klingai.com/kos/s101/nlav112918/api-doc/images/loadimage%20(18).7ca542c35af915e0.png>) | ![](<https://s15-kling.klingai.com/kos/s101/nlav112918/api-doc/images/loadimage%20(19).47d7e80f8a1542fc.png>) |

#### Emergency Rotation

Once a new Webhook Secret is created, **the previous Webhook Secret is invalidated immediately**. Webhook signature verification using the previous Webhook Secret will fail. To avoid webhook signature verification failures, update your server configuration with the new Webhook Secret **as soon as possible**.

| Step-1 **Navigate to [「Webhook」](https://kling.ai/dev/account-info?tab=webhooks)**, and click **「Rotate」**.               | Step-2 Select **「Emergency Rotation」** to create a new Secret.                                              | Step-3 Click **「Copy」** and **immediately update** your server configuration.                               |
| ------------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------- |
| ![](<https://s15-kling.klingai.com/kos/s101/nlav112918/api-doc/images/loadimage%20(17).6493102fd81c4da2.png>) | ![](<https://s15-kling.klingai.com/kos/s101/nlav112918/api-doc/images/loadimage%20(21).14faad5249ada26e.png>) | ![](<https://s15-kling.klingai.com/kos/s101/nlav112918/api-doc/images/loadimage%20(22).cc6b7608f17833d6.png>) |

#### Signature Verification During Rotation

During the 7-day grace period, the `webhook-signature` header may contain multiple signatures.

- Each signature follows the format: `version,signature`
- Multiple signatures are separated by spaces
- Verification succeeds if any signature matches

Signature example:

```text
webhook-signature: v1,K5oZfzeVuQvI4x1jrjAgMlkpJDoe1JhVmAbjR6eKeTM= v1,7qGhfzeVuQvI4x1jrjAgMlkpJDoe1JhVmAbjR6eKeTM=
```

### Delete Old Webhook Secret

After completing the service update, you can delete the old Webhook Secret that is no longer in use. **After deletion:** The new secret remains active, and the old secret becomes invalid immediately.

| Step-1 Navigate to **[「Webhook」](https://kling.ai/dev/account-info?tab=webhooks)**.                                         | Step-2 Navigate to the old rotating secret, click **「Delete previous」** and confirm.                        |
| ------------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------- |
| ![](<https://s15-kling.klingai.com/kos/s101/nlav112918/api-doc/images/loadimage%20(23).2cf78e15a071cb90.png>) | ![](<https://s15-kling.klingai.com/kos/s101/nlav112918/api-doc/images/loadimage%20(24).f7505878fa4874ee.png>) |

### Delete All Webhook Secrets

To **disable Webhook Signature**, you can delete all Webhook Secrets. **After deletion:** All Webhook Secrets become invalid immediately. Your callback requests will **no longer include signature headers**.

| Step-1 Navigate to **[「Webhook」](https://kling.ai/dev/account-info?tab=webhooks)**.                                         | Step-2 Click **「Delete All」** and confirm deletion.                                                         |
| ------------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------- |
| ![](<https://s15-kling.klingai.com/kos/s101/nlav112918/api-doc/images/loadimage%20(31).5f9d7ce54fa49a04.png>) | ![](<https://s15-kling.klingai.com/kos/s101/nlav112918/api-doc/images/loadimage%20(26).5acefbc37d2e686d.png>) |

> Update your server configuration with the new Webhook Secret **as soon as possible** after rotation.
>
> 
>
> Do not delete the old Webhook Secret until you have verified a successful migration.
>
> 
>
> Once an **old** Webhook Secret is **deleted**, it can **no longer be used** to verify webhook signatures.

---

## Test Callback

### Send Test Callback

Use Send Test Callback to verify that your callback endpoint is reachable and the callback signature can be successfully verified. A successful verification should return HTTP 200.

**Rate limit**: You can send only one test callback request **every 6 seconds**.

| Step-1 Navigate to **[「Webhook」](https://kling.ai/dev/account-info?tab=webhooks)**, click **「Send Test Callback」**.       | Step-2 Enter your `callback_url` and click **「Send Test Callback」**. Kling will immediately send a test callback request to your configured `callback_url`. | Step-3 Receive the callback request, verify the signature, and return HTTP 200.                               |
| ------------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------- |
| ![](<https://s15-kling.klingai.com/kos/s101/nlav112918/api-doc/images/loadimage%20(27).89b45233d89ba1d0.png>) | ![](<https://s15-kling.klingai.com/kos/s101/nlav112918/api-doc/images/loadimage%20(28).2b25087ba9d78905.png>)                                                 | ![](<https://s15-kling.klingai.com/kos/s101/nlav112918/api-doc/images/loadimage%20(29).16818e461f3ebb96.png>) |

### Callback Request Headers

When Webhook Signature is enabled, Kling adds the following HTTP headers to callback requests:

| Header              | Description                                                                                                                                                        |
| ------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------ |
| `webhook-id`        | Unique identifier of the callback request.                                                                                                                         |
| `webhook-timestamp` | Unix timestamp (in seconds) when the callback request was generated. Verify that the timestamp is within ±5 minutes of your server time to prevent replay attacks. |
| `webhook-signature` | Signature generated from the request payload. Use your Webhook Secret to verify that the request was sent by Kling and has not been modified.                      |

> Webhook request headers are case-insensitive.
>
> 
>
> When receiving Webhook callback requests, you should **support case-insensitive handling** of the above headers.
>
> 
>
> The Kling platform will **not include** these headers in callback requests if no Webhook Secret has been created.

### Request Header Example

```bash
curl -X POST 'http://example.com/your/callback/path' \   # Callback request URL
  -H 'Content-Type: application/json; charset=utf-8' \  # Request body format
  -H 'webhook-id: 8674665223082153551' \                # Unique callback identifier
  -H 'webhook-timestamp: 1781080794' \                  # Callback timestamp
  -H 'webhook-signature: v1,K5oZfzeVuQvI4x1jrjAgMlkpJDoe1JhVmAbjR6eKeTM=' \ # Callback signature
  -d '{"id":"913827032734642185","status":"submitted","outputs":[],"message":"","create_time":1785901861504,"update_time":1785901861504}' # Callback request body
```

### Test Callback Request Body Example

```JSON
{
  "id": "913827032734642185",             // Task ID
  "status": "submitted",                  // Task status. Fixed to "submitted" for test callbacks.
  "outputs": [],                          // Output results. Always an empty array for test callbacks.
  "message": "",                          // Message. Always an empty string for test callbacks.
  "create_time": 1785901861504,           // Task creation time (Unix timestamp in milliseconds)
  "update_time": 1785901861504            // Task update time (Unix timestamp in milliseconds)
}
```

## Verify Signature

After receiving a callback request, developers can verify the signature using either:

- SDK Examples Verification：We recommend using the sample code for signature verification, See **Appendix A: SDK Examples** [See sample code →](#sdk-examples)
- Manual Signature Verification: For custom implementations, see **Appendix B: Manual Signature Verification**. [See details →](#manual-verification)

---

## FAQ

### 1. What happens after deleting all Webhook Secrets?

After deleting all Webhook Secrets, Kling AI will no longer include the signature header (Webhook-Signature) in subsequent webhook requests. As a result, your server will no longer be able to verify the authenticity of webhook requests using Webhook Signature. If your server relies on Webhook Signature verification to process webhook requests, it may fail to handle task results properly.

You can choose one of the following options based on your business requirements:

- **Continue using Webhook Signature:** Create a new Webhook Secret and update your server configuration accordingly.
- **Stop using Webhook Signature:** Update your webhook handling logic to no longer depend on signature verification.

### 2. Will deleting all Webhook Secrets affect task submission or task execution?

**No.** Deleting all Webhook Secrets does not affect API requests for creating tasks, and task processing will continue as usual.

For tasks that are already in progress when the Secret is deleted:

- The tasks will continue to execute normally.
- A webhook request will still be sent after the task is completed.
- However, if no valid Webhook Secret is configured at that time, the webhook request will not include signature information.
- If your server requires signature verification to process webhook results, it may fail to receive and process the task results correctly.

### 3. How can I restore Webhook Signature?

**Step-1** Create a new Webhook Secret in the [Kling API Console](https://kling.ai/dev/api-key).

**Step-2** Copy the new Secret and securely store it on your server.

**Step-3** Update your server configuration to use the new Secret for webhook request verification.

After completing the configuration, subsequent webhook requests will include signature information again.

## SDK Examples

Kling SDK provides built-in Signature verification. We recommend using the SDK for verification. Refer to this appendix for complete integration examples.

#### Python

```python
# Python: The official Standard Webhooks library is recommended.
# Install: pip install standardwebhooks

from standardwebhooks import Webhook

wh = Webhook("whsec_XXXX")  # Webhook Secret generated from the console

payload = wh.verify(raw_body, {
    "webhook-id": headers["webhook-id"],
    "webhook-timestamp": headers["webhook-timestamp"],
    "webhook-signature": headers["webhook-signature"],
})

# Raises WebhookVerificationError if signature verification fails
# SDK automatically handles multiple signatures and timestamp verification
```

#### Java

```java
package test;

import com.standardwebhooks.Webhook;
import com.standardwebhooks.exceptions.WebhookVerificationException;

import java.util.Collections;
import java.util.HashMap;
import java.util.List;
import java.util.Map;

public class WebhookVerifyDemo {

    static String secret = System.getenv("KLING_WEBHOOK_SECRET"); // Read the Webhook Secret from the environment variable

    public static void main(String[] args) {
        String rawBody = ""; // The raw HTTP request body
        String webhookId = ""; // The value of the "webhook-id" request header
        String webhookTimestamp = ""; // The value of the "webhook-timestamp" request header
        String webhookSignature = ""; // The value of the "webhook-signature" request header

        boolean verified = verify(secret, rawBody, webhookId, webhookTimestamp, webhookSignature);
        System.out.println(verified); // Print the verification result

        if (verified) {
            // Process the request body after successful signature verification,
            // then return HTTP 200
        } else {
            // Signature verification failed or the timestamp is outside the
            // allowed time window, return an HTTP 4xx response
        }
    }

    static boolean verify(String secret, String rawBody, String webhookId, String webhookTimestamp, String webhookSignature) {
        try {
            Webhook webhook = new Webhook(secret);

            // Build the headers required for signature verification
            Map<String, List<String>> webhookHeaders = new HashMap<String, List<String>>();
            webhookHeaders.put("webhook-id", Collections.singletonList(webhookId));
            webhookHeaders.put("webhook-timestamp", Collections.singletonList(webhookTimestamp));
            webhookHeaders.put("webhook-signature", Collections.singletonList(webhookSignature));

            // Verify the callback signature
            webhook.verify(rawBody, webhookHeaders);
            return true;
        } catch (WebhookVerificationException e) {
            e.printStackTrace();
            return false;
        }
    }
}
```

## Manual Verification

If the official SDK is not available, you can manually verify the callback signature using the following steps.

1. Retrieve the following values from the request: `webhook-id`, `webhook-timestamp`, the raw request body (`rawBody`)
2. Construct the signed content by concatenating the values with a period (.): `{webhook-id}.{webhook-timestamp}.{rawBody}`
3. Generate the signature using the Webhook Secret and encode the result with Base64：`Base64(HMAC-SHA256(key, signed_content))`

    Where: `key = Base64Decode(the secret value after removing the whsec_ prefix)`

4. **Compare** the generated signature with each signature value in the `webhook-signature` header **(excluding the `v1,` prefix)**. Use **constant-time comparison** to prevent timing attacks. Verification succeeds if any signature matches.
5. Verify that the difference between `webhook-timestamp` and the local server time **does not exceed 5 minutes**.

#### Java Manual Signature Verification Example

```java
package test;

import javax.crypto.Mac;
import javax.crypto.spec.SecretKeySpec;

import java.nio.charset.StandardCharsets;
import java.security.MessageDigest;
import java.util.Base64;

public class WebhookManualVerifyDemo {

    static String secret = System.getenv("KLING_WEBHOOK_SECRET"); // Read the Webhook Secret from the environment variable
    static long tolerance = 5 * 60; // Allowed timestamp tolerance (5 minutes in this example, in seconds)

    public static void main(String[] args) {
        String rawBody = ""; // The raw HTTP request body
        String webhookId = ""; // The value of the "webhook-id" request header
        String webhookTimestamp = ""; // The value of the "webhook-timestamp" request header
        String webhookSignature = ""; // The value of the "webhook-signature" request header, e.g. "v1,xxx" or "v1,xxx v1,yyy"

        boolean verified = verify(secret, rawBody, webhookId, webhookTimestamp, webhookSignature);
        System.out.println(verified); // Print the verification result

        if (verified) {
            // Process the request body after successful signature verification,
            // then return HTTP 200
        } else {
            // Signature verification failed or the timestamp is outside the
            // allowed time window, return an HTTP 4xx response
        }
    }

    static boolean verify(String secret, String rawBody, String webhookId, String webhookTimestamp, String webhookSignature) {
        try {
            // Verify that the timestamp is within the allowed time window
            // to prevent replay attacks
            long timestamp = Long.parseLong(webhookTimestamp);
            long now = System.currentTimeMillis() / 1000;
            if (Math.abs(now - timestamp) > tolerance) {
                return false;
            }

            // Build the signed content:
            // webhook-id.webhook-timestamp.rawBody
            String signedContent = webhookId + "." + webhookTimestamp + "." + rawBody;

            // Remove the "whsec_" prefix from the Secret and Base64-decode it
            // to obtain the HMAC signing key
            byte[] key = Base64.getDecoder().decode(secret.replaceFirst("^whsec_", ""));

            // Compute the expected signature using HMAC-SHA256
            Mac mac = Mac.getInstance("HmacSHA256");
            mac.init(new SecretKeySpec(key, "HmacSHA256"));
            String expected = Base64.getEncoder()
                    .encodeToString(mac.doFinal(signedContent.getBytes(StandardCharsets.UTF_8)));

            // The webhook-signature header may contain multiple space-separated
            // signatures in the format "version,signature". Compare each one.
            for (String versionedSignature : webhookSignature.split(" ")) {
                String[] parts = versionedSignature.split(",", 2);
                if (parts.length < 2 || !"v1".equals(parts[0])) {
                    continue;
                }

                // Use constant-time comparison to prevent timing attacks
                if (MessageDigest.isEqual(
                        expected.getBytes(StandardCharsets.UTF_8),
                        parts[1].getBytes(StandardCharsets.UTF_8))) {
                    return true;
                }
            }
            return false;
        } catch (Exception e) {
            e.printStackTrace();
            return false;
        }
    }
}
```

#### Verification Example

Use the following test data to verify your webhook signature implementation.

```text
Secret:
whsec_dGVzdHNlY3JldHRlc3RzZWNyZXR0ZXN0c2VjcmV0MTI=

Webhook-ID:
9876543210

Webhook-Timestamp:
1781080794

Raw Body (original bytes, single line, no leading or trailing whitespace):
{"id":"1234567890","status":"succeeded","message":"","create_time":1781080778802,"update_time":1781080794151}

Expected Signature:
v1,UsKlJP00XoQyOn410NM9xv34sP+Gl0jnOO9Lcpr7NJ4=
```
