# 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](/fr/docs/inbound/set-up/) 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](/fr/docs/developers/api-keys/).
- 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.

```json title="email.received (un événement du tableau de la requête)"
{
  "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](/fr/docs/api-reference/emails/get/) pour obtenir le message analysé :

```json title="GET /v2/emails/em_2xGk9Rz2NcV7bL4pWqH8sYdT5fA (extrait)"
{
  "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](/fr/docs/api-reference/emails/body/), [Lister les pièces jointes](/fr/docs/api-reference/emails/attachments/), ou [Récupérer le MIME brut](/fr/docs/api-reference/emails/raw/) pour la source d’origine.

## Créer le webhook

**Tableau de bord**

  1. **Ajoutez le webhook.** Accédez à **Email API → Webhooks**, sélectionnez **Add webhook**, saisissez un nom et l’URL de votre endpoint, puis sélectionnez **Create**.

  2. **Copiez le secret.** La boîte de dialogue affiche une seule fois le secret du webhook (`whsec_…`). Stockez-le sous le nom `EMAILIT_WEBHOOK_SECRET` dans l’environnement de votre application.

  3. **Abonnez-vous uniquement à email.received.** Dans l’onglet **Settings** du webhook, désactivez **All events**, sélectionnez `email.received` sous **Emails**, puis sélectionnez **Save**.

**API**

  Appelez [Créer un webhook](/fr/docs/api-reference/webhooks/create/) avec la liste des événements. La réponse inclut le `secret`.

```bash
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](/fr/docs/webhooks/set-up/#filter-events-by-payload) 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](/fr/docs/webhooks/request-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.

**Node.js**

```javascript title="server.js"
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);
```

**Python**

```python title="app.py"
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**

```php title="webhook.php"
<?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 `2xx` pendant 30 secondes au maximum. Un timeout, un statut autre que `2xx` ou une redirection comptent comme un échec, et tout le lot fait l’objet d’une nouvelle tentative selon le [calendrier des nouvelles tentatives](/fr/docs/webhooks/retries-and-failures/). Si la récupération et le traitement peuvent prendre plus de temps, placez les événements dans une file d’attente, renvoyez `200` et 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 chaque `event_id` aprè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 `2xx` mê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](/fr/docs/data-retention/).

## 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 :

```javascript
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

1. Envoyez un message à une adresse de votre sous-domaine de réception.
2. Dans l’onglet **Requests** du webhook, la requête `email.received` affiche **Delivered**.
3. 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](/fr/docs/webhooks/retries-and-failures/).

## Voir aussi

  - [Vérifier les signatures de webhook](/fr/docs/webhooks/request-signature/)
  - [Format des requêtes de webhook](/fr/docs/webhooks/webhook-requests/)
  - [Transfert par automatisation](/fr/docs/inbound/forward-with-automations/)
  - [Récupérer un e-mail](/fr/docs/api-reference/emails/get/)

---
Source: https://emailit.com/fr/docs/inbound/process-with-webhooks/
