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.
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
401o400con «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ì:
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-Timestampe 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
-
Leggi il corpo grezzo. Cattura i byte prima che venga eseguito qualsiasi parser JSON. In Express, monta
express.raw()solo sulla rotta del webhook: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, usarequest.get_data(). -
Recupera il secret corretto. Chiama Recupera un webhook, che restituisce il
secretattuale. 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. -
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.
-
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.
-
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.