Guida pratica
Elabora le email in entrata con i webhook
Iscrivi un webhook a email.received, verifica ogni richiesta, poi recupera con l’API corpo, header e allegati di ogni messaggio ricevuto.
Questa guida mostra come reagire alle email ricevute nel tuo codice. Emailit notifica il tuo endpoint con un evento email.received, e il tuo codice recupera il messaggio completo con l’API. Gli esempi verificano la firma, gestiscono i batch di eventi, scaricano gli allegati e saltano i duplicati.
Prima di iniziare
- Le email in entrata sono configurate e un messaggio di prova compare nella scheda Incoming.
- Una chiave API con Full Access. Le chiavi Sending Only non possono leggere il contenuto delle email. Vedi Chiavi API.
- Un endpoint HTTPS pubblico che accetti richieste
POST. Per lo sviluppo in locale, usa un tunnel come ngrok o Cloudflare Tunnel.
Come funziona il flusso
Il webhook ti dice che è arrivato un messaggio. Non contiene il corpo né gli allegati: così le richieste restano leggere e recuperi il contenuto solo quando ti serve.
{
"event_id": "evt_2xGk9Tb4QmF6wN2cJpR7uZsE1kD",
"type": "email.received",
"object": {
"id": "em_2xGk9Rz2NcV7bL4pWqH8sYdT5fA",
"object": "email",
"from": "ada@example.com",
"to": "support@inbound.acme.com",
"subject": "Question about order 1042",
"created_at": "2026-10-01T09:14:05.317000+00:00"
},
"data": {
"object": {
"id": "em_2xGk9Rz2NcV7bL4pWqH8sYdT5fA",
"object": "email",
"from": "ada@example.com",
"to": "support@inbound.acme.com",
"subject": "Question about order 1042",
"created_at": "2026-10-01T09:14:05.317000+00:00"
}
}
}Usa data.object.id con Recupera un’email per ottenere il messaggio analizzato:
{
"object": "email",
"id": "em_2xGk9Rz2NcV7bL4pWqH8sYdT5fA",
"type": "inbound",
"status": "received",
"from": "ada@example.com",
"to": "support@inbound.acme.com",
"subject": "Question about order 1042",
"message_id": "<CAF3x1@mail.example.com>",
"headers": {
"From": "Ada Lovelace <ada@example.com>",
"Reply-To": "ada@example.com",
"Date": "Thu, 01 Oct 2026 09:14:02 +0000"
},
"body": {
"text": "Hi, my order hasn't arrived yet...",
"html": "<p>Hi, my order hasn't arrived yet...</p>"
},
"attachments": [
{
"filename": "receipt.pdf",
"content_type": "application/pdf",
"size": 48213,
"content_id": null,
"content_disposition": "attachment",
"content": "JVBERi0xLjcKJcfsj6IK..."
}
]
}Il content degli allegati è codificato in Base64. I nomi degli header mantengono le maiuscole e minuscole originali, e quando un header compare più volte (come Received) viene mantenuto solo l’ultimo valore. Se ti serve solo una parte del messaggio, usa gli endpoint più specifici: Recupera il corpo, Elenca gli allegati, oppure Recupera il MIME grezzo per il sorgente originale.
Crea il webhook
-
Aggiungi il webhook. Vai a Email APIWebhooks, seleziona Add webhook, inserisci un nome e l’URL del tuo endpoint e seleziona Create.
-
Copia il secret. La finestra mostra il secret del webhook (
whsec_…) una sola volta. Memorizzalo comeEMAILIT_WEBHOOK_SECRETnell’ambiente della tua app. -
Iscriviti solo a email.received. Nella scheda Settings del webhook, disattiva All events, seleziona
email.receivedin Emails e seleziona Save.
Chiama Crea un webhook con l’elenco degli eventi. La risposta include il secret.
curl https://api.emailit.com/v2/webhooks \
-H "Authorization: Bearer $EMAILIT_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"name": "Inbound mail",
"url": "https://acme.com/webhooks/emailit",
"events": ["email.received"]
}'Nei piani Pro, Business e Custom puoi aggiungere un filtro sul payload, così il webhook riceve solo la posta per alcuni indirizzi, ad esempio quelli in cui to termina con @inbound.acme.com.
Scrivi il gestore
Il corpo di ogni richiesta è un array JSON di massimo 100 eventi. Il gestore qui sotto verifica la firma, scorre l’array, salta tutto ciò che non è email.received o che è già stato elaborato e recupera ogni messaggio.
import crypto from 'node:crypto';
import express from 'express';
const app = express();
const WEBHOOK_SECRET = process.env.EMAILIT_WEBHOOK_SECRET; // whsec_...
const API_KEY = process.env.EMAILIT_API_KEY; // secret_..., Full Access
const processed = new Set(); // use a database table with a unique key in production
function verify(rawBody, signature, timestamp) {
if (!signature || !timestamp) return false;
const age = Math.abs(Math.floor(Date.now() / 1000) - Number(timestamp));
if (!(age <= 300)) return false; // 5-minute tolerance
const expected = crypto
.createHmac('sha256', WEBHOOK_SECRET)
.update(`${timestamp}.`)
.update(rawBody)
.digest('hex');
const a = Buffer.from(expected);
const b = Buffer.from(signature);
return a.length === b.length && crypto.timingSafeEqual(a, b);
}
async function fetchEmail(id) {
const res = await fetch(`https://api.emailit.com/v2/emails/${id}`, {
headers: { Authorization: `Bearer ${API_KEY}` },
});
if (!res.ok) throw new Error(`Emailit API returned ${res.status}`);
return res.json();
}
async function handleReceived(event) {
const email = await fetchEmail(event.data.object.id);
console.log(`From ${email.from} to ${email.to}: ${email.subject}`);
console.log(email.body.text ?? email.body.html);
for (const file of email.attachments ?? []) {
const bytes = Buffer.from(file.content, 'base64');
console.log(`Attachment ${file.filename} (${file.content_type}, ${bytes.length} bytes)`);
}
}
// Keep the raw body: the signature is computed over the exact bytes.
app.post('/webhooks/emailit', express.raw({ type: 'application/json' }), async (req, res) => {
if (!verify(req.body, req.get('X-Emailit-Signature'), req.get('X-Emailit-Timestamp'))) {
return res.status(401).send('Invalid signature');
}
const events = JSON.parse(req.body.toString('utf8'));
try {
for (const event of events) {
if (event.type !== 'email.received' || processed.has(event.event_id)) continue;
await handleReceived(event);
processed.add(event.event_id);
}
res.sendStatus(200);
} catch (err) {
console.error(err);
res.sendStatus(500); // Emailit retries the whole batch later
}
});
app.listen(3000);import base64
import hashlib
import hmac
import json
import os
import time
import requests
from flask import Flask, abort, request
app = Flask(__name__)
WEBHOOK_SECRET = os.environ["EMAILIT_WEBHOOK_SECRET"].encode() # whsec_...
API_KEY = os.environ["EMAILIT_API_KEY"] # secret_..., Full Access
processed = set() # use a database table with a unique key in production
def verify(raw_body: bytes, signature: str | None, timestamp: str | None) -> bool:
if not signature or not timestamp:
return False
try:
if abs(time.time() - int(timestamp)) > 300: # 5-minute tolerance
return False
except ValueError:
return False
signed = timestamp.encode() + b"." + raw_body
expected = hmac.new(WEBHOOK_SECRET, signed, hashlib.sha256).hexdigest()
return hmac.compare_digest(expected, signature)
def fetch_email(email_id: str) -> dict:
response = requests.get(
f"https://api.emailit.com/v2/emails/{email_id}",
headers={"Authorization": f"Bearer {API_KEY}"},
timeout=10,
)
response.raise_for_status()
return response.json()
def handle_received(event: dict) -> None:
email = fetch_email(event["data"]["object"]["id"])
print(f"From {email['from']} to {email['to']}: {email['subject']}")
print(email["body"]["text"] or email["body"]["html"])
for attachment in email.get("attachments") or []:
data = base64.b64decode(attachment["content"])
print(f"Attachment {attachment['filename']} ({len(data)} bytes)")
@app.post("/webhooks/emailit")
def emailit_webhook():
raw_body = request.get_data() # raw bytes, before any JSON parsing
if not verify(
raw_body,
request.headers.get("X-Emailit-Signature"),
request.headers.get("X-Emailit-Timestamp"),
):
abort(401)
for event in json.loads(raw_body):
if event["type"] != "email.received" or event["event_id"] in processed:
continue
handle_received(event) # an exception returns 500 and Emailit retries
processed.add(event["event_id"])
return "", 200<?php
$webhookSecret = getenv('EMAILIT_WEBHOOK_SECRET'); // whsec_...
$apiKey = getenv('EMAILIT_API_KEY'); // secret_..., Full Access
// Dedupe table: CREATE TABLE processed_events (event_id TEXT PRIMARY KEY)
$db = new PDO('sqlite:' . __DIR__ . '/events.sqlite');
$rawBody = file_get_contents('php://input');
$signature = $_SERVER['HTTP_X_EMAILIT_SIGNATURE'] ?? '';
$timestamp = $_SERVER['HTTP_X_EMAILIT_TIMESTAMP'] ?? '';
$expected = hash_hmac('sha256', $timestamp . '.' . $rawBody, $webhookSecret);
$fresh = ctype_digit($timestamp) && abs(time() - (int) $timestamp) <= 300;
if (!$fresh || !hash_equals($expected, $signature)) {
http_response_code(401);
exit('Invalid signature');
}
function fetchEmail(string $id, string $apiKey): array
{
$ch = curl_init('https://api.emailit.com/v2/emails/' . rawurlencode($id));
curl_setopt_array($ch, [
CURLOPT_RETURNTRANSFER => true,
CURLOPT_HTTPHEADER => ["Authorization: Bearer {$apiKey}"],
CURLOPT_TIMEOUT => 10,
]);
$body = curl_exec($ch);
$status = curl_getinfo($ch, CURLINFO_RESPONSE_CODE);
curl_close($ch);
if ($status !== 200) {
throw new RuntimeException("Emailit API returned {$status}");
}
return json_decode($body, true);
}
try {
foreach (json_decode($rawBody, true) as $event) {
if ($event['type'] !== 'email.received') {
continue;
}
$seen = $db->prepare('SELECT 1 FROM processed_events WHERE event_id = ?');
$seen->execute([$event['event_id']]);
if ($seen->fetchColumn()) {
continue;
}
$email = fetchEmail($event['data']['object']['id'], $apiKey);
error_log("From {$email['from']} to {$email['to']}: {$email['subject']}");
foreach ($email['attachments'] ?? [] as $attachment) {
$bytes = base64_decode($attachment['content']);
file_put_contents(sys_get_temp_dir() . '/' . basename($attachment['filename']), $bytes);
}
$db->prepare('INSERT INTO processed_events (event_id) VALUES (?)')
->execute([$event['event_id']]);
}
http_response_code(200);
} catch (Throwable $e) {
error_log($e->getMessage());
http_response_code(500); // Emailit retries the whole batch later
}Gestisci nuovi tentativi e duplicati
- Rispondi entro 30 secondi. Emailit attende una risposta
2xxper un massimo di 30 secondi. Un timeout, uno stato diverso da2xxo un reindirizzamento contano come errore, e l’intero batch viene ritentato secondo il calendario dei nuovi tentativi. Se il recupero e l’elaborazione possono richiedere più tempo, memorizza gli eventi in una coda, restituisci200ed elaborali in un job in background. - Elimina i duplicati in base a
event_id. Un batch ritentato contiene di nuovo tutti i suoi eventi, compresi quelli che avevi già gestito prima dell’errore. Registra ognievent_iddopo l’elaborazione e salta gli ID già visti. Ogni messaggio ricevuto ha anche un proprio ID email, che puoi usare come seconda chiave. - Conferma anche gli eventi che non gestisci. Restituisci
2xxanche quando un batch contiene solo tipi di evento che ignori. Altrimenti Emailit continua a ritentarli. - Recupera il contenuto subito. Il contenuto dei messaggi viene conservato per un periodo limitato che dipende dal piano (7 giorni con Pay as you go). Dopo, l’API restituisce l’email senza corpo né allegati. Vedi Conservazione dei dati.
Instrada i messaggi in base all’indirizzo
Poiché ogni parte locale viene accettata, puoi codificare informazioni nell’indirizzo e rileggerle da to. Ad esempio, invia le notifiche con reply_to impostato su reply+4821@inbound.acme.com, poi instrada le risposte al ticket 4821:
const match = email.to.match(/^reply\+(\d+)@inbound\.acme\.com$/i);
if (match) {
await addReplyToTicket(match[1], email.body.text);
}Verifica che funzioni
- Invia un messaggio a un indirizzo del sottodominio di ricezione.
- Nella scheda Requests del webhook, la richiesta
email.receivedrisulta Delivered. - La tua applicazione registra nei log mittente, oggetto ed eventuali allegati.
Se la richiesta risulta Attempting o Failed, seleziona View su una riga non riuscita per vedere il codice di stato e il corpo della risposta restituiti dal tuo endpoint. Vedi Nuovi tentativi ed errori.