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 (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', 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);
$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"))
}