Vai al contenuto
Docs

Risoluzione dei problemi

Perché la firma del webhook non corrisponde?

Risolvi gli errori di verifica della firma dei webhook. Firma il corpo grezzo con il secret whsec_ completo e il valore di X-Emailit-Timestamp.

Aggiornato il 1 ott 2026

Questo articolo ti aiuta quando il tuo endpoint rifiuta i webhook di Emailit perché la firma che calcoli non è uguale all’header X-Emailit-Signature. In quasi tutti i casi i byte che firmi o il secret che usi sono diversi da quelli usati da Emailit.

Sintomi

  • Il gestore restituisce 401 o 400 con «invalid signature» per ogni webhook, compresi gli eventi Send test inviati dal pannello.
  • Le richieste nella scheda Requests del webhook mostrano tentativi non riusciti con il codice di stato di errore del tuo endpoint.
  • La verifica funziona in un test unitario con un payload creato a mano, ma non con le consegne reali.

Causa

Emailit firma ogni richiesta così:

Text
signature = hex( HMAC-SHA256( secret, "<X-Emailit-Timestamp>.<raw request body>" ) )

La firma non corrisponde quando uno dei tre input è diverso:

  • Il corpo è stato serializzato di nuovo. Spesso i framework analizzano il JSON prima che venga eseguito il gestore. Chiamare JSON.stringify(req.body) produce spazi e ordine delle chiavi diversi dai byte inviati da Emailit. Il corpo è un array JSON di massimo 100 eventi, quindi anche il codice che si aspetta un oggetto potrebbe modificarne la struttura.
  • Il secret è sbagliato. Usa il valore completo, compreso il prefisso whsec_. Ogni webhook ha il proprio secret, e reimpostarlo invalida subito quello vecchio, anche per i nuovi tentativi in sospeso.
  • Il timestamp manca o è diverso. Usa il valore esatto dell’header X-Emailit-Timestamp e uniscilo al corpo con un solo punto.
  • Il confronto è sbagliato. L’header è in esadecimale minuscolo, non in Base64, e non ha il prefisso sha256=.
  • Qualcosa davanti alla tua app ha modificato il corpo, ad esempio un proxy che lo ricodifica o lo decomprime.

Soluzione

  1. Leggi il corpo grezzo. Cattura i byte prima che venga eseguito qualsiasi parser JSON. In Express, monta express.raw() solo sulla rotta del webhook:

    webhook.js
    import crypto from 'node:crypto';
    
    app.post('/webhooks/emailit', express.raw({ type: 'application/json' }), (req, res) => {
      const timestamp = req.get('X-Emailit-Timestamp');
      const expected = crypto
        .createHmac('sha256', process.env.EMAILIT_WEBHOOK_SECRET) // whsec_...
        .update(`${timestamp}.${req.body.toString('utf8')}`)
        .digest('hex');
      const received = req.get('X-Emailit-Signature') ?? '';
      const valid = received.length === expected.length &&
        crypto.timingSafeEqual(Buffer.from(expected), Buffer.from(received));
      if (!valid) return res.sendStatus(401);
    
      const events = JSON.parse(req.body); // an array of events
      res.sendStatus(200);
    });

    Nei route handler di Next.js, usa await request.text(). In Laravel, usa $request->getContent(). In Flask, usa request.get_data().

  2. Recupera il secret corretto. Chiama Recupera un webhook, che restituisce il secret attuale. Nel pannello il secret viene mostrato una sola volta, quindi se l’hai perso apri il webhook, scegli Webhook secret per reimpostarlo e aggiorna subito la variabile d’ambiente.

  3. Invia un evento di test. Nella pagina del webhook, scegli Send test e seleziona un tipo di evento. Il test è firmato con lo stesso secret, e la finestra mostra il codice di stato e il corpo della risposta del tuo endpoint.

  4. Controlla la tolleranza sull’orario. Se rifiuti i timestamp vecchi per evitare attacchi di replay, concedi qualche minuto di margine e mantieni sincronizzato l’orologio del server. Ogni tentativo di consegna, compresi i nuovi tentativi, ha un timestamp nuovo.

  5. Ritenta ciò che non è riuscito. Quando la verifica funziona, scegli Retry failed per inviare di nuovo le consegne non riuscite degli ultimi 7 giorni.

Per il codice in altri linguaggi, vedi Verifica le firme dei webhook. Per il formato del corpo, vedi Richieste webhook.

Serve ancora aiuto?

Contatta il supporto o chiedi su Discord. Indica l’ID del webhook (wh_…) e l’ID di un evento non riuscito, mai il secret.

Questa pagina ti è stata utile?

Grazie del feedback.

Grazie, leggiamo ogni messaggio.