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

# Webhook verification

> Verify callbacks with HMAC-SHA256

export const apiKeyUrl = "https://www.vmeg.ai/open-api-setting";

## Why verify

Your webhook URL is public. Without checks, a forged completion POST could be mistaken for a real task result. Verify every callback before you parse the JSON.

## Signing scheme

VMEG signs each webhook POST with **HMAC-SHA256**:

1. You configure a **Webhook URL** on <a href={apiKeyUrl}>API Configuration</a>. VMEG generates a **Webhook Secret** on that page — copy it to your server; you never send it in the callback.
2. When a task completes, VMEG POSTs the JSON body to your URL and attaches:
   * **`X-Timestamp`** — send time in Unix **milliseconds** (string)
   * **`X-Signature`** — hex (base16) string of `HMAC-SHA256(secret, X-Timestamp + rawBody)`
3. Your endpoint recomputes the same HMAC with your copy of the secret. If it matches `X-Signature`, the sender is VMEG and the body was not tampered with. Reject callbacks whose timestamp is more than **300 seconds** from your server clock (replay protection).

## How to verify

1. Read `X-Timestamp` and `X-Signature` from the request headers.
2. Reject if `X-Timestamp` is outside the 300-second window.
3. Hash the **raw request body bytes** — do not re-serialize JSON, pretty-print, sort keys, or change whitespace.
4. Compare your computed hex to `X-Signature` (exact match; do not transform case).

Return `401` or `403` on failure. Still respond promptly.

## Examples

<CodeGroup>
  ```javascript Node.js theme={null}
  import crypto from "node:crypto";

  function verify(secret, timestamp, rawBody, signature) {
    if (Math.abs(Number(timestamp) - Date.now()) > 300_000) return false;
    const message = Buffer.concat([Buffer.from(timestamp), rawBody]);
    const expected = crypto.createHmac("sha256", secret).update(message).digest("hex");
    return crypto.timingSafeEqual(Buffer.from(expected), Buffer.from(signature));
  }
  ```

  ```python Python theme={null}
  import hmac
  import hashlib
  import time

  def verify(secret: str, timestamp: str, raw_body: bytes, signature: str) -> bool:
      if abs(int(timestamp) - int(time.time() * 1000)) > 300_000:
          return False
      message = timestamp.encode() + raw_body
      expected = hmac.new(secret.encode(), message, hashlib.sha256).hexdigest()
      return hmac.compare_digest(expected, signature)
  ```

  ```java Java theme={null}
  import javax.crypto.Mac;
  import javax.crypto.spec.SecretKeySpec;
  import java.nio.charset.StandardCharsets;
  import java.security.MessageDigest;

  public final class WebhookVerifier {
    private static final long MAX_SKEW_MS = 300_000L;

    public static boolean verify(String secret, String timestamp, byte[] rawBody, String signature)
        throws Exception {
      long ts = Long.parseLong(timestamp);
      if (Math.abs(ts - System.currentTimeMillis()) > MAX_SKEW_MS) {
        return false;
      }

      Mac mac = Mac.getInstance("HmacSHA256");
      mac.init(new SecretKeySpec(secret.getBytes(StandardCharsets.UTF_8), "HmacSHA256"));

      byte[] tsBytes = timestamp.getBytes(StandardCharsets.UTF_8);
      byte[] message = new byte[tsBytes.length + rawBody.length];
      System.arraycopy(tsBytes, 0, message, 0, tsBytes.length);
      System.arraycopy(rawBody, 0, message, tsBytes.length, rawBody.length);

      String expected = toHex(mac.doFinal(message));
      return MessageDigest.isEqual(
          expected.getBytes(StandardCharsets.UTF_8),
          signature.getBytes(StandardCharsets.UTF_8));
    }

    private static String toHex(byte[] bytes) {
      StringBuilder sb = new StringBuilder(bytes.length * 2);
      for (byte b : bytes) {
        sb.append(String.format("%02x", b));
      }
      return sb.toString();
    }
  }
  ```
</CodeGroup>

## After verification

1. Parse JSON and route on `event` (`openapi-tts`, `openapi-translate`, `openapi-clone-voice`, or `openapi-media-translation`) — see [Webhook request body](/guides/webhook-request).
2. Process `data` using the **Callbacks** schema on the matching `create-async` endpoint in [API reference](/api-reference/introduction).
3. Return **2xx** immediately; queue long-running work.

See [Webhooks](/guides/webhooks) for URL configuration and receiver requirements.
