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 eventwebhook-timestamp
: The Unix timestamp when the event was generatedwebhook-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.
Step 5: Validate Timestamp (recommended)
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
, andwebhook-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.