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:

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:

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.