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 :
platform_default: KeenAuth envoie depuis sa propre adresse. Seulreply_topeut être défini.platform_managed_domain: KeenAuth envoie depuis votre propre domaine, signé avec DKIM pour ce domaine. Ajoutez le domaine avec domaines d'expédition des e-mails et publiez d'abord ses enregistrements DNS ; le domaine doit êtreverified. Définissezsender_domain_idet unfrom_addressappartenant exactement à ce domaine (et non à un sous-domaine). Environnements de production uniquement. Si le domaine cesse ultérieurement d'être vérifié, les e-mails sont envoyés enplatform_defaultjusqu'à ce que le domaine soit à nouveau vérifié ; la configuration elle-même ne change pas.customer_smtp: votre serveur SMTP sur le port465(TLS implicite) ou587(STARTTLS obligatoire). Le certificat doit être valide pour le nom d'hôte.customer_api: votre compte chezses,sendgrid,postmark,mailgunoubrevo.customer_webhook: KeenAuth génère le message et le transmet à l'un de vos points de terminaison webhook ; votre application l'envoie (voir Livraison par webhook). Définissezwebhook_endpoint_idsur un point de terminaison activé de cet environnement.
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 :
- La requête est un
POSTavecContent-Type: application/jsonet les en-têtes Standard Webhooks v1webhook-id,webhook-timestampetwebhook-signature.webhook-idest l'identifiant du message (msg_…) et il est identique à chaque tentative pour un même message ; dédupliquez donc à partir de cette valeur. Le type d'événement n'a pas besoin de figurer dans l'abonnement aux événements du point de terminaison : celui-ci le reçoit parce qu'il est le point de terminaison de livraison. - Une réponse
2xxest considérée comme un succès. Les redirections ne sont pas suivies et comptent comme des échecs. Une tentative expire après 10 secondes. - Une tentative échouée est relancée 15 secondes plus tard, avec 3 tentatives au maximum au total. Après le troisième échec, le message est marqué comme échoué et aucun e-mail n'est envoyé. Un point de terminaison supprimé ou désactivé après l'enregistrement de la configuration fait échouer le message immédiatement, sans tentative.
- Le contenu du message n'est pas stocké par le service webhook. N'envoyez qu'à des points de terminaison de confiance : le corps contient l'adresse du destinataire ainsi que tout code de connexion ou lien.
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.