Light Logo

Verify webhooks

Verify that WriftAI webhook requests are authentic and untampered

Short on time?

If you just want to verify webhook requests, skip ahead to Verification Steps or Using official clients.

Why you should verify webhooks

Webhook endpoints are public by design, which means anyone on the internet can send requests to them. If your application treats every incoming webhook as trusted, an attacker could:

  • Fake a prediction update (e.g. mark a prediction as succeeded)
  • Trigger downstream jobs in your system
  • Insert malicious or incorrect data into your database
  • Spam your webhook endpoint to cause operational issues

To prevent this, WriftAI supports signed webhooks which lets you verify the request:

  1. Was sent by WriftAI (authenticity)
  2. Was not modified in transit (integrity)
  3. Is recent enough to reduce replay attacks (freshness)

The signature header

When a webhook secret is configured, WriftAI includes the wriftai-webhook-signature header on every webhook request.

Header format

The signature header is composed of a timestamp followed by a list of signatures and their corresponding version identifiers.

The header is formatted as a comma-separated list of key-value pairs: t=timestamp,<version>=signature[,<version>=signature...]

Where:

  • timestamp is a Unix timestamp (in seconds) indicating when the request was signed
  • <version> is the identifier for the signing scheme
  • signature is a hex-encoded HMAC digest

The signature list is most commonly of length one, though there may be multiple signatures present. Multiple signatures can be included to support key rotation or signing scheme evolution.

Example:

t=1729168452,v1=4f9c2a6b8e3d1a7c0f5b9d6e2c8a4e1f7d3b5a9c6e8f2d4a1b0c9e7f6a3d8b2,v1=9a3e6f5c2b7d4a8f1c0e9b6d5a2f3e8c7d4b1a0f9e6c5b2a8d7f4e3c1b9,v2=8c7a2e9d5f4b6c1a0e3d9f8b2c5a7e6d4f1b9a8c3e5d7a6b2f0c4

Your verifier should treat the webhook request as valid if any signature in the header matches the expected value for a supported signing scheme.

Signing Scheme Version

Currently, WriftAI uses the v1 signing scheme. Additional versions may be added in the future.

What is signed

To generate the signature, WriftAI constructs a string to sign in the format: timestamp.payload

Where:

  • timestamp is the value from the signature header
  • payload is the raw request body exactly as sent

This string is then signed using your webhook secret.

Signature algorithm

WriftAI computes webhook signatures using the following parameters:

ParameterValue
AlgorithmHMAC
Hash functionSHA-256
Signing keyYour webhook secret
Signed messagetimestamp.payload

The resulting HMAC digest is hex-encoded and included in the signature header.

Preventing replay attacks

A replay attack occurs when an attacker captures a legitimate webhook request and re-sends it at a later time. If your server accepts the request again, this can lead to duplicate processing, incorrect state transitions, or unintended side effects.

When verifying a webhook request, you should check that the timestamp in the signature header is recent.

A common and recommended approach is to:

  • Reject requests with timestamps older than a short window (for example, 5 minutes)
  • Allow a small amount of clock skew between your server and WriftAI (for example, accept timestamps within ±5 minutes of your server’s current time)

Verification Steps

To verify an incoming webhook request:

Read the raw request body.

Do not parse or modify the body before verification.

Extract the signature header

Read the wriftai-webhook-signature header from the request.

Parse the header values

  • Extract the timestamp (t)
  • Extract all versioned signatures (<version>=signature)

Validate the timestamp

  • Compute the age of the request using the timestamp
  • Reject the request if it falls outside your allowed time window (for example, ±5 minutes)

Reconstruct the string to sign

timestamp.payload Where payload is the raw request body.

Compute the expected signature

  • Use the signing algorithm for the supported scheme
  • Sign the string using your webhook secret

Compare signatures securely

  • Compare the computed signature against each signature provided in the header
  • Use a constant-time comparison function
  • Treat the request as valid if any signature matches

Reject unsupported schemes

  • Ignore signatures with versions you do not support

Use the raw request body

Verification must be done using the raw request body, exactly as received. Parsing JSON and re-serializing it may change whitespace or key ordering and cause signature verification to fail.

If no provided signature matches, the webhook request must be rejected.

Verifying webhooks using official clients

While you are free to implement webhook verification yourself, WriftAI provides verification helpers in its official clients.

from wriftai import verify_webhook, WebhookSignatureVerificationError

try:
    # use default tolerance and scheme
    verify_webhook(
        payload=raw_body, # bytes exactly as received
        signature=signature_header,
        secret=webhook_secret,
    )
    # Signature is valid — continue processing the webhook
except WebhookSignatureVerificationError:
    # Signature verification failed
    # Reject the webhook and do not process it
import { verifyWebhook, SignatureVerificationError } from 'wriftai';

try {
  // use default tolerance and scheme
  await verifyWebhook(
    rawBody,          // Uint8Array, exactly as received
    signatureHeader,
    webhookSecret,
  );
  // Signature is valid — continue processing the webhook
} catch (err) {
  if (err instanceof WebhookSignatureVerificationError) {
    // Signature verification failed
    // Reject the webhook and do not process it
  } else {
    // Unexpected error
    throw err;
  }
}
import (
	"errors"

	"github.com/wriftai/wriftai-go"
)

err := wriftai.VerifyWebhook(
	rawBody,          // []byte exactly as received
	signatureHeader,
	webhookSecret,
	nil,              // Use default tolerance
	nil,              // Use default scheme
)

if err == nil {
	// Signature is valid — continue processing the webhook
} else if errors.Is(err, wriftai.ErrWebhookTimestampOutsideTolerance) ||
	errors.Is(err, wriftai.ErrWebhookNoTimestamp) ||
	errors.Is(err, wriftai.ErrWebhookNoSignatures) ||
	errors.Is(err, wriftai.ErrWebhookSignatureMismatch) {
	// Signature verification failed
	// Reject the webhook and do not process it
} else {
	// Unexpected error
	panic(err)
}

Advanced verification options

You can customize verification behavior by providing optional arguments such as timestamp tolerance or signing scheme. In most cases, the defaults are sufficient. Refer to the API reference of each client for more details.