Search docs

Search the API documentation

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', 200

PHP

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"))
}