Sostituisci il provider email

PUT /v1/email-provider · autorizzazione email-provider:write · tutti i piani

Sostituisce il modo in cui l'ambiente invia le email. Puoi scrivere cinque modalità:

If-Match è obbligatorio e contiene la versione che hai letto (l'ETag di get). Per un ambiente mai configurato, invia If-Match: "0"; * non corrisponde. Una mancata corrispondenza restituisce 412.

La credenziale può essere scritta una sola volta. Inviala in credentials; nessuna risposta la restituisce. Se la ometti, la credenziale salvata viene mantenuta solo se modalità e destinazione (host SMTP, porta e nome utente, oppure provider) non cambiano. Altrimenti la richiesta fallisce con /credentials required.

Una modifica cancella il risultato dell'ultimo test di connessione e scrive un record di audit (email_provider.updated) con la modalità, il provider e solo l'id del dominio mittente, mai un indirizzo o la credenziale. Dopo una modifica, esegui un test di connessione.

Richiesta

Campo Tipo Obbligatorio Descrizione
mode stringa sì platform_default, platform_managed_domain, customer_smtp, customer_api o customer_webhook.
sender_domain_id stringa platform_managed_domain Un dominio mittente verified di questo ambiente (sdom_…). Non ammesso in altre modalità.
webhook_endpoint_id stringa customer_webhook Un endpoint webhook abilitato di questo ambiente (whe_…). Non ammesso in altre modalità.
from_address stringa tutte tranne platform_default Indirizzo mittente semplice, ad esempio noreply@example.com. In platform_managed_domain il suo dominio deve essere esattamente il dominio mittente. Non ammesso in platform_default.
reply_to stringa no Indirizzo di risposta semplice.
provider stringa customer_api ses, sendgrid, postmark, mailgun o brevo.
smtp_host stringa customer_smtp Nome DNS con almeno due etichette (non un indirizzo IP).
smtp_port intero customer_smtp 465 o 587.
smtp_username stringa customer_smtp Nome utente SMTP AUTH, da 1 a 256 caratteri.
credentials stringa vedi sopra Solo scrittura, da 1 a 1024 byte. customer_smtp: la password SMTP. customer_api: <region>:<access key id>:<secret access key> per ses; la chiave API per sendgrid e mailgun (prefisso eu: per l'endpoint UE del provider); il token del server per postmark; la chiave API per brevo.

Risposta

200 OK con la configurazione salvata (stessa struttura di get) e ETag impostato sulla nuova versione.

Errori

HTTP code Quando
400 unknown_field Il corpo contiene un campo sconosciuto oppure non è un oggetto JSON.
412 precondition_failed If-Match non corrisponde alla versione salvata (qualcuno ha salvato nel frattempo), oppure è * prima della prima scrittura.
413 payload_too_large Il corpo supera i 16 KiB.
428 precondition_required If-Match manca.
422 validation_failed Un campo non supera la validazione; errors[] indica il campo e la regola (vedi sotto).
401 unauthenticated La credenziale manca, non è valida o è stata revocata.
403 insufficient_permissions La credenziale non ha l'autorizzazione richiesta.
429 rate_limited È stato superato il limite di richieste; riprova dopo il periodo indicato in Retry-After.
503 service_unavailable Una dipendenza non è disponibile temporaneamente; riprova dopo il periodo indicato in Retry-After.

Regole in errors[]:

Puntatore Regola Significato
/mode required, invalid La modalità manca o è sconosciuta.
/mode delivery_mode_unavailable La modalità non può inviare in questa installazione: customer_webhook richiede che la connessione di consegna webhook sia abilitata, platform_managed_domain richiede che la firma DKIM sia abilitata, e le modalità cliente richiedono che la consegna in uscita sia abilitata.
/mode production_only platform_managed_domain è consentito solo negli ambienti di produzione.
/mode fields_not_allowed È stato inviato un campo che la modalità non accetta. customer_webhook non accetta provider, campi SMTP, credentials o sender_domain_id.
/sender_domain_id required, invalid platform_managed_domain è stato inviato senza un id di dominio mittente, oppure l'id non è un id sdom_….
/sender_domain_id not_found Questo ambiente non ha un dominio mittente con quell'id. Un dominio di un altro ambiente riceve la stessa risposta.
/sender_domain_id not_verified Il dominio mittente esiste ma non è verified (record DNS in attesa, degradato o disabilitato).
/from_address not_in_sender_domain L'indirizzo mittente non è esattamente nel dominio mittente.
/webhook_endpoint_id required, invalid customer_webhook è stato inviato senza un id endpoint, oppure l'id non è un id whe_….
/webhook_endpoint_id not_found Questo ambiente non ha un endpoint webhook con quell'id. Un endpoint di un altro ambiente riceve la stessa risposta.
/webhook_endpoint_id not_enabled L'endpoint esiste ma non è abilitato.
/credentials required La modalità o la destinazione sono cambiate e non è stata inviata una nuova credenziale.
/credentials invalid La credenziale è vuota, troppo lunga, contiene caratteri di controllo o non rispetta il formato del provider.
/from_address, /reply_to, /provider, /smtp_host, /smtp_port, /smtp_username required, invalid Il campo manca o non è valido per la modalità.

Consegna webhook

Nella modalità customer_webhook KeenAuth non invia il messaggio e non ricorre mai al proprio invio. Per ogni messaggio che genera (oggetto, HTML e testo), invia un evento firmato all'endpoint tramite HTTPS:

Corpo (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 di data Descrizione
message_id, family, locale L'id del messaggio (uguale all'id dell'envelope), la famiglia di template e la lingua in cui il messaggio è stato generato.
to Indirizzo del destinatario.
from, from_name, reply_to Presenti solo se impostati: from è il from_address di questa configurazione.
subject Oggetto generato.
html, text Contenuto generato; almeno uno dei due è presente.
link, code Presenti solo quando il messaggio contiene un link di accesso o un codice monouso.

La firma è v1, seguito dal base64 di HMAC-SHA256(key, "<webhook-id>.<webhook-timestamp>.<raw body>"). La chiave è il valore di 32 byte codificato dai 43 caratteri base62 che seguono il prefisso idp_whsec_ del segreto di firma dell'endpoint. Verifica il corpo grezzo della richiesta prima di analizzarlo, e rifiuta un timestamp che si discosta più di 300 secondi dal tuo orologio. Durante la rotazione di un segreto, webhook-signature contiene due firme separate da spazi.

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

Esempio

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

Per inviare i messaggi a un endpoint webhook:

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

In questa modalità non c'è alcuna connessione da verificare con test; testa l'endpoint con Testa un endpoint webhook.

Per inviare dal tuo dominio mittente verificato:

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

In questa modalità non c'è alcuna connessione da verificare con test; controlla invece il dominio con verify.

CLI: keenauth email-provider put --if-match '"0"' --data @email-provider.json. Conserva la credenziale in un file oppure passala dallo standard input (--data -), così non finisce nella cronologia della shell.