Dokumentation
Öffentliche API & MCP-Server
Bauen Sie Agenten, CLIs und Backoffice-Integrationen auf QMailing auf. PLUS und höher schaltet Bearer-Token-Authentifizierung an jedem öffentlichen Endpunkt frei – plus einen fertigen MCP-Server, den Sie in Claude Desktop, Cursor oder einen beliebigen anderen kompatiblen Client einbinden können.
Erste Schritte
- 1. Auf PLUS upgraden — die öffentliche API ist auf
PLUS,PRO,PRO_PLUSoderPRO_MAXverfügbar. FREE-Aufrufer erhalten unabhängig vom Token402 Payment Required. - 2. Token erstellen — gehen Sie zu Einstellungen → Entwickler, klicken Sie auf Neues Token, wählen Sie die benötigten Scopes und kopieren Sie den
qm_live_…-Wert, sobald er angezeigt wird. Token erscheinen nur einmal. - 3. Führen Sie Ihren ersten Aufruf aus:
curl https://qmailing.com/api/v1/pub/mailboxes \ -H "Authorization: Bearer qm_live_your_token_here"
Authentifizierung
Jeder Aufruf der öffentlichen API trägt einen Authorization: Bearer qm_live_<…>-Header. Das Token-Format ist mit Namespace versehen (qm_live_ gefolgt von 32 zufälligen Zeichen), sodass Lecks in Logs und Repositories sofort auffallen.
Token werden gehasht (SHA-256) gespeichert. Der Klartext wird zum Erstellungszeitpunkt genau einmal angezeigt und nie wieder — wenn Sie ihn verlieren, erstellen Sie ein neues. Jede Anfrage löst einen Hash-Vergleich in konstanter Zeit aus; last_used_at wird bei Erfolg aktualisiert, damit die Entwickler-UI veraltete Anmeldedaten anzeigen kann.
apiAccess bei jeder signierten Anfrage erneut — Sie müssen Token bei einem Planwechsel nicht einzeln widerrufen.Scopes
Jedes Token trägt eine oder mehrere Scope-Strings. Endpunkte deklarieren, welche Scopes sie benötigen; fehlende Scopes liefern 403 InsufficientScope. Der Wildcard (*) gewährt alles — aktivieren Sie ihn nur, wenn Sie ihn wirklich brauchen.
| Scope | Erlaubt |
|---|---|
| mailboxes:read | Postfächer auflisten und einsehen |
| mailboxes:write | Postfächer erstellen, ändern, löschen |
| domains:read | Domains und DNS-Einträge auflisten |
| domains:write | Eigene Domains hinzufügen, verifizieren, löschen (in v1 nur per FE) |
| email:read | Postfachnachrichten auflisten und einsehen |
| email:send | Ausgehende E-Mails über die API senden |
| webhooks:read | Webhook-Endpunkte auflisten |
| webhooks:write | Webhook-Endpunkte registrieren, auflisten und widerrufen |
| * | Wildcard — gewährt alles. Sparsam einsetzen. |
Postfächer
Auflisten
GET /api/v1/pub/mailboxes
# scope: mailboxes:readEinzelnes abrufen
GET /api/v1/pub/mailboxes/{id}
# scope: mailboxes:readErstellen
POST /api/v1/pub/mailboxes
Content-Type: application/json
# scope: mailboxes:write
{
"localPart": "support",
"domain": "yourbrand.com",
"displayName": "Support team",
"forwardTo": "ops@yourbrand.com"
}Das Feld domain ist optional — lassen Sie es weg, und das Postfach wird auf qmailing.com angelegt. Bei einer eigenen Domain muss diese sowohl claimed ALS AUCH fullyVerified sein; eine noch nicht bereite Domain liefert 400 zurück, statt stillschweigend ein nicht zustellbares Postfach zu erstellen.
Domains & DNS
Eigene Domains auflisten
GET /api/v1/pub/domains
# scope: domains:readDNS-Einträge-Checkliste
GET /api/v1/pub/domains/{id}/dns-records
# scope: domains:readLiefert die vollständige 8-Eintrag-Checkliste (Challenge-TXT, MX, SPF, drei DKIM-CNAMEs, DMARC, optional _amazonses) mit einem zeilenweisen status aus PENDING / FOUND / MISMATCH / NOT_REQUIRED. Damit kann der Agent dem Nutzer genau sagen, was beim DNS-Anbieter zu veröffentlichen ist.
Postfachinhalte lesen und ausgehende E-Mails versenden. Die Ordner-Pagination entspricht der internen API des Frontends — Agenten, die zwischen Cookie-JWT- und Bearer-Auth wechseln, sehen keine Formänderung.
Nach Ordner auflisten
GET /api/v1/pub/email?folder=INBOX&offset=0&limit=25
# scope: email:read
# Optional query params:
# mailboxId filter to one mailbox (omit for all-mailboxes view)
# folder INBOX | SENT | DRAFTS | TRASH | STARRED | SPAM
# offset 0-based index, default 0
# limit 1..100, default 25folder ist standardmäßig INBOX; zulässige Werte: INBOX, SENT, DRAFTS, TRASH, STARRED, SPAM. mailboxId ist optional — ohne Angabe werden alle Postfächer des Aufrufers ausgelesen. Pagination via offset + limit; limit wird auf den Bereich 1–100 begrenzt (Standard 25).
Einzelne abrufen
GET /api/v1/pub/email/{id}
# scope: email:readLiefert das vollständige EmailDetailDto: HTML-/Textkörper, Label-Liste, Anhang-Metadaten. Anhang-Inhalte sind nicht inline enthalten — laden Sie jeden über GET /{emailId}/attachments/{index} unten herunter; dieser Endpunkt streamt die Rohbytes mit dem ursprünglichen Dateinamen und dem Content-Type.
Anhang herunterladen
GET /api/v1/pub/email/{emailId}/attachments/{index}
# scope: email:read
# response: streamed bytes, Content-Type from the attachmentStreamt die Rohbytes des Anhangs (nullbasierter Index in der Anhangsliste der Nachricht). Gleiche Form wie der sitzungsbasierte Download: Content-Type wird durchgereicht, Content-Disposition nach RFC 6266 mit dem ursprünglichen Dateinamen (UTF-8 prozent-codiert für Unicode). MCP-Agenten, die über @qmailing/mcp-server aufrufen, nutzen das Tool qmailing_get_attachment, das die Bytes base64-kodiert inline zurückgibt (Cap: 5 MiB).
Senden
Compose-Endpunkt. multipart/form-data-Body mit einem command-JSON-Part (Empfänger / Betreff / Body) und null oder mehr attachments-Datei-Parts:
POST /api/v1/pub/email/send
Content-Type: multipart/form-data
# scope: email:send
--boundary
Content-Disposition: form-data; name="command"
Content-Type: application/json
{
"mailboxId": "11111111-2222-3333-4444-555555555555",
"to": ["alice@example.com"],
"cc": [],
"bcc": [],
"subject": "Order #1428 confirmed",
"bodyText": "Plain-text body",
"bodyHtml": "<p>Rich body</p>",
"replyToId": null
}
--boundary
Content-Disposition: form-data; name="attachments"; filename="receipt.pdf"
Content-Type: application/pdf
<binary bytes>
--boundary--Form des command-JSON:
{
"mailboxId": "uuid (required) — must be SES-verified",
"to": "string[] (required, max 50)",
"cc": "string[] (optional, max 50)",
"bcc": "string[] (optional, max 50)",
"subject": "string (max 998 chars)",
"bodyText": "string — plain-text body",
"bodyHtml": "string — HTML body",
"replyToId": "uuid | null — set for in-thread replies"
}Jeder Aufruf zählt gegen das tägliche Sende-Limit des Plans, die SES-Sendekontingente des Postfachs und das Speicherkontingent des Nutzers für die resultierende SENT-Kopie. Empfänger werden als RFC-5322-Adressen validiert; ungültige liefern 400 mit der betreffenden Adresse zurück. Das in mailboxId referenzierte Postfach muss SES-verifiziert sein — sonst 409 MailboxNotVerified mit einem Hinweis auf die Domains-Seite.
multipart/form-data, sodass das Drahtformat identisch bleibt.filename am command-Teil ist optional — Node fetch stempelt automatisch filename="blob" auf jeden Blob, Browser- und Node-Clients können command.json übergeben, und konforme curl --form-Clients dürfen ihn ganz weglassen. Alle drei Formen werden gleich geparst; ausschlaggebend ist nur Content-Type: application/json auf den Bytes des Teils.Scopes: email:read für Liste + Einzelabruf, email:send für Versand.
Webhooks
Registrieren Sie einen HTTPS-Endpunkt, und QMailing POSTet einen signierten JSON-Envelope an diesen, sobald eines der abonnierten Ereignisse eintritt. Die Zustellung ist live — der Worker pollt eine Per-Event-Queue alle 10s, signiert jeden Request mit HMAC-SHA256 und wiederholt Fehler mit exponentiellem Backoff (1m / 5m / 15m / 1h / 6h), bevor die Zeile zur manuellen Prüfung im DLQ landet.
Endpunkt registrieren
POST /api/v1/pub/webhooks
Content-Type: application/json
# scope: webhooks:write
{
"url": "https://your-server.example.com/qmailing-events",
"label": "Production listener",
"events": ["email.received", "email.bounced", "domain.verified"]
}
# Response (201):
{
"endpoint": {
"id": "uuid",
"url": "https://your-server.example.com/qmailing-events",
"label": "Production listener",
"events": ["email.received", "email.bounced", "domain.verified"],
"secretPrefix": "whk_AbCd",
"createdAt": "2026-05-04T20:30:00Z",
"active": true
},
"plaintext": "whk_AbCd…<32 random chars>" // shown ONCE
}url muss ein HTTPS-Endpunkt sein (HTTP nur für localhost im Dev erlaubt). Der Host wird gegen eine Allow-List für SSRF geprüft — private / Loopback / Cloud-Metadata-Adressen werden bereits bei der Registrierung abgelehnt. Die Antwort enthält plaintext genau einmal; speichern Sie ihn serverseitig und nutzen Sie ihn, um HMAC-Signaturen eingehender Zustellungen zu prüfen.
Ereigniskatalog
| Ereignis | Feuert wenn |
|---|---|
| email.received | Eine eingehende E-Mail trifft in einem Ihrer Postfächer ein. |
| email.sent | Eine ausgehende E-Mail wurde von SES akzeptiert. |
| email.bounced | Eine ausgehende E-Mail ist zurückgeprallt (Hard oder Soft Bounce). |
| domain.verified | Eine eigene Domain hat die DNS-Verifizierung vollständig durchlaufen. |
Endpunkte auflisten
GET /api/v1/pub/webhooks
# scopes: webhooks:read OR webhooks:writeLiefert aktive und widerrufene Endpunkte, neueste zuerst. Lesezugriff funktioniert mit webhooks:read oder webhooks:write — ein Inspektor-Token muss damit nicht den breiteren Write-Scope tragen.
Wire-Format der Zustellung
Jede Zustellung ist ein einzelner POST an Ihren Endpunkt mit Content-Type: application/json. Der Body ist der Event-Envelope mit den Feldern event (Ereignisname), occurred_at (ISO-Zeitstempel) und einem data-Block, dessen Form vom Ereignis abhängt (siehe Tabelle oben). Drei Header tragen Routing + Verifikations-Metadaten:
POST https://your-server.example.com/qmailing-events
Content-Type: application/json
User-Agent: qmailing-webhook/1 (+https://qmailing.com)
X-Qmailing-Event: email.received
X-Qmailing-Delivery: 5f3b2a91-7c4d-4d52-9c3e-aa1bcd8a4f12
X-Qmailing-Signature: t=1685120800,v1=abc123def…
{
"event": "email.received",
"occurred_at": "2026-05-23T19:46:40Z",
"data": {
"email_id": "uuid",
"mailbox_id": "uuid",
"from": "alice@example.com",
"subject": "Hello",
"preview_text": "Just checking in…"
}
}Signatur-Schema
# Receiver side (Node example)
const crypto = require("node:crypto");
function verify(headers, body, secret) {
const sig = headers["x-qmailing-signature"]; // "t=1685120800,v1=abc123…"
const m = /t=(\d+),v1=([0-9a-f]+)/.exec(sig);
if (!m) return false;
const ts = Number(m[1]);
if (Math.abs(Date.now() / 1000 - ts) > 300) return false; // 5-min replay window
const expected = crypto
.createHmac("sha256", secret)
.update(`${ts}.${body}`)
.digest("hex");
return crypto.timingSafeEqual(Buffer.from(expected), Buffer.from(m[2]));
}Lehnen Sie den Request ab, wenn der Timestamp älter als 5 Minuten ist (Replay-Fenster) oder wenn der neu berechnete HMAC nicht mit dem v1-Hex übereinstimmt — unser BE-Signer folgt den Stripe / GitHub-Konventionen exakt, sodass jede der Standard-Verifier-Bibliotheken sofort funktioniert.
Retry-Policy
Nicht-2xx-Antworten (oder Transportfehler — DNS, TLS, Connection Refused, 10s Timeout) erhöhen attempt_count und schieben next_attempt_at auf den nächsten Slot in 1m / 5m / 15m / 1h / 6h. Nach dem fünften Fehlversuch geht die Zustellung in FAILED (DLQ) — sichtbar auf /settings/developers, wo Sie manuell wiederholen können. Idempotenz: jeder Retry trägt dieselbe X-Qmailing-Delivery-UUID — Empfänger deduplizieren über diesen Header.
Letzte Zustellungen auflisten
GET /api/v1/pub/webhooks/{endpointId}/deliveries?limit=100
# scopes: webhooks:read OR webhooks:writeLiefert die letzten limit Zustellungen eines einzelnen Endpunkts (Default 100, max 100), neueste zuerst. Jede Zeile enthält Status, Versuchszähler, letzten HTTP-Code, letzte Fehlermeldung und das Original-Payload-JSON — versorgt die History-Ansicht im Developer-Dashboard.
Testzustellung senden
POST /api/v1/pub/webhooks/{endpointId}/test
# scope: webhooks:write
# Response 202: WebhookDeliveryDto (enqueued, picked up next worker tick)Feuert ein synthetisches webhook.test-Ereignis am Endpunkt ab — derselbe Dispatch-Pfad wie echte Ereignisse, nützlich zur Verifikation von Signatur + TLS vor dem Abonnieren von Live-Traffic.
Zustellung wiederholen
POST /api/v1/pub/webhooks/deliveries/{deliveryId}/retry
# scope: webhooks:write
# Response 200: WebhookDeliveryDto (status=PENDING, attempt=MAX-1)Stellt eine Zustellungszeile manuell wieder in die Queue. Bereits zugestellte Zeilen können nicht wiederholt werden (409). Der Retry gibt genau einen weiteren Versuch (Zähler auf MAX-1), sodass ein dauerhaft kaputter Endpunkt nicht durch wiederholte Klicks endlos geloopt wird.
Zustellung aus History löschen
DELETE /api/v1/pub/webhooks/deliveries/{deliveryId}
# scope: webhooks:write
# Response: 204 No ContentHard-Delete einer Zustellungszeile aus der History-Tabelle. Das Compliance-Audit-Log erfasst das Löschen selbst, sodass Traceability auch ohne die Zeile erhalten bleibt.
Endpunkt widerrufen
DELETE /api/v1/pub/webhooks/{id}
# scope: webhooks:write
# Response: 204 No ContentIdempotent — das Widerrufen eines bereits widerrufenen Endpunkts gelingt geräuschlos. Die Zeile bleibt erhalten (revoked-at-Zeitstempel gesetzt), damit Audit-Logs ihre Referenzen behalten.
409 WebhookEndpointLimitExceeded / 400 InvalidWebhookEvent.Fehler
Jede Antwort, die nicht 2xx ist, ist ein RFC-7807-ProblemDetail mit einem stabilen code-Feld, auf das Ihr Agent verzweigen kann:
| HTTP | code | Wann |
|---|---|---|
| 400 | InvalidApiTokenScope | Unbekannter Scope-String beim Anlegen. |
| 400 | InvalidEmailAddress | Ein Empfänger hat die RFC-5322-Syntaxprüfung nicht bestanden. |
| 400 | InvalidWebhookEvent | Die registrierte Ereignisliste enthält einen unbekannten Ereignisnamen. |
| 401 | — | Bearer-Header fehlt, ist fehlerhaft, oder Token wurde widerrufen / ist abgelaufen. |
| 402 | PlanFeatureRequired | Aufrufer hat einen Plan ohne API-Zugang — Upgrade nötig. |
| 403 | InsufficientScope | Token enthält keinen der erforderlichen Scopes. |
| 404 | ApiTokenNotFound | Widerrufsziel existiert nicht oder gehört zu einem anderen Konto. |
| 409 | MailboxNotVerified | Die Domain des Sende-Postfachs ist noch nicht vollständig verifiziert — DNS in /settings/domains abschließen. |
| 409 | ApiTokenLimitExceeded | Konto hat das Maximum (20) aktiver Token erreicht; widerrufen Sie zuerst eines. |
| 409 | WebhookEndpointLimitExceeded | Konto hält bereits 10 aktive Webhook-Endpunkte; widerrufen Sie zuerst einen. |
| 429 | RateLimitExceeded | Token hat 300 Anfragen pro Minute überschritten. Retry-After in Sekunden. |
Rate Limits
Jedes Token erhält einen eigenen Bucket von 300 Anfragen pro Minute. Multi-Agent-Setups (CLI + Cron + IDE-Plugin) mit unterschiedlichen Token für dasselbe Konto laufen unabhängig — sie kämpfen nie um einen gemeinsamen Bucket. 429 RateLimitExceeded enthält einen Retry-After-Header in Sekunden.
MCP-Server
Das Paket @qmailing/mcp-server läuft lokal und stellt die öffentliche API als Werkzeuge des Model Context Protocol bereit. Binden Sie es in Claude Desktop, Cursor, Continue oder einen beliebigen MCP-kompatiblen Client ein.
@qmailing/mcp-server@0.2.0), wenn Sie keine automatischen Upgrades wünschen.{
"mcpServers": {
"qmailing": {
"command": "npx",
"args": ["-y", "@qmailing/mcp-server"],
"env": {
"QMAILING_API_TOKEN": "qm_live_your_token_here"
}
}
}
}Mitgelieferte Werkzeuge: qmailing_list_mailboxes, qmailing_get_mailbox, qmailing_create_mailbox, qmailing_list_domains, qmailing_get_dns_records, qmailing_list_emails, qmailing_get_email, qmailing_get_attachment, qmailing_send_email, qmailing_register_webhook, qmailing_list_webhooks, qmailing_delete_webhook. Der Katalog wächst, sobald neue Endpunkte hinzukommen.