E-Mail-Anbieter ersetzen
PUT /v1/email-provider · Berechtigung email-provider:write · alle Tarife
Ersetzt, wie die Umgebung E-Mails versendet. Sie können fünf Modi festlegen:
platform_default: KeenAuth sendet von einer eigenen Adresse. Nurreply_todarf gesetzt werden.platform_managed_domain: KeenAuth sendet von Ihrer eigenen Domain, signiert mit DKIM für diese Domain. Fügen Sie die Domain zuerst über E-Mail-Absenderdomains hinzu und veröffentlichen Sie vorher ihre DNS-Einträge; die Domain mussverifiedsein. Setzen Siesender_domain_idund einefrom_addressgenau in dieser Domain (nicht in einer Subdomain davon). Nur für Produktionsumgebungen. Wenn die Domain später nicht mehr verifiziert ist, wird E-Mail alsplatform_defaultgesendet, bis die Domain erneut verifiziert ist; die Konfiguration selbst ändert sich nicht.customer_smtp: Ihr SMTP-Server auf Port465(implizites TLS) oder587(STARTTLS ist erforderlich). Das Zertifikat muss für den Hostnamen gültig sein.customer_api: Ihr Konto beises,sendgrid,postmark,mailgunoderbrevo.customer_webhook: KeenAuth rendert die Nachricht und übergibt sie an einen Ihrer Webhook-Endpunkte; Ihre Anwendung versendet sie (siehe Webhook-Zustellung). Setzen Siewebhook_endpoint_idauf einen aktivierten Endpunkt dieser Umgebung.
If-Match ist erforderlich und enthält die Version, die Sie gelesen haben (das ETag von get). Für eine Umgebung, die noch nie konfiguriert wurde, senden Sie If-Match: "0"; * passt darauf nicht. Bei einer Abweichung wird 412 zurückgegeben.
Die Zugangsdaten sind nur beim Schreiben möglich. Senden Sie sie in credentials; keine Antwort gibt sie zurück. Wenn Sie das Feld weglassen, bleiben die gespeicherten Zugangsdaten nur erhalten, wenn Modus und Ziel (SMTP-Host, Port und Benutzername oder Anbieter) unverändert sind. Andernfalls schlägt die Anfrage mit /credentials required fehl.
Eine Änderung löscht das letzte Ergebnis des Verbindungstests und schreibt einen Audit-Eintrag (email_provider.updated) nur mit Modus, Anbieter und ID der Absenderdomain, niemals mit einer Adresse oder den Zugangsdaten. Führen Sie nach einer Änderung einen Verbindungstest aus.
Anfrage
| Feld | Typ | Pflicht | Beschreibung |
|---|---|---|---|
mode |
string | ja | platform_default, platform_managed_domain, customer_smtp, customer_api oder customer_webhook. |
sender_domain_id |
string | platform_managed_domain |
Eine verified Absenderdomain dieser Umgebung (sdom_…). In anderen Modi nicht erlaubt. |
webhook_endpoint_id |
string | customer_webhook |
Ein aktivierter Webhook-Endpunkt dieser Umgebung (whe_…). In anderen Modi nicht erlaubt. |
from_address |
string | alle außer platform_default |
Reine Absenderadresse, etwa noreply@example.com. Bei platform_managed_domain muss ihre Domain genau die Absenderdomain sein. Bei platform_default nicht erlaubt. |
reply_to |
string | nein | Reine Antwortadresse. |
provider |
string | customer_api |
ses, sendgrid, postmark, mailgun oder brevo. |
smtp_host |
string | customer_smtp |
DNS-Name mit mindestens zwei Labels (keine IP-Adresse). |
smtp_port |
integer | customer_smtp |
465 oder 587. |
smtp_username |
string | customer_smtp |
Benutzername für SMTP AUTH, 1 bis 256 Zeichen. |
credentials |
string | siehe oben | Nur beim Schreiben, 1 bis 1024 Byte. customer_smtp: das SMTP-Passwort. customer_api: <region>:<access key id>:<secret access key> für ses; der API-Schlüssel für sendgrid und mailgun (Präfix eu: für den EU-Endpunkt des Anbieters); das Server-Token für postmark; der API-Schlüssel für brevo. |
Antwort
200 OK mit der gespeicherten Konfiguration (gleiche Struktur wie bei get) und ETag auf die neue Version gesetzt.
Fehler
| HTTP | code | Wann |
|---|---|---|
| 400 | unknown_field |
Der Body enthält ein unbekanntes Feld oder ist kein JSON-Objekt. |
| 412 | precondition_failed |
If-Match ist nicht die gespeicherte Version (zwischenzeitlich wurde gespeichert) oder ist * vor dem ersten Schreiben. |
| 413 | payload_too_large |
Der Body ist größer als 16 KiB. |
| 428 | precondition_required |
If-Match fehlt. |
| 422 | validation_failed |
Ein Feld besteht die Validierung nicht; errors[] nennt das Feld und die Regel (siehe unten). |
| 401 | unauthenticated |
Die Zugangsdaten fehlen, sind ungültig oder widerrufen. |
| 403 | insufficient_permissions |
Die Zugangsdaten haben nicht die erforderliche Berechtigung. |
| 429 | rate_limited |
Das Ratenlimit wurde überschritten; wiederholen Sie die Anfrage nach der Zeit aus Retry-After. |
| 503 | service_unavailable |
Eine Abhängigkeit ist vorübergehend nicht verfügbar; wiederholen Sie die Anfrage nach der Zeit aus Retry-After. |
Regeln in errors[]:
| Pointer | Regel | Bedeutung |
|---|---|---|
/mode |
required, invalid |
Der Modus fehlt oder ist unbekannt. |
/mode |
delivery_mode_unavailable |
Der Modus kann in dieser Bereitstellung nicht senden: customer_webhook benötigt eine aktivierte Webhook-Zustellverbindung, platform_managed_domain benötigt aktivierte DKIM-Signierung, und die Kundenmodi benötigen aktivierten ausgehenden Versand. |
/mode |
production_only |
platform_managed_domain ist nur in Produktionsumgebungen erlaubt. |
/mode |
fields_not_allowed |
Ein Feld, das der Modus nicht akzeptiert, wurde gesendet. customer_webhook akzeptiert kein provider, keine SMTP-Felder, keine credentials und keine sender_domain_id. |
/sender_domain_id |
required, invalid |
platform_managed_domain wurde ohne ID der Absenderdomain gesendet oder die ID ist keine sdom_…-ID. |
/sender_domain_id |
not_found |
Diese Umgebung hat keine Absenderdomain mit dieser ID. Eine Domain einer anderen Umgebung erhält dieselbe Antwort. |
/sender_domain_id |
not_verified |
Die Absenderdomain existiert, ist aber nicht verified (DNS-Einträge ausstehend, beeinträchtigt oder deaktiviert). |
/from_address |
not_in_sender_domain |
Die Absenderadresse liegt nicht genau in der Absenderdomain. |
/webhook_endpoint_id |
required, invalid |
customer_webhook wurde ohne Endpunkt-ID gesendet oder die ID ist keine whe_…-ID. |
/webhook_endpoint_id |
not_found |
Diese Umgebung hat keinen Webhook-Endpunkt mit dieser ID. Ein Endpunkt einer anderen Umgebung erhält dieselbe Antwort. |
/webhook_endpoint_id |
not_enabled |
Der Endpunkt existiert, ist aber nicht aktiviert. |
/credentials |
required |
Modus oder Ziel wurden geändert, und es wurden keine neuen Zugangsdaten gesendet. |
/credentials |
invalid |
Die Zugangsdaten sind leer, zu lang, enthalten Steuerzeichen oder entsprechen nicht dem Format des Anbieters. |
/from_address, /reply_to, /provider, /smtp_host, /smtp_port, /smtp_username |
required, invalid |
Das Feld fehlt oder ist für den Modus ungültig. |
Webhook-Zustellung
Im Modus customer_webhook versendet KeenAuth die Nachricht nicht selbst und weicht niemals auf den eigenen Versand aus. Für jede Nachricht, die gerendert wird (Betreff, HTML und Text), sendet KeenAuth ein signiertes Ereignis per HTTPS an den Endpunkt:
- Die Anfrage ist ein
POSTmitContent-Type: application/jsonund den Standard-Webhooks-v1-Headernwebhook-id,webhook-timestampundwebhook-signature.webhook-idist die Nachrichten-ID (msg_…) und bleibt bei jedem Versuch derselben Nachricht gleich, deduplizieren Sie daher anhand dieses Werts. Der Ereignistyp muss nicht in der Ereignisliste des Endpunkts enthalten sein: Der Endpunkt erhält ihn, weil er der Zustellendpunkt ist. - Erfolg ist jede Antwort mit
2xx. Weiterleitungen werden nicht verfolgt und gelten als Fehler. Ein Versuch läuft nach 10 Sekunden ab. - Ein fehlgeschlagener Versuch wird 15 Sekunden später wiederholt, insgesamt höchstens 3 Versuche. Nach dem dritten Fehlschlag wird die Nachricht als fehlgeschlagen markiert und keine E-Mail gesendet. Ein Endpunkt, der nach dem Speichern der Konfiguration gelöscht oder deaktiviert wurde, lässt die Nachricht sofort ohne Versuch fehlschlagen.
- Der Nachrichteninhalt wird vom Webhook-Dienst nicht gespeichert. Senden Sie nur an Endpunkte, denen Sie vertrauen: Der Body enthält die Empfängeradresse sowie etwaige Anmelde-Codes oder Links.
Body (schema_version 1):
{
"id": "msg_01k7xq3m5r8t9v0w1x2y3z4a5b",
"type": "email.send_requested",
"schema_version": 1,
"environment_id": "env_01k7xq3m5r8t9v0w1x2y3z4a5b",
"occurred_at": "2026-10-27T09:00:00Z",
"data": {
"message_id": "msg_01k7xq3m5r8t9v0w1x2y3z4a5b",
"family": "email_otp",
"locale": "en",
"to": "ada@example.com",
"from": "auth@example.com",
"from_name": "Example",
"reply_to": "support@example.com",
"subject": "Your sign-in code",
"html": "<p>Your code is 123456</p>",
"text": "Your code is 123456",
"link": "https://example.keenauth.app/...",
"code": "123456"
}
}
Feld in data |
Beschreibung |
|---|---|
message_id, family, locale |
Die Nachrichten-ID (identisch mit der id des Envelopes), die Vorlagenfamilie und die Sprache, in der die Nachricht gerendert wurde. |
to |
Empfängeradresse. |
from, from_name, reply_to |
Nur vorhanden, wenn gesetzt: from ist die from_address dieser Konfiguration. |
subject |
Gerenderter Betreff. |
html, text |
Gerenderter Inhalt; mindestens einer ist vorhanden. |
link, code |
Nur vorhanden, wenn die Nachricht einen Anmeldelink oder einen Einmalcode enthält. |
Die Signatur ist v1, plus die Base64-Kodierung von HMAC-SHA256(key, "<webhook-id>.<webhook-timestamp>.<raw body>"). Der Schlüssel ist der 32-Byte-Wert, der durch die 43 Base62-Zeichen nach dem Präfix idp_whsec_ des Signiergeheimnisses des Endpunkts kodiert ist. Prüfen Sie den rohen Request-Body, bevor Sie ihn parsen, und weisen Sie einen Zeitstempel ab, der mehr als 300 Sekunden von Ihrer Uhr abweicht. Während einer Geheimnis-Rotation enthält webhook-signature zwei durch Leerzeichen getrennte Signaturen.
Python:
import base64, hashlib, hmac, time
ALPHABET = "0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz"
PREFIX = "idp_whsec_"
def signing_key(secret: str) -> bytes:
"""The HMAC key is the 32-byte value behind the 43 base62 characters after the prefix."""
body = secret[len(PREFIX):len(PREFIX) + 43]
n = 0
for ch in body:
n = n * 62 + ALPHABET.index(ch)
return n.to_bytes(32, "big")
def verify(secret: str, headers: dict, raw_body: bytes, tolerance: int = 300) -> bool:
msg_id = headers["webhook-id"]
ts = headers["webhook-timestamp"]
if abs(time.time() - int(ts)) > tolerance:
return False
signed = f"{msg_id}.{ts}.".encode() + raw_body
mac = hmac.new(signing_key(secret), signed, hashlib.sha256).digest()
expected = "v1," + base64.b64encode(mac).decode()
# webhook-signature holds one or more space-separated signatures.
return any(hmac.compare_digest(expected, s) for s in headers["webhook-signature"].split(" "))
Node.js:
import { createHmac, timingSafeEqual } from "node:crypto";
const ALPHABET = "0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz";
const PREFIX = "idp_whsec_";
// The HMAC key is the 32-byte value behind the 43 base62 characters after the prefix.
function signingKey(secret) {
let n = 0n;
for (const ch of secret.slice(PREFIX.length, PREFIX.length + 43)) n = n * 62n + BigInt(ALPHABET.indexOf(ch));
return Buffer.from(n.toString(16).padStart(64, "0"), "hex");
}
export function verify(secret, headers, rawBody, tolerance = 300) {
const id = headers["webhook-id"];
const ts = headers["webhook-timestamp"];
if (Math.abs(Date.now() / 1000 - Number(ts)) > tolerance) return false;
const mac = createHmac("sha256", signingKey(secret)).update(`${id}.${ts}.`).update(rawBody).digest();
const expected = Buffer.from(`v1,${mac.toString("base64")}`);
// webhook-signature holds one or more space-separated signatures.
return headers["webhook-signature"].split(" ").some((s) => {
const got = Buffer.from(s);
return got.length === expected.length && timingSafeEqual(got, expected);
});
}
Beispiel
curl -X PUT https://api.eu.keenauth.com/v1/email-provider \
-H "Authorization: Bearer $KEENAUTH_SECRET_KEY" \
-H "Content-Type: application/json" \
-H 'If-Match: "0"' \
-d @email-provider.json
email-provider.json:
{
"mode": "customer_smtp",
"from_address": "no-reply@example.com",
"smtp_host": "smtp.example.com",
"smtp_port": 587,
"smtp_username": "mailer",
"credentials": "<SMTP password>"
}
Um Nachrichten an einen Webhook-Endpunkt zu übergeben:
{
"mode": "customer_webhook",
"webhook_endpoint_id": "whe_01k7xq3m5r8t9v0w1x2y3z4a5b",
"from_address": "auth@example.com"
}
In diesem Modus gibt es keine Verbindung zu test; testen Sie den Endpunkt mit Einen Webhook-Endpunkt testen.
Um von Ihrer verifizierten Absenderdomain zu senden:
{
"mode": "platform_managed_domain",
"sender_domain_id": "sdom_01k7xq3m5r8t9v0w1x2y3z4a5b",
"from_address": "auth@mail.example.com",
"reply_to": "support@example.com"
}
In diesem Modus gibt es keine Verbindung zu test; prüfen Sie stattdessen die Domain mit verify.
CLI: keenauth email-provider put --if-match '"0"' --data @email-provider.json. Bewahren Sie die Zugangsdaten in einer Datei auf oder übergeben Sie sie über die Standardeingabe (--data -), damit sie nicht in Ihrem Shell-Verlauf landen.