Webhook security
Webhook signature verification
All webhook requests include an HMAC-SHA256 signature in the X-Moveo-Signature header. Always verify this signature before processing the request.
Algorithm:
signature = HEX(HMAC-SHA256(webhook_secret, request_body))
- Node.js
- Python
- Go
- PHP
const crypto = require("crypto");
function verifySignature(body, signature, secret) {
const computed = crypto
.createHmac("sha256", secret)
.update(body, "utf8")
.digest("hex");
return crypto.timingSafeEqual(Buffer.from(computed), Buffer.from(signature));
}
// Express.js middleware example
app.post("/webhook", express.raw({ type: "application/json" }), (req, res) => {
const signature = req.headers["x-moveo-signature"];
const isValid = verifySignature(req.body, signature, WEBHOOK_SECRET);
if (!isValid) {
return res.status(401).send("Invalid signature");
}
const event = JSON.parse(req.body);
// Process event...
res.status(200).send("OK");
});
import hmac
import hashlib
def verify_signature(body: bytes, signature: str, secret: str) -> bool:
computed = hmac.new(
secret.encode('utf-8'),
body,
hashlib.sha256
).hexdigest()
return hmac.compare_digest(computed, signature)
# Flask example
from flask import Flask, request
@app.route('/webhook', methods=['POST'])
def webhook():
signature = request.headers.get('X-Moveo-Signature')
is_valid = verify_signature(request.data, signature, WEBHOOK_SECRET)
if not is_valid:
return 'Invalid signature', 401
event = request.json
# Process event...
return 'OK', 200
import (
"crypto/hmac"
"crypto/sha256"
"encoding/hex"
)
func verifySignature(body []byte, signature, secret string) bool {
mac := hmac.New(sha256.New, []byte(secret))
mac.Write(body)
expected := hex.EncodeToString(mac.Sum(nil))
return hmac.Equal([]byte(expected), []byte(signature))
}
function verifySignature($body, $signature, $secret) {
$computed = hash_hmac('sha256', $body, $secret);
return hash_equals($computed, $signature);
}
// Usage
$body = file_get_contents('php://input');
$signature = $_SERVER['HTTP_X_MOVEO_SIGNATURE'];
$isValid = verifySignature($body, $signature, $webhookSecret);
if (!$isValid) {
http_response_code(401);
exit('Invalid signature');
}
$event = json_decode($body, true);
// Process event...
Replay attack prevention
Every webhook payload includes a root timestamp field (Unix milliseconds). To guard against replay attacks, compare this value to the current time and reject requests older than your tolerance window (e.g., 5 minutes). Always verify the signature first — the timestamp is only trustworthy if the payload has not been tampered with.
Webhook response requirements
Your webhook endpoint must:
- Return HTTP 2xx status (200-299) to acknowledge receipt.
- Respond quickly (within a few seconds).
| Your response | Moveo behavior |
|---|---|
| 2xx | Success — message marked as delivered. |
| 4xx | Permanent failure — message marked as failed. |
| 5xx | Temporary failure — retried with exponential backoff. |
| Timeout | Temporary failure — retried with exponential backoff. |