メールプロバイダを置き換える

PUT /v1/email-provider · 権限 email-provider:write · すべてのプラン

環境がメールを送信する方法を置き換えます。次の5つのモードを指定できます:

If-Match は必須で、読み取ったバージョン(get の ETag)を指定します。一度も設定されていない環境では If-Match: "0" を送信してください。* は一致しません。不一致の場合は 412 が返ります。

認証情報は書き込み専用です。credentials で送信してください。レスポンスには返されません。省略した場合、モードと送信先(SMTP ホスト、ポート、ユーザー名、またはプロバイダ)が変わらない場合にのみ保存済みの認証情報が保持されます。それ以外の場合、リクエストは /credentials の required で失敗します。

変更すると、最後の接続テストの結果はクリアされ、モード、プロバイダ、送信元ドメイン ID のみを含む監査レコード(email_provider.updated)が書き込まれます。アドレスや認証情報は記録されません。変更後は 接続テスト を実行してください。

リクエスト

フィールド 型 必須 説明
mode string はい platform_default、platform_managed_domain、customer_smtp、customer_api または customer_webhook。
sender_domain_id string platform_managed_domain この環境の verified な送信元ドメイン(sdom_…)。他のモードでは使用できません。
webhook_endpoint_id string customer_webhook この環境で有効な Webhook エンドポイント(whe_…)。他のモードでは使用できません。
from_address string platform_default 以外すべて noreply@example.com のような送信元アドレス(表示名なし)。platform_managed_domain では、そのドメインが送信元ドメインと完全に一致する必要があります。platform_default では使用できません。
reply_to string いいえ 返信先アドレス(表示名なし)。
provider string customer_api ses、sendgrid、postmark、mailgun または brevo。
smtp_host string customer_smtp ラベルが 2 つ以上ある DNS 名(IP アドレスは不可)。
smtp_port integer customer_smtp 465 または 587。
smtp_username string customer_smtp SMTP AUTH のユーザー名。1 から 256 文字。
credentials string 上記を参照 書き込み専用。1 から 1024 バイト。customer_smtp: SMTP パスワード。customer_api: ses の場合は <region>:<access key id>:<secret access key>、sendgrid と mailgun の場合は API キー(プロバイダの EU エンドポイントを使う場合は eu: を接頭辞とする)、postmark の場合はサーバートークン、brevo の場合は API キー。

レスポンス

200 OK と、保存された設定(get と同じ形式)が返ります。ETag には新しいバージョンが設定されます。

エラー

HTTP code 発生条件
400 unknown_field ボディに未知のフィールドがあるか、JSON オブジェクトではありません。
412 precondition_failed If-Match が保存済みのバージョンと一致しない(その間に別の更新があった)か、最初の書き込み前に * が指定されています。
413 payload_too_large ボディが 16 KiB を超えています。
428 precondition_required If-Match がありません。
422 validation_failed フィールドが検証に失敗しました。errors[] にフィールドとルールが示されます(下記を参照)。
401 unauthenticated 認証情報がないか、無効または取り消されています。
403 insufficient_permissions 認証情報に必要な権限がありません。
429 rate_limited レート制限を超えました。Retry-After の期間を待ってから再試行してください。
503 service_unavailable 依存先が一時的に利用できません。Retry-After の期間を待ってから再試行してください。

errors[] のルール:

ポインタ ルール 意味
/mode required、invalid モードがないか、不明です。
/mode delivery_mode_unavailable このデプロイメントではモードで送信できません。customer_webhook には Webhook 配信接続の有効化が必要で、platform_managed_domain には DKIM 署名の有効化が必要です。お客様のモードには送信配信の有効化が必要です。
/mode production_only platform_managed_domain は本番環境でのみ使用できます。
/mode fields_not_allowed モードが受け付けないフィールドが送信されました。customer_webhook では provider、SMTP フィールド、credentials、sender_domain_id を指定できません。
/sender_domain_id required、invalid platform_managed_domain で送信元ドメイン ID が指定されていないか、sdom_… 形式の ID ではありません。
/sender_domain_id not_found この環境にその ID の送信元ドメインがありません。別の環境のドメインを指定した場合も同じ応答になります。
/sender_domain_id not_verified 送信元ドメインは存在しますが、verified ではありません(DNS レコードの確認待ち、劣化、または無効化)。
/from_address not_in_sender_domain 送信元アドレスが、送信元ドメインと完全に一致するドメイン内にありません。
/webhook_endpoint_id required、invalid customer_webhook でエンドポイント ID が指定されていないか、whe_… 形式の ID ではありません。
/webhook_endpoint_id not_found この環境にその ID の Webhook エンドポイントがありません。別の環境のエンドポイントを指定した場合も同じ応答になります。
/webhook_endpoint_id not_enabled エンドポイントは存在しますが、有効になっていません。
/credentials required モードまたは送信先が変更されたのに、新しい認証情報が送信されていません。
/credentials invalid 認証情報が空、長すぎる、制御文字を含む、またはプロバイダの形式に合っていません。
/from_address、/reply_to、/provider、/smtp_host、/smtp_port、/smtp_username required、invalid フィールドがないか、モードに対して無効です。

Webhook 配信

customer_webhook モードでは、KeenAuth はメッセージを送信せず、自身の送信に切り替えることもありません。レンダリングしたすべてのメッセージ(件名、HTML、テキスト)について、HTTPS で署名付きイベントを 1 件エンドポイントに送信します。

ボディ(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"
  }
}
data のフィールド 説明
message_id、family、locale メッセージ ID(エンベロープの id と同じ)、テンプレートファミリー、およびメッセージのレンダリングに使用された言語。
to 受信者のアドレス。
from、from_name、reply_to 設定されている場合のみ含まれます。from はこの設定の from_address です。
subject レンダリングされた件名。
html、text レンダリングされた内容。少なくとも一方が含まれます。
link、code メッセージにサインイン用のリンクまたはワンタイムコードが含まれる場合のみ含まれます。

署名は v1, に続けて、HMAC-SHA256(key, "<webhook-id>.<webhook-timestamp>.<raw body>") を base64 エンコードした値です。鍵は、エンドポイントの署名シークレットの idp_whsec_ 接頭辞に続く 43 文字の base62 文字列でエンコードされた 32 バイトの値です。解析する前に生のリクエストボディを検証し、タイムスタンプがお客様の時計から 300 秒以上ずれている場合は拒否してください。シークレットのローテーション中は、webhook-signature に半角スペースで区切られた 2 つの署名が含まれます。

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

例

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

メッセージを Webhook エンドポイントに渡す場合:

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

このモードでは テスト への接続はありません。代わりに Webhook エンドポイントをテストする でエンドポイントをテストしてください。

検証済みの送信元ドメインから送信する場合:

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

このモードでは テスト への接続はありません。代わりに 検証 でドメインを確認してください。

CLI: keenauth email-provider put --if-match '"0"' --data @email-provider.json。認証情報はファイルに保存するか、標準入力(--data -)で渡してください。そうすることで、シェルの履歴に残ることを防げます。