Webhook Implementation
Guida tecnica per implementare un endpoint webhook sicuro per ricevere eventi da UniMsg.
Signature Verification
Ogni webhook include una firma HMAC-SHA256 per verificare l'autenticità:
Header di Sicurezza
| Header | Descrizione |
|---|---|
X-UniMsg-Signature |
HMAC-SHA256 del body |
X-UniMsg-Timestamp |
Unix timestamp della richiesta |
X-UniMsg-Event |
Tipo di evento |
Algoritmo di Verifica
// La firma è calcolata come:
signature = HMAC-SHA256(
key: webhook_secret,
message: timestamp + "." + raw_body
)
Implementazione PHP
<?php
class WebhookHandler
{
private string $secret;
private int $toleranceSeconds = 300; // 5 minuti
public function __construct(string $secret)
{
$this->secret = $secret;
}
public function handle(): void
{
$payload = file_get_contents('php://input');
$signature = $_SERVER['HTTP_X_UNIMSG_SIGNATURE'] ?? '';
$timestamp = $_SERVER['HTTP_X_UNIMSG_TIMESTAMP'] ?? '';
// 1. Verifica timestamp (previeni replay attack)
if (abs(time() - (int)$timestamp) > $this->toleranceSeconds) {
http_response_code(401);
exit('Timestamp expired');
}
// 2. Calcola firma attesa
$signedPayload = $timestamp . '.' . $payload;
$expectedSignature = hash_hmac('sha256', $signedPayload, $this->secret);
// 3. Confronto timing-safe
if (!hash_equals($expectedSignature, $signature)) {
http_response_code(401);
exit('Invalid signature');
}
// 4. Processa evento
$event = json_decode($payload, true);
$this->processEvent($event);
http_response_code(200);
}
private function processEvent(array $event): void
{
switch ($event['event']) {
case 'message.delivered':
$this->handleDelivered($event['data']);
break;
case 'message.failed':
$this->handleFailed($event['data']);
break;
// ... altri eventi
}
}
}
Implementazione Node.js
const crypto = require('crypto');
const express = require('express');
const app = express();
const WEBHOOK_SECRET = process.env.WEBHOOK_SECRET;
const TOLERANCE_SECONDS = 300;
// Importante: raw body per verifica signature
app.use('/webhook', express.raw({ type: 'application/json' }));
app.post('/webhook', (req, res) => {
const signature = req.headers['x-unimsg-signature'];
const timestamp = req.headers['x-unimsg-timestamp'];
const rawBody = req.body;
// 1. Verifica timestamp
if (Math.abs(Date.now() / 1000 - parseInt(timestamp)) > TOLERANCE_SECONDS) {
return res.status(401).send('Timestamp expired');
}
// 2. Calcola firma attesa
const signedPayload = `${timestamp}.${rawBody}`;
const expectedSignature = crypto
.createHmac('sha256', WEBHOOK_SECRET)
.update(signedPayload)
.digest('hex');
// 3. Confronto timing-safe
const signatureBuffer = Buffer.from(signature, 'hex');
const expectedBuffer = Buffer.from(expectedSignature, 'hex');
if (!crypto.timingSafeEqual(signatureBuffer, expectedBuffer)) {
return res.status(401).send('Invalid signature');
}
// 4. Processa evento
const event = JSON.parse(rawBody);
processEvent(event);
res.status(200).send('OK');
});
function processEvent(event) {
console.log(`Received: ${event.event}`, event.data);
// ... gestione evento
}
Implementazione Python
import hmac
import hashlib
import time
from flask import Flask, request
app = Flask(__name__)
WEBHOOK_SECRET = 'your_webhook_secret'
TOLERANCE_SECONDS = 300
@app.route('/webhook', methods=['POST'])
def webhook():
signature = request.headers.get('X-UniMsg-Signature', '')
timestamp = request.headers.get('X-UniMsg-Timestamp', '')
raw_body = request.get_data(as_text=True)
# 1. Verifica timestamp
if abs(time.time() - int(timestamp)) > TOLERANCE_SECONDS:
return 'Timestamp expired', 401
# 2. Calcola firma attesa
signed_payload = f"{timestamp}.{raw_body}"
expected_signature = hmac.new(
WEBHOOK_SECRET.encode(),
signed_payload.encode(),
hashlib.sha256
).hexdigest()
# 3. Confronto timing-safe
if not hmac.compare_digest(signature, expected_signature):
return 'Invalid signature', 401
# 4. Processa evento
event = request.json
process_event(event)
return 'OK', 200
def process_event(event):
print(f"Received: {event['event']}", event['data'])
Idempotency
I webhook possono essere riconsegnati. Usa l'ID evento per deduplicazione:
function processEvent(event) {
const eventId = event.id;
// Check se già processato
if (await redis.exists(`webhook:${eventId}`)) {
console.log(`Event ${eventId} already processed`);
return;
}
// Processa
await handleEvent(event);
// Marca come processato (TTL 7 giorni)
await redis.setex(`webhook:${eventId}`, 604800, '1');
}
Retry Handling
UniMsg ritenta i webhook falliti con backoff esponenziale:
| Tentativo | Ritardo | Timeout |
|---|---|---|
| 1 | Immediato | 30s |
| 2 | 1 min | 30s |
| 3 | 5 min | 30s |
| 4 | 30 min | 30s |
| 5 | 2 ore | 30s |
Il tuo endpoint deve:
- Rispondere entro 30 secondi
- Restituire status 2xx per successo
- Qualsiasi altro status = fallimento, retry
Best Practice: Async Processing
Rispondi subito e processa in background:
app.post('/webhook', async (req, res) => {
// Verifica signature...
// Rispondi subito
res.status(200).send('OK');
// Processa in background
setImmediate(() => {
processEvent(JSON.parse(req.body));
});
});
// Oppure usa una queue
app.post('/webhook', async (req, res) => {
// Verifica signature...
// Accoda per elaborazione
await queue.add('webhook-event', JSON.parse(req.body));
res.status(200).send('OK');
});
Testing Locale
Per testare webhook in locale, usa ngrok:
# 1. Avvia ngrok
ngrok http 3000
# 2. Usa l'URL ngrok nella dashboard UniMsg
# https://abc123.ngrok.io/webhook
# 3. Invia webhook di test dalla dashboard