# Eingehende E-Mails mit Webhooks verarbeiten

> Abonnieren Sie mit einem Webhook das Event email.received, verifizieren Sie jede Anfrage und rufen Sie dann Body, Header und Anhänge jeder empfangenen Nachricht per API ab.

Diese Anleitung zeigt, wie Sie in Ihrem eigenen Code auf empfangene E-Mails reagieren. Emailit benachrichtigt Ihren Endpunkt mit einem Event `email.received`, und Ihr Code ruft die vollständige Nachricht per API ab. Die Beispiele verifizieren die Signatur, verarbeiten Batches von Events, laden Anhänge herunter und überspringen Duplikate.

## Voraussetzungen

- [Eingehende E-Mails sind eingerichtet](/de/docs/inbound/set-up/), und eine Testnachricht erscheint im Tab **Incoming**.
- Ein API-Schlüssel mit **Full Access**. Mit Schlüsseln mit Sending Only lassen sich E-Mail-Inhalte nicht lesen. Siehe [API-Schlüssel](/de/docs/developers/api-keys/).
- Ein öffentlicher HTTPS-Endpunkt, der `POST`-Anfragen annimmt. Für die lokale Entwicklung nutzen Sie einen Tunnel wie ngrok oder Cloudflare Tunnel.

## So funktioniert der Ablauf

Der Webhook teilt Ihnen mit, dass eine Nachricht angekommen ist. Er enthält weder Body noch Anhänge. Das hält die Anfragen klein, und Sie rufen Inhalte nur ab, wenn Sie sie brauchen.

```json title="email.received (one event in the request array)"
{
  "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"
    }
  }
}
```

Verwenden Sie `data.object.id` mit [E-Mail abrufen](/de/docs/api-reference/emails/get/), um die geparste Nachricht zu erhalten:

```json title="GET /v2/emails/em_2xGk9Rz2NcV7bL4pWqH8sYdT5fA (abridged)"
{
  "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..."
    }
  ]
}
```

Das Feld `content` eines Anhangs ist Base64-kodiert. Header-Namen behalten ihre ursprüngliche Groß- und Kleinschreibung, und wenn ein Header mehrfach vorkommt (etwa `Received`), wird nur der letzte Wert behalten. Wenn Sie nur einen Teil der Nachricht brauchen, nutzen Sie die spezifischeren Endpunkte: [Body abrufen](/de/docs/api-reference/emails/body/), [Anhänge auflisten](/de/docs/api-reference/emails/attachments/) oder [Roh-MIME abrufen](/de/docs/api-reference/emails/raw/) für die Originalquelle.

## Webhook erstellen

**Weboberfläche**

  1. **Webhook hinzufügen.** Öffnen Sie **Email API → Webhooks**, wählen Sie **Add webhook**, geben Sie einen Namen und die URL Ihres Endpunkts ein und wählen Sie **Create**.

  2. **Secret kopieren.** Der Dialog zeigt das Webhook-Secret (`whsec_…`) einmalig an. Speichern Sie es als `EMAILIT_WEBHOOK_SECRET` in der Umgebung Ihrer App.

  3. **Nur email.received abonnieren.** Deaktivieren Sie im Tab **Settings** des Webhooks die Option **All events**, wählen Sie unter **Emails** das Event `email.received` und wählen Sie **Save**.

**API**

  Rufen Sie [Webhook erstellen](/de/docs/api-reference/webhooks/create/) mit der Liste der Events auf. Die Antwort enthält das `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"]
  }'
```

In den Tarifen Pro, Business und Custom können Sie einen [Payload-Filter](/de/docs/webhooks/set-up/#filter-events-by-payload) hinzufügen, damit der Webhook nur E-Mails für bestimmte Adressen erhält, zum Beispiel wenn `to` auf `@inbound.acme.com` endet.

## Handler schreiben

Jeder Anfrage-Body ist ein JSON-Array mit bis zu 100 Events. Der folgende Handler verifiziert die [Signatur](/de/docs/webhooks/request-signature/), durchläuft das Array, überspringt alles, was nicht `email.received` ist oder bereits verarbeitet wurde, und ruft jede Nachricht ab.

**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
}
```

## Wiederholungen und Duplikate behandeln

- **Antworten Sie innerhalb von 30 Sekunden.** Emailit wartet bis zu 30 Sekunden auf eine `2xx`-Antwort. Ein Timeout, ein anderer Status als `2xx` oder eine Weiterleitung gilt als Fehlschlag, und der gesamte Batch wird nach dem [Zeitplan für Wiederholungen](/de/docs/webhooks/retries-and-failures/) erneut gesendet. Wenn Abruf und Verarbeitung länger dauern können, legen Sie die Events in einer Warteschlange ab, geben Sie `200` zurück und verarbeiten Sie sie in einem Hintergrundjob.
- **Entfernen Sie Duplikate anhand von `event_id`.** Ein wiederholter Batch enthält erneut alle seine Events, auch solche, die Sie vor dem Fehlschlag bereits verarbeitet haben. Speichern Sie jede `event_id` nach der Verarbeitung und überspringen Sie bereits gesehene IDs. Jede empfangene Nachricht hat außerdem eine eigene E-Mail-ID, die Sie als zweiten Schlüssel verwenden können.
- **Bestätigen Sie auch Events, die Sie nicht verarbeiten.** Geben Sie `2xx` zurück, auch wenn ein Batch nur Event-Typen enthält, die Sie ignorieren. Sonst wiederholt Emailit sie immer weiter.
- **Rufen Sie Inhalte zügig ab.** Nachrichteninhalte werden nur begrenzte Zeit aufbewahrt, abhängig von Ihrem Tarif (7 Tage bei Pay as you go). Danach liefert die API die E-Mail ohne Body und Anhänge. Siehe [Datenaufbewahrung](/de/docs/data-retention/).

## Nachrichten nach Adresse verteilen

Da jeder lokale Teil angenommen wird, können Sie Informationen in der Adresse kodieren und aus `to` wieder auslesen. Senden Sie zum Beispiel Benachrichtigungen mit `reply_to` auf `reply+4821@inbound.acme.com` und ordnen Sie Antworten dann Ticket 4821 zu:

```javascript
const match = email.to.match(/^reply\+(\d+)@inbound\.acme\.com$/i);
if (match) {
  await addReplyToTicket(match[1], email.body.text);
}
```

## Ergebnis prüfen

1. Senden Sie eine Nachricht an eine Adresse auf Ihrer Inbound-Subdomain.
2. Im Tab **Requests** des Webhooks zeigt die Anfrage `email.received` den Status **Delivered**.
3. Ihre Anwendung protokolliert Absender, Betreff und etwaige Anhänge.

Zeigt die Anfrage **Attempting** oder **Failed**, wählen Sie in einer fehlgeschlagenen Zeile **View**, um den Statuscode und den Antwort-Body zu sehen, die Ihr Endpunkt zurückgegeben hat. Siehe [Wiederholungen und Fehlschläge](/de/docs/webhooks/retries-and-failures/).

## Siehe auch

  - [Webhook-Signaturen verifizieren](/de/docs/webhooks/request-signature/)
  - [Format von Webhook-Anfragen](/de/docs/webhooks/webhook-requests/)
  - [Mit Automatisierungen weiterleiten](/de/docs/inbound/forward-with-automations/)
  - [E-Mail abrufen](/de/docs/api-reference/emails/get/)

---
Quelle: https://emailit.com/de/docs/inbound/process-with-webhooks/
