Anleitung
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, 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.
- 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.
{
"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, um die geparste Nachricht zu erhalten:
{
"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, Anhänge auflisten oder Roh-MIME abrufen für die Originalquelle.
Webhook erstellen
-
Webhook hinzufügen. Öffnen Sie Email APIWebhooks, wählen Sie Add webhook, geben Sie einen Namen und die URL Ihres Endpunkts ein und wählen Sie Create.
-
Secret kopieren. Der Dialog zeigt das Webhook-Secret (
whsec_…) einmalig an. Speichern Sie es alsEMAILIT_WEBHOOK_SECRETin der Umgebung Ihrer App. -
Nur email.received abonnieren. Deaktivieren Sie im Tab Settings des Webhooks die Option All events, wählen Sie unter Emails das Event
email.receivedund wählen Sie Save.
Rufen Sie Webhook erstellen mit der Liste der Events auf. Die Antwort enthält das 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"]
}'In den Tarifen Pro, Business und Custom können Sie einen Payload-Filter 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, durchläuft das Array, überspringt alles, was nicht email.received ist oder bereits verarbeitet wurde, und ruft jede Nachricht ab.
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
}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 als2xxoder eine Weiterleitung gilt als Fehlschlag, und der gesamte Batch wird nach dem Zeitplan für Wiederholungen erneut gesendet. Wenn Abruf und Verarbeitung länger dauern können, legen Sie die Events in einer Warteschlange ab, geben Sie200zurü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 jedeevent_idnach 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
2xxzurü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.
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:
const match = email.to.match(/^reply\+(\d+)@inbound\.acme\.com$/i);
if (match) {
await addReplyToTicket(match[1], email.body.text);
}Ergebnis prüfen
- Senden Sie eine Nachricht an eine Adresse auf Ihrer Inbound-Subdomain.
- Im Tab Requests des Webhooks zeigt die Anfrage
email.receivedden Status Delivered. - 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.