Guide pratique
Traiter les e-mails entrants avec des webhooks
Abonnez un webhook à email.received, vérifiez chaque requête, puis récupérez via l’API le corps, les en-têtes et les pièces jointes de chaque message reçu.
Ce guide explique comment réagir aux e-mails reçus dans votre propre code. Emailit notifie votre endpoint avec un événement email.received, et votre code récupère le message complet via l’API. Les exemples vérifient la signature, gèrent les lots d’événements, téléchargent les pièces jointes et ignorent les doublons.
Avant de commencer
- La réception est configurée et un message de test apparaît dans l’onglet Incoming.
- Une clé API Full Access. La lecture du contenu des e-mails n’est pas autorisée avec les clés Sending Only. Consultez Clés API.
- Un endpoint HTTPS public qui accepte les requêtes
POST. En développement local, utilisez un tunnel comme ngrok ou Cloudflare Tunnel.
Fonctionnement du flux
Le webhook vous indique qu’un message est arrivé. Il ne contient ni le corps ni les pièces jointes, ce qui garde les requêtes légères et vous permet de ne récupérer le contenu que lorsque vous en avez besoin.
{
"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"
}
}
}Utilisez data.object.id avec Récupérer un e-mail pour obtenir le message analysé :
{
"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..."
}
]
}Le content des pièces jointes est encodé en Base64. Les noms d’en-têtes conservent leur casse d’origine et, lorsqu’un en-tête apparaît plusieurs fois (comme Received), seule la dernière valeur est conservée. Si vous n’avez besoin que d’une partie du message, utilisez les endpoints plus ciblés : Récupérer le corps, Lister les pièces jointes, ou Récupérer le MIME brut pour la source d’origine.
Créer le webhook
-
Ajoutez le webhook. Accédez à Email APIWebhooks, sélectionnez Add webhook, saisissez un nom et l’URL de votre endpoint, puis sélectionnez Create.
-
Copiez le secret. La boîte de dialogue affiche une seule fois le secret du webhook (
whsec_…). Stockez-le sous le nomEMAILIT_WEBHOOK_SECRETdans l’environnement de votre application. -
Abonnez-vous uniquement à email.received. Dans l’onglet Settings du webhook, désactivez All events, sélectionnez
email.receivedsous Emails, puis sélectionnez Save.
Appelez Créer un webhook avec la liste des événements. La réponse inclut le 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"]
}'Sur les forfaits Pro, Business et Custom, vous pouvez ajouter un filtre de contenu pour que le webhook ne reçoive que les e-mails de certaines adresses, par exemple lorsque to se termine par @inbound.acme.com.
Écrire le gestionnaire
Chaque corps de requête est un tableau JSON de 100 événements au maximum. Le gestionnaire ci-dessous vérifie la signature, parcourt le tableau, ignore tout ce qui n’est pas email.received ou a déjà été traité, et récupère chaque message.
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
}Gérer les nouvelles tentatives et les doublons
- Répondez dans les 30 secondes. Emailit attend une réponse
2xxpendant 30 secondes au maximum. Un timeout, un statut autre que2xxou une redirection comptent comme un échec, et tout le lot fait l’objet d’une nouvelle tentative selon le calendrier des nouvelles tentatives. Si la récupération et le traitement peuvent prendre plus de temps, placez les événements dans une file d’attente, renvoyez200et traitez-les dans une tâche en arrière-plan. - Dédoublonnez par
event_id. Un lot renvoyé contient de nouveau tous ses événements, y compris ceux que vous aviez déjà traités avant l’échec. Enregistrez chaqueevent_idaprès traitement et ignorez les ID déjà vus. Chaque message reçu a aussi son propre ID d’e-mail, utilisable comme seconde clé. - Accusez réception des événements que vous ne traitez pas. Renvoyez
2xxmême quand un lot ne contient que des types d’événements que vous ignorez. Sinon, Emailit continue de les renvoyer. - Récupérez le contenu rapidement. Le contenu des messages est conservé pendant une durée limitée qui dépend de votre forfait (7 jours sur Pay as you go). Passé ce délai, l’API renvoie l’e-mail sans son corps ni ses pièces jointes. Consultez Conservation des données.
Router les messages selon l’adresse
Comme toutes les parties locales sont acceptées, vous pouvez encoder des informations dans l’adresse et les relire dans to. Par exemple, envoyez des notifications avec reply_to défini sur reply+4821@inbound.acme.com, puis routez les réponses vers le ticket 4821 :
const match = email.to.match(/^reply\+(\d+)@inbound\.acme\.com$/i);
if (match) {
await addReplyToTicket(match[1], email.body.text);
}Vérifier le résultat
- Envoyez un message à une adresse de votre sous-domaine de réception.
- Dans l’onglet Requests du webhook, la requête
email.receivedaffiche Delivered. - Votre application consigne l’expéditeur, l’objet et les éventuelles pièces jointes.
Si la requête affiche Attempting ou Failed, sélectionnez View sur une ligne en échec pour voir le code de statut et le corps de réponse renvoyés par votre endpoint. Consultez Nouvelles tentatives et échecs.