Remplacer le fournisseur d'e-mail

PUT /v1/email-provider · autorisation email-provider:write · tous les forfaits

Remplace la manière dont l'environnement envoie les e-mails. Vous pouvez définir cinq modes :

If-Match est obligatoire et contient la version que vous avez lue (le ETag de get). Pour un environnement qui n'a jamais été configuré, envoyez If-Match: "0" ; * ne correspond pas à cette valeur. Une non-correspondance renvoie 412.

L'identifiant est écrit une seule fois. Envoyez-le dans credentials ; aucune réponse ne le renvoie. Si vous l'omettez, l'identifiant enregistré est conservé uniquement si le mode et la destination (hôte, port et nom d'utilisateur SMTP, ou fournisseur) sont inchangés. Sinon, la requête échoue avec /credentials required.

Une modification efface le dernier résultat de test de connexion et écrit un enregistrement d'audit (email_provider.updated) contenant uniquement le mode, le fournisseur et l'identifiant du domaine d'expédition, jamais une adresse ni l'identifiant. Après une modification, lancez un test de connexion.

Requête

Champ Type Obligatoire Description
mode string oui platform_default, platform_managed_domain, customer_smtp, customer_api ou customer_webhook.
sender_domain_id string platform_managed_domain Un domaine d'expédition verified de cet environnement (sdom_…). Non autorisé dans les autres modes.
webhook_endpoint_id string customer_webhook Un point de terminaison webhook activé de cet environnement (whe_…). Non autorisé dans les autres modes.
from_address string tous sauf platform_default Adresse d'expédition simple, par exemple noreply@example.com. En platform_managed_domain, son domaine doit être exactement le domaine d'expédition. Non autorisé en platform_default.
reply_to string non Adresse de réponse simple.
provider string customer_api ses, sendgrid, postmark, mailgun ou brevo.
smtp_host string customer_smtp Nom DNS comportant au moins deux étiquettes (pas d'adresse IP).
smtp_port integer customer_smtp 465 ou 587.
smtp_username string customer_smtp Nom d'utilisateur SMTP AUTH, de 1 à 256 caractères.
credentials string voir ci-dessus Écriture seule, de 1 à 1024 octets. customer_smtp : le mot de passe SMTP. customer_api : <region>:<access key id>:<secret access key> pour ses ; la clé API pour sendgrid et mailgun (préfixe eu: pour le point de terminaison UE du fournisseur) ; le jeton serveur pour postmark ; la clé API pour brevo.

Réponse

200 OK avec la configuration enregistrée (même structure que get) et ETag défini sur la nouvelle version.

Erreurs

HTTP code Quand
400 unknown_field Le corps contient un champ inconnu ou n'est pas un objet JSON.
412 precondition_failed If-Match ne correspond pas à la version enregistrée (quelqu'un a enregistré entre-temps), ou vaut * avant la première écriture.
413 payload_too_large Le corps dépasse 16 KiB.
428 precondition_required If-Match est absent.
422 validation_failed Un champ échoue à la validation ; errors[] indique le champ et la règle (voir ci-dessous).
401 unauthenticated L'identifiant est absent, invalide ou révoqué.
403 insufficient_permissions L'identifiant ne dispose pas de l'autorisation requise.
429 rate_limited La limite de débit a été dépassée ; réessayez après la période indiquée par Retry-After.
503 service_unavailable Une dépendance est temporairement indisponible ; réessayez après la période indiquée par Retry-After.

Règles dans errors[] :

Pointeur Règle Signification
/mode required, invalid Le mode est absent ou inconnu.
/mode delivery_mode_unavailable Le mode ne peut pas envoyer dans ce déploiement : customer_webhook nécessite que la connexion de livraison webhook soit activée, platform_managed_domain nécessite que la signature DKIM soit activée, et les modes client nécessitent que la livraison sortante soit activée.
/mode production_only platform_managed_domain n'est autorisé que dans les environnements de production.
/mode fields_not_allowed Un champ que le mode n'accepte pas a été envoyé. customer_webhook n'accepte ni provider, ni les champs SMTP, ni credentials, ni sender_domain_id.
/sender_domain_id required, invalid platform_managed_domain a été envoyé sans identifiant de domaine d'expédition, ou l'identifiant n'est pas un identifiant sdom_….
/sender_domain_id not_found Cet environnement n'a aucun domaine d'expédition avec cet identifiant. Un domaine d'un autre environnement reçoit la même réponse.
/sender_domain_id not_verified Le domaine d'expédition existe mais n'est pas verified (enregistrements DNS en attente, dégradé ou désactivé).
/from_address not_in_sender_domain L'adresse d'expédition n'appartient pas exactement au domaine d'expédition.
/webhook_endpoint_id required, invalid customer_webhook a été envoyé sans identifiant de point de terminaison, ou l'identifiant n'est pas un identifiant whe_….
/webhook_endpoint_id not_found Cet environnement n'a aucun point de terminaison webhook avec cet identifiant. Un point de terminaison d'un autre environnement reçoit la même réponse.
/webhook_endpoint_id not_enabled Le point de terminaison existe mais n'est pas activé.
/credentials required Le mode ou la destination a changé et aucun nouvel identifiant n'a été envoyé.
/credentials invalid L'identifiant est vide, trop long, contient des caractères de contrôle ou ne correspond pas au format du fournisseur.
/from_address, /reply_to, /provider, /smtp_host, /smtp_port, /smtp_username required, invalid Le champ est absent ou invalide pour le mode.

Livraison par webhook

Dans le mode customer_webhook, KeenAuth n'envoie pas le message et ne bascule jamais sur son propre envoi. Pour chaque message qu'il génère (objet, HTML et texte), il envoie un événement signé au point de terminaison en HTTPS :

Corps (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"
  }
}
Champ data Description
message_id, family, locale L'identifiant du message (identique à id de l'enveloppe), la famille de modèles et la langue dans laquelle le message a été généré.
to Adresse du destinataire.
from, from_name, reply_to Présents uniquement lorsqu'ils sont définis : from correspond au from_address de cette configuration.
subject Objet généré.
html, text Contenu généré ; au moins l'un des deux est présent.
link, code Présents uniquement lorsque le message contient un lien de connexion ou un code à usage unique.

La signature est v1, suivi du base64 de HMAC-SHA256(key, "<webhook-id>.<webhook-timestamp>.<raw body>"). La clé est la valeur de 32 octets encodée par les 43 caractères base62 qui suivent le préfixe idp_whsec_ du secret de signature du point de terminaison. Vérifiez le corps brut de la requête avant de l'analyser, et rejetez tout horodatage qui s'écarte de plus de 300 secondes de votre horloge. Pendant une rotation de secret, webhook-signature contient deux signatures séparées par des espaces.

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);
  });
}

Exemple

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>"
}

Pour transmettre les messages à un point de terminaison webhook :

{
  "mode": "customer_webhook",
  "webhook_endpoint_id": "whe_01k7xq3m5r8t9v0w1x2y3z4a5b",
  "from_address": "auth@example.com"
}

Ce mode ne comporte aucune connexion à test ; testez le point de terminaison avec Tester un point de terminaison webhook.

Pour envoyer depuis votre domaine d'expédition vérifié :

{
  "mode": "platform_managed_domain",
  "sender_domain_id": "sdom_01k7xq3m5r8t9v0w1x2y3z4a5b",
  "from_address": "auth@mail.example.com",
  "reply_to": "support@example.com"
}

Ce mode ne comporte aucune connexion à test ; vérifiez le domaine avec verify à la place.

CLI : keenauth email-provider put --if-match '"0"' --data @email-provider.json. Conservez l'identifiant dans un fichier ou transmettez-le sur l'entrée standard (--data -) afin qu'il n'apparaisse pas dans l'historique de votre shell.