Webhook Verification
When the partner key has a webhook_secret configured, every webhook request includes an HMAC-SHA256 signature in the X-Loyva-Signature header. Always verify this signature before processing the webhook.
Signature format
The header value is always prefixed with sha256=:
X-Loyva-Signature: sha256=a1b2c3d4e5f6...
Strip the prefix before comparing. The signature itself is the lowercase hex-encoded HMAC-SHA256 of the raw request body using the key's webhook_secret.
How verification works
- Loyva signs the raw request body with your webhook secret using HMAC-SHA256
- The signature is sent as
sha256=<hex>inX-Loyva-Signature - You recompute the signature over the raw body and compare with timing-safe equality
Implementation
Node.js (Express)
import crypto from 'crypto';
function verifyWebhookSignature(rawBody, signatureHeader, secret) {
if (!signatureHeader?.startsWith('sha256=')) return false;
const provided = signatureHeader.slice('sha256='.length);
const expected = crypto
.createHmac('sha256', secret)
.update(rawBody)
.digest('hex');
const a = Buffer.from(provided, 'hex');
const b = Buffer.from(expected, 'hex');
if (a.length !== b.length) return false;
return crypto.timingSafeEqual(a, b);
}
// IMPORTANT: use express.raw so req.body is the exact bytes Loyva signed
app.post(
'/webhooks/loyva',
express.raw({ type: 'application/json' }),
(req, res) => {
const signature = req.headers['x-loyva-signature'];
const valid = verifyWebhookSignature(
req.body,
signature,
process.env.LOYVA_WEBHOOK_SECRET,
);
if (!valid) return res.status(401).json({ error: 'Invalid signature' });
res.status(200).json({ received: true });
const event = JSON.parse(req.body.toString('utf8'));
handleWebhook(event);
},
);
Python
import hmac
import hashlib
def verify_signature(raw_body: bytes, signature_header: str, secret: str) -> bool:
if not signature_header or not signature_header.startswith("sha256="):
return False
provided = signature_header.removeprefix("sha256=")
expected = hmac.new(secret.encode("utf-8"), raw_body, hashlib.sha256).hexdigest()
return hmac.compare_digest(provided, expected)
Go
func verifySignature(rawBody []byte, signatureHeader, secret string) bool {
if !strings.HasPrefix(signatureHeader, "sha256=") {
return false
}
provided := strings.TrimPrefix(signatureHeader, "sha256=")
mac := hmac.New(sha256.New, []byte(secret))
mac.Write(rawBody)
expected := hex.EncodeToString(mac.Sum(nil))
return hmac.Equal([]byte(provided), []byte(expected))
}
Important notes
- Always use timing-safe comparison to prevent timing attacks
- Verify against the raw request body bytes — parsed JSON may be re-serialized differently and invalidate the signature
- Remember to strip the
sha256=prefix before comparing - Retries reuse the same
event_id; dedupe onevent_idto ensure idempotent processing - If
X-Loyva-Signatureis missing, your key has nowebhook_secretconfigured — set one viaPATCH /partners/keys/:key_idand rotate to require signature verification