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:

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:

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.