WaveSpeedAI APIVerifying Webhook

Verifying Webhooks from WaveSpeedAI

To ensure the authenticity and integrity of webhook events sent by WaveSpeedAI, we strongly recommend verifying every incoming webhook request. This protects your system from spoofed or replayed requests and ensures that only legitimate events are processed.


Why Verify Webhooks?

Webhook endpoints are publicly accessible and may receive requests from anyone. Without verification, a malicious actor could send forged requests to your endpoint, which might trigger unintended or insecure behavior in your system.

By verifying WaveSpeedAI webhook signatures, you can:

  • Ensure the event came from WaveSpeedAI
  • Detect any payload tampering
  • Prevent replay attacks by checking timestamps

Getting Your Webhook Secret

You can retrieve your webhook secret by calling the WaveSpeedAI API:

curl --location --request GET 'https://api.wavespeed.ai/api/v3/webhook/secret' \
--header 'Authorization: Bearer ${YOUR_API_KEY}'

This secret is used to generate HMAC-SHA256 signatures for each webhook request. Keep this key safe and secure. If it’s ever compromised, regenerate it immediately.


Signature Header Format

WaveSpeedAI includes the following headers in every webhook request:

webhook-id: webhook-abc123
webhook-timestamp: 1724100135
webhook-signature: v3,abcdef1234567890...
Content-Type: application/json
  • webhook-id: Unique identifier for the webhook event
  • webhook-timestamp: The Unix timestamp when the event was generated
  • webhook-signature: v3,<signature> formatted HMAC signature for verifying the request

The signature is computed using:

{webhook-id}.{webhook-timestamp}.{raw_body}

Constants used:

const (
  WebhookSignatureVersion = "v3"
  WebhookSignatureSeparator = ","
  WebhookContentSeparator = "."
  WebhookMaxAgeSeconds = 300 // 5 minutes
)

How to Verify the Signature

Step 1: Extract Required Headers and Raw Body

From the incoming HTTP request:

  • Get the webhook-id
  • Get the webhook-timestamp
  • Get the webhook-signature
  • Capture the raw, unparsed request body

Step 2: Construct the Signature Payload

Join the fields using the . separator:

{webhook-id}.{webhook-timestamp}.{raw_body}

Step 3: Compute the HMAC SHA256 Signature

Use your webhook secret to compute the signature:

HMAC_SHA256(secret, "{webhook-id}.{webhook-timestamp}.{raw_body}")

Step 4: Compare Signatures Securely

The webhook-signature header is structured as:

v3,<hex_signature>

To extract the actual signature value, split by the comma (,) and use the second part. Then, use a constant-time comparison function to compare it against your computed HMAC.

Reject requests where the timestamp is too old (older than WebhookMaxAgeSeconds, default: 5 minutes).


Demo Code

Python Example

import hmac
import hashlib
import time
 
def verify_wavespeed_signature(payload: bytes, headers: dict, secret: str):
    webhook_id = headers.get("webhook-id")
    timestamp = headers.get("webhook-timestamp")
    signature_header = headers.get("webhook-signature")
 
    if not (webhook_id and timestamp and signature_header):
        raise Exception("Missing required headers")
 
    parts = signature_header.split(',')
    if parts[0] != 'v3' or len(parts) != 2:
        raise Exception("Invalid signature header format")
 
    received_signature = parts[1]
    signed_content = f"{webhook_id}.{timestamp}.{payload.decode()}".encode()
    expected_signature = hmac.new(secret.encode(), signed_content, hashlib.sha256).hexdigest()
 
    if abs(time.time() - int(timestamp)) > 300:
        raise Exception("Signature timestamp too old")
 
    if not hmac.compare_digest(expected_signature, received_signature):
        raise Exception("Invalid signature")
 
    return True

JavaScript (Node.js) Example

const crypto = require('crypto');
 
function verifyWaveSpeedAISignature(payload, headers, secret) {
  const id = headers['webhook-id'];
  const timestamp = headers['webhook-timestamp'];
  const signatureHeader = headers['webhook-signature'];
 
  if (!id || !timestamp || !signatureHeader) {
    throw new Error('Missing required headers');
  }
 
  const [version, receivedSignature] = signatureHeader.split(',');
  if (version !== 'v3') {
    throw new Error('Invalid signature version');
  }
 
  const signedContent = `${id}.${timestamp}.${payload}`;
  const expectedSignature = crypto
    .createHmac('sha256', secret)
    .update(signedContent)
    .digest('hex');
 
  const ageSeconds = Math.abs(Date.now() / 1000 - parseInt(timestamp, 10));
  if (ageSeconds > 300) {
    throw new Error('Signature timestamp too old');
  }
 
  if (!crypto.timingSafeEqual(Buffer.from(expectedSignature), Buffer.from(receivedSignature))) {
    throw new Error('Invalid signature');
  }
 
  return true;
}

Raw HTTP Shell (for manual testing)

ID="webhook-abc123"
TIMESTAMP=$(date +%s)
BODY='{"id":"webhook-abc123","model":"wavespeed-ai/flux-dev-lora","input":{"prompt":"hello"},"outputs":["https://demo.png"],"urls":{"get":"https://api.wavespeed.ai/api/v3/predictions/webhook-abc123/result"},"has_nsfw_contents":[false],"status":"completed"}'
MESSAGE="$ID.$TIMESTAMP.$BODY"
SECRET="your_webhook_secret_here"
SIGNATURE=$(echo -n "$MESSAGE" | openssl dgst -sha256 -hmac "$SECRET" | sed 's/^.* //')
 
curl -X POST https://your-webhook-url.com \
  -H "webhook-id: $ID" \
  -H "webhook-timestamp: $TIMESTAMP" \
  -H "webhook-signature: v3,$SIGNATURE" \
  -H "Content-Type: application/json" \
  -d "$BODY"

Summary

  • Every webhook from WaveSpeedAI includes: webhook-id, webhook-timestamp, and webhook-signature
  • The signature is computed as: HMAC_SHA256(secret, "webhook-id.timestamp.body")
  • Signature format: v3,<hex_signature>
  • Verify both the signature and the timestamp for security

Webhook verification is critical for security. Never trust webhook data without validation.


For any questions or issues, contact support@wavespeed.ai or refer to the WaveSpeedAI developer portal.

© 2025 WaveSpeedAI. All rights reserved.