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 (hex-encoded). Always use a constant-time comparison to prevent timing attacks, and never re-serialize parsed JSON before verifying — use the exact bytes you received.
Each request also carries X-Artos-Event and X-Artos-Event-Id headers. Use the event id to deduplicate, since automatic and manual retries resend the same event.
Node.js / TypeScript
Node.js / TypeScript
import express from 'express'
import { createHmac, timingSafeEqual } from 'crypto'
function verifyWebhook(rawBody: Buffer, signature: string, secret: string): boolean {
const expected = Buffer.from(
createHmac('sha256', secret).update(rawBody).digest('hex'),
'utf8'
)
const received = Buffer.from(signature, 'utf8')
// timingSafeEqual throws if lengths differ, so check first
return expected.length === received.length && timingSafeEqual(expected, received)
}
// Use express.raw() so req.body is the exact bytes Artos signed
app.post('/webhooks/artos', express.raw({ type: 'application/json' }), async (req, res) => {
const signature = req.header('X-Artos-Signature') ?? ''
const secret = process.env.ARTOS_WEBHOOK_SECRET!
if (!verifyWebhook(req.body, signature, secret)) {
return res.status(401).send('Invalid signature')
}
const { id, event, data } = JSON.parse(req.body.toString('utf8'))
// Deduplicate by event id — retries resend the same id
if (await alreadyProcessed(id)) return res.status(200).send('OK')
switch (event) {
case 'payment.success':
// Fulfill order for data.merchantOrderId
break
case 'payment.failed':
break
case 'payment.expired':
break
}
await markProcessed(id)
res.status(200).send('OK')
})Python
Python
import hmac
import hashlib
import json
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']
raw_body = request.get_data() # raw bytes, before any JSON parsing
if not verify_webhook(raw_body, signature, secret):
abort(401)
payload = json.loads(raw_body)
event_id = payload['id'] # deduplicate on this — retries resend the same id
event = payload['event']
data = payload['data']
if event == 'payment.success':
pass # fulfill order for data['merchantOrderId']
elif event == 'payment.failed':
pass
elif event == 'payment.expired':
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);
$eventId = $payload['id']; // deduplicate on this — retries resend the same id
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
}
// Parse rawBody as JSON and deduplicate by its "id" field before processing
w.WriteHeader(http.StatusOK)
w.Write([]byte("OK"))
}