Verifying Signatures
To verify a webhook request is authentic, compute the HMAC-SHA256 of the raw request body using your webhook secret and compare it to the X-Artos-Signature header. Always use a timing-safe comparison to prevent timing attacks.
Node.js / TypeScript
Node.js / TypeScript
import { createHmac, timingSafeEqual } from 'crypto'
function verifyWebhook(
rawBody: string | Buffer,
signature: string,
secret: string
): boolean {
const expected = createHmac('sha256', secret)
.update(rawBody)
.digest('hex')
return timingSafeEqual(
Buffer.from(expected, 'hex'),
Buffer.from(signature, 'hex')
)
}
app.post('/webhooks/artos', express.raw({ type: 'application/json' }), (req, res) => {
const signature = req.headers['x-artos-signature'] as string
const secret = process.env.ARTOS_WEBHOOK_SECRET!
if (!signature || !verifyWebhook(req.body, signature, secret)) {
return res.status(401).send('Invalid signature')
}
const { event, data } = JSON.parse(req.body.toString())
switch (event) {
case 'payment.success':
// data.transactionId for idempotency
break
case 'payment.failed':
break
case 'payment.expired':
break
}
res.status(200).send('OK')
})Python
Python
import hmac
import hashlib
import os
from flask import Flask, request, abort
app = Flask(__name__)
def verify_webhook(raw_body: bytes, signature: str, secret: str) -> bool:
expected = hmac.new(
secret.encode('utf-8'),
raw_body,
hashlib.sha256
).hexdigest()
return hmac.compare_digest(expected, signature)
@app.route('/webhooks/artos', methods=['POST'])
def webhook():
signature = request.headers.get('X-Artos-Signature', '')
secret = os.environ['ARTOS_WEBHOOK_SECRET']
if not verify_webhook(request.get_data(), signature, secret):
abort(401)
payload = request.get_json()
event = payload['event']
if event == 'payment.success':
pass
elif event == 'payment.failed':
pass
return 'OK', 200PHP
PHP
<?php
function verifyWebhook(string $rawBody, string $signature, string $secret): bool {
$expected = hash_hmac('sha256', $rawBody, $secret);
return hash_equals($expected, $signature);
}
$rawBody = file_get_contents('php://input');
$signature = $_SERVER['HTTP_X_ARTOS_SIGNATURE'] ?? '';
$secret = getenv('ARTOS_WEBHOOK_SECRET');
if (!verifyWebhook($rawBody, $signature, $secret)) {
http_response_code(401);
exit('Invalid signature');
}
$payload = json_decode($rawBody, true);
switch ($payload['event']) {
case 'payment.success':
break;
case 'payment.failed':
break;
case 'payment.expired':
break;
}
http_response_code(200);
echo 'OK';Go
Go
package main
import (
"crypto/hmac"
"crypto/sha256"
"encoding/hex"
"io"
"net/http"
"os"
)
func verifyWebhook(rawBody []byte, signature, secret string) bool {
mac := hmac.New(sha256.New, []byte(secret))
mac.Write(rawBody)
expected := mac.Sum(nil)
sigBytes, err := hex.DecodeString(signature)
if err != nil {
return false
}
return hmac.Equal(expected, sigBytes)
}
func webhookHandler(w http.ResponseWriter, r *http.Request) {
rawBody, _ := io.ReadAll(r.Body)
signature := r.Header.Get("X-Artos-Signature")
secret := os.Getenv("ARTOS_WEBHOOK_SECRET")
if !verifyWebhook(rawBody, signature, secret) {
http.Error(w, "Invalid signature", http.StatusUnauthorized)
return
}
w.WriteHeader(http.StatusOK)
w.Write([]byte("OK"))
}