MailLandDocs
Eingehende E-Mail

Eingehende Webhooks

Empfangen Sie geparste eingehende E-Mail-Daten über Webhook-Benachrichtigungen

Wenn eine eingehende E-Mail einer Route entspricht, deren actions webhook enthält, sendet MailLand die geparste E-Mail an den Webhook dieser Route als JSON-POST-Anfrage. Diese Seite beschreibt das Payload-Format, die Signaturprüfung und das Zustellverhalten.

Webhook-Payload

MailLand sendet eine POST-Anfrage an Ihre Webhook-URL mit folgendem JSON-Body:

Inbound webhook payload
{
  "event": "inbound.received",
  "event_id": "evt_a1b2c3d4e5f60718293a4b5c6d7e8f90",
  "timestamp": "2026-02-17T14:30:00Z",
  "occurred_at": "2026-02-17T14:29:58Z",
  "schema_version": 1,
  "data": {
    "idempotency_key": "k7m2x9q4p1w8z3_r7k3m9q2p4w8x1",
    "email": {
      "id": "k7m2x9q4p1w8z3",
      "received_at": "2026-02-17T14:29:58Z",
      "size_bytes": 48213,
      "envelope": {
        "from": "[email protected]",
        "to": ["[email protected]"]
      },
      "headers": {
        "from": { "address": "[email protected]", "name": "Jane Customer" },
        "to": [{ "address": "[email protected]", "name": null }],
        "cc": [],
        "subject": "Need help with my account",
        "message_id": "<[email protected]>",
        "in_reply_to": null,
        "reply_to": "[email protected]",
        "date": "2026-02-17T14:29:55Z"
      },
      "body": {
        "text_preview": "I need assistance with my account settings.",
        "has_text": true,
        "has_html": true
      },
      "attachments": [
        {
          "id": "a3k7m2q9p4w1x8",
          "file_name": "screenshot.png",
          "content_type": "image/png",
          "size_bytes": 34500,
          "is_inline": false,
          "content_id": null,
          "download_url": "https://r2.example.com/screenshot.png?X-Amz-Signature=..."
        }
      ],
      "authentication": {
        "spf": { "result": "pass" },
        "dkim": { "result": "pass" },
        "dmarc": { "result": "pass" }
      },
      "spam": {
        "score": 1.2,
        "is_spam": false,
        "reason": null
      },
      "source": {
        "ip": "203.0.113.42",
        "hostname": "mail.example.com"
      }
    },
    "matched_route": {
      "id": "r7k3m9q2p4w8x1",
      "name": "Support Inbox"
    }
  }
}

Der vollständige E-Mail-Text ist nie inline enthalten: nur eine gekürzte text_preview und die Flags has_text/has_html. Es gibt kein Feld body.html.

Felder ohne Wert (name, in_reply_to, content_id, reason, data.matched_route und so weiter) werden vollständig aus dem JSON weggelassen, statt als null gesendet zu werden. Das obige Beispiel zeigt null zur besseren Lesbarkeit; verlassen Sie sich nicht darauf, dass der Schlüssel vorhanden ist.

Payload-Felder

FeldTypBeschreibung
eventstringImmer inbound.received
event_idstringEreignis-ID, identisch mit dem Header X-MLND-Event-Id. Sie ist pro eingehender Nachricht und Webhook stabil (evt_ plus 32 Hexadezimalzeichen): Jeder Wiederholungsversuch und jede erneute Veröffentlichung derselben Zustellung trägt dieselbe event_id, denselben Header und denselben Body. Ein manueller Replay trägt eine neue event_id und setzt data.replay_of.
data.idempotency_keystring{emailPublicId}_{routePublicId}, wenn eine Route zugetroffen hat, sonst nur {emailPublicId} (auch wenn die Route inzwischen nicht mehr existiert). Über Wiederholungen und manuelle Replays hinweg stabil
data.replay_ofstringNur bei einem manuellen Replay vorhanden: die event_id der ursprünglichen Zustellung. Bei normalen Zustellungen und bei Replays von Nachrichten, die vor Einführung stabiler Ereignis-IDs empfangen wurden, entfällt das Feld
timestampstringISO-8601-Zeitstempel der Ereigniserzeugung
occurred_atstringZeitpunkt, zu dem die E-Mail empfangen wurde; derselbe Zeitpunkt wie data.email.received_at
schema_versionnumberSchemaversion der Nutzdaten, 1; siehe Versionierung
data.email.idstringÖffentliche ID der eingehenden E-Mail
data.email.received_atstringEmpfangszeitpunkt der E-Mail
data.email.size_bytesnumberRohnachrichtengröße in Bytes
data.email.envelope.fromstringSMTP-MAIL FROM-Adresse
data.email.envelope.toarraySMTP-RCPT TO-Adressen
data.email.headers.fromobject{ address, name }, geparst aus dem From-Header
data.email.headers.to / .ccarrayArrays von { address, name }
data.email.headers.subjectstringBetreff der E-Mail
data.email.headers.message_idstring | nullMessage-ID-Header
data.email.headers.in_reply_tostring | nullIn-Reply-To-Header
data.email.headers.reply_tostring | nullReply-To-Header
data.email.headers.datestring | nullDate-Header
data.email.body.text_previewstring | nullGekürzte Klartextvorschau
data.email.body.has_text / .has_htmlbooleanOb ein Text- bzw. HTML-Teil vorhanden war
data.email.attachmentsarraySiehe unten
data.email.authentication.spf/dkim/dmarc.resultstringKleingeschriebenes Ergebnis, z. B. pass, fail, none
data.email.spam.scorenumberSpam-Score
data.email.spam.is_spambooleanOb die E-Mail als Spam eingestuft wurde
data.email.spam.reasonstring | nullKlassifizierungsgrund, falls verfügbar
data.email.source.ip / .hostnamestring | nullIP und Reverse-DNS-Hostname des sendenden Servers
data.matched_routeobject | null{ id, name } der zugetroffenen Route, oder null

Anhänge

Anhangsinhalte sind nie inline enthalten. Jeder Eintrag in data.email.attachments enthält eine download_url:

FeldTypBeschreibung
idstringAnhangs-ID. Stabil: Jeder Wiederholungsversuch und jeder Replay des Ereignisses trägt für denselben Anhang dieselbe id. Erkennen Sie daran einen bereits heruntergeladenen Anhang. download_url wird bei jedem Versuch neu erzeugt
file_namestringUrsprünglicher Dateiname
content_typestringMIME-Typ
size_bytesnumberGröße in Bytes
is_inlinebooleanOb der Anhang inline referenziert wird (z. B. ein eingebettetes Bild)
content_idstring | nullContent-ID für Inline-Anhänge
download_urlstring | nullVorsignierte URL, gültig für 15 Minuten ab Erzeugung des Payloads

download_url erfordert keinen Authorization-Header: die URL selbst ist das Anmeldedatum und läuft 15 Minuten nach Erzeugung des Webhook-Payloads ab. Laden Sie Anhänge sofort beim Empfang des Webhooks herunter. Ist die URL abgelaufen, holen Sie stattdessen die gesamte Rohnachricht:

GET https://api.mail.land/v1/transactional/inbound/emails/{emailId}/raw

Dies erfordert Ihren normalen Authorization-Header und liefert die ursprüngliche .eml-Datei (Content-Type: message/rfc822): es gibt keinen Endpunkt zum Herunterladen einzelner Anhänge.

Signaturprüfung

Eingehende Webhooks werden genau wie jeder andere MailLand-Webhook signiert: gleicher Header, gleiches Schema, gleiche Behandlung des Secrets. Die maßgebliche Referenz mit einem durchgerechneten Beispiel und Verifizierungscode in acht Sprachen ist Signature Verification; alles dort gilt hier unverändert.

Kurz zusammengefasst:

X-MLND-Signature: t=1708171200,v1=<hex>
  • t ist ein Unix-Zeitstempel in Sekunden, v1 ist ein HMAC-SHA256 in Kleinbuchstaben-Hex;
  • der signierte String ist {timestamp}.{raw_body}: der Zeitstempel, ein wörtlicher Punkt, dann der Body genau wie empfangen, vor dem JSON-Parsing;
  • der Schlüssel ist das Signierungssecret des Webhooks, das einmalig bei der Erstellung angezeigt wird;
  • vergleichen Sie in konstanter Zeit und lehnen Sie einen Zeitstempel ab, der älter als 300 Sekunden ist.

Es gibt keinen unsignierten Zustellweg: Hat ein Webhook kein verwendbares Secret, schlägt die Zustellung fehl, statt sie unsigniert zu senden. Lehnen Sie jede Anfrage ab, die ohne X-MLND-Signature-Header eintrifft.

Zustellverhalten

Antwortcodes

Ihr Endpunkt muss mit einem 2xx-Statuscode antworten, um den Empfang zu bestätigen. Alles andere, ein Nicht-2xx-Status oder ein Timeout, zählt als fehlgeschlagener Zustellversuch.

Ihre AntwortMailLand-Aktion
200–299Zustellung erfolgreich, keine Wiederholung
Jeder andere Status, oder keine Antwort innerhalb von 15 SekundenZustellung fehlgeschlagen, Wiederholung (vorbehaltlich des unten genannten Versuchslimits)

Wiederholungsrichtlinie

Ein fehlgeschlagener Versuch wird nach demselben Zeitplan wiederholt wie jedes andere Webhook-Ereignis, bis zu insgesamt 6 Versuchen pro Ereignis: der erste Versuch, dann Wiederholungen sofort, nach 1 Minute, 5 Minuten, 15 Minuten und 1 Stunde. Nach dem 6. fehlgeschlagenen Versuch wird die Zustellung als dauerhaft fehlgeschlagen markiert. Sie können sie manuell erneut zustellen lassen. Den vollständigen Zeitplan finden Sie unter Wiederholungslogik.

Ein Webhook-Endpunkt, der weniger als etwa anderthalb Stunden ausfällt, wird über dieses gesamte Fenster hinweg erneut angesprochen; ein längerer Ausfall endet in einer dauerhaft fehlgeschlagenen Zustellung. Gestalten Sie Ihren Endpunkt so, dass er auch im Fehlerfall zügig antwortet, und verlassen Sie sich bei längeren Ausfällen auf den manuellen Replay.

Jeder fehlgeschlagene Zustellungsversuch (nicht jedes Ereignis) zählt zur Gesamtzahl aufeinanderfolgender Fehlschläge des Webhooks, und der Webhook wird automatisch deaktiviert, sobald diese Gesamtzahl seinen eigenen pauseThreshold erreicht (wenn dieser auf einen Wert größer als 0 gesetzt ist), andernfalls den Standardwert von 10 aufeinanderfolgenden Fehlschlägen: danach werden für neue eingehende E-Mails überhaupt keine Webhook-Zustellungen mehr eingeplant, und da es sich um dieselbe Webhook-Zeile handelt, die auch für Nachrichtenereignisse verwendet wird, stoppen auch diese. Beim Standardwert reichen zwei vollständig fehlgeschlagene eingehende E-Mails (je 6 Versuche) aus, um ihn zu deaktivieren. Aktivieren Sie den Webhook auf der Webhooks-Seite erneut und spielen Sie dann die verpassten E-Mails erneut ab.

Einen Webhook erneut zustellen

Den Webhook für eine bestimmte eingehende E-Mail erneut zustellen:

POST https://api.mail.land/v1/transactional/inbound/emails/{id}/replay-webhook
Replay webhook delivery
curl -X POST https://api.mail.land/v1/transactional/inbound/emails/k7m2x9q4p1w8z3/replay-webhook \
  -H "Authorization: Bearer eyJhbGciOiJIUzI1NiIs..."
Replay response (202 Accepted)
{
  "emailId": "k7m2x9q4p1w8z3",
  "queued": true,
  "webhookUrl": "https://yourapp.com/webhooks/support",
  "message": "Webhook replay queued successfully"
}

Dadurch wird eine dauerhafte Zustellung an den Webhook der passenden Route eingereiht, und 202 Accepted wird zurückgegeben, sobald diese Zeile committet ist, es wird nicht synchron gesendet. Der Replay erhält eine neue Ereignis-ID (neue event_id und neuer X-MLND-Event-Id) und führt die ID der ursprünglichen Zustellung in data.replay_of. Ihre Deduplizierung sieht ihn daher als neues Ereignis, Sie können ihn aber weiterhin dem Original zuordnen. data.idempotency_key bleibt gleich wie bei der ursprünglichen Zustellung. Die Anfrage enthält X-MLND-Source: inbound und X-MLND-Inbound-Email-Id; jede download_url eines Anhangs im Payload wird beim Senden signiert und ist ab diesem Zeitpunkt 15 Minuten gültig, nicht ab dem Aufruf des Replays.

Ereignisse deduplizieren

Die Zustellung eingehender Webhooks erfolgt mindestens einmal (at-least-once): Ein Wiederholungsversuch, ein Timeout auf Ihrer Seite nach bereits verarbeiteter Anfrage oder eine Übergabe zwischen Workern kann dasselbe Ereignis mehr als einmal zustellen. Empfänger müssen deduplizieren.

  • Jeder eingehende Webhook trägt eine stabile Ereignis-ID in event_id und im Header X-MLND-Event-Id. Sie ist pro eingehender Nachricht und Webhook deterministisch, sodass alle Zustellversuche derselben Nachricht an denselben Webhook dieselbe ID haben.
  • Deduplizieren Sie anhand dieser ID. Speichern Sie verarbeitete IDs und überspringen Sie jede bereits gesehene.
  • Ein manueller Replay ist ein neues Ereignis. Er hat eine neue event_id und ein Feld data.replay_of mit der ursprünglichen ID. Soll ein Replay ignoriert werden, wenn das Original bereits verarbeitet wurde, prüfen Sie zusätzlich data.replay_of gegen Ihre gespeicherten IDs.
Idempotent receiver (Node.js / Express)
app.post("/webhooks/support", express.raw({ type: "application/json" }), async (req, res) => {
  // 1. Verify X-MLND-Signature first (see Signature Verification).
  const eventId = req.header("X-MLND-Event-Id");
  const client = await db.connect();
  try {
    await client.query("BEGIN");
    // 2. Record the event id; the unique constraint makes this atomic.
    const inserted = await client.query(
      "INSERT INTO processed_events (event_id) VALUES ($1) ON CONFLICT (event_id) DO NOTHING",
      [eventId],
    );
    // 3. Only a first-time id enqueues work, in the SAME transaction (jobs table).
    if (inserted.rowCount > 0) {
      await client.query("INSERT INTO jobs (kind, payload) VALUES ($1, $2)", [
        "process-inbound-email",
        req.body.toString(),
      ]);
    }
    await client.query("COMMIT");
    res.sendStatus(200); // new or duplicate: acknowledge
  } catch (err) {
    await client.query("ROLLBACK"); // id and job are both undone, so the retry is not a "duplicate"
    res.sendStatus(500);
  } finally {
    client.release();
  }
});

Die ID und das Einreihen der Arbeit in einer Transaktion zu speichern ist wichtig: Würde die ID zuerst gespeichert und das Einreihen schlüge fehl, würde der Wiederholungsversuch von MailLand als Duplikat übersprungen und die E-Mail ginge verloren.

Bewahren Sie verarbeitete IDs mindestens so lange auf wie Ihr längstes Wiederholungsfenster plus jeden Replay, den Sie auslösen könnten.

Webhook-Header

Jede Webhook-Anfrage enthält folgende Header:

HeaderBeispielBeschreibung
Content-Typeapplication/json
X-MLND-Signaturet=1708171200,v1=abc123...Zeitstempel und HMAC-SHA256-Signatur
X-MLND-Eventinbound.receivedEreignistyp
X-MLND-Event-Idevt_a1b2c3d4...Eindeutige Ereignis-ID
X-MLND-Event-Version1Payload-Schemaversion
X-MLND-Webhook-Idw4k8m2q7p9x3z1Öffentliche ID des Webhooks
X-MLND-Attempt1Zustellversuchsnummer (beginnend bei 1)
X-MLND-SourceinboundWelche Pipeline das Ereignis erzeugt hat
X-MLND-Inbound-Email-Idk7m2x9q4p1w8z3Die eingehende E-Mail, um die es bei dieser Zustellung geht

Best Practices

  • Schnell antworten: bestätigen Sie den Webhook sofort mit 200 und verarbeiten Sie die E-Mail asynchron in einem Hintergrundjob.
  • Signaturen prüfen: validieren Sie immer den X-MLND-Signature-Header, um gefälschte Payloads zu verhindern.
  • Duplikate behandeln: die Zustellung erfolgt mindestens einmal, daher per X-MLND-Event-Id (die event_id im Payload) deduplizieren. Ein manueller Replay hat eine neue ID und data.replay_of verweist auf die ursprüngliche; siehe Ereignisse deduplizieren.
  • Anhänge sofort herunterladen: jede download_url läuft 15 Minuten nach Erzeugung des Payloads ab; danach die Rohnachricht abrufen.
  • HTTPS verwenden: Webhook-URLs müssen in Produktion HTTPS verwenden, um die Payload-Zustellung abzusichern.

Fehlerantworten

StatusCodeBeschreibung
400INVALID_REQUESTKeine Route für die E-Mail gefunden
400INVALID_REQUESTDie Route hat keinen konfigurierten Webhook
400INVALID_REQUESTDer Webhook der Route ist inaktiv, vor dem Replay aktivieren
404RESOURCE_NOT_FOUNDE-Mail für Replay nicht gefunden

Nächste Schritte

  • Inbound Overview: Einstellungen für eingehende E-Mails konfigurieren und E-Mails anzeigen
  • Inbound Routes: Routing-Regeln für die Webhook-Zustellung einrichten
  • Webhooks: Konfiguration ausgehender Webhooks für E-Mail-Ereignisse

Auf dieser Seite