---
title: Webhook eventi
description: Il webhook outbound di Audin Mail — ricevere sugli endpoint dei tuoi sistemi gli eventi delle email inviate (consegne, aperture, click, bounce), verificare la firma HMAC, gestire retry e idempotenza, ruotare il signing secret.
---

Audin Mail può inoltrare in tempo reale a un **tuo endpoint HTTPS** ogni
evento delle email inviate dall'account: consegne, aperture, click, bounce,
segnalazioni spam. Utile per CRM esterni, sistemi di BI o automazioni sui
tuoi sistemi.

Si configura da **Impostazioni → Audin Mail → "Webhook outbound"**
(l'integrazione deve essere attiva): inserisci l'URL, premi **"Configura e
genera secret"** e **salva subito il signing secret** — è mostrato **una
sola volta**. Dalla stessa card puoi attivare/disattivare il webhook,
modificare l'URL, aggiungere header custom, inviare un **evento di test** e
consultare le **ultime consegne** con payload e risposta del tuo endpoint.

## Requisiti dell'endpoint

- **HTTPS** obbligatorio, raggiungibile da internet pubblico.
- Accetta `POST` con corpo `application/json`.
- Risponde con un **2xx entro 10 secondi** (elabora in asincrono se serve).

## Cosa ricevi

Gli eventi arrivano **in batch**: una richiesta può contenere più eventi.

```json
{
  "audin_event_id": "evt_…",
  "audin_account_id": "…",
  "received_at": "2026-08-17T10:15:00.000Z",
  "version": "1",
  "events": [
    {
      "audin_message_id": "8b1f…",
      "sendgrid": {
        "event": "delivered",
        "email": "cliente@esempio.it",
        "timestamp": 1755425700
      }
    }
  ]
}
```

- **`audin_message_id`** è la chiave di correlazione con l'email inviata da
  Audin (può essere `null` per eventi non riconducibili a un invio, es.
  l'evento di test).
- **`sendgrid`** è il payload originale del fornitore di recapito,
  inoltrato così com'è: il nome della chiave fa parte del contratto e non
  cambia. I campi utili sono `event` (tipo evento), `email`, `timestamp`,
  ed eventuali `reason` (bounce), `url` (click).
- Tipi di evento inoltrati: `processed`, `delivered`, `open`, `click`,
  `bounce`, `dropped`, `deferred`, `spamreport`.
- Un bump di `version` (oggi `"1"`) segnalerà eventuali cambi di formato
  incompatibili.

### Header di ogni richiesta

| Header | Contenuto |
|---|---|
| `User-Agent` | `Audin-Mail-Webhook/1.0` |
| `X-Audin-Webhook-Id` | Identificativo fisso del webhook del tuo account |
| `X-Audin-Delivery-Id` | Identificativo della singola consegna — **usalo come chiave di idempotenza** (i retry lo ripetono) |
| `X-Audin-Signature-Timestamp` | Unix timestamp (secondi) usato nella firma |
| `X-Audin-Signature` | `v1=<hex>` — firma HMAC-SHA256 (vedi sotto) |

Gli eventuali **header custom** configurati (max 5) vengono aggiunti a ogni
richiesta.

## Verifica della firma (obbligatoria)

Ogni richiesta è firmata: verifica **sempre** la firma prima di fidarti del
contenuto.

1. `signed_payload = timestamp + "." + corpo_raw_della_richiesta`
2. `atteso = HMAC_SHA256(secret, signed_payload)` in esadecimale
3. confronta (in **constant-time**) con il valore dopo `v1=` nell'header
4. **anti-replay**: rifiuta richieste con timestamp più vecchio di 5 minuti

<Callout type="warn">
  La firma va calcolata sul **corpo raw** della richiesta, non sul JSON
  ri-serializzato: qualsiasi middleware che fa il parse del body prima della
  verifica (es. `express.json()`) la invalida.
</Callout>

<Tabs items={["Node.js / Express", "Python / Flask"]}>
<Tab value="Node.js / Express">

```js
const crypto = require("crypto");
const express = require("express");
const app = express();

app.post("/audin/webhook",
  express.raw({ type: "application/json" }), // body RAW, non json()
  (req, res) => {
    const ts = req.header("X-Audin-Signature-Timestamp");
    const sig = req.header("X-Audin-Signature"); // "v1=<hex>"

    if (Math.abs(Date.now() / 1000 - Number(ts)) > 300) {
      return res.status(400).send("timestamp too old");
    }

    const expected = "v1=" + crypto
      .createHmac("sha256", process.env.AUDIN_WEBHOOK_SECRET)
      .update(`${ts}.${req.body.toString("utf8")}`)
      .digest("hex");

    const ok = sig && crypto.timingSafeEqual(
      Buffer.from(sig), Buffer.from(expected));
    if (!ok) return res.status(401).send("bad signature");

    const payload = JSON.parse(req.body.toString("utf8"));
    // elabora payload.events in asincrono, poi:
    res.sendStatus(200);
  });
```

</Tab>
<Tab value="Python / Flask">

```python
import hashlib, hmac, os, time
from flask import Flask, request, abort

app = Flask(__name__)

@app.post("/audin/webhook")
def audin_webhook():
    ts = request.headers.get("X-Audin-Signature-Timestamp", "")
    sig = request.headers.get("X-Audin-Signature", "")
    raw = request.get_data()  # body RAW, non request.json

    if abs(time.time() - int(ts or 0)) > 300:
        abort(400)

    expected = "v1=" + hmac.new(
        os.environ["AUDIN_WEBHOOK_SECRET"].encode(),
        f"{ts}.".encode() + raw,
        hashlib.sha256,
    ).hexdigest()

    if not hmac.compare_digest(sig, expected):
        abort(401)

    # elabora in asincrono, poi:
    return "", 200
```

</Tab>
</Tabs>

## Risposta attesa e retry

- Qualunque **2xx** = consegna riuscita.
- **410 Gone** = "non inviarmi più questo": la consegna viene marcata
  fallita **senza** retry.
- Ogni altro errore (4xx, 5xx, timeout oltre 10s, DNS irraggiungibile)
  attiva i retry: **6 tentativi in tutto** — il primo immediato, poi dopo
  1 minuto, 5 minuti, 30 minuti, 2 ore e 12 ore (circa 15 ore complessive)
  — dopodiché la consegna è marcata **Fallita**.
- I retry ripetono lo stesso `X-Audin-Delivery-Id`: rendi l'elaborazione
  **idempotente** su quella chiave.
- Nella card "Ultime consegne" vedi ogni tentativo con stato HTTP, durata e
  corpo della risposta del tuo endpoint (troncato).

## Rotazione del signing secret

**"Rigenera"** genera un nuovo secret, attivo **immediatamente**: Audin
firma da subito solo col nuovo. Il precedente resta consultabile per 24 ore
a fini di supporto. Procedura senza downtime: predisponi la verifica a
doppio secret (o preparati ad aggiornare l'env), clicca "Rigenera", salva
il nuovo secret (mostrato una volta sola) e aggiornalo nei tuoi sistemi.

## Test in sviluppo

Per sviluppare in locale esponi l'endpoint con un tunnel (ngrok, Cloudflare
Tunnel) o ispeziona i payload con un servizio tipo webhook.site. Il
pulsante **"Invia evento di test"** fa una POST sincrona (evento
`event: "test"`, `audin_message_id: null`) e mostra subito status, durata e
risposta — senza retry.

## Checklist di sicurezza

- endpoint solo HTTPS, secret in un secret manager (mai nel codice);
- verifica HMAC in constant-time su **ogni** richiesta + anti-replay 5 min;
- risposta 2xx entro 10 secondi, elaborazione asincrona;
- idempotenza su `X-Audin-Delivery-Id`;
- gli header custom **non** sostituiscono la verifica della firma.

## Prossimi passi

<Cards>
  <Card
    href="/docs/api-rest/outbound-webhooks"
    title="Webhook in uscita (CRM)"
    description="Il sistema gemello per gli eventi CRM (lead, commenti): stessa firma, stesso stile di verifica."
  />
  <Card
    href="/docs/audin-mail"
    title="Audin Mail"
    description="Attivazione, domini, mittenti e quota del canale email nativo."
  />
</Cards>
