Reemplazar el proveedor de correo electrónico
PUT /v1/email-provider · permiso email-provider:write · todos los planes
Reemplaza la forma en que el entorno envía correos electrónicos. Puede escribir cinco modos:
platform_default: KeenAuth envía desde su propia dirección. Solo puede establecersereply_to.platform_managed_domain: KeenAuth envía desde su propio dominio, firmado con DKIM para ese dominio. Añada el dominio con dominios remitentes de correo electrónico y publique antes sus registros DNS; el dominio debe estarverified. Establezcasender_domain_idy unfrom_addressque esté exactamente en ese dominio (no en un subdominio). Solo en entornos de producción. Si el dominio deja de estar verificado más adelante, el correo se envía comoplatform_defaulthasta que el dominio vuelva a verificarse; la configuración en sí no cambia.customer_smtp: su servidor SMTP en el puerto465(TLS implícito) o587(se requiere STARTTLS). El certificado debe ser válido para el nombre de host.customer_api: su cuenta enses,sendgrid,postmark,mailgunobrevo.customer_webhook: KeenAuth genera el mensaje y lo entrega a uno de sus endpoints de webhook; su aplicación lo envía (consulte Entrega por webhook). Establezcawebhook_endpoint_iden un endpoint habilitado de este entorno.
If-Match es obligatorio y contiene la versión que leyó (el ETag de obtener). Para un entorno que nunca se ha configurado, envíe If-Match: "0"; * no coincide con él. Una discrepancia devuelve 412.
La credencial se escribe una sola vez. Envíela en credentials; ninguna respuesta la devuelve. Si la omite, la credencial almacenada solo se conserva cuando el modo y el destino (host SMTP, puerto y nombre de usuario, o proveedor) no cambian. En caso contrario, la solicitud falla con /credentials required.
Un cambio borra el último resultado de la prueba de conexión y escribe un registro de auditoría (email_provider.updated) con el modo, el proveedor y solo el identificador del dominio remitente, nunca una dirección ni la credencial. Después de un cambio, ejecute una prueba de conexión.
Solicitud
| Campo | Tipo | Obligatorio | Descripción |
|---|---|---|---|
mode |
string | sí | platform_default, platform_managed_domain, customer_smtp, customer_api o customer_webhook. |
sender_domain_id |
string | platform_managed_domain |
Un dominio remitente verified de este entorno (sdom_…). No se permite en otros modos. |
webhook_endpoint_id |
string | customer_webhook |
Un endpoint de webhook habilitado de este entorno (whe_…). No se permite en otros modos. |
from_address |
string | todos excepto platform_default |
Dirección remitente simple, como noreply@example.com. En platform_managed_domain, su dominio debe ser exactamente el dominio remitente. No se permite en platform_default. |
reply_to |
string | no | Dirección de respuesta simple. |
provider |
string | customer_api |
ses, sendgrid, postmark, mailgun o brevo. |
smtp_host |
string | customer_smtp |
Nombre DNS con al menos dos etiquetas (sin dirección IP). |
smtp_port |
integer | customer_smtp |
465 o 587. |
smtp_username |
string | customer_smtp |
Nombre de usuario de SMTP AUTH, de 1 a 256 caracteres. |
credentials |
string | véase arriba | Solo escritura, de 1 a 1024 bytes. customer_smtp: la contraseña SMTP. customer_api: <region>:<access key id>:<secret access key> para ses; la clave de API para sendgrid y mailgun (con el prefijo eu: para el endpoint de la UE del proveedor); el token del servidor para postmark; la clave de API para brevo. |
Respuesta
200 OK con la configuración almacenada (misma forma que obtener) y ETag establecido en la nueva versión.
Errores
| HTTP | código | Cuándo |
|---|---|---|
| 400 | unknown_field |
El cuerpo tiene un campo desconocido o no es un objeto JSON. |
| 412 | precondition_failed |
If-Match no es la versión almacenada (alguien guardó en el intervalo), o es * antes de la primera escritura. |
| 413 | payload_too_large |
El cuerpo supera los 16 KiB. |
| 428 | precondition_required |
Falta If-Match. |
| 422 | validation_failed |
Un campo no supera la validación; errors[] indica el campo y la regla (véase abajo). |
| 401 | unauthenticated |
La credencial falta, no es válida o fue revocada. |
| 403 | insufficient_permissions |
La credencial no tiene el permiso requerido. |
| 429 | rate_limited |
Se superó el límite de frecuencia; reintente después del período Retry-After. |
| 503 | service_unavailable |
Una dependencia no está disponible temporalmente; reintente después del período Retry-After. |
Reglas en errors[]:
| Puntero | Regla | Significado |
|---|---|---|
/mode |
required, invalid |
El modo falta o es desconocido. |
/mode |
delivery_mode_unavailable |
El modo no puede enviar en esta implementación: customer_webhook requiere que la conexión de entrega por webhook esté habilitada, platform_managed_domain requiere que la firma DKIM esté habilitada, y los modos de cliente requieren que la entrega saliente esté habilitada. |
/mode |
production_only |
platform_managed_domain solo se permite en entornos de producción. |
/mode |
fields_not_allowed |
Se envió un campo que el modo no acepta. customer_webhook no acepta provider, campos SMTP, credentials ni sender_domain_id. |
/sender_domain_id |
required, invalid |
Se envió platform_managed_domain sin un identificador de dominio remitente, o el identificador no es un identificador sdom_…. |
/sender_domain_id |
not_found |
Este entorno no tiene un dominio remitente con ese identificador. Un dominio de otro entorno recibe la misma respuesta. |
/sender_domain_id |
not_verified |
El dominio remitente existe pero no está verified (registros DNS pendientes, degradado o deshabilitado). |
/from_address |
not_in_sender_domain |
La dirección remitente no está exactamente en el dominio remitente. |
/webhook_endpoint_id |
required, invalid |
Se envió customer_webhook sin un identificador de endpoint, o el identificador no es un identificador whe_…. |
/webhook_endpoint_id |
not_found |
Este entorno no tiene un endpoint de webhook con ese identificador. Un endpoint de otro entorno recibe la misma respuesta. |
/webhook_endpoint_id |
not_enabled |
El endpoint existe pero no está habilitado. |
/credentials |
required |
El modo o el destino cambió y no se envió una credencial nueva. |
/credentials |
invalid |
La credencial está vacía, es demasiado larga, tiene caracteres de control o no coincide con el formato del proveedor. |
/from_address, /reply_to, /provider, /smtp_host, /smtp_port, /smtp_username |
required, invalid |
El campo falta o no es válido para el modo. |
Entrega por webhook
En el modo customer_webhook, KeenAuth no envía el mensaje y nunca recurre a su propio envío. Por cada mensaje que genera (asunto, HTML y texto), publica un evento firmado en el endpoint a través de HTTPS:
- La solicitud es
POSTconContent-Type: application/jsony los encabezados Standard Webhooks v1webhook-id,webhook-timestampywebhook-signature.webhook-ides el identificador del mensaje (msg_…) y es el mismo en cada intento del mismo mensaje, así que deduplique por ese valor. El tipo de evento no tiene que estar en la suscripción de eventos del endpoint: el endpoint lo recibe porque es el endpoint de entrega. - Un éxito es cualquier respuesta
2xx. No se siguen redirecciones y cuentan como fallos. Un intento agota el tiempo de espera a los 10 segundos. - Un intento fallido se reintenta 15 segundos después, con un máximo de 3 intentos en total. Después del tercer fallo, el mensaje se marca como fallido y no se envía ningún correo. Un endpoint que se elimine o deshabilite después de guardar la configuración hace que el mensaje falle de inmediato, sin intento.
- El servicio de webhook no almacena el contenido del mensaje. Envíelo solo a endpoints de confianza: el cuerpo incluye la dirección del destinatario y cualquier código de inicio de sesión o enlace.
Cuerpo (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"
}
}
Campo de data |
Descripción |
|---|---|
message_id, family, locale |
El identificador del mensaje (igual que el id del sobre), la familia de plantillas y el idioma en el que se generó el mensaje. |
to |
Dirección del destinatario. |
from, from_name, reply_to |
Presentes solo cuando están establecidos: from es el from_address de esta configuración. |
subject |
Asunto generado. |
html, text |
Contenido generado; al menos uno está presente. |
link, code |
Presentes solo cuando el mensaje contiene un enlace de inicio de sesión o un código de un solo uso. |
La firma es v1, más el base64 de HMAC-SHA256(key, "<webhook-id>.<webhook-timestamp>.<raw body>"). La clave es el valor de 32 bytes codificado por los 43 caracteres base62 que siguen al prefijo idp_whsec_ del secreto de firma del endpoint. Verifique el cuerpo de la solicitud sin procesar, antes de analizarlo, y rechace una marca de tiempo que difiera más de 300 segundos de su reloj. Durante una rotación de secretos, webhook-signature contiene dos firmas separadas por espacios.
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);
});
}
Ejemplo
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>"
}
Para entregar mensajes a un endpoint de webhook:
{
"mode": "customer_webhook",
"webhook_endpoint_id": "whe_01k7xq3m5r8t9v0w1x2y3z4a5b",
"from_address": "auth@example.com"
}
En este modo no hay conexión con probar; pruebe el endpoint con Probar un endpoint de webhook.
Para enviar desde su dominio remitente verificado:
{
"mode": "platform_managed_domain",
"sender_domain_id": "sdom_01k7xq3m5r8t9v0w1x2y3z4a5b",
"from_address": "auth@mail.example.com",
"reply_to": "support@example.com"
}
En este modo no hay conexión con probar; compruebe el dominio con verificar en su lugar.
CLI: keenauth email-provider put --if-match '"0"' --data @email-provider.json. Guarde la credencial en un archivo o páselala por la entrada estándar (--data -) para que no quede en el historial de su shell.