KaiMailのWebhook配信と連携するための開発者向けガイドです。カスタムドメイン宛のメールを、HTTP POSTで構造化されたJSONペイロードとして受信できます。メールサーバーの構築は不要です。
KaiMailはSaaS型のメール転送サービスです。カスタムドメインをKaiMailに登録し、MXレコードをKaiMailのメールサーバーに向けるだけで、そのドメイン宛のメールを指定した転送先に届けます。転送先は別のメールアドレスか、HTTP Webhookエンドポイントから選べます。
Webhook配信では、受信メールがJSONに変換され、HTTP POSTでアプリケーションに届きます。以下のようなユースケースに活用できます。
始める前に以下をご用意ください。
KaiMailでアカウントを作成し、PLUSプラン以上を選択します。Webhook配信には有料プランが必要です。
KaiMailダッシュボードのルーティングページで、カスタムドメイン(例:yourdomain.com)を追加します。
DNSプロバイダーにログインし、ドメインのMXレコードを更新します。
| タイプ | ホスト | 値 | 優先度 |
|---|---|---|---|
| MX | yourdomain.com | mail.kaimail.net | 10 |
既存のMXレコードは配信の競合を避けるため削除してください。
KaiMailダッシュボードに戻り、ドメインの横にあるMXチェックボタンをクリックします。KaiMailがDNSを照会し、MXレコードがmail.kaimail.netを指していることを確認します。
メールボックスを追加します(例:[email protected])。ルートタイプにWebhookを選択し、HTTPSエンドポイントURL(例:https://app.example.com/webhooks/email)を入力します。
これで[email protected]宛のメールが、JSON形式のPOSTリクエストとしてエンドポイントに届くようになります。
補足: Webhookルートごとに一意の署名用シークレットが自動生成されます。確認するには、メールボックスの編集をクリックしてください。編集ページにシークレットが表示されます。詳しくは下記の「Webhookシークレットの確認方法」ご覧ください。
送信者 KaiMail アプリケーション
| | |
|-- SMTPメール ---------->| |
| |-- SPF/DKIM/ARC 認証チェック |
| |-- JSONへシリアライズ |
| |-- HMAC-SHA256 署名 |
| |-- HTTP POST(JSON)-------------->|
| | |-- 200 OK を返却
| |<-- 200 OK -------------------------|
2xxレスポンスを返します。KaiMailはContent-Type: application/jsonのPOSTリクエストを、以下のカスタムヘッダーとともに送信します。
| ヘッダー | 説明 |
|---|---|
X-KAI-Webhook-Signature |
リクエストボディのHMAC-SHA256署名 |
X-KAI-Tracking-ID |
この配信試行の一意な識別子 |
X-KAI-Event |
イベントタイプ ―常にemail.received |
User-Agent |
常にKaiMail-Webhook/1.0 |
{
"version": "1.0",
"timestamp": "ISO 8601 UTCタイムスタンプ",
"headers": {
"Subject": "文字列",
"From": "文字列",
"To": "文字列",
"...": "すべてのメールヘッダー(複数値のヘッダーは配列)"
},
"body_text": "文字列またはnull",
"body_html": "文字列またはnull",
"attachments": [
{
"filename": "文字列",
"content_type": "文字列",
"size": 0,
"url": "文字列(署名付きURL)"
}
],
"envelope": {
"sender": "SMTPエンベロープの送信者",
"recipient": "SMTPエンベロープの受信者"
},
"account": {
"email": "KaiMailアカウントのメールアドレス",
"domain": "カスタムドメイン",
"mailbox": "メールを受信したメールボックスアドレス"
},
"metadata": {
"authentication_results": {
"PASS_REV_IP": true,
"PASS_SPF": true,
"PASS_DKIM": true,
"PASS_ARC": true
}
}
}
| フィールド | 型 | 説明 |
|---|---|---|
version |
string | ペイロード形式のバージョン。現在は常に"1.0"です。 |
timestamp |
string | KaiMailがメールを処理した時刻のISO 8601 UTCタイムスタンプです。 |
headers |
object | メールの全ヘッダーです。複数回出現するヘッダー(例:Received)は配列で表現されます。 |
body_text |
string または null | メール本文のプレーンテキスト版です。存在しない場合はnullです。 |
body_html |
string または null | メール本文のHTML版です。存在しない場合はnullです。 |
attachments |
array | 添付ファイルのリストです。添付がない場合は空の配列です。各要素にfilename、content_type、size(バイト)、url(署名付きダウンロードURL)が含まれます。 |
envelope.sender |
string | SMTP MAIL FROМアドレス(実際の送信者)です。 |
envelope.recipient |
string | SMTP RCPT TOアドレス(メールの宛先)です。 |
account.email |
string | KaiMailアカウントのメールアドレスです。 |
account.domain |
string | メールを受信したカスタムドメインです。 |
account.mailbox |
string | マッチしたメールボックスアドレスです。 |
metadata.authentication_results |
object | メール認証チェックの結果です。詳細は「認証結果」をご覧ください。 |
HTTPリクエストヘッダー
POST /webhooks/email HTTP/1.1
Content-Type: application/json
X-KAI-Event: email.received
X-KAI-Tracking-ID: aB3kLm9xQz-7yN2pR4wHtg
X-KAI-Webhook-Signature: sha256=e4a21c38b7d0f65a91c3d4e8f2b6a7c5d0e9f8a1b3c4d5e6f7a8b9c0d1e2f3a4
User-Agent: KaiMail-Webhook/1.0
JSONペイロード
{
"version": "1.0",
"timestamp": "2026-02-26T15:03:17.984266+00:00",
"headers": {
"Authentication-Results": "kaimail.net; iprev=pass policy.iprev=209.85.208.174 (mail-lj1-f174.google.com); spf=pass reason=\"sender SPF authorized\" smtp.helo=mail-lj1-f174.google.com [email protected]; dkim=pass (good signature) header.d=senderdomain.com; arc=pass",
"Received-SPF": "pass (kaimail.net: domain of [email protected] designates 209.85.208.174 as permitted sender)",
"Received": [
"(qmail 100942 invoked from network); 26 Feb 2026 15:03:16 -0000",
"from mail-lj1-f174.google.com (209.85.208.174) by mail.kaimail.net with ESMTPS; 26 Feb 2026 15:03:16 -0000"
],
"X-KAI-Sender-IP": "209.85.208.174",
"X-KAI-Sender-Host": "mail-lj1-f174.google.com",
"X-KAI-Received-Datetime": "2026-02-26 15:03:16",
"X-KAI-DKIM-Status": "pass;d=senderdomain.com",
"DKIM-Signature": "v=1; a=rsa-sha256; c=relaxed/relaxed; d=senderdomain.com; s=20230601; ...",
"MIME-Version": "1.0",
"From": "John Smith <[email protected]>",
"Date": "Thu, 26 Feb 2026 21:03:01 +0600",
"Message-ID": "<CALo1LC2VYJ9K5YNNBE8Cfu=2rxv6y3ATYSDLCy6_0t=WzSYM9A@mail.senderdomain.com>",
"Subject": "Hello from a customer",
"To": "[email protected]",
"Content-Type": "multipart/alternative; boundary=\"0000000000006df8bb064bbb6b83\""
},
"body_text": "Hello\n",
"body_html": "<div dir=\"auto\">Hello</div>\n",
"attachments": [],
"envelope": {
"sender": "[email protected]",
"recipient": "[email protected]"
},
"account": {
"email": "[email protected]",
"domain": "yourdomain.com",
"mailbox": "[email protected]"
},
"metadata": {
"authentication_results": {
"PASS_REV_IP": true,
"PASS_SPF": true,
"PASS_DKIM": true,
"PASS_ARC": true
}
}
}
補足:
ARC-Seal、ARC-Message-Signature、Google内部ヘッダーなどは実際のペイロードに含まれますが、ここでは簡略化のため省略しています。
HTTPリクエストヘッダー
POST /webhooks/email HTTP/1.1
Content-Type: application/json
X-KAI-Event: email.received
X-KAI-Tracking-ID: xK4mNp8qTv-3wR5sY7zJbA
X-KAI-Webhook-Signature: sha256=f8c72d41a9e0b35c82d4f6a1e3b7c9d5f0a2e8b4c6d1f3a5b7c9d0e2f4a6b8c0
User-Agent: KaiMail-Webhook/1.0
JSONペイロード
{
"version": "1.0",
"timestamp": "2026-03-02T14:05:59.147593+00:00",
"headers": {
"Authentication-Results": "kaimail.net; iprev=pass policy.iprev=209.85.208.173 (mail-lj1-f173.google.com); spf=pass reason=\"sender SPF authorized\" smtp.helo=mail-lj1-f173.google.com [email protected]; dkim=fail (bad signature) header.d=senderdomain.com; arc=fail",
"Received-SPF": "pass (kaimail.net: domain of [email protected] designates 209.85.208.173 as permitted sender)",
"Received": [
"(qmail 146414 invoked from network); 2 Mar 2026 14:05:54 -0000",
"from mail-lj1-f173.google.com (209.85.208.173) by mail.kaimail.net with ESMTPS; 2 Mar 2026 14:05:54 -0000"
],
"X-KAI-Sender-IP": "209.85.208.173",
"X-KAI-Sender-Host": "mail-lj1-f173.google.com",
"X-KAI-Received-Datetime": "2026-03-02 14:05:54",
"X-KAI-DKIM-Status": "fail;d=senderdomain.com",
"DKIM-Signature": "v=1; a=rsa-sha256; c=relaxed/relaxed; d=senderdomain.com; s=20230601; ...",
"MIME-Version": "1.0",
"From": "John Smith <[email protected]>",
"Date": "Mon, 2 Mar 2026 20:05:38 +0600",
"Message-ID": "<CALo1LC3UtaUEiuDJzU+Ce35Rx2Ptea3dkKLJfTOG7-O758FEqQ@mail.senderdomain.com>",
"Subject": "Order receipt with photo",
"To": "[email protected]",
"Content-Type": "multipart/related; boundary=\"000000000000a0584a064c0b1542\""
},
"body_text": "Please find the photo attached.\n",
"body_html": "<div dir=\"auto\">Please find the photo attached.<br><img src=\"cid:ii_19caede5e3f45621bd61\" style=\"max-width: 100%; height: auto;\"><br><br></div>\n",
"attachments": [
{
"filename": "photo.jpg",
"content_type": "image/jpeg",
"size": 362004,
"url": "https://kaimail-attachments.s3.amazonaws.com/xK4mNp8qTv-3wR5sY7zJbA/photo.jpg?response-content-disposition=attachment%3B%20filename%3D%22photo.jpg%22&AWSAccessKeyId=AKIAEXAMPLE123456&Signature=EXAMPLE_SIGNATURE&Expires=1772546759"
}
],
"envelope": {
"sender": "[email protected]",
"recipient": "[email protected]"
},
"account": {
"email": "[email protected]",
"domain": "yourdomain.com",
"mailbox": "[email protected]"
},
"metadata": {
"authentication_results": {
"PASS_REV_IP": true,
"PASS_SPF": true,
"PASS_DKIM": false,
"PASS_ARC": false
}
}
}
補足: この例では、DKIMとARCのチェックが失敗しています。これはメーリングリストや転送サービスによってメール本文が変更された場合に起こることがあります。
metadata.authentication_resultsフィールドを使って、このようなケースの処理方法を判断してください。
Webhookエンドポイントへの配信をシミュレートできます。
curl -X POST https://your-app.example.com/webhooks/email \
-H "Content-Type: application/json" \
-H "X-KAI-Event: email.received" \
-H "X-KAI-Tracking-ID: test-tracking-id-001" \
-H "X-KAI-Webhook-Signature: sha256=your_computed_signature" \
-H "User-Agent: KaiMail-Webhook/1.0" \
-d '{
"version": "1.0",
"timestamp": "2026-03-01T12:00:00.000000+00:00",
"headers": {"Subject": "Test", "From": "[email protected]", "To": "[email protected]"},
"body_text": "Test email body",
"body_html": null,
"attachments": [],
"envelope": {"sender": "[email protected]", "recipient": "[email protected]"},
"account": {"email": "[email protected]", "domain": "yourdomain.com", "mailbox": "[email protected]"},
"metadata": {"authentication_results": {"PASS_REV_IP": true, "PASS_SPF": true, "PASS_DKIM": true, "PASS_ARC": true}}
}'
すべてのWebhookリクエストのX-KAI-Webhook-Signatureヘッダーには、HMAC-SHA256署名が含まれています。ペイロードを処理する前に、必ずこの署名を検証してください。これにより、リクエストがKaiMailから送信され、改ざんされていないことを確認できます。
HMAC-SHA256(Webhookシークレット, リクエストボディ)を計算します。sha256=<16進ダイジェスト>の形式でX-KAI-Webhook-Signatureヘッダーに設定されます。Webhookルートごとに一意の署名用シークレット(64文字の16進数文字列)が自動生成されます。
Pythonを使った検証のコード例です。
import hmac
import hashlib
def verify_webhook_signature(payload: bytes, signature_header: str, secret: str) -> bool:
"""
Verify the HMAC-SHA256 signature of a KaiMail webhook request.
Args:
payload: Raw request body (bytes).
signature_header: Value of the X-KAI-Webhook-Signature header.
secret: Webhookシークレット(KaiMailのメールボックス編集ページから取得)。
Returns:
True if the signature is valid.
"""
expected = hmac.new(
secret.encode(), payload, hashlib.sha256
).hexdigest()
expected_signature = f"sha256={expected}"
return hmac.compare_digest(expected_signature, signature_header)
セキュリティに関する注意: タイミング攻撃を防ぐため、署名の比較には
hmac.compare_digest()を使用してください。==演算子は使わないでください。
PHPを使っても検証を行えます。
<?php
/**
* Verify the HMAC-SHA256 signature of a KaiMail webhook request.
* * @param string $payload Raw request body (from php://input).
* @param string $signatureHeader Value of the X-KAI-Webhook-Signature header.
* @param string $secret Your webhook secret.
* @return bool True if the signature is valid.
*/
function verify_webhook_signature($payload, $signatureHeader, $secret) {
// 1. Generate the HMAC-SHA256 hash in hex format
$expectedHash = hash_hmac('sha256', $payload, $secret);
// 2. Prefix with 'sha256=' to match the KaiMail header format
$expectedSignature = "sha256=" . $expectedHash;
// 3. Use hash_equals for a timing-attack safe comparison
// This is the PHP equivalent of hmac.compare_digest
return hash_equals($expectedSignature, $signatureHeader);
}
// --- Usage Example ---
// Get the raw body
$payload = file_get_contents('php://input');
// Get the signature from headers (case varies by server/framework)
$signatureHeader = $_SERVER['HTTP_X_KAI_WEBHOOK_SIGNATURE'] ?? '';
$secret = 'your-webhook-secret';
if (verify_webhook_signature($payload, $signatureHeader, $secret)) {
// Signature is valid, process the data
$data = json_decode($payload, true);
$logFile = 'webhook_log_' . date('Y-m-d') . '.json';
file_put_contents($logFile, $payload . PHP_EOL, FILE_APPEND | LOCK_EX);
// Optional: Log the headers too (very helpful for debugging signatures)
$headers = getallheaders();
file_put_contents('headers_log.txt', print_r($headers, true), FILE_APPEND);
http_response_code(200);
} else {
// Invalid signature, reject the request
http_response_code(401);
exit("Invalid signature");
}
添付ファイルはJSONペイロードに埋め込まれません。代わりに、クラウドストレージ(Amazon S3)への署名付きURLが各添付ファイルに含まれます。
filename、content_type、size(バイト単位)、urlが含まれます。import requests
import os
def download_attachments(payload: dict, save_dir: str = "./attachments"):
"""Download all attachments from a webhook payload."""
os.makedirs(save_dir, exist_ok=True)
for attachment in payload.get("attachments", []):
filename = attachment["filename"]
url = attachment["url"]
filepath = os.path.join(save_dir, filename)
response = requests.get(url, timeout=30)
response.raise_for_status()
with open(filepath, "wb") as f:
f.write(response.content)
print(f"Downloaded {filename} ({attachment['size']} bytes)")
KaiMail Webhookの受信、検証、処理を行う完全なFlaskアプリケーションの例です。
import hmac
import hashlib
import os
import requests
from flask import Flask, request, jsonify
app = Flask(__name__)
WEBHOOK_SECRET = os.environ["KAIMAIL_WEBHOOK_SECRET"]
def verify_signature(payload: bytes, signature_header: str) -> bool:
expected = hmac.new(
WEBHOOK_SECRET.encode(), payload, hashlib.sha256
).hexdigest()
return hmac.compare_digest(f"sha256={expected}", signature_header)
@app.route("/webhooks/email", methods=["POST"])
def receive_email():
# 1. Webhook署名を検証
signature = request.headers.get("X-KAI-Webhook-Signature", "")
if not verify_signature(request.data, signature):
return jsonify({"error": "Invalid signature"}), 401
# 2. JSONペイロードをパース
payload = request.get_json()
sender = payload["envelope"]["sender"]
recipient = payload["envelope"]["recipient"]
subject = payload["headers"].get("Subject", "(no subject)")
body = payload.get("body_text") or ""
print(f"Email from {sender} to {recipient}: {subject}")
# 3. 添付ファイルをダウンロード(ある場合)
for attachment in payload.get("attachments", []):
resp = requests.get(attachment["url"], timeout=30)
if resp.ok:
filepath = os.path.join("attachments", attachment["filename"])
os.makedirs("attachments", exist_ok=True)
with open(filepath, "wb") as f:
f.write(resp.content)
print(f" Saved attachment: {attachment['filename']}")
# 4. 200を返して受信を確認
return jsonify({"status": "ok"}), 200
if __name__ == "__main__":
app.run(port=8888)
重要:
2xxレスポンスを30秒以内に返してください。処理に時間がかかる場合は、先に200を返し、非同期で処理してください(例:タスクキューの利用)。
エンドポイントが2xxステータスを返さない場合、KaiMailは指数バックオフでリトライします。
| 試行 | 失敗後の待機時間 |
|---|---|
| 1回目のリトライ | 5秒 |
| 2回目のリトライ | 10秒 |
| 3回目のリトライ | 20秒 |
3回のリトライ(合計4回の試行)が失敗すると、配信は失敗として記録されます。
| 状況 | リトライ |
|---|---|
| 5xxサーバーエラー | はい |
| 408 Request Timeout | はい |
| 429 Too Many Requests | はい |
| 接続タイムアウト | はい |
| 接続拒否・DNS解決失敗 | はい |
| 4xxクライアントエラー(408、429を除く) | いいえ |
| SSL/TLSエラー | いいえ |
X-KAI-Tracking-IDヘッダーを使って重複配信を検出してください。リトライでは同じトラッキングIDが使われます。metadata.authentication_resultsオブジェクトは、受信メールが標準的なメール認証チェックを通過したかどうかを示します。
| フィールド | 説明 |
|---|---|
PASS_REV_IP |
逆引きIPルックアップに成功しました。送信サーバーのIPがホスト名と一致しています。 |
PASS_SPF |
SPF(Sender Policy Framework)チェックに成功しました。送信者のドメインが送信サーバーを承認しています。 |
PASS_DKIM |
DKIM(DomainKeys Identified Mail)チェックに成功しました。メールの暗号署名が有効です。 |
PASS_ARC |
ARC(Authenticated Received Chain)チェックに成功しました。転送チェーンの認証が保持されています。 |
def is_email_authenticated(metadata: dict) -> bool:
"""Check if the email passed basic authentication."""
auth = metadata.get("authentication_results", {})
return auth.get("PASS_SPF", False) and auth.get("PASS_DKIM", False)
Webhook配信は有料プランで利用できます。詳細は価格ページをご確認ください。
無料のBASICプランはメール転送のみ対応しており、Webhookは利用できません。
mail.kaimail.net(kaimail.netではない)であることを確認してください。2xxステータスコードを返していることを確認してください。kaimail-attachments.s3.amazonaws.comにアクセスできることを確認してください(ファイアウォールでブロックされていないか)。4xxエラーはエンドポイントがリクエストを拒否したことを意味します。アプリケーションログを確認してください。5xxエラーはサーバーの内部エラーです。KaiMailは最大3回リトライします。200を返し、非同期で処理してください。