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:
Dove:
t: Unix timestamp in secondi (quando è stata generata la richiesta)v1: Firma HMAC-SHA256 calcolata come segue:- Concatena il timestamp, un punto e il payload JSON:
{timestamp}.{jsonPayload} - Calcola HMAC-SHA256 usando il secret del webhook come chiave
- Converti il risultato in stringa esadecimale minuscola
- Concatena il timestamp, un punto e il payload JSON:
Validazione lato client
Per validare un webhook ricevuto, devi:
- Estrarre timestamp e firma dall'header
Invoicetronic-Signature - Verificare che il timestamp non sia troppo vecchio (es. max 5 minuti)
- Ricalcolare la firma usando il secret e confrontarla con quella ricevuta
- 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 inviatasend.delete- Fattura inviata eliminatareceive.add- Nuova fattura ricevutareceive.delete- Fattura ricevuta eliminataupdate.add- Nuovo aggiornamento di statoupdate.delete- Aggiornamento di stato eliminatocompany.add- Nuova azienda creatacompany.delete- Azienda eliminata*- Tutti gli eventi
Testare i Webhook
Durante lo sviluppo, puoi usare servizi come:
- webhook.site - Ispeziona le richieste ricevute
- ngrok - Esponi il tuo server locale
- Postman - Testa endpoint webhook
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
- Verifica che l'URL sia raggiungibile pubblicamente
- Controlla che il webhook sia abilitato (
enabled: true) - Verifica che l'evento sia nella lista degli eventi registrati
- Consulta lo storico webhook nella dashboard
Firma non valida
- Assicurati di leggere il body prima di deserializzarlo
- Verifica di usare il secret corretto (inizia con
wh_sec_) - Controlla che il confronto della firma sia timing-safe
- 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).