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.

Come creare un webhook

Puoi creare e gestire i tuoi webhook dalla dashboard, nella pagina Webhooks, oppure programmaticamente tramite l'endpoint /webhook/. Durante la creazione dovrai specificare:

  • Descrizione: un nome o una nota per identificare il webhook
  • URL: l'endpoint del tuo server che riceverà le notifiche
  • Eventi: gli eventi per i quali vuoi ricevere notifiche (vedi Eventi supportati)
  • Azienda: l'azienda per la quale il webhook sarà attivo. Se non ne indichi nessuna, il webhook sarà attivo per tutte le aziende del tuo account
  • Abilitato: se il webhook è attivo o disabilitato

Al termine della creazione ti verrà mostrato il secret del webhook. Conservalo in modo sicuro: è necessario per validare le firme e non sarà più visibile in seguito.

Perché usare i webhook invece del polling

L'alternativa ai webhook è il polling: interrogare periodicamente l'API per verificare se ci sono novità. Questo approccio presenta diversi svantaggi:

  • Latenza: con il polling scopri le novità solo al prossimo ciclo di interrogazione, che può essere di minuti o ore. I webhook ti notificano in tempo reale, non appena l'evento si verifica.
  • Spreco di risorse: la maggior parte delle chiamate di polling non restituisce nulla di nuovo, consumando inutilmente banda e risorse sia della API che della tua integrazione.
  • Rate limiting: le chiamate ripetute del polling possono farti raggiungere i limiti di frequenza, causando errori e ritardi.
  • Complessità: il polling richiede di gestire intervalli, paginazione e stato dell'ultima interrogazione. I webhook invertono il flusso: è Invoicetronic a chiamare te quando c'è qualcosa di nuovo.

I diagrammi seguenti illustrano la differenza tra i due approcci.

Polling

Il client interroga ripetutamente l'API, sprecando risorse quando non ci sono novità:

sequenceDiagram
  participant Client
  box Invoicetronic
    participant API
  end
  Client->>API: GET (/receive/) - novità?
  API-->>Client: Nessuna novità
  Client->>API: GET (/receive/) - novità?
  API-->>Client: Nessuna novità
  Client->>API: GET (/receive/) - novità?
  API-->>Client: Nessuna novità
  Note over Client,API: ⏱️ minuti o ore dopo...
  Client->>API: GET (/receive/) - novità?
  API-->>Client: Sì, nuova fattura!
  Note right of Client: Scoperta con ritardo

Webhook

L'API notifica il client in tempo reale, solo quando c'è qualcosa di nuovo:

sequenceDiagram
  participant Client
  box Invoicetronic
    participant API
  end
  Note over Client,API: Evento: nuova fattura ricevuta
  API->>Client: POST (webhook endpoint)
  Note right of API: Include header Invoicetronic-Signature
  Client->>Client: Valida firma HMAC-SHA256
  Client-->>API: 200 OK
  Note right of Client: Notifica in tempo reale,<br/>nessuna chiamata sprecata

In sintesi, i webhook sono più efficienti, più reattivi e più semplici da gestire rispetto al polling.

Quando il polling ha senso

Se per motivi tecnici o infrastrutturali non hai la possibilità di esporre un endpoint pubblico raggiungibile da Invoicetronic, il polling resta un'alternativa valida. In questo caso, consulta le best practice sul rate limiting per ottimizzare la frequenza delle chiamate.

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

Esempi di validazione

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.

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.

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.

<?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.

import javax.crypto.Mac;
import javax.crypto.spec.SecretKeySpec;
import java.nio.charset.StandardCharsets;
import java.security.InvalidKeyException;
import java.security.MessageDigest;
import java.security.NoSuchAlgorithmException;
import java.time.Instant;

public class WebhookValidator {
    private final String secret;

    public WebhookValidator(String secret) {
        this.secret = secret;
    }

    public boolean validateSignature(String signatureHeader, String payload) {
        try {
            // Parse l'header: "t=1234567890,v1=abcdef..."
            String[] parts = signatureHeader.split(",");
            if (parts.length != 2) {
                return false;
            }

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

            // Verifica che il timestamp non sia troppo vecchio (max 5 minuti)
            long timestampValue = Long.parseLong(timestamp);
            long now = Instant.now().getEpochSecond();
            if (Math.abs(now - timestampValue) > 300) {
                return false;
            }

            // Calcola la firma attesa
            String message = timestamp + "." + payload;
            String expectedSignature = computeHmacSha256(message);

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

        } catch (Exception e) {
            return false;
        }
    }

    private String computeHmacSha256(String message)
            throws NoSuchAlgorithmException, InvalidKeyException {
        Mac hmac = Mac.getInstance("HmacSHA256");
        SecretKeySpec secretKey = new SecretKeySpec(
            secret.getBytes(StandardCharsets.UTF_8),
            "HmacSHA256"
        );
        hmac.init(secretKey);
        byte[] hashBytes = hmac.doFinal(message.getBytes(StandardCharsets.UTF_8));

        // Converti in esadecimale minuscolo
        StringBuilder hexString = new StringBuilder();
        for (byte b : hashBytes) {
            String hex = Integer.toHexString(0xff & b);
            if (hex.length() == 1) hexString.append('0');
            hexString.append(hex);
        }
        return hexString.toString();
    }

    private boolean timingSafeEqual(String a, String b) {
        if (a.length() != b.length()) {
            return false;
        }

        int result = 0;
        for (int i = 0; i < a.length(); i++) {
            result |= a.charAt(i) ^ b.charAt(i);
        }
        return result == 0;
    }
}

// Uso in Spring Boot
import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.*;
import java.io.IOException;
import java.util.Map;

@RestController
public class WebhookController {
    private final WebhookValidator validator =
        new WebhookValidator("wh_sec_your_secret_here");

    @PostMapping("/webhook")
    public ResponseEntity<?> handleWebhook(
            @RequestHeader("Invoicetronic-Signature") String signature,
            @RequestBody String payload) {

        if (!validator.validateSignature(signature, payload)) {
            return ResponseEntity
                .status(HttpStatus.UNAUTHORIZED)
                .body(Map.of("error", "Invalid signature"));
        }

        // Processa il webhook
        // WebhookEvent event = objectMapper.readValue(payload, WebhookEvent.class);

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

        return ResponseEntity.ok(Map.of("status", "ok"));
    }
}

@RequestBody String

Usa @RequestBody String invece di deserializzare direttamente in un oggetto per mantenere il payload raw necessario alla validazione.

require 'openssl'
require 'json'

class WebhookValidator
  def initialize(secret)
    @secret = secret
  end

  def validate_signature(signature_header, payload)
    # Parse l'header
    parts = signature_header.split(',')
    return false if parts.length != 2

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

    # Verifica timestamp (max 5 minuti)
    now = Time.now.to_i
    return false if (now - timestamp.to_i).abs > 300

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

    # Confronto sicuro
    timing_safe_equal(expected_signature, received_signature)
  end

  private

  def compute_hmac_sha256(message)
    OpenSSL::HMAC.hexdigest('sha256', @secret, message)
  end

  def timing_safe_equal(a, b)
    return false if a.length != b.length

    # Ruby 2.5.1+ ha Rack::Utils.secure_compare
    # Per versioni precedenti, implementiamo il confronto XOR
    result = 0
    a.bytes.zip(b.bytes) { |x, y| result |= x ^ y }
    result.zero?
  end
end

# Uso in Sinatra
require 'sinatra'

validator = WebhookValidator.new('wh_sec_your_secret_here')

post '/webhook' do
  signature = request.env['HTTP_INVOICETRONIC_SIGNATURE']
  payload = request.body.read

  unless validator.validate_signature(signature, payload)
    status 401
    return { error: 'Invalid signature' }.to_json
  end

  # Processa il webhook
  webhook_data = JSON.parse(payload)

  # ... elabora l'evento ...

  content_type :json
  { status: 'ok' }.to_json
end

# Uso in Rails
# class WebhooksController < ApplicationController
#   skip_before_action :verify_authenticity_token
#
#   def create
#     validator = WebhookValidator.new('wh_sec_your_secret_here')
#     signature = request.headers['Invoicetronic-Signature']
#     payload = request.raw_post
#
#     unless validator.validate_signature(signature, payload)
#       render json: { error: 'Invalid signature' }, status: :unauthorized
#       return
#     end
#
#     webhook_data = JSON.parse(payload)
#     # ... elabora l'evento ...
#
#     render json: { status: 'ok' }
#   end
# end

request.body.read / request.raw_post

In Sinatra usa request.body.read, in Rails usa request.raw_post per ottenere il payload raw prima del parsing JSON.

package main

import (
    "crypto/hmac"
    "crypto/sha256"
    "crypto/subtle"
    "encoding/hex"
    "encoding/json"
    "io"
    "math"
    "net/http"
    "strconv"
    "strings"
    "time"
)

type WebhookValidator struct {
    secret string
}

func NewWebhookValidator(secret string) *WebhookValidator {
    return &WebhookValidator{secret: secret}
}

func (v *WebhookValidator) ValidateSignature(signatureHeader, payload string) bool {
    // Parse l'header: "t=1234567890,v1=abcdef..."
    parts := strings.Split(signatureHeader, ",")
    if len(parts) != 2 {
        return false
    }

    timestamp := strings.TrimPrefix(parts[0], "t=")
    receivedSignature := strings.TrimPrefix(parts[1], "v1=")

    // Verifica che il timestamp non sia troppo vecchio (max 5 minuti)
    timestampValue, err := strconv.ParseInt(timestamp, 10, 64)
    if err != nil {
        return false
    }
    now := time.Now().Unix()
    if math.Abs(float64(now-timestampValue)) > 300 {
        return false
    }

    // Calcola la firma attesa
    message := timestamp + "." + payload
    expectedSignature := v.computeHmacSha256(message)

    // Confronto sicuro contro timing attacks
    return subtle.ConstantTimeCompare(
        []byte(expectedSignature),
        []byte(receivedSignature),
    ) == 1
}

func (v *WebhookValidator) computeHmacSha256(message string) string {
    h := hmac.New(sha256.New, []byte(v.secret))
    h.Write([]byte(message))
    return hex.EncodeToString(h.Sum(nil))
}

// Uso in un handler HTTP
func main() {
    validator := NewWebhookValidator("wh_sec_your_secret_here")

    http.HandleFunc("/webhook", func(w http.ResponseWriter, r *http.Request) {
        // Leggi l'header
        signature := r.Header.Get("Invoicetronic-Signature")

        // Leggi il body RAW
        bodyBytes, err := io.ReadAll(r.Body)
        if err != nil {
            http.Error(w, "Error reading body", http.StatusBadRequest)
            return
        }
        payload := string(bodyBytes)

        // Valida la firma
        if !validator.ValidateSignature(signature, payload) {
            w.Header().Set("Content-Type", "application/json")
            w.WriteHeader(http.StatusUnauthorized)
            json.NewEncoder(w).Encode(map[string]string{
                "error": "Invalid signature",
            })
            return
        }

        // Processa il webhook
        var webhookData map[string]interface{}
        if err := json.Unmarshal(bodyBytes, &webhookData); err != nil {
            http.Error(w, "Invalid JSON", http.StatusBadRequest)
            return
        }

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

        w.Header().Set("Content-Type", "application/json")
        json.NewEncoder(w).Encode(map[string]string{
            "status": "ok",
        })
    })

    http.ListenAndServe(":8080", nil)
}

io.ReadAll(r.Body)

Leggi il body con io.ReadAll(r.Body) prima di deserializzarlo, così puoi usare i byte raw per la validazione.

subtle.ConstantTimeCompare()

Go fornisce crypto/subtle.ConstantTimeCompare() per confronti timing-safe nativi.

import crypto from 'crypto';
import express, { Request, Response } from 'express';

class WebhookValidator {
    private secret: string;

    constructor(secret: string) {
        this.secret = secret;
    }

    validateSignature(signatureHeader: string, payload: string): boolean {
        // 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);
    }

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

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

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

interface WebhookEvent {
    id: number;
    user_id: number;
    company_id: number;
    resource_id: number;
    endpoint: string;
    method: string;
    status_code: number;
    success: boolean;
    date_time: string;
    api_version: number;
}

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

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

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

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

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

app.listen(3000, () => {
    console.log('Webhook server listening on port 3000');
});

Type safety

TypeScript ti permette di definire interfacce per i dati del webhook, migliorando la sicurezza dei tipi e l'autocomplete nell'IDE.

Best Practices per la Sicurezza

1. Validazione del Timestamp

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

var maxAge = TimeSpan.FromMinutes(5);
var requestAge = DateTimeOffset.UtcNow - DateTimeOffset.FromUnixTimeSeconds(timestamp);
if (requestAge > maxAge)
    return false;
const now = Math.floor(Date.now() / 1000);
if (Math.abs(now - parseInt(timestamp)) > 300)
    return false;
now = int(time.time())
if abs(now - int(timestamp)) > 300:
    return False
$now = time();
if (abs($now - (int)$timestamp) > 300) {
    return false;
}
long now = Instant.now().getEpochSecond();
if (Math.abs(now - timestampValue) > 300) {
    return false;
}
now = Time.now.to_i
return false if (now - timestamp.to_i).abs > 300
now := time.Now().Unix()
if math.Abs(float64(now-timestampValue)) > 300 {
    return false
}
const now = Math.floor(Date.now() / 1000);
if (Math.abs(now - parseInt(timestamp)) > 300)
    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!
// CORRETTO
const payload = req.body.toString('utf8');
if (!validator.validateSignature(signature, payload))
    return res.status(401).json({ error: 'Invalid signature' });
const data = JSON.parse(payload);

// SBAGLIATO
const data = req.body; // già parsato da express.json()
if (!validator.validateSignature(signature, JSON.stringify(data)))
    return res.status(401).json({ error: 'Invalid' }); // stringify potrebbe differire!
# CORRETTO
payload = request.get_data(as_text=True)
if not validator.validate_signature(signature, payload):
    return jsonify({'error': 'Invalid'}), 401
data = json.loads(payload)

# SBAGLIATO
data = request.get_json()
if not validator.validate_signature(signature, json.dumps(data)):
    return jsonify({'error': 'Invalid'}), 401  # dumps potrebbe differire!
// CORRETTO
$payload = file_get_contents('php://input');
if (!$validator->validateSignature($signature, $payload)) {
    http_response_code(401);
    exit;
}
$data = json_decode($payload, true);

// SBAGLIATO
$data = json_decode(file_get_contents('php://input'), true);
if (!$validator->validateSignature($signature, json_encode($data))) {
    http_response_code(401); // json_encode potrebbe differire!
    exit;
}
// CORRETTO
String payload = requestBody; // raw string
if (!validator.validateSignature(signature, payload))
    return ResponseEntity.status(401).build();
Event data = objectMapper.readValue(payload, Event.class);

// SBAGLIATO
Event data = objectMapper.readValue(requestBody, Event.class);
if (!validator.validateSignature(signature, objectMapper.writeValueAsString(data)))
    return ResponseEntity.status(401).build(); // writeValueAsString potrebbe differire!
# CORRETTO
payload = request.body.read
unless validator.validate_signature(signature, payload)
  return [401, { error: 'Invalid' }.to_json]
end
data = JSON.parse(payload)

# SBAGLIATO
data = JSON.parse(request.body.read)
unless validator.validate_signature(signature, data.to_json)
  return [401, { error: 'Invalid' }.to_json] # to_json potrebbe differire!
end
// CORRETTO
bodyBytes, _ := io.ReadAll(r.Body)
payload := string(bodyBytes)
if !validator.ValidateSignature(signature, payload) {
    http.Error(w, "Invalid", http.StatusUnauthorized)
    return
}
json.Unmarshal(bodyBytes, &data)

// SBAGLIATO
json.NewDecoder(r.Body).Decode(&data)
reEncoded, _ := json.Marshal(data)
if !validator.ValidateSignature(signature, string(reEncoded)) { // Marshal potrebbe differire!
    http.Error(w, "Invalid", http.StatusUnauthorized)
}
// CORRETTO
const payload = req.body.toString('utf8');
if (!validator.validateSignature(signature, payload))
    return res.status(401).json({ error: 'Invalid signature' });
const data: WebhookEvent = JSON.parse(payload);

// SBAGLIATO
const data: WebhookEvent = req.body; // già parsato
if (!validator.validateSignature(signature, JSON.stringify(data)))
    return res.status(401).json({ error: 'Invalid' }); // stringify potrebbe differire!

5. Disabilitazione Automatica

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

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

    return Ok();
}
app.post('/webhook', (req, res) => {
    if (shouldDisableWebhook)
        return res.status(410).send(); // Gone

    res.json({ status: 'ok' });
});
@app.route('/webhook', methods=['POST'])
def handle_webhook():
    if should_disable_webhook:
        return '', 410  # Gone

    return jsonify({'status': 'ok'})
if ($shouldDisableWebhook) {
    http_response_code(410); // Gone
    exit;
}

http_response_code(200);
echo json_encode(['status' => 'ok']);
@PostMapping("/webhook")
public ResponseEntity<?> handleWebhook() {
    if (shouldDisableWebhook)
        return ResponseEntity.status(HttpStatus.GONE).build(); // 410

    return ResponseEntity.ok(Map.of("status", "ok"));
}
post '/webhook' do
  if should_disable_webhook
    status 410 # Gone
    return
  end

  { status: 'ok' }.to_json
end
if shouldDisableWebhook {
    w.WriteHeader(http.StatusGone) // 410
    return
}

w.Header().Set("Content-Type", "application/json")
json.NewEncoder(w).Encode(map[string]string{"status": "ok"})
app.post('/webhook', (req: Request, res: Response) => {
    if (shouldDisableWebhook)
        return res.status(410).send(); // Gone

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

Dashboard

Puoi anche disabilitare (o riabilitare) un webhook dalla dashboard, nella pagina Webhooks, senza dover modificare il codice.

Notifiche di fallimento

Se la consegna di un webhook fallisce 5 volte consecutive, Invoicetronic invia automaticamente un'email di notifica all'indirizzo associato al tuo account. L'email include:

  • L'URL del webhook che sta fallendo
  • L'ultimo codice di stato HTTP ricevuto
  • Il messaggio di errore associato

Dopo l'invio della notifica, ne verrà inviata un'altra solo dopo 24 ore, per evitare spam. Il contatore dei fallimenti si resetta automaticamente alla prima consegna riuscita.

Storico consegne

Puoi consultare lo storico delle consegne nella dashboard, nella pagina Webhooks, per diagnosticare e correggere eventuali problemi con il tuo endpoint.

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).