Vai al contenuto

Webhooks

I webhook ti permettono di ricevere notifiche in tempo reale quando si verificano eventi nella tua integrazione con Invoicetronic API. Ogni webhook include un secret che devi usare per validare l'autenticità delle chiamate ricevute.

Sicurezza

È fondamentale validare sempre la firma dei webhook per garantire che le richieste provengano effettivamente da Invoicetronic e non da fonti malevole.

Come funziona la firma

Quando Invoicetronic invia una notifica webhook al tuo endpoint, include un header HTTP Invoicetronic-Signature con la seguente struttura:

Invoicetronic-Signature: t=1733395200,v1=a1b2c3d4e5f6789...

Dove:

  • t: Unix timestamp in secondi (quando è stata generata la richiesta)
  • v1: Firma HMAC-SHA256 calcolata come segue:
    1. Concatena il timestamp, un punto e il payload JSON: {timestamp}.{jsonPayload}
    2. Calcola HMAC-SHA256 usando il secret del webhook come chiave
    3. Converti il risultato in stringa esadecimale minuscola

Validazione lato client

Per validare un webhook ricevuto, devi:

  1. Estrarre timestamp e firma dall'header Invoicetronic-Signature
  2. Verificare che il timestamp non sia troppo vecchio (es. max 5 minuti)
  3. Ricalcolare la firma usando il secret e confrontarla con quella ricevuta
  4. Usare un confronto sicuro contro timing attacks

Esempio C# / .NET

using System.Security.Cryptography;
using System.Text;

public class WebhookValidator
{
    private readonly string _secret;

    public WebhookValidator(string secret)
    {
        _secret = secret;
    }

    public bool ValidateSignature(string signatureHeader, string payload)
    {
        // Parse l'header: "t=1234567890,v1=abcdef..."
        var parts = signatureHeader.Split(',');
        if (parts.Length != 2) return false;

        var timestamp = parts[0].Replace("t=", "");
        var receivedSignature = parts[1].Replace("v1=", "");

        // Verifica che il timestamp non sia troppo vecchio (max 5 minuti)
        var timestampValue = long.Parse(timestamp);
        var now = DateTimeOffset.UtcNow.ToUnixTimeSeconds();
        if (Math.Abs(now - timestampValue) > 300)
            return false;

        // Calcola la firma attesa
        var message = $"{timestamp}.{payload}";
        var expectedSignature = ComputeHmacSha256(message, _secret);

        // Confronto sicuro contro timing attacks
        return TimingSafeEqual(expectedSignature, receivedSignature);
    }

    private string ComputeHmacSha256(string message, string secret)
    {
        var encoding = Encoding.UTF8;
        var keyBytes = encoding.GetBytes(secret);
        var messageBytes = encoding.GetBytes(message);

        using var hmac = new HMACSHA256(keyBytes);
        var hashBytes = hmac.ComputeHash(messageBytes);

        return BitConverter.ToString(hashBytes)
            .Replace("-", "")
            .ToLower();
    }

    private bool TimingSafeEqual(string a, string b)
    {
        if (a.Length != b.Length) return false;

        var result = 0;
        for (var i = 0; i < a.Length; i++)
            result |= a[i] ^ b[i];

        return result == 0;
    }
}

// Uso nell'endpoint ASP.NET Core
[HttpPost("webhook")]
public async Task<IActionResult> HandleWebhook(
    [FromHeader(Name = "Invoicetronic-Signature")] string signature)
{
    // Leggi il body come stringa RAW
    using var reader = new StreamReader(Request.Body, Encoding.UTF8);
    var payload = await reader.ReadToEndAsync();

    var validator = new WebhookValidator("wh_sec_your_secret_here");

    if (!validator.ValidateSignature(signature, payload))
        return Unauthorized(new { error = "Invalid signature" });

    // Processa il webhook
    var webhookData = JsonSerializer.Deserialize<WebhookEvent>(payload);

    // ... elabora l'evento ...

    return Ok();
}

Body RAW

È importante leggere il body della richiesta prima di deserializzarlo in JSON, per mantenere la rappresentazione esatta dei byte ricevuti.

Esempio Node.js / JavaScript

const crypto = require('crypto');
const express = require('express');

class WebhookValidator {
    constructor(secret) {
        this.secret = secret;
    }

    validateSignature(signatureHeader, payload) {
        // Parse l'header
        const parts = signatureHeader.split(',');
        if (parts.length !== 2) return false;

        const timestamp = parts[0].replace('t=', '');
        const receivedSignature = parts[1].replace('v1=', '');

        // Verifica timestamp (max 5 minuti)
        const now = Math.floor(Date.now() / 1000);
        if (Math.abs(now - parseInt(timestamp)) > 300)
            return false;

        // Calcola firma attesa
        const message = `${timestamp}.${payload}`;
        const expectedSignature = this.computeHmacSha256(message);

        // Confronto sicuro
        return this.timingSafeEqual(expectedSignature, receivedSignature);
    }

    computeHmacSha256(message) {
        return crypto
            .createHmac('sha256', this.secret)
            .update(message, 'utf8')
            .digest('hex');
    }

    timingSafeEqual(a, b) {
        if (a.length !== b.length) return false;
        return crypto.timingSafeEqual(
            Buffer.from(a, 'utf8'),
            Buffer.from(b, 'utf8')
        );
    }
}

// Uso in Express
const app = express();
const validator = new WebhookValidator('wh_sec_your_secret_here');

app.post('/webhook', express.raw({ type: 'application/json' }), (req, res) => {
    const signature = req.headers['invoicetronic-signature'];
    const payload = req.body.toString('utf8');

    if (!validator.validateSignature(signature, payload)) {
        return res.status(401).json({ error: 'Invalid signature' });
    }

    // Processa il webhook
    const webhookData = JSON.parse(payload);

    // ... elabora l'evento ...

    res.json({ status: 'ok' });
});

express.raw()

Usa express.raw({ type: 'application/json' }) invece di express.json() per mantenere il body originale necessario alla validazione.

Esempio Python

import hmac
import hashlib
import time
from flask import Flask, request, jsonify

class WebhookValidator:
    def __init__(self, secret: str):
        self.secret = secret

    def validate_signature(self, signature_header: str, payload: str) -> bool:
        # Parse l'header
        parts = signature_header.split(',')
        if len(parts) != 2:
            return False

        timestamp = parts[0].replace('t=', '')
        received_signature = parts[1].replace('v1=', '')

        # Verifica timestamp (max 5 minuti)
        now = int(time.time())
        if abs(now - int(timestamp)) > 300:
            return False

        # Calcola firma attesa
        message = f"{timestamp}.{payload}"
        expected_signature = self.compute_hmac_sha256(message)

        # Confronto sicuro
        return hmac.compare_digest(expected_signature, received_signature)

    def compute_hmac_sha256(self, message: str) -> str:
        return hmac.new(
            self.secret.encode('utf-8'),
            message.encode('utf-8'),
            hashlib.sha256
        ).hexdigest()

# Uso in Flask
app = Flask(__name__)
validator = WebhookValidator('wh_sec_your_secret_here')

@app.route('/webhook', methods=['POST'])
def handle_webhook():
    signature = request.headers.get('Invoicetronic-Signature')
    payload = request.get_data(as_text=True)

    if not validator.validate_signature(signature, payload):
        return jsonify({'error': 'Invalid signature'}), 401

    # Processa il webhook
    webhook_data = request.get_json()

    # ... elabora l'evento ...

    return jsonify({'status': 'ok'})

request.get_data()

Usa request.get_data(as_text=True) prima di request.get_json() per ottenere il payload raw necessario alla validazione.

Esempio PHP

<?php

class WebhookValidator
{
    private string $secret;

    public function __construct(string $secret)
    {
        $this->secret = $secret;
    }

    public function validateSignature(string $signatureHeader, string $payload): bool
    {
        // Parse l'header: "t=1234567890,v1=abcdef..."
        $parts = explode(',', $signatureHeader);
        if (count($parts) !== 2) {
            return false;
        }

        $timestamp = str_replace('t=', '', $parts[0]);
        $receivedSignature = str_replace('v1=', '', $parts[1]);

        // Verifica che il timestamp non sia troppo vecchio (max 5 minuti)
        $now = time();
        if (abs($now - (int)$timestamp) > 300) {
            return false;
        }

        // Calcola la firma attesa
        $message = $timestamp . '.' . $payload;
        $expectedSignature = $this->computeHmacSha256($message);

        // Confronto sicuro contro timing attacks
        return hash_equals($expectedSignature, $receivedSignature);
    }

    private function computeHmacSha256(string $message): string
    {
        return hash_hmac('sha256', $message, $this->secret);
    }
}

// Uso in uno script PHP standalone o con framework
$validator = new WebhookValidator('wh_sec_your_secret_here');

// Leggi l'header
$signature = $_SERVER['HTTP_INVOICETRONIC_SIGNATURE'] ?? '';

// Leggi il body RAW (importante!)
$payload = file_get_contents('php://input');

if (!$validator->validateSignature($signature, $payload)) {
    http_response_code(401);
    header('Content-Type: application/json');
    echo json_encode(['error' => 'Invalid signature']);
    exit;
}

// Processa il webhook
$webhookData = json_decode($payload, true);

// ... elabora l'evento ...

http_response_code(200);
header('Content-Type: application/json');
echo json_encode(['status' => 'ok']);

file_get_contents('php://input')

Usa file_get_contents('php://input') per leggere il body raw della richiesta. Non usare $_POST perché non funziona con application/json.

hash_equals()

PHP fornisce hash_equals() (da PHP 5.6+) che effettua un confronto timing-safe per prevenire timing attacks.

Best Practices per la Sicurezza

1. Validazione del Timestamp

Verifica sempre che il timestamp non sia troppo vecchio per prevenire replay attacks:

// Esempio: max 5 minuti di differenza
var maxAge = TimeSpan.FromMinutes(5);
var requestAge = DateTimeOffset.UtcNow - DateTimeOffset.FromUnixTimeSeconds(timestamp);
if (requestAge > maxAge)
    return false;

2. Confronto Sicuro

Usa sempre funzioni di confronto timing-safe per prevenire timing attacks:

  • C#: Implementa un confronto custom XOR-based
  • Node.js: crypto.timingSafeEqual()
  • Python: hmac.compare_digest()
  • PHP: hash_equals()

3. Gestione del Secret

Il secret è mostrato solo al momento della creazione del webhook. Conservalo in modo sicuro:

  • Usa variabili d'ambiente
  • Usa secret manager (Azure Key Vault, AWS Secrets Manager, etc.)
  • Cripta i secret nel database
  • Non committare mai i secret nel codice sorgente

4. Payload Originale

Valida sempre il payload nella sua forma originale prima di deserializzarlo:

// CORRETTO
var payload = await ReadBodyAsString();
if (!validator.ValidateSignature(signature, payload))
    return Unauthorized();
var data = JsonSerializer.Deserialize<Event>(payload);

// SBAGLIATO
var data = await Request.ReadFromJsonAsync<Event>();
if (!validator.ValidateSignature(signature, JsonSerializer.Serialize(data)))
    return Unauthorized(); // La serializzazione potrebbe differire!

5. Disabilitazione Automatica

Se vuoi disabilitare un webhook, rispondi con HTTP 410 Gone. Invoicetronic lo disabiliterà automaticamente:

[HttpPost("webhook")]
public IActionResult HandleWebhook()
{
    // Se vuoi disabilitare il webhook
    if (shouldDisableWebhook)
        return StatusCode(410); // Gone

    // Altrimenti processa normalmente
    return Ok();
}

Struttura dell'Evento

Gli eventi webhook seguono la struttura della risorsa Event (vedi API Reference):

{
  "id": 12345,
  "user_id": 100,
  "company_id": 42,
  "resource_id": 789,
  "endpoint": "send",
  "method": "POST",
  "status_code": 201,
  "success": true,
  "date_time": "2024-01-20T10:30:00Z",
  "api_version": 1
}

Eventi Supportati

Puoi registrare webhook per i seguenti eventi:

  • send.add - Nuova fattura inviata
  • send.delete - Fattura inviata eliminata
  • receive.add - Nuova fattura ricevuta
  • receive.delete - Fattura ricevuta eliminata
  • update.add - Nuovo aggiornamento di stato
  • update.delete - Aggiornamento di stato eliminato
  • company.add - Nuova azienda creata
  • company.delete - Azienda eliminata
  • * - Tutti gli eventi

Testare i Webhook

Durante lo sviluppo, puoi usare servizi come:

Ambiente Sandbox

Ricorda che puoi testare i webhook nell'ambiente sandbox usando la tua API key di test (ik_test_...).

Risoluzione Problemi

Il webhook non arriva

  1. Verifica che l'URL sia raggiungibile pubblicamente
  2. Controlla che il webhook sia abilitato (enabled: true)
  3. Verifica che l'evento sia nella lista degli eventi registrati
  4. Consulta lo storico webhook nella dashboard

Firma non valida

  1. Assicurati di leggere il body prima di deserializzarlo
  2. Verifica di usare il secret corretto (inizia con wh_sec_)
  3. Controlla che il confronto della firma sia timing-safe
  4. Verifica che la codifica sia ASCII per HMAC-SHA256

Il webhook viene disabilitato

Invoicetronic disabilita automaticamente i webhook che rispondono con HTTP 410 Gone. Se non vuoi che un webhook venga disabilitato, assicurati di rispondere con altri status code anche in caso di errore (es. 200, 500).