API & Automatisierung

Wegwerf-E-Mail-API: Postfach per Skript automatisieren

Drei Endpunkte, kein Schlüssel und kein Konto: genug, damit ein Skript eine Adresse öffnet, liest, was dort ankommt, und hinter sich aufräumt. Hier ist die ganze Schleife – die zwei Ratenbudgets, die bestimmen, wie schnell Sie vorgehen dürfen, und die eine Frist, die sich nicht verschieben lässt.

  • Fortgeschritten
  • 22 Min. Lesezeit
Ein graues Förderband, angetrieben von einem blauen Zahnrad, das zwei blaue Briefumschläge zu einer offenen grauen Ablage trägt

Drei Aufrufe, und nichts einzurichten

Die gesamte Schnittstelle besteht aus drei Endpunkten unter https://grabmail.io/api/v1, dazu einer Adresse für Anhänge, die die anderen bereits fertig mitliefern. Es gibt keinen Aufruf, der ein Postfach anlegt, und dieses Fehlen ist keine Lücke: Eine Adresse beginnt zu existieren, sobald Mail dort ankommt – für einen solchen Aufruf gibt es also nichts zu tun.

AufrufWas er beantwortetWas Sie übergeben
GET /mailboxAlles, was an einer Adresse wartet, neueste zuerst.address und optional limit sowie before
GET /message/{id}Eine vollständige Nachricht: der Klartextteil, der HTML-Teil und jeder Anhang mit einer bereits fertigen URL.mailbox
DELETE /message/{id}Entfernt sie sofort, statt auf das Ende des Aufbewahrungszeitraums zu warten.mailbox
GET /attachment/{id}Die Bytes einer Datei, genau so, wie sie angekommen ist.mailbox

Jede Antwort ist JSON, auch jeder Fehler. Jede Zeitangabe ist UTC im RFC-3339-Format. Nachrichten-IDs sind opak: geben Sie sie unverändert zurück, zerlegen Sie sie nie.

Der erste Aufruf, und was eine leere Adresse antwortet

Wählen Sie einen Namen, setzen Sie eine der öffentlichen Domains dahinter, und lesen Sie sie aus. Nichts muss vorher existieren, und nichts wird durch das Nachfragen angelegt.

shell
$ curl -sG https://grabmail.io/api/v1/mailbox \
  --data-urlencode "address=k7fq2m@grabmail.io"
Antwort
{
  "address": "k7fq2m@grabmail.io",
  "alias": "q4v8n2mt7xkd@example.net",
  "count": 1,
  "next": null,
  "messages": [
    {
      "id": "3QK7ZB5M9WVXR2HD4TNFJ0PC6A",
      "from": "no-reply@example.com",
      "from_name": "Example",
      "subject": "Your verification code",
      "preview": "Your code is 481920. It expires in 10 minutes.",
      "has_html": false,
      "date": "2026-08-29T09:14:02Z",
      "seen": false,
      "attachments": 0,
      "expires_at": "2026-09-03T09:14:02Z"
    }
  ]
}

Fünf Felder, und zwei davon sind interessanter, als sie aussehen:

count
Wie viele Nachrichten in dieser Antwort stehen – nicht, wie viele das Postfach enthält. Sobald Sie limit übergeben, sind das zwei verschiedene Zahlen.
next
Der Cursor für die Seite nach dieser, oder null, wenn danach nichts mehr kommt. Es ist die ID der letzten Nachricht, die Sie gerade erhalten haben – deshalb kostet das Blättern keinen zusätzlichen Aufruf, um sie herauszufinden.
messages
Die Liste selbst, neueste zuerst. Jeder Eintrag enthält bereits subject, from, date, seen, eine kurze preview des Texts, ob es einen HTML-Teil gibt und wie viele Anhänge vorhanden sind.
alias
Eine zweite Adresse, die hierher zustellt und nichts über diese hier verrät. Geben Sie sie statt der echten Adresse in ein Formular ein; wer auch immer damit endet und sie in diesen Dienst eintippt, findet ein leeres Postfach vor.
address
Die Adresse so, wie sie verstanden wurde: klein geschrieben und getrimmt. Vergleichen Sie sie mit dem, was Sie gesendet haben, wenn Sie die Adresse aus Teilen zusammensetzen.

Über die ersten fünfzig hinaus lesen

Ein Aufruf liefert standardmäßig höchstens fünfzig Nachrichten, im äußersten Fall zweihundert. Ein stark frequentiertes Catch-all übertrifft beide Werte an einem einzigen Nachmittag, und der Teil, den man dabei falsch einschätzt, ist das, was als Nächstes kommt – denn es ist keine Seitenzahl.

limit
Wie viele dieser Aufruf zurückgibt, 1 bis 200. Werte außerhalb dieses Bereichs werden begrenzt statt abgelehnt, sodass limit=5000 Ihnen stillschweigend 200 liefert.
before
Die ID der ältesten Nachricht, die Sie schon haben. Sie erhalten die Nachrichten danach. Geben Sie zurück, was die vorherige Antwort in next geliefert hat.
next
null bedeutet, dass Sie das Ende des Postfachs erreicht haben. Es ist das einzige verlässliche Signal für das Listenende: Eine kurze Seite ist keins, denn eine Seite ist nur dann kurz, wenn der Server das so entscheidet.
was ein Aufruf zurückgabwas before= zurückbringtnextneuesteältesteKeine Seitenzahl – eine Position in einer Liste.
limit begrenzt eine Antwort, next benennt, wo diese Antwort endete, und before fragt nach dem, was danach kommt.
ein ganzes Postfach durchlaufen, älteste Seite zuletzt
ADDR="k7fq2m@grabmail.io"
CURSOR=""

while :; do
  PAGE=$(curl -fsG https://grabmail.io/api/v1/mailbox \
           --data-urlencode "address=$ADDR" \
           --data-urlencode "limit=200" \
           ${CURSOR:+--data-urlencode "before=$CURSOR"})

  printf '%s' "$PAGE" | jq -c '.messages[]'

  CURSOR=$(printf '%s' "$PAGE" | jq -r '.next // empty')
  [ -n "$CURSOR" ] || break
  sleep 1
done

Solange schleifen, wie next nicht null ist, und Sie haben das ganze Postfach, egal wie groß es geworden ist. Jeder Aufruf ist ein Bereichslesevorgang auf einem Index statt auf einem Offset, sodass die tausendste Seite genauso viel kostet wie die erste.

Ein Cursor aus einem anderen Postfach, oder einer, der inzwischen abgelaufen ist, ist kein Fehler: Sie erhalten eine leere Seite und next: null. Das ist die richtige Antwort – die neueste Seite stattdessen zu wiederholen, würde einem Skript Mail geben, die es schon verarbeitet hat –, bedeutet aber auch, dass ein veralteter Cursor genau wie das Ende der Liste aussieht.

Eine Nachricht öffnen, und wann Sie das nicht müssen

Die ID aus der Auflistung plus das Postfach, an das sie zugestellt wurde, liefert die Nachricht selbst. Beides ist erforderlich: Eine ID, die aus einem Postfach durchgesickert ist, lässt sich nicht zum Lesen eines anderen verwenden, denn jede Abfrage ist auch an die Adresse gebunden.

shell
$ curl -sG https://grabmail.io/api/v1/message/3QK7ZB5M9WVXR2HD4TNFJ0PC6A \
  --data-urlencode "mailbox=k7fq2m@grabmail.io"
Antwort
{
  "id": "3QK7ZB5M9WVXR2HD4TNFJ0PC6A",
  "from": "no-reply@example.com",
  "to": "k7fq2m@grabmail.io",
  "subject": "Your verification code",
  "date": "2026-08-29T09:14:02Z",
  "expires_at": "2026-09-03T09:14:02Z",
  "text": "Your code is 481920. It expires in 10 minutes.",
  "html": null,
  "attachments": []
}
text
Der Klartextteil. Parsen Sie diesen, wenn er vorhanden ist: Er ist stabil, enthält kein Markup, und ein sechsstelliger Code darin ist ein sechsstelliger Code.
html
Der HTML-Teil, oder null, wenn der Absender keinen gesendet hat. Bestätigungslinks gibt es oft nur hier.
attachments
Ein Eintrag pro Datei, jeweils mit der bereits fertigen URL zum Abrufen. Eine leere Liste, kein null, wenn es keine gibt.
expires_at
Wann diese Nachricht gelöscht wird, im selben RFC-3339-Format wie date. Lesen Sie es aus, statt es zu berechnen – der Aufbewahrungszeitraum ist keine Einstellung, auf die Sie sich von außen verlassen können.

Sehr oft können Sie diesen Aufruf ganz überspringen. Die Auflistung liefert bereits Betreff, Absender, Datum und eine kurze Vorschau des Texts – genug, um zu entscheiden, dass eine Nachricht nicht die ist, auf die Sie warten. Jede Nachricht eines Postfachs abzurufen, nur um festzustellen, dass keine davon gebraucht wurde, ist der häufigste Grund, warum ein Skript langsam wird.

Eine Datei herausholen

Jeder Anhang trägt seine eigene url, und das Detail, das sich zu kennen lohnt, bevor Sie die Schleife schreiben, ist, dass es sich um einen Pfad auf diesem Origin handelt statt um eine absolute Adresse – mit dem Parameter mailbox bereits darin. Setzen Sie den Origin davor, rufen Sie ihn ab, und es gibt nichts weiter zu übergeben und nichts zu autorisieren.

shell
ADDR="k7fq2m@grabmail.io"
ID="3QK7ZB5M9WVXR2HD4TNFJ0PC6A"

curl -fsG https://grabmail.io/api/v1/message/$ID \
  --data-urlencode "mailbox=$ADDR" \
| jq -r '.attachments[] | "\(.url)\t\(.filename)"' \
| while IFS=$'\t' read -r path name; do
    curl -fs "https://grabmail.io$path" -o "$name"
  done

Es antwortet immer mit application/octet-stream und Content-Disposition: attachment, unabhängig davon, was der Absender als Dateityp angegeben hat. Das ist Absicht – ein fremdes text/html unverändert zurückzugeben, würde einen Anhang als Seite auf diesem Origin laufen lassen –, also liest ein Skript, dem der Typ wichtig ist, ihn aus dem Nachrichten-JSON, wo er Daten und keine Anweisung ist.

Die gesamte Nachricht, mit allen Dateien, ist auf 5 MB begrenzt. Was diese Obergrenze bedeutet, sobald Base64 mit einer Binärdatei fertig ist, ist ein eigenes Thema, für das es eine Anleitung zu Anhängen gibt.

Zwei Ratenbudgets, nicht eines

Das ist der Teil, den man kennen sollte und leicht übersieht: Das Auflisten einer Adresse und das Lesen daraus werden getrennt gemessen, weil es nicht dasselbe Risiko ist. Wer eine Adresse kennt, kann ihre Auflistung pollen; das Lesen einer Nachricht braucht eine ID, und die lässt sich nicht erraten.

Was Sie aufrufenDas BudgetWas das in der Praxis bedeutet
GET /mailboxEine Anfrage pro Sekunde, pro AdresseDer vorgesehene Polling-Rhythmus, in diesem Tempo nie gedrosselt. Schneller wird abgelehnt und hätte ohnehin nichts gebracht.
GET /message/{id}, GET /attachment/{id}, DELETEDeutlich großzügiger, pro AdresseEine Seite Nachrichten in einem Rutsch leeren, ohne Pausen dazwischen. Deshalb kann eine Oberfläche eine Nachricht in derselben Sekunde öffnen, in der ein Poll lief.
Alles zusammengerechnet1200 Anfragen pro Minute, pro ClientZwanzig Adressen, einmal pro Sekunde gepollt – komfortabel jenseits jeder echten Automatisierung, und eine Bremse für einen einzelnen Host, der zehntausend Adressen durchläuft.

Überschreiten Sie eines davon, erhalten Sie 429 mit der Wartezeit in Sekunden im Retry-After-Header. Halten Sie sich daran, statt mit einer selbst ausgedachten Zahl zurückzuweichen: Der Server sagt Ihnen damit genau, wann er Ja sagen wird.

genau so lange schlafen, wie verlangt wurde, und nicht länger
read_box() {
  local wait
  while :; do
    BODY=$(curl -s -D /tmp/gm.h -G https://grabmail.io/api/v1/mailbox \
             --data-urlencode "address=$1")
    grep -qi '^HTTP/[0-9.]* 429' /tmp/gm.h || { printf '%s' "$BODY"; return 0; }
    wait=$(awk 'tolower($1) == "retry-after:" { print $2 + 0 }' /tmp/gm.h)
    sleep "${wait:-1}"
  done
}

Wie man auf eine Nachricht wartet, die noch nicht angekommen ist – eine Frist statt einer Anzahl von Versuchen, und was zu tun ist, wenn sie verstreicht – ist das Thema der Anleitung zum Testen von Verifizierungsabläufen. Die Schleife dort ist dieselbe Schleife, die auch ein geplanter Job braucht.

Löschen, und die feste Grenze darunter

Eine Nachricht, mit der Sie fertig sind, kann sofort weg, statt ihren Aufbewahrungszeitraum abzusitzen. Der Aufruf ist idempotent: Dieselbe ID zweimal zu löschen, antwortet beide Male mit 200, sodass eine wiederholte Anfrage nie wie ein Fehlschlag aussieht.

shell
$ curl -s -X DELETE -G https://grabmail.io/api/v1/message/3QK7ZB5M9WVXR2HD4TNFJ0PC6A \
  --data-urlencode "mailbox=k7fq2m@grabmail.io"
Löschen, sobald Sie haben, wofür Sie gekommen sind
Ein Skript, das eine Nachricht verarbeitet und liegen lässt, verarbeitet sie beim nächsten Lauf erneut, sofern es nicht selbst Buch führt über das, was es schon gesehen hat. Löschen ist die billigere Buchführung.
Verlassen Sie sich dabei nicht auf Privatsphäre
Zwischen Ankunft und Löschung konnte jeder, der die Adresse kennt, sie gelesen haben. Löschen schließt das Zeitfenster; es macht es nicht ungeschehen.
Alles ist nach 5 Tagen weg, ohne Ausnahme
Gelesen oder ungelesen, gelöscht oder nicht: Eine Nachricht ist 5 Tage nach ihrer Ankunft verschwunden. Das ist eine feste Grenze und keine Einstellung, und kein Parameter verlängert sie.

Die Slugs, auf die man verzweigen sollte, und das Feld, das man nie lesen sollte

Jeder Fehlschlag ist JSON mit denselben zwei Feldern. error ist ein stabiler, maschinenlesbarer Slug; message ist für Menschen gedacht und kann jederzeit umformuliert werden. Auf das zweite Feld zu verzweigen ist die Art, wie ein Skript an einem Tag kaputtgeht, an dem sich nichts geändert hat.

Status und SlugWas passiert istWas ein Skript tun sollte
400 invalid_addressDie Adresse fehlt, oder sie hat nicht die Form einer Adresse.Sofort fehlschlagen. Kein noch so häufiges Wiederholen behebt einen Tippfehler.
400 bad_cursorbefore ist keine Nachrichten-ID.Sofort fehlschlagen, und prüfen, ob Sie wirklich next zurückgeben und nicht etwas selbst Gebautes.
404 unknown_domainDiese Domain wird hier nicht gehostet.Sofort fehlschlagen. Bei einer eigenen Domain liegt es am MX-Eintrag – siehe eine Domain verbinden.
404 not_foundKeine solche Nachricht in diesem Postfach, oder ihr Aufbewahrungszeitraum ist abgelaufen.Als verschwunden behandeln. Das bekommen Sie auch für eine gültige ID, die gegen das falsche Postfach gelesen wird.
429 rate_limitedEines der Budgets von oben.Retry-After Sekunden schlafen und weitermachen. Nie als fehlgeschlagenen Lauf zählen.

Ein Job, der eine Adresse stündlich leert

Setzt man die Teile zusammen, wird ein geplanter Job kurz. Dieser hier nimmt jede Nachricht, die an einer Adresse wartet, schreibt sie als JSON auf die Festplatte und löscht sie – sodass der nächste Lauf mit einem leeren Postfach beginnt und niemals dieselbe Nachricht zweimal verarbeiten kann.

drain.sh
#!/usr/bin/env bash
set -euo pipefail

ADDR="orders@example.com"
OUT="/var/lib/mailsink"
API="https://grabmail.io/api/v1"

mkdir -p "$OUT"

while :; do
  page=$(curl -fsG "$API/mailbox" \
           --data-urlencode "address=$ADDR" \
           --data-urlencode "limit=200")

  ids=$(printf '%s' "$page" | jq -r '.messages[].id')
  [ -n "$ids" ] || break

  for id in $ids; do
    curl -fsG "$API/message/$id" \
      --data-urlencode "mailbox=$ADDR" > "$OUT/$id.json"
    curl -fs -X DELETE -G "$API/message/$id" \
      --data-urlencode "mailbox=$ADDR" > /dev/null
  done

  sleep 1
done
crontab
17 * * * * /usr/local/bin/drain.sh

Vier Eigenschaften lohnt es sich zu benennen, denn sie unterscheiden einen Job, den man einfach laufen lassen kann, von einem, den man beobachten muss:

  1. Er lässt sich gefahrlos zweimal ausführen. Zwei gleichzeitig gestartete Kopien erledigen dieselbe Arbeit in anderer Reihenfolge und löschen dieselben Nachrichten; die zweite findet ein leeres Postfach vor und stoppt.
  2. Er schreibt, bevor er löscht. Ist die Festplatte voll oder wird der Prozess beendet, ist die Nachricht beim nächsten Lauf noch im Postfach. Die umgekehrte Reihenfolge verliert Mail genau an dem Tag, an dem es darauf ankommt.
  3. Er leert, statt nur zu lesen. Weil jede Nachricht verschwindet, sobald sie sicher auf der Festplatte liegt, liefert die nächste Auflistung die nächsten zweihundert – ein Postfach, das zwischen zwei Läufen vierhundert Nachrichten erhalten hat, wird also vollständig geleert, nicht nur bis auf die neuesten fünfzig.
  4. Er scheitert laut. Ein Exit-Code ungleich null ist es, der cron dazu bringt, Ihnen die Ausgabe zu schicken. Ein Job, der seine eigenen Fehler verschluckt, ist ein Job, der schon seit einem Monat kaputt ist.

Was diese API nicht für Sie tut

Vier Dinge, die sie nicht tut, jedes davon absichtlich, und keins davon kommt später noch dazu. Besser, jetzt darum herum zu planen, als es an einem Skript zu entdecken, das still vor sich hin halb funktioniert hat:

Sie versendet nie
Nur Empfang. Es gibt keinen Endpunkt, der eine Nachricht auf die Leitung legt – deshalb lässt sich hiermit nichts von einer Adresse aus versenden, die Ihnen nicht gehört.
Sie schiebt nie etwas zu
Keine Webhooks und keine Callbacks: Sie fragen, sie antwortet. Ein KI-Agent, der lieber blockiert, bis die Mail ankommt, hat stattdessen wait_for_message über MCP – siehe die Anleitung für Agenten.
Sie sucht nie
Es gibt keinen Query-Parameter für einen Absender oder einen Betreff. Filtern passiert auf Ihrer Seite, über die Auflistung – ein Grund, warum die Auflistung eine Vorschau mitliefert.
Sie authentifiziert nie, auf einer öffentlichen Domain
Wer die Adresse kennt, liest das Postfach. Die Adresse ist das gesamte Geheimnis – behandeln Sie sie auch so: Leiten Sie sie nie aus dem Namen eines Kunden ab, und schicken Sie nichts an eine gemeinsam genutzte Domain, das Sie nicht laut vorgelesen hören wollten.

Die Antwort auf den letzten Punkt ist eine eigene Domain. Richten Sie deren MX auf smtp.grabmail.io, und jede Adresse darauf antwortet auf denselben drei Endpunkten, ohne zweite API zu lernen und ohne Schlüssel zu rotieren – und, auf Anfrage, geschlossen, sodass nur ein Bearer-Token sie öffnet. Eine Domain verbinden braucht einen einzigen DNS-Eintrag.

10 smtp.grabmail.io

Bevor Sie es laufen lassen

Sechs Dinge, die sich zu prüfen lohnen, bei einem Job, der läuft, ohne dass Sie zusehen:

  1. Nicht schneller als einmal pro Sekunde pro Adresse pollen, und Retry-After beachten, wenn Sie zum Warten aufgefordert werden.
  2. next bis zum Ende folgen, statt anzunehmen, ein Aufruf sei schon das ganze Postfach.
  3. Auf den Statuscode und error verzweigen, nie auf message.
  4. Schreiben Sie alles, was Sie behalten müssen, bevor Sie es löschen, und denken Sie daran, dass 5 Tage eine feste Grenze sind, die sich nicht verschieben lässt.
  5. Verankern Sie, was Sie extrahieren, an Ihrer eigenen Vorlage. Ein bloßes sechsstelliges Muster passt bereitwillig auch auf eine Jahreszahl, einen Preis oder eine Bestellnummer, die zuerst ankam.
  6. Gehen Sie davon aus, dass die Adresse öffentlich ist, außer sie liegt auf einer Domain, die Sie kontrollieren – und legen Sie alles, worauf es ankommt, auf eine solche.

Nichts davon braucht ein Konto. Wachsen Sie über die öffentlichen Domains hinaus, ändert sich nur die Domain in der Adresse – die drei Aufrufe oben bleiben exakt, wie sie sind.

Fragen

Brauche ich einen API-Key?

Nein. Auf den öffentlichen Domains gibt es kein Konto, kein Token und nichts zu registrieren, und eine Domain, die Sie hierher zeigen lassen, antwortet auf denselben Endpunkten ebenfalls ohne Schlüssel. Die einzige Ausnahme ist eine Domain, die wir auf Anfrage geschlossen haben – die wird mit einem Authorization: Bearer-Header gelesen.

Wie schnell darf ich pollen?

Einmal pro Sekunde pro Adresse für die Auflistung – das ist der vorgesehene Rhythmus und wird nie gedrosselt. Das Lesen einer Nachricht oder eines Anhangs wird getrennt und deutlich großzügiger gemessen, sodass Sie eine Seite Nachrichten in einem Schub abarbeiten können. Alles zusammen ist auf 1200 Anfragen pro Minute pro Client begrenzt.

Woran erkenne ich, dass ich das ganze Postfach gelesen habe?

Wenn next als null zurückkommt. Schließen Sie nicht von einer kurzen Seite darauf: Der Server entscheidet, was eine Seite ist, und eine Seite, die kürzer als limit ist, ist allein noch nicht das Ende.

Kann ich das aus einem Browser aufrufen?

Ja. Antworten tragen Access-Control-Allow-Origin: *, sodass eine Seite auf jedem Origin die Endpunkte direkt aufrufen kann, ohne eigenen Proxy dazwischen. Autorisierung ist hier nie ein Cookie, sodass es nichts kostet, das so weit zu öffnen.

Was passiert, wenn ich nach einer abgelaufenen Nachricht frage?

404 mit not_found, genau wie bei einer ID, die es nie gab. Alles wird 5 Tage nach der Ankunft gelöscht, gelesen oder nicht, und kein Parameter verlängert das.

Kann ich einen Webhook bekommen, wenn Mail ankommt?

Nein – die REST-API funktioniert nach dem Prinzip Fragen und Antworten, ohne Callbacks. Wenn Sie Code brauchen, der blockiert, bis die Nachricht ankommt, hat der MCP-Server wait_for_message, das genau das tut und für Agenten gedacht ist.

Ist es sicher, eine öffentliche Adresse in Produktion zu verwenden?

Nur für Dinge, bei denen es Ihnen nichts ausmachen würde, wenn ein Fremder sie liest. Wer die Adresse kennt, kann ihr Postfach lesen, über die API genauso wie über die Website. Für alles andere richten Sie eine eigene Domain hierher – die Aufrufe ändern sich dadurch nicht.

Warum erscheint eine Nachricht, die ich nie geöffnet habe, als gelesen?

Weil etwas anderes sie geöffnet hat. Das Lesen einer Nachricht über die API setzt ihr seen-Flag, und das Flag wird mit jedem geteilt, der auf diese Adresse schaut. Ein Skript und ein Mensch, die dasselbe Postfach beobachten, werden sich gegenseitig ständig überraschen – filtern Sie deshalb nach bereits verarbeiteten IDs statt nach seen.

Muss ich Nachrichten löschen?

Nein – alles läuft nach 5 Tagen von selbst ab. Löschen lohnt sich in einem geplanten Job trotzdem, weil ein geleertes Postfach die einfachstmögliche Aufzeichnung dessen ist, was Sie bereits verarbeitet haben.

Probieren Sie es aus, solange es frisch ist

Eine Adresse braucht einen Klick, kein Konto und keine Karte. Alles in dieser Anleitung funktioniert damit sofort.

Willkommen zurück

Ihre Postfächer und Ihre Domains an einem Ort.