Skip to main content
Every webhook request from VIZOCHOK includes an HMAC-SHA256 signature that you should verify to ensure the request is authentic and has not been tampered with.

How It Works

VIZOCHOK signs each webhook request using these steps:
  1. Get the current Unix timestamp (seconds)
  2. Construct the signed payload: {timestamp}.{request_body}
  3. Compute HMAC-SHA256 of the signed payload using your webhook secret
  4. Send the signature and timestamp in request headers
Signed Payload = "{timestamp}.{JSON body}"

HMAC-SHA256(webhook_secret, signed_payload) → hex digest

Headers:
  X-VIZOCHOK-Signature: sha256={hex_digest}
  X-VIZOCHOK-Timestamp: {timestamp}

Verification Steps

1

Extract headers

Read the X-VIZOCHOK-Signature and X-VIZOCHOK-Timestamp headers from the incoming request.The signature header has the format sha256={hex_digest}.
2

Check timestamp (replay protection)

Parse the timestamp and verify it is within 5 minutes of the current time. Reject requests with timestamps outside this window.
if abs(current_time - timestamp) > 300:
    reject("Timestamp expired")
3

Construct the signed payload

Concatenate the timestamp, a dot, and the raw request body:
signed_payload = "{timestamp}.{raw_body_bytes}"
Use the raw request body bytes, not a re-serialized version. JSON serialization is not guaranteed to be deterministic, so re-serializing the parsed body may produce a different byte sequence.
4

Compute expected signature

Calculate HMAC-SHA256 of the signed payload using your webhook secret as the key:
expected = HMAC-SHA256(webhook_secret, signed_payload)
5

Compare signatures

Use a constant-time comparison function to compare the expected signature with the one from the header. This prevents timing attacks.
if not constant_time_equal(expected, received):
    reject("Invalid signature")

Code Examples

import hashlib
import hmac
import time

WEBHOOK_SECRET = "your_webhook_secret"
TIMESTAMP_TOLERANCE = 300  # 5 minutes


def verify_webhook(headers: dict, body: bytes) -> bool:
    """Verify VIZOCHOK webhook signature.

    Args:
        headers: Request headers dict
        body: Raw request body bytes

    Returns:
        True if signature is valid

    Raises:
        ValueError: If signature is missing, expired, or invalid
    """
    sig_header = headers.get("x-vizochok-signature", "")
    timestamp = headers.get("x-vizochok-timestamp", "")

    # 1. Check signature format
    if not sig_header.startswith("sha256="):
        raise ValueError("Missing or malformed signature header")

    received_sig = sig_header[7:]  # Strip "sha256=" prefix

    # 2. Check timestamp (replay protection)
    if not timestamp:
        raise ValueError("Missing timestamp header")

    try:
        ts = int(timestamp)
    except ValueError:
        raise ValueError("Invalid timestamp format")

    if abs(time.time() - ts) > TIMESTAMP_TOLERANCE:
        raise ValueError("Webhook timestamp expired (possible replay attack)")

    # 3. Construct signed payload
    signed_payload = f"{timestamp}.".encode() + body

    # 4. Compute expected signature
    expected_sig = hmac.new(
        WEBHOOK_SECRET.encode(),
        signed_payload,
        hashlib.sha256,
    ).hexdigest()

    # 5. Constant-time comparison
    if not hmac.compare_digest(received_sig, expected_sig):
        raise ValueError("Invalid webhook signature")

    return True


# Usage with FastAPI
from fastapi import Request, HTTPException

async def verify_request(request: Request):
    body = await request.body()
    try:
        verify_webhook(dict(request.headers), body)
    except ValueError as e:
        raise HTTPException(status_code=401, detail=str(e))

Replay Protection

The timestamp-based replay protection works as follows:
  1. VIZOCHOK includes the current Unix timestamp in the X-VIZOCHOK-Timestamp header
  2. The timestamp is incorporated into the signed payload ({timestamp}.{body}), so it cannot be forged independently of the signature
  3. Your server rejects requests where the timestamp is more than 5 minutes old
This prevents an attacker from capturing a valid webhook request and replaying it later.
Requests are accepted if the timestamp is within 5 minutes of the current time (before or after). A request from 2 minutes ago is accepted; a request from 10 minutes ago is rejected.
The 5-minute window accounts for clock drift between VIZOCHOK’s servers and yours. If your server’s clock is significantly skewed, consider using NTP to synchronize it.

Troubleshooting

Common causes:
  • Re-serialized body: Make sure you verify against the raw request bytes, not a re-serialized version of the parsed JSON
  • Wrong secret: Double-check that you are using the correct webhook secret from your VIZOCHOK admin panel
  • Middleware interference: Some frameworks modify the request body before your handler sees it. Capture the raw bytes before JSON parsing
  • Check that your server’s clock is accurate (use NTP)
  • The tolerance is 5 minutes — if your server clock is off by more than that, timestamps will fail
  • In development, you can temporarily increase the tolerance for testing
During development, you can skip signature verification by not setting a webhook secret in your VIZOCHOK tenant config. However, always verify signatures in production.