Zum Inhalt springen

Basis-URL https://api.proxymail.qa/v1, mit Authorization: Bearer qa_…. Alle Endpunkte stehen in der REST API-Referenz, erzeugt aus der OpenAPI-Beschreibung.

Du nutzt JavaScript oder TypeScript? Das Paket proxymail-qa auf npm kapselt diese Aufrufe, mit Typen.

  1. Nutze einen API-Schlüssel aus dem Projekt, das du testest.

  2. Erstelle eine Adresse mit Persona und Passwort und registriere dich damit in deiner App.

    curl -X POST https://api.proxymail.qa/v1/addresses \
    -H "Authorization: Bearer $PROXYMAIL_QA_API_KEY" -H "Content-Type: application/json" \
    -d '{"label": "Sign-up test", "persona": true, "password": true}'
  3. Warte dann auf den Code. Er kommt von selbst zurück.

    curl "https://api.proxymail.qa/v1/addresses/k7f2x9ab12cd@mailclub.dev/code" \
    -H "Authorization: Bearer $PROXYMAIL_QA_API_KEY"
    {
    "type": "code",
    "subtype": "otp",
    "value": "986512",
    "service": "Spotify",
    "received_at": "2026-10-12T10:00:03Z",
    "expires_at": "2026-10-12T10:10:03Z"
    }
Methode und Pfad Was es tut
GET /v1/project Das Projekt des Schlüssels, seine Mail-Domains und Limits
PATCH /v1/project Ändere name, website, environments, email_prefix oder mail_domain des Projekts, wie in den Projekteinstellungen. Erfordert die Projektleitung oder eine Inhaberin bzw. einen Admin der Organisation; jede Änderung steht im Aktivitätsprotokoll
POST /v1/project/dev-hosts Einen Host wie localhost:3000 als Entwicklungsumgebung markieren (host), damit die E-Mail-Prüfungen Links dorthin akzeptieren
POST /v1/addresses Adresse erstellen (label, name, persona, password, lifetime_days, domain, age oder date_of_birth, country und wo sie genutzt wird: site und environment)
POST /v1/form-values Was in jedes Feld eines Anmeldeformulars einzutragen oder auszuwählen ist, aus einer neu dafür erstellten oder einer früher erstellten Adresse (mehr)
GET /v1/addresses Adressen auflisten, neueste zuerst, oder früher erstellte finden (tag, country, min_age, max_age, has_password, search, site, environment)
GET / PATCH / DELETE /v1/addresses/{address} Eine lesen, aktualisieren (first_name, last_name, username, date_of_birth, country, label, notes, tags) oder löschen
GET /v1/addresses/{address}/code Auf einen Code warten (timeout, service, since; unused=true überspringt bereits verwendete Codes; type=coupon für einen Gutschein- oder Aktionscode, mit Rabatt und Gültigkeitsdatum)
GET /v1/addresses/{address}/link Auf einen Link warten (type: verify, magic_signin, invite, reset oder any; unsubscribe oder list_unsubscribe für einen Abmeldelink; app=true für einen Link, der eine mobile App öffnet)
GET /v1/addresses/{address}/count Eingegangene E-Mails zählen (since, service, type: Code oder Link, subtype, category), jede zusammengefasst ohne Inhalt
GET /v1/addresses/{address}/none Prüfen, dass keine E-Mail eingeht: wartet bis zu 30 Sekunden (timeout), 200, wenn nichts kam, 409, sobald etwas kommt; category=marketing achtet nur auf Angebote und Newsletter, und until hält ein Fenster über mehrere Aufrufe bis zu einer Stunde offen
GET, PUT /v1/addresses/{address}/consent Labs. Wozu die Adresse bei der Anmeldung zugestimmt hat (Marketing, ein vorab angekreuztes Kästchen, Double-Opt-in), für die Marketingregeln je Land
POST /v1/addresses/{address}/coupon-result Labs. Was der Checkout mit einem Gutschein getan hat (accepted, rejected oder error, mit der Meldung der App), und das coupon_result-Urteil
POST /v1/addresses/{address}/sequence Labs. Eine Willkommensserie oder Drip-Kampagne: ob jede E-Mail nach since in der richtigen Reihenfolge und rechtzeitig kam
GET /v1/addresses/{address}/offers Labs. Die Gutscheine jedes Absenders und an welche Variante der Adresse (normal, Großbuchstaben, +tag) jeder ging: reused, wenn eine Person ein Angebot zweimal bekam
GET, POST, DELETE /v1/addresses/{address}/unsubscribed Labs. Abmeldeüberwachung: POST hält fest, dass sich die Adresse bei einem Absender abgemeldet hat, DELETE, dass sie sich wieder angemeldet hat, und GET listet die Marketing-E-Mails, die danach kamen
GET /v1/addresses/{address}/checks Eine E-Mail prüfen: SPF, DKIM und DMARC, lokale und einfache http-Links, wohin ihr Link führt (expect_host), ein angegebenes Ablaufdatum, wann ein Gutschein endet, ein Klartextteil, Größe, reine Bild-E-Mails, vorgegebene Formulierungen (expect_text), ihre Sprache (expect_language), wie schnell sie kam (expect_within) und ihre Anhänge (expect_attachment). Nur Urteile
GET /v1/addresses/{address}/messages E-Mails mit ihren Codes und Links; include=text fügt den Text hinzu, include=html das HTML, include=text,html beides
GET /v1/addresses/{address}/variants Ungewöhnliche Varianten der Adresse zum Testen von Anmeldeformularen: solche, die ihren Posteingang erreichen, solche, die es nicht können, und ungültige (mehr)
GET /v1/variants/names Knifflige Namen für das Namensfeld eines Formulars (field, max_length): solche zum Akzeptieren, solche zum Ablehnen, HTML oder SQL, das als Text gespeichert werden soll, und solche, bei denen Apps sich unterscheiden (mehr)
GET /v1/variants/passwords Knifflige Passwörter innerhalb der Feldregeln (min_length, max_length, symbols): Minimum und Maximum, jede Zeichenklasse, Leerzeichen, Nicht-ASCII und solche, bei denen Apps sich unterscheiden (mehr)
GET /v1/addresses/{address}/delivery Eine Adresse prüfen: was sie erreicht hat, was wir abgelehnt haben, was die Mailserver von Cloudflare abgewiesen haben und warum, und ein Hinweis
GET /v1/addresses/{address}/password Ein gespeichertes Passwort, für Schlüssel, die es lesen dürfen
POST /v1/addresses/sign-in Das gespeicherte Testkonto zum Anmelden und sein Passwort, in einem Aufruf (site, environment, tags), für Schlüssel, die Passwörter lesen dürfen (mehr)
PUT /v1/addresses/{address}/password Das aktuelle Passwort des Testkontos speichern (password, oder weglassen, um eins zu generieren, das einmal zurückgegeben wird); jeder Schlüssel kann eins speichern
POST /v1/addresses/checkout Ein Konto aus dem Pool ausleihen: eins, das den Filtern entspricht (tags, country, min_age, max_age, has_password, search, site, environment) und das niemand sonst hält, für minutes (mehr)
POST /v1/addresses/{address}/extend, POST /v1/addresses/{address}/return Länger behalten oder zurückgeben (token; bei Rückgabe halten add_tags und remove_tags seinen Zustand fest)
POST /v1/runs Einen Lauf starten (name, source, meta): Aufrufe, die seine ID im Header Proxy-Mail-QA-Run senden, fügen hinzu, was sie gesehen haben (mehr)
GET /v1/runs, GET /v1/runs/{id} Läufe auflisten oder einen mit seinen Belegen lesen; format=markdown liefert den Bericht
POST /v1/runs/{id}/notes, POST /v1/runs/{id}/tests, POST /v1/runs/{id}/finish Eine eigene Zeile hinzufügen (text, failed), ein Testergebnis oder die Summen, oder ihn abschließen (failed)

Mit Labs markierte Endpunkte gehören zu Marketing- und Angebots-E-Mails, einer Labs-Funktion, die die Person, die den Schlüssel erstellt hat, in der App unter Account settings → Labs einschaltet. Bis dahin antworten sie mit 403 forbidden und einem entsprechenden Hinweis.

Warte nach der Anmeldung für ein Angebot mit type=coupon auf den Gutschein (any für einen Gutschein oder einen Anmeldecode). Angebote dauern oft Minuten, also warte länger oder frag erneut; mit since sagt after_seconds, wie lange danach die E-Mail ankam:

curl "https://api.proxymail.qa/v1/addresses/$ADDRESS/code?type=coupon&since=$SIGNED_UP&timeout=30" -H "Authorization: Bearer $PROXYMAIL_QA_API_KEY"
# { "value": "WELCOME20", "subtype": "coupon", "discount": "20% off", "valid_until": "2026-10-31", "after_seconds": 212, … }

discount ist unsere Auslegung des Angebots (20% off, $10 off, Free shipping, Buy one, get one) und valid_until das Datum, an dem es laut E-Mail endet („gültig bis 31. Oktober“, „endet am 31. Okt.“, „7 Tage gültig“). Beide sind null, wenn die E-Mail nichts dazu sagt. Wartevorgänge ohne Typ lassen Gutscheine weg, sodass ein Anmeldetest nie den Code eines Angebots erwischt. Um zu prüfen, ob er rechtzeitig kam, lässt GET /checks?since=$SIGNED_UP&expect_within=5m arrival_time fehlschlagen, wenn er später kam.

Ein Angebot pro Person. Mails an name+promo1@… und NAME@… landen im selben Posteingang, daher eignen sie sich gut als „zweite Kunden“ für ein einmaliges oder Willkommensangebot. Melde dich mit der normalen Adresse und mit Varianten aus GET /variants an, warte auf jeden Gutschein, dann GET /v1/addresses/{address}/offers: für jeden Absender seine Gutscheine und die Variante, an die jeder ging (plain, capitals oder +tag), reused, wenn zwei oder mehr Varianten einen bekamen, und same_code, wenn es ein gemeinsamer Code war. passed ist false, wenn ein Angebot wiederverwendet wurde, und jeder Gutschein nach dem ersten lässt offer_reuse in /checks fehlschlagen. Den Punkt-Trick von Gmail kannst du hier nicht testen: Auf diesen Domains ergibt ein Punkt eine andere Adresse.

An der Kasse. Löse den Gutschein mit deinen eigenen Tools ein, nur auf Staging mit Testzahlungen, und halte dann fest, was die App getan hat:

curl -X POST "https://api.proxymail.qa/v1/addresses/$ADDRESS/coupon-result" \
-H "Authorization: Bearer $PROXYMAIL_QA_API_KEY" -H "Content-Type: application/json" \
-d '{"result": "rejected", "code": "WELCOME20", "message": "Code already used", "one_time": true}'

result ist accepted, rejected oder error; code ist standardmäßig der neueste Gutschein der Adresse. Jeder Versuch wird gespeichert, und check in der Antwort ist das coupon_result-Urteil: Es schlägt fehl bei einem Gutschein, der nach seinem Enddatum akzeptiert wurde, für ein Angebot, das bereits an eine andere Variante der Adresse ging, oder zweimal bei one_time, und warnt bei einem Fehler oder einer Ablehnung ohne Begründung. Mit einem Lauf lässt ein fehlgeschlagenes Urteil diesen fehlschlagen.

Wenn ein Formular mehr als eine E-Mail-Adresse verlangt, sende, was die Seite zu jedem Feld sagt, und erhalte zurück, was du in jedes eintragen oder auswählen sollst. Die Werte stammen von einer neu für das Formular erstellten Adresse: Sie bekommt ein Geburtsdatum, wenn das Formular danach fragt (volljährig, außer du übergibst age), ein Land, das das Dropdown des Formulars anbietet (oder country), und ein Passwort, das zu min_length, max_length und pattern des Passwortfelds passt, auf der Adresse gespeichert und nur in dieser Antwort angezeigt. Übergib address, um stattdessen eine früher erstellte zu nutzen (ihr gespeichertes Passwort wird nicht zurückgegeben).

curl -s -X POST https://api.proxymail.qa/v1/form-values -H "Authorization: Bearer $PROXYMAIL_QA_API_KEY" \
-H "Content-Type: application/json" -d '{
"fields": [
{ "id": "email", "type": "email", "label": "Email" },
{ "id": "user", "name": "username", "label": "Username", "max_length": 15 },
{ "id": "pw", "type": "password", "label": "Password", "min_length": 12 },
{ "id": "dob", "type": "text", "label": "Date of birth", "placeholder": "DD/MM/YYYY" },
{ "id": "country", "type": "select", "label": "Country", "options": [{ "value": "de", "label": "Germany" }, { "value": "uk", "label": "United Kingdom" }] },
{ "id": "terms", "type": "checkbox", "label": "I agree to the terms" }
]
}'
# { "address": "k7f2x9ab12cd@mailclub.dev", "created": true, "password": "…", "persona": {…},
# "values": [ { "id": "email", "kind": "email", "value": "k7f2x9ab12cd@mailclub.dev" },
# { "id": "dob", "kind": "date_of_birth", "value": "04/05/1990" },
# { "id": "country", "kind": "country", "value": "uk" },
# { "id": "terms", "kind": "other", "value": null, "note": "Not a sign-up detail Proxy Mail QA fills: fill it yourself, or leave it." }, … ] }

Jedes Feld kann id haben (deine, mit ihrem Wert zurückgegeben), type (oder select), name, label, placeholder, autocomplete, group (was die umgebende Gruppe sagt, etwa „Geburtsdatum“ über Dropdowns für Tag, Monat und Jahr), pattern, min_length, max_length und options (value und label). Felder für Tag, Monat und Jahr erhalten jeweils ihren Teil, eine Datumsauswahl erhält YYYY-MM-DD, und ein getipptes Datum erhält das Format, das Label oder Platzhalter vorgeben (US-Reihenfolge mit note, wenn nichts angegeben ist). Proxy Mail QA sieht oder füllt die Seite nie aus: Das machst du, dann wartest du wie gewohnt auf Code oder Link.

Überall, wo API eine Adresse annimmt, funktioniert auch eine Variante: Großbuchstaben oder ein Plus-Tag wie k7f2x9ab12cd+o'brien@mailclub.dev (im Pfad URL-kodiert). Sie wirkt auf die normale Adresse, und ein Warten oder eine Nachrichtenliste für eine +tag-Variante findet nur Mails an diese Variante. Siehe Ungewöhnliche E-Mail-Adressen.

Die anderen Felder des Formulars werden genauso behandelt. GET /v1/variants/names?field=first_name&max_length=50 listet Namen in vier Gruppen: accept (der Name einer realen Person: O'Brien, Smith-Jones, José, Straße, 李, Мария, محمد, สมชาย, ein Buchstabe, genau das Maximum), reject (eins über dem Maximum, leer, nur Leerzeichen), store_as_text (<b>Jane</b>, ein Script-Tag, eine SQL-Injection, {{7*7}}: Die App sollte sie genau wie eingegeben speichern und anzeigen, nie ausführen) und decide (Ziffern, Symbole, ein Emoji, Leerzeichen drumherum, zusammengesetzte und zerlegte Akzente sowie 64, 128 und 255 Zeichen, wenn kein Maximum angegeben ist: Apps unterscheiden sich, also notiere, was die App getan hat). GET /v1/variants/passwords?min_length=8&max_length=64&symbols=true macht dasselbe für ein Passwort: accept (genau Minimum und Maximum, jede Zeichenklasse einschließlich Anführungszeichen, Backslash und Leerzeichen, Nicht-ASCII-Buchstaben), reject und decide (nur Ziffern, nur Kleinbuchstaben, ein Leerzeichen am Anfang oder Ende, ein Emoji und ein Paar mit denselben ersten 72 Bytes, wenn das Maximum es erlaubt, da bcrypt dort aufhört). Passwörter sind bei jedem Aufruf neu. Jeder Eintrag hat tests (was er prüft) und value (was einzugeben ist). Siehe Ungewöhnliche Namen und Passwörter.

Füge "age": "under13" (oder adult, over21, under18, under15, any, turns18tomorrow, turned18today) hinzu, um der Persona ein Geburtsdatum in diesem Bereich zu geben, oder sende dein eigenes als "date_of_birth": "2012-05-01". Es kommt als persona.date_of_birth zurück, YYYY-MM-DD, und nur, wenn die Adresse eins hat.

GET /v1/addresses nimmt Filter an, sodass ein Test oder Agent zu einem früher erstellten Konto zurückkehren kann, statt ein neues zu erstellen. Jeder angegebene Filter muss passen:

  • tag: wiederhole ihn oder trenne Tags mit Kommas, um alle zu verlangen
  • country: das Land der Persona, etwa GB
  • min_age und max_age: heutiges Alter in ganzen Jahren, aus dem Geburtsdatum der Persona (max_age=17 findet Unter-18-Jährige); Adressen ohne Geburtsdatum passen nicht
  • has_password: true oder false, ob ein Passwort darauf gespeichert ist
  • search: Teil der Adresse, des Labels, der Notizen, des Namens oder des Benutzernamens
  • site und environment: wo sie genutzt wird, als Host oder Seiten-URL (site=staging.example.com) und als Name (environment=staging). Setze sie beim Erstellen einer Adresse; die Chrome-Erweiterung speichert sie ebenfalls, und die App zeigt sie bei jedem Konto an
curl "https://api.proxymail.qa/v1/addresses?tag=age-gate&country=GB&max_age=17&has_password=true" \
-H "Authorization: Bearer $PROXYMAIL_QA_API_KEY"

POST /v1/addresses/sign-in wählt das gespeicherte Testkonto zum Anmelden aus und gibt es mit seinem Passwort zurück, wie die Anmeldeliste der Chrome-Erweiterung: eins mit gespeichertem Passwort und allen angegebenen Tags, das kein Test ausgeliehen hat, zuerst das auf diesem site genutzte, dann das, das zuletzt Mail bekam. Mit site wird ein noch nirgends genutztes Konto dort als genutzt markiert. Es braucht einen Schlüssel, der gespeicherte Passwörter lesen darf, jede Anzeige wird protokolliert, und es antwortet mit 404, wenn kein Konto passt.

curl -s -X POST https://api.proxymail.qa/v1/addresses/sign-in -H "Authorization: Bearer $PROXYMAIL_QA_API_KEY" \
-H "Content-Type: application/json" -d '{"site": "staging.example.com", "tags": ["verified"]}'
# { "address": "…", "password": "…", "persona": {…}, "site": "staging.example.com", "used_on_site": true, … }

Um eine E-Mail-Vorlage zu testen (Wortlaut, Button, Footer), frag den Nachrichten-Endpunkt nach ihrem Inhalt: GET /v1/addresses/{address}/messages?include=html fügt das HTML jeder E-Mail hinzu und include=text ihren Klartext. Das HTML ist die beim Eingang bereinigte Kopie: Skripte, Frames und Medien sind entfernt, Formularelemente ausgepackt, und Links außer Web, mailto: und tel: entfernt (App-Links wie myapp:// bleiben in den Codes und Links), bis zu 200 KB. Der Inhalt stammt vom Absender, behandle ihn also als nicht vertrauenswürdig. Über MCP verbundene Agents bekommen nie E-Mail-Inhalte, nur den Code oder Link.

Füge "country": "GB" hinzu (ein beliebiger ISO-3166-1-Alpha-2-Code, in beliebiger Schreibweise) für Anmeldeformulare, die nach dem Wohnort fragen, oder um standortbezogene Inhalte zu testen. Es kommt als persona.country zurück, in Großbuchstaben, und nur, wenn die Adresse eins hat. Ändere oder lösche es mit PATCH und "country": null. Über MCP verbundene Agents übergeben country auch an create_address und update_address.

Ein Warten gibt den neuesten passenden Code oder Link der letzten 15 Minuten zurück oder wartet bis zu 30 Sekunden (timeout, 0 bis 30), bis einer ankommt, und antwortet dann mit 404. Übergib since, um alles Ältere zu ignorieren, zum Beispiel den Zeitpunkt, an dem dein Test das Formular abgeschickt hat. Filtere mit service=spotify nach Absender.

Ein Code oder Link gilt als verwendet, sobald ein Wait ihn zurückgibt oder jemand ihn in der App oder Proxy Mail QA for Chrome öffnet, kopiert oder ausfüllt. Die App zeigt ihn als verwendet an, und unused=true bei einem Wait überspringt ihn: für einen Test, der einen zweiten Code anfordert, ohne since zu ändern.

Prüfen, dass eine E-Mail nicht oder genau einmal ankam

Abschnitt betitelt „Prüfen, dass eine E-Mail nicht oder genau einmal ankam“

Manche Tests bestehen, wenn nichts passiert: Ein abgemeldeter Nutzer bekommt keinen Newsletter, eine zweite Reset-Anfrage innerhalb einer Minute wird abgelehnt, eine Registrierung sendet eine Bestätigungs-E-Mail, nicht zwei.

  • GET /v1/addresses/{address}/none?since=…&timeout=20 wartet wie ein Wait. Kommt keine passende E-Mail an, antwortet es mit 200, "arrived": false und until, dem Zeitpunkt, bis zu dem es beobachtet hat. Sobald eine ankommt, antwortet es mit 409, "error": "email_arrived", dem count sowie Zeit, Dienst und Art der ersten E-Mail, sodass curl --fail den Schritt fehlschlagen lässt. Gib since als Zeitpunkt der Aktion an, damit auch eine E-Mail zählt, die vor dem Aufruf kam; ohne das beobachtet es ab der Anfrage. Für längere Zeiträume füge until hinzu (bis zu 60 Minuten nach since) und rufe erneut mit denselben since und until auf, solange die Antwort "done": false sagt; seconds_left gibt an, wie viel Zeit noch bleibt.
  • GET /v1/addresses/{address}/count?since=… antwortet sofort mit count und den ersten 50 E-Mails, älteste zuerst. Für „genau eine“ zählst du, nachdem der Wait auf die erste E-Mail zurückgekehrt ist und ein Duplikat Zeit hatte anzukommen.

Beide filtern wie ein Wait: service und type=code (Codes außer Gutscheinen) oder type=link (Bestätigungs-, Magic-Sign-in-, Einladungs- und Reset-Links), eingegrenzt mit subtype, etwa type=link&subtype=reset, und nach category. Für „ein abgemeldeter Nutzer bekommt keinen Newsletter“ nimm category=marketing, damit eine Quittung oder ein Passwort-Reset, die weiterhin ankommen sollen, den Test nicht fehlschlagen lassen. Eine +tag-Variante zählt nur ihre eigene Mail. Keine von beiden gibt zurück, was in den E-Mails steht: Jede wird durch received_at, service sowie type und subtype ihres Haupt-Codes oder -Links zusammengefasst. Der Node.js-Client hat countEmails und expectNoEmail, der Python-Helper count_emails und expect_no_email.

Jede ankommende E-Mail bekommt einen Typ, mit den Gründen dafür:

category Wann
auth Ihr Haupt-Code oder -Link meldet an, bestätigt, setzt ein Passwort zurück oder lädt ein, oder der Betreff sagt das
transactional Der Betreff klingt nach Quittung, Bestellung, Zahlung, Buchung oder Sicherheitshinweis, oder eine Rechnungs-PDF oder Kalendereinladung ist angehängt; außerdem alles, was weder Anmeldung noch Marketing ist
marketing Sie hat List-Unsubscribe-, List-Unsubscribe-Post- oder List-Id-Header, Precedence: bulk, einen Gutschein, einen Werbebetreff oder einen Abmeldelink

Quittungen haben bewusst Vorrang vor Marketing: Eine Quittung mit einem Code „10 % Rabatt beim nächsten Mal“ bleibt eine Quittung. /count, /none und /checks akzeptieren category, und /count liefert für jede E-Mail category, gap_seconds (seit der vorherigen) sowie Typen, Endungen und Größen ihrer Anhänge. E-Mails, die vor Einführung der Typen ankamen, haben keinen.

Die Abmeldelinks einer E-Mail werden als Werte gespeichert: GET /link?type=unsubscribe liefert ihren eigenen Abmelde- oder E-Mail-Einstellungslink und type=list_unsubscribe den des List-Unsubscribe-Headers, den der Abmelden-Button von Gmail nutzt. Um zu tun, was dieser Button tut, sende mit deinen eigenen Tools ein POST mit List-Unsubscribe=One-Click daran. Proxy Mail QA öffnet keinen der beiden.

Von Anhängen werden Name, Typ und Größe jeder Datei gespeichert, nie die Datei selbst. GET /checks?expect_attachment=.pdf (für mehrere wiederholen) lässt expected_attachment fehlschlagen, wenn einer fehlt; ein Muster ist ein Typ (application/pdf, image/*), eine Endung (.ics) oder ein Dateiname mit * (invoice-*.pdf). /messages listet sie mit ihren Namen auf.

Um zu testen, ob die Abmeldung funktioniert, melde die Adresse mit deinen eigenen Tools ab: Öffne den Abmeldelink der E-Mail (GET /link?type=unsubscribe) oder sende ein POST mit List-Unsubscribe=One-Click an den Link aus dem Header (type=list_unsubscribe). Dann erfasse es:

curl -X POST "https://api.proxymail.qa/v1/addresses/k7f2x9ab12cd@mailclub.dev/unsubscribed" \
-H "Authorization: Bearer $PROXYMAIL_QA_API_KEY" -H "Content-Type: application/json" \
-d '{"email_id": "…", "at": "2026-10-19T10:02:00Z"}'

email_id ist die E-Mail, von der du dich abgemeldet hast; service (ein Name oder eine Domain wie acme.com) geht auch, und ohne beides entscheidet die neueste Marketing-E-Mail. at datiert es zurück; doppeltes Markieren schadet nicht. grace (etwa 2h oder 1d, bis zu 30 Tage) lässt Marketing durch, das schon unterwegs war: Eine E-Mail innerhalb dieser Frist zählt nicht. Ab dann lässt jede Marketing-E-Mail dieses Absenders after_unsubscribe in /checks fehlschlagen, egal wie viel später, und erscheint im Reports-Dashboard. Quittungen, Passwort-Resets und Anmelde-E-Mails zählen nie. GET /unsubscribed listet jeden Watch mit den danach eingegangenen E-Mails (after_unsubscribe: IDs, Zeiten und after_seconds, nie deren Inhalt) und passed; mit einem Run lässt eine fehlgeschlagene Prüfung ihn fehlschlagen. DELETE /unsubscribed?service=acme.com erfasst, dass die Adresse sich wieder angemeldet hat.

Für eine Prüfung innerhalb des Tests löst du eine weitere Kampagne aus und hältst GET /none?category=marketing&since=…&until=… einige Minuten offen.

Bei einer Willkommensserie oder Drip-Kampagne prüft POST /v1/addresses/{address}/sequence, ob jede E-Mail in der richtigen Reihenfolge und pünktlich kam:

curl -X POST "https://api.proxymail.qa/v1/addresses/$ADDRESS/sequence" \
-H "Authorization: Bearer $PROXYMAIL_QA_API_KEY" -H "Content-Type: application/json" \
-d '{"since": "2026-10-01T10:00:00Z", "steps": [{"subject": "Welcome"}, {"subject": "tips", "after": "1d"}, {"category": "marketing", "after": "7d", "within": "12h"}]}'

Jeder Schritt kann eine Phrase aus seinem subject, seinen category, seinen type (Code oder Link), after (wann er kommen soll, ab since, bis zu 90 Tage) und within (welche Abweichung noch besteht; Standard ein Zehntel von after, mindestens 10 Minuten) angeben. Jeder nimmt die erste passende E-Mail nach dem vorherigen Schritt und antwortet mit pass, early, late, missing oder waiting (noch nicht fällig), mit received_at und off_seconds. done ist false, solange ein Schritt wartet, also prüfe später noch einmal. Betreffs werden auf dem Server abgeglichen und nie zurückgegeben.

Erfasse, wozu das Konto bei der Registrierung zugestimmt hat, und /checks bewertet jede Marketing-E-Mail nach den Regeln am Wohnort der Persona (ihr country, festgelegt beim Erstellen der Adresse):

curl -X PUT "https://api.proxymail.qa/v1/addresses/$ADDRESS/consent" \
-H "Authorization: Bearer $PROXYMAIL_QA_API_KEY" -H "Content-Type: application/json" \
-d '{"marketing": "declined", "prechecked": false}'

marketing ist opted_in oder declined, prechecked gibt an, dass das Kästchen beim Laden des Formulars schon angehakt war, und double_opt_in ist confirmed oder pending (es bestätigt sich selbst, wenn der Bestätigungslink genutzt wird). Gib nur an, was sich geändert hat; GET /consent liest es zurück. Abgedeckt sind EU und EWR, Großbritannien, USA, Kanada, Australien, Neuseeland, die Schweiz, Japan, Südkorea, Singapur und Hongkong, und jedes Urteil nennt seine offizielle Quelle und das Datum, an dem wir sie geprüft haben. Zu jeder Regel siehe Regeln für Marketing-E-Mails nach Land. Das ist eine Testhilfe, keine Rechtsberatung.

GET /v1/addresses/{address}/checks führt Prüfungen an einer E-Mail aus: der neuesten, die denselben Filtern wie eine Zählung entspricht (since, service, type, subtype), oder der durch email_id benannten. Es wartet nicht, also warte zuerst auf die E-Mail. Jede Prüfung besteht, schlägt fehl, warnt oder wird übersprungen, mit einer einzeiligen Begründung, und passed ist true, wenn keine fehlgeschlagen ist:

Prüfung Schlägt fehl oder warnt, wenn
spf, dkim, dmarc Der sendende Server darf nicht für die Domain senden, die E-Mail ist nicht signiert, oder sie verletzt die DMARC-Richtlinie der From-Domain: Posteingänge sortieren sie dann eventuell als Spam ein oder lehnen sie ab. Das sind die eigenen Urteile des empfangenden Servers
local_links Ein Link führt zu localhost oder einer privaten Adresse, direkt oder nach einer Weiterleitung wie redirect_to von Supabase. Links zu einem als Entwicklungsumgebung markierten Host (in den Projekteinstellungen oder POST /v1/project/dev-hosts) bestehen
plain_http Ein Link auf eine öffentliche Website ist nicht https
expected_host Mit expect_host=staging.example.com: Der Anmelde-, Bestätigungs-, Reset- oder Einladungslink landet woanders, etwa in Produktion. Links, die eine App öffnen (myapp://…), haben keine Website und werden nicht verglichen. Ohne das übersprungen
expiry Die E-Mail sagt nicht, wann ihr Anmeldecode oder -link abläuft (eine Warnung; bei anderen Links, etwa in einem Newsletter, übersprungen)
coupon_expiry Labs. Die E-Mail enthält einen Gutschein, sagt aber nicht, wann er endet, etwa „gültig bis 31. Oktober“ oder „7 Tage gültig“ (eine Warnung). Jeder Gutschein darin zählt, nicht nur der Haupt-Code. Übersprungen, wenn es keinen Gutschein gibt
plain_text Es gibt keine Nur-Text-Version (eine Warnung)
size Das HTML ist größer als 102 KB, ab denen Gmail es abschneidet (eine Warnung)
images_only Sie besteht fast nur aus Bildern und sagt daher bei deaktivierten Bildern fast nichts
layout Sie passt eventuell nicht auf ein Smartphone: fest breiter als 640 px gesetzte Teile ohne Mobile-Styles (@media), Bilder ohne Alt-Text (leer bei deaktivierten Bildern) oder Text unter 11 px. Eine Warnung, nur aus dem HTML, ohne es zu rendern
expected_text Mit expect_text (für bis zu 10 Phrasen wiederholen): Im Betreff oder Text fehlt eine, etwa der Vorname des Nutzers oder der Produktname. Groß-/Kleinschreibung, typografische Anführungszeichen und Leerzeichen spielen keine Rolle, und die Begründung nennt nur die fehlenden Phrasen. Ohne das übersprungen
expected_attachment Mit expect_attachment=.pdf (für mehrere wiederholen): Einer davon ist nicht angehängt. Ohne das übersprungen, oder bei einer E-Mail, die vor der Erfassung von Anhängen ankam
arrival_time Mit since und expect_within=5m (oder 90s, 2h, eine Anzahl Sekunden): Sie kam später als das nach since an, etwa ein Willkommensgutschein, der 14 Minuten brauchte. Ohne das übersprungen
list_unsubscribe Labs. Nur Marketing-E-Mails: Es gibt keinen List-Unsubscribe-Header (schlägt fehl), oder er enthält nur eine E-Mail-Adresse oder eine einfache http-Adresse (warnt). Posteingänge zeigen damit ihren eigenen Abmelden-Button, und Gmail und Yahoo verlangen ihn von Massenversendern
one_click_unsubscribe Labs. Nur Marketing-E-Mails: kein List-Unsubscribe-Post: List-Unsubscribe=One-Click-Header mit einer https-Adresse, oder eine verifizierte DKIM-Signatur deckt nicht beide List-Unsubscribe-Header ab (RFC 8058 verlangt das, und Posteingänge bieten sonst kein One-Click an), sodass der Button eines Posteingangs nicht in einem Schritt abmelden kann (eine Warnung)
unsubscribe_link Labs. Nur Marketing-E-Mails: Die E-Mail selbst hat keinen Abmelde- oder E-Mail-Einstellungslink
after_unsubscribe Labs. Nur Marketing-E-Mails: Sie kam, nachdem sich die Adresse von ihrem Absender abgemeldet hatte (siehe Abmelde-Watches). Übersprungen, wenn die Adresse nicht als von ihm abgemeldet markiert ist
offer_reuse Labs. Ein Gutschein von einem Absender, der schon einen an eine andere Variante der Adresse geschickt hat (name+promo1@…, Großbuchstaben): Die App hat eine Person als mehrere behandelt. Bei E-Mails ohne Gutschein übersprungen
coupon_result Labs. Was der Checkout mit dem Gutschein gemacht hat, sobald ein Ergebnis erfasst ist: nach dem Enddatum akzeptiert, für ein wiederverwendetes Angebot oder bei Einmalgutscheinen zweimal (schlägt fehl); ein Fehler oder eine Ablehnung bei der ersten Nutzung innerhalb der Gültigkeit (warnt). Ohne Ergebnis übersprungen
marketing_consent Labs. Marketing-E-Mail an ein Konto, das abgelehnt hat, wo zuerst eine Einwilligung nötig ist (EU und EWR, Großbritannien, Kanada, Australien, Neuseeland, Schweiz, Japan, Südkorea). In den USA übersprungen oder wenn keine Einwilligung erfasst ist
prechecked_consent Labs. Die Einwilligung kam aus einem vorab angehakten Kästchen (EU und EWR, Großbritannien)
double_opt_in Labs. Marketing, bevor die Adresse ihr Abonnement bestätigt hat: eine Warnung in Deutschland, ein Fehler, wo das Projekt Double-Opt-in verlangt
unsubscribe_deadline Labs. Marketing nach einer Abmeldung: eine Warnung innerhalb der erlaubten Frist (USA, Kanada, Singapur und Hongkong 10 Werktage, Australien und Neuseeland 5, EU, Großbritannien, Schweiz, Japan und Südkorea unverzüglich), danach ein Fehler
postal_address Labs. Keine Postanschrift in einer Marketing-E-Mail gefunden (USA, Kanada; eine Warnung)
sender_identity Labs. Die From-Zeile nennt keinen Absender (eine Warnung)
minor_marketing Labs. Marketing an eine Persona unter 13 in Großbritannien oder unter 16 in EU und EWR (eine Warnung)
misleading_subject Labs. USA, Kanada, Hongkong: ein Betreff, der wie eine Antwort oder Weiterleitung beginnt (Re:, Fwd:) oder sagt, das Angebot ende heute, obwohl der Gutschein noch Tage länger gilt (eine Warnung; für US-Personas gelten die Gesetze von Washington und Kalifornien)
ad_label Labs. Singapur: Marketing an ein Konto, das abgelehnt hat, dessen Betreff nicht mit <ADV> beginnt (eine Warnung)
harmful_attachment Labs. Ein Anhang hat einen Dateityp, den Gmail blockiert (.exe, .js, .iso und andere), oder ist ein Programm, das wie ein Dokument benannt ist (.pdf.exe): schlägt fehl. Office-Dateien, die Makros enthalten können, HTML- oder SVG-Seiten und Archive warnen. Die Begründung nennt Endungen, nie Dateien. Ohne Anhänge übersprungen
spam_risk Labs. Ein Spam-Score von 0 bis 10 (score), der die Merkmale addiert, die Spamfilter gewichten (markers, jeweils mit Punkten): schlägt ab 5 fehl, wo Posteingänge die E-Mail wahrscheinlich ablehnen oder als Spam einsortieren, und warnt ab 1,5. Harte Blocker zählen 5 oder 6: DMARC schlägt bei einer Reject- oder Quarantine-Richtlinie fehl, weder SPF noch DKIM besteht, ein blockierter Anhang. Dann 2 bis 3: keine DMARC-Richtlinie, kein DKIM, fast nur Bilder, ein Link auf eine nackte IP-Adresse, ein Linkkürzer, Marketing ohne List-Unsubscribe-Header. Dann bis zu 1,5: SPF schlägt fehl, riskante Anhänge, unsigniertes One-Click-Abmelden, ein Marketing-Betreff, der mit Re: beginnt, ein Betreff in Großbuchstaben oder mit !!!, klassische Spam-Phrasen, Linktext, der eine andere Website zeigt, kein Nur-Text. Eine Schätzung: Posteingänge gewichten auch die Reputation des Absenders, die nur sie sehen
preheader Labs. Nur Marketing-E-Mails: kein versteckter Vorschautext am Anfang des HTML, sodass Posteingänge stattdessen die ersten Wörter zeigen, oft „Im Browser ansehen“ (eine Warnung)
language Mit expect_language=de (oder pt-BR): Sie ist in einer anderen Sprache. Warnt, wenn zu wenig Text für eine Erkennung da ist. Englisch, Deutsch, Französisch, Spanisch, Italienisch, Niederländisch, Portugiesisch, Schwedisch, Polnisch, Türkisch, Finnisch, Russisch, Ukrainisch, Japanisch, Chinesisch, Koreanisch, Arabisch, Hebräisch, Griechisch, Thai und Hindi. Ohne das übersprungen

Mit Labs markierte Prüfungen entfallen, bis der Ersteller des Schlüssels die zugehörige Labs-Funktion einschaltet: Marketing- und Angebots-E-Mails für die Gutschein- und Marketingprüfungen, Spam-Score- und Anhangsprüfungen für harmful_attachment und spam_risk.

curl "https://api.proxymail.qa/v1/addresses/k7f2x9ab12cd@mailclub.dev/checks?type=link&expect_host=staging.example.com" \
-H "Authorization: Bearer $PROXYMAIL_QA_API_KEY"
{
"address": "k7f2x9ab12cd@mailclub.dev",
"email": { "id": "…", "received_at": "2026-10-17T10:00:01Z", "service": "Supabase", "service_domain": "supabase.io", "type": "link", "subtype": "verify" },
"passed": false,
"failed": ["local_links"],
"warnings": ["expiry"],
"checks": [
{ "check": "spf", "status": "pass", "detail": "The sending server is allowed to send for supabase.io." },
{ "check": "local_links", "status": "fail", "detail": "A link goes to localhost:3000, which only opens on the computer running that server." }
]
}

Anmelde-E-Mails und Quittungen überspringen die vier Marketingprüfungen, ebenso E-Mails von vor der Einführung der Typen. Eine Prüfung der neuesten E-Mail kann jetzt auf einem Newsletter landen und list_unsubscribe fehlschlagen lassen: Gib type, subtype oder category an, um die gemeinte E-Mail zu prüfen.

Bei lokalisierten E-Mails prüfst du die Fassung jeder Locale: ?type=code&expect_language=fr&expect_text=Bonjour%20Maya.

Die Antwort enthält nie, was in der E-Mail steht, oder ihre Links: Begründungen nennen höchstens eine Domain, den Host eines Links oder deine eigenen Phrasen, sodass ein Agent darauf reagieren kann, ohne nicht vertrauenswürdigen E-Mail-Text zu lesen. Der Node.js-Client hat checkEmail, der Python-Helper check_email.

Halte Testkonten in bekannten Zuständen und lass jeden Test eines ausleihen: Markiere sie mit ihrem Zustand (verified, premium, trial) und checke dann eines aus. Du bekommst ein Konto, das zu jedem Filter passt und das niemand sonst ausgecheckt hat, das am längsten nicht genutzte zuerst, reserviert für minutes (10, wenn du nichts anderes angibst, höchstens 60). Parallele Tests bekommen nie dasselbe.

curl -s -X POST https://api.proxymail.qa/v1/addresses/checkout -H "Authorization: Bearer $PROXYMAIL_QA_API_KEY" \
-H "Content-Type: application/json" -d '{"tags":["verified","premium"],"country":"DE","minutes":5}'
# { "address": "…", "persona": {…}, "tags": ["premium", "verified"], "checkout": { "token": "…", "until": "…" } }

Melde dich damit an (mit seinem gespeicherten Passwort, bei Schlüsseln, die es lesen dürfen), führe den Test aus und gib es dann mit dem Token zurück. Bei der Rückgabe kannst du den Zustand speichern, in dem der Test es hinterlassen hat, damit der Pool stimmt:

curl -s -X POST "https://api.proxymail.qa/v1/addresses/$ADDRESS/return" -H "Authorization: Bearer $PROXYMAIL_QA_API_KEY" \
-H "Content-Type: application/json" -d "{\"token\":\"$TOKEN\",\"add_tags\":[\"cancelled\"],\"remove_tags\":[\"premium\"]}"

Der Node.js-Client und der Python-Helper können Konten auch mit Hooks aus deinen Tests anlegen, prüfen und zurücksetzen, sodass ein Test ein Konto im benötigten Zustand bekommt.

Ein Checkout, der nicht zurückgegeben wird, gibt sich selbst frei, wenn seine Zeit abläuft. So hält ein abgestürzter Test ein Konto nie lange fest; extend gibt ihm mehr Zeit. Wenn jeder Treffer ausgecheckt ist, lautet die Antwort 409 und nennt, wann der erste frei wird; gibt es keinen Treffer, 404. Die App zeigt, wer ein Konto hat und bis wann, und jeder im Projekt kann es freigeben.

Ein Run fasst einen CI-Job oder die Sitzung eines Agents zusammen und bewahrt auf, was seine Aufrufe gesehen haben. Starte einen und sende dann seine id im Header Proxy-Mail-QA-Run jedes Aufrufs:

RUN=$(curl -s -X POST https://api.proxymail.qa/v1/runs -H "Authorization: Bearer $PROXYMAIL_QA_API_KEY" \
-H "Content-Type: application/json" -d '{"name":"Sign-up tests","source":"ci"}' | jq -r .id)
curl -s "https://api.proxymail.qa/v1/addresses/$ADDRESS/code?since=$SUBMITTED" \
-H "Authorization: Bearer $PROXYMAIL_QA_API_KEY" -H "Proxy-Mail-QA-Run: $RUN"
curl -s -X POST "https://api.proxymail.qa/v1/runs/$RUN/finish" -H "Authorization: Bearer $PROXYMAIL_QA_API_KEY"
curl -s "https://api.proxymail.qa/v1/runs/$RUN?format=markdown" -H "Authorization: Bearer $PROXYMAIL_QA_API_KEY"

GET /v1/runs/{id} vergleicht den Run außerdem mit den letzten 10 abgeschlossenen Runs mit demselben Namen (comparison in JSON, eine Liste weit oben im Markdown-Bericht): deutlich langsamere E-Mails, eine andere Anzahl E-Mails, neue und sporadische Testfehler, neu fehlschlagende Prüfungen sowie ein neuer Absender oder eine neue Link-Website. Wenn E-Mails geprüft wurden, gibt deliverability an, wie viele wahrscheinlich abgelehnt oder als Spam einsortiert würden (likely_spam), im Spam landen könnten (may_spam) oder einen Anhang enthalten, den Posteingänge blockieren. Jede E-Mail zählt einmal, nach ihrer letzten Prüfung; der Markdown-Bericht sagt dasselbe in der Zeile unter seiner Überschrift. Es ist eine Schätzung aus der spam_risk-Prüfung jeder E-Mail, daher müssen Spam-Score- und Anhangsprüfungen in Labs eingeschaltet sein.

Jeder Aufruf fügt hinzu, was er gesehen hat: eine angelegte (inklusive Formularwerten) oder aus dem Pool ausgecheckte Adresse, eine eingegangene E-Mail (der Code oder Website und Pfad des Links, wobei alles, was wie ein Token aussieht, verborgen wird, und wie viele Sekunden nach since), eine Anzahl, E-Mail-Prüfungen, Prüfungen zu Abmeldung, Angeboten und Gutscheinen sowie eine E-Mail, die nie kam oder kam, obwohl sie nicht sollte. Beim Abschließen gilt der Run als bestanden, es sei denn, etwas davon ist fehlgeschlagen, eine Notiz hat ihn scheitern lassen oder du sendest {"failed": true}. Der Bericht ist Markdown, für einen Pull-Request-Kommentar oder eine Job-Zusammenfassung, und der Run ist 90 Tage lang in der App für alle im Projekt sichtbar.

Um anzugeben, zu welchem Test ein Aufruf gehört, sende dessen Namen prozentkodiert in Proxy-Mail-QA-Test (etwa sign-up%20%E2%80%BA%20sends%20one%20code): Der Bericht zeigt ihn neben dem, was der Aufruf gesehen hat. Der Reporter eines Test-Runners sendet Ergebnisse an POST /v1/runs/{id}/tests: jeden fehlgeschlagenen oder instabilen Test als {"name", "status": "failed", "duration_ms", "error"} (ein fehlgeschlagener Test lässt den Run scheitern), dann {"totals": {"passed", "failed", "skipped", "flaky"}} für den Kopf des Berichts.

Nachweise lassen nie einen Aufruf scheitern: Wenn ein Aufruf nicht hinzugefügt werden kann (der Run ist abgeschlossen oder gehört nicht zum Projekt dieses Schlüssels), funktioniert er trotzdem, und der Header Proxy-Mail-QA-Run-Warning nennt den Grund. Ein Client, der erneut fragt, wenn ein Warten leer zurückkommt, sendet Proxy-Mail-QA-Run-Retry: true bei allen Versuchen außer dem letzten, sodass nur das letzte „nichts angekommen“ zählt. In GitHub Actions erledigt die Proxy Mail QA-Action all das.

Die 404 nennt auch den Grund, soweit wir ihn erkennen: hint nennt alles, was wir für die Adresse abgelehnt haben (zu groß oder über einem Limit), oder, wenn sie noch nie etwas erreicht hat, die wahrscheinlichste Ursache; refused listet, was wir seit since abgewiesen haben; blocked listet E-Mails, die die Mailserver von Cloudflare abgewiesen haben, bevor sie uns erreichten (abgelehnt wegen fehlgeschlagener SPF-, DKIM- oder DMARC-Prüfung, verworfen oder nicht zugestellt), mit der Domain des Absenders und deren SPF-, DKIM- und DMARC-Ergebnissen; und help verlinkt auf Test-E-Mail kommt nicht an?. Die MCP-Tools beantworten found: false mit denselben message, refused und help. Um eine Adresse jederzeit ohne Warten zu prüfen, rufe GET /v1/addresses/{address}/delivery auf: Es liefert die bisher empfangenen E-Mails, jede noch gespeicherte Ablehnung und ein hint. Deine App muss an jede Adresse E-Mails senden können: Ein E-Mail-Anbieter im Sandbox- oder Testmodus sendet nur an Adressen, die du verifiziert hast.

Fehler kommen als {"error": "not_found", "message": "…"} mit dem passenden Status zurück: 400, 401, 402 (eine Organisation ohne Tarif), 403 (keine Berechtigung oder eine ausgeschaltete Labs-Funktion), 404, 409 (eine E-Mail ist während einer none-Prüfung eingegangen) oder 429. Anfragelimits findest du unter Limits und Sicherheit. Eine 500 oder Anfragen, die bei allen in ein Timeout laufen, können auf einen Vorfall bei uns hindeuten: siehe unsere Statusseite.

Eine Frage zu dieser Seite?

Frag in jeder Sprache. Die Doku kann dir sofort antworten oder deine Frage an das Team von Proxy Mail QA schicken, das auf Englisch antwortet.

Doku fragen