メールプロバイダを置き換える
PUT /v1/email-provider · 権限 email-provider:write · すべてのプラン
環境がメールを送信する方法を置き換えます。次の5つのモードを指定できます:
platform_default: KeenAuth が独自のアドレスから送信します。設定できるのはreply_toのみです。platform_managed_domain: KeenAuth が お客様のドメインから送信し、そのドメインの DKIM で署名します。先に メール送信元ドメイン でドメインを追加し、DNS レコードを公開してください。ドメインはverifiedである必要があります。sender_domain_idと、そのドメイン内(サブドメインは不可)のfrom_addressを設定してください。本番環境でのみ使用できます。後からドメインが検証済みでなくなった場合、ドメインが再び検証されるまでメールはplatform_defaultとして送信されます。設定自体は変わりません。customer_smtp: ポート465(暗黙的 TLS)または587(STARTTLS が必須)で、お客様の SMTP サーバーを使用します。証明書はホスト名に対して有効である必要があります。customer_api:ses、sendgrid、postmark、mailgunまたはbrevoのお客様アカウントを使用します。customer_webhook: KeenAuth がメッセージをレンダリングし、お客様の Webhook エンドポイント のいずれかに渡します。送信はお客様のアプリケーションが行います(Webhook 配信を参照)。webhook_endpoint_idには、この環境で有効になっているエンドポイントを設定してください。
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 件エンドポイントに送信します。
- リクエストは
POSTで、Content-Type: application/jsonと Standard Webhooks v1 のヘッダーwebhook-id、webhook-timestamp、webhook-signatureを含みます。webhook-idはメッセージ ID(msg_…)で、同じメッセージのすべての試行で同じ値になります。そのため、この値で重複を排除してください。イベントタイプがエンドポイントのイベント購読に含まれている必要はありません。配信先のエンドポイントであるため、イベントは受信されます。 2xxのレスポンスを成功とみなします。リダイレクトは追跡されず、失敗として扱われます。1 回の試行は 10 秒でタイムアウトします。- 失敗した試行は 15 秒後に再試行され、合計で最大 3 回試行されます。3 回目の失敗後、メッセージは失敗としてマークされ、メールは送信されません。設定を保存した後にエンドポイントが削除または無効化された場合、試行せずに即座にメッセージが失敗します。
- メッセージの内容は Webhook サービスに保存されません。信頼できるエンドポイントにのみ送信してください。ボディには受信者のアドレスと、サインイン用のコードやリンクが含まれます。
ボディ(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 -)で渡してください。そうすることで、シェルの履歴に残ることを防げます。