Webhook Security
Verifying webhooks
When you receive a webhook, verify it came from Wava before processing it. We recommend:
- Verify the signature — API integrations with signing enabled receive an
X-Wava-Signature header. See Signature Verification below.
- Verify the order — After receiving a webhook, call
GET /v1/orders/{orderId} with your merchant key to confirm the order status matches what the webhook reported.
- Use HTTPS — Always use an HTTPS endpoint for your webhook URL in production.
Signature verification
API integrations with HMAC signing enabled receive a signature in the X-Wava-Signature header on every webhook request. Use it to confirm the payload has not been tampered with.
How it works
Wava generates the signature by creating an HMAC-SHA256 hash of the JSON payload using your shared secret, then sends the hexadecimal result in the X-Wava-Signature header.
Content-Type: application/json
X-Wava-Event: order_payment
X-Wava-Timestamp: 2024-12-18T10:30:00.000Z
X-Wava-Signature: a1b2c3d4e5f6...
User-Agent: Wava-Webhooks/1.0
Verification examples
const crypto = require('crypto');
function verifyWavaWebhook(req, res, next) {
const signature = req.headers['x-wava-signature'];
if (!signature) {
return res.status(401).json({ error: 'Missing signature' });
}
const payloadString = JSON.stringify(req.body);
const expectedSignature = crypto
.createHmac('sha256', process.env.WAVA_WEBHOOK_SECRET)
.update(payloadString)
.digest('hex');
const isValid = crypto.timingSafeEqual(
Buffer.from(signature),
Buffer.from(expectedSignature)
);
if (!isValid) {
return res.status(401).json({ error: 'Invalid signature' });
}
next();
}
import hmac
import hashlib
import json
import os
def verify_webhook_signature(payload, signature):
secret = os.environ['WAVA_WEBHOOK_SECRET']
payload_string = json.dumps(payload, separators=(',', ':'))
expected = hmac.new(
secret.encode('utf-8'),
payload_string.encode('utf-8'),
hashlib.sha256
).hexdigest()
return hmac.compare_digest(signature, expected)
function verifyWebhookSignature($payload, $signature) {
$secret = $_ENV['WAVA_WEBHOOK_SECRET'];
$payloadString = json_encode($payload, JSON_UNESCAPED_SLASHES);
$expected = hash_hmac('sha256', $payloadString, $secret);
return hash_equals($signature, $expected);
}
Always use a constant-time comparison function (timingSafeEqual, compare_digest, hash_equals) to prevent timing attacks. Never use === or == to compare signatures.
Important notes
- The
X-Wava-Signature is a plain hexadecimal string — no prefix like sha256=.
- Serialize the payload using
JSON.stringify() with default options. Property order matters.
- Store your secret in an environment variable — never commit it to source control.
- Signature signing is optional and must be enabled in your integration configuration. Contact support if you need it enabled.
Idempotency
Your webhook handler should be idempotent — processing the same webhook multiple times should produce the same result. Wava may send the same webhook more than once in rare cases (retries, network issues).
Use the id_order or id_external field to deduplicate incoming webhooks.
Never trust webhook data alone for critical business logic (e.g., shipping an order). Always verify the order status via the API before taking action.