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