KaiMail Webhookの連携ガイド


KaiMailのWebhook配信と連携するための開発者向けガイドです。カスタムドメイン宛のメールを、HTTP POSTで構造化されたJSONペイロードとして受信できます。メールサーバーの構築は不要です。

KaiMailとは

KaiMailはSaaS型のメール転送サービスです。カスタムドメインをKaiMailに登録し、MXレコードをKaiMailのメールサーバーに向けるだけで、そのドメイン宛のメールを指定した転送先に届けます。転送先は別のメールアドレスか、HTTP Webhookエンドポイントから選べます。

Webhook配信では、受信メールがJSONに変換され、HTTP POSTでアプリケーションに届きます。以下のようなユースケースに活用できます。

  • カスタマーサポートのチケット自動作成
  • 注文確認メールの処理
  • ドキュメントの自動取り込み
  • SaaSアプリケーションへのメール取り込み

前提条件

始める前に以下をご用意ください。

  1. PLUSプラン以上のKaiMailアカウント。 無料のBASICプランではWebhook配信を利用できません。kaimail.netからお申し込みください。
  2. ご自身で管理するカスタムドメイン。 MXレコードを更新するためDNSへのアクセスが必要です。
  3. Webhookを受信するHTTPSエンドポイント。 本番環境では有効なTLS証明書を持つHTTPSが必要です。

クイックスタート

ステップ1 ―アカウント登録とプラン選択

KaiMailでアカウントを作成し、PLUSプラン以上を選択します。Webhook配信には有料プランが必要です。

ステップ2 ―カスタムドメインの追加

KaiMailダッシュボードのルーティングページで、カスタムドメイン(例:yourdomain.com)を追加します。

ステップ3 ―MXレコードの設定

DNSプロバイダーにログインし、ドメインのMXレコードを更新します。

タイプ ホスト 優先度
MX yourdomain.com mail.kaimail.net 10

既存のMXレコードは配信の競合を避けるため削除してください。

ステップ4 ―MXレコードの確認

KaiMailダッシュボードに戻り、ドメインの横にあるMXチェックボタンをクリックします。KaiMailがDNSを照会し、MXレコードがmail.kaimail.netを指していることを確認します。

ステップ5 ―Webhookルートの作成

メールボックスを追加します(例:[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 -------------------------|
  1. 送信者がSMTPでメールを送信します。
  2. KaiMailがメールを受信し、認証チェック(SPF、DKIM、ARC)を実行します。
  3. メールがJSONペイロードにシリアライズされます。添付ファイルはクラウドストレージにアップロードされます。
  4. KaiMailがHMAC-SHA256でペイロードに署名し、HTTP POSTでエンドポイントに送信します。
  5. アプリケーションがペイロードを処理し、2xxレスポンスを返します。

Webhookリクエスト形式

KaiMailはContent-Type: application/jsonPOSTリクエストを、以下のカスタムヘッダーとともに送信します。

ヘッダー 説明
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 添付ファイルのリストです。添付がない場合は空の配列です。各要素にfilenamecontent_typesize(バイト)、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 メール認証チェックの結果です。詳細は「認証結果」をご覧ください。

実際のペイロード例

例1 ―テキストメール(添付なし)

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-SealARC-Message-Signature、Google内部ヘッダーなどは実際のペイロードに含まれますが、ここでは簡略化のため省略しています。

例2 ―画像添付付きメール

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フィールドを使って、このようなケースの処理方法を判断してください。

curlでテストする

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署名の検証

すべてのWebhookリクエストのX-KAI-Webhook-Signatureヘッダーには、HMAC-SHA256署名が含まれています。ペイロードを処理する前に、必ずこの署名を検証してください。これにより、リクエストがKaiMailから送信され、改ざんされていないことを確認できます。

仕組み

  1. KaiMailがHMAC-SHA256(Webhookシークレット, リクエストボディ)を計算します。
  2. 結果がsha256=<16進ダイジェスト>の形式でX-KAI-Webhook-Signatureヘッダーに設定されます。
  3. アプリケーション側で同じHMACを計算し、ヘッダーの値と比較します。

Webhookシークレットの確認方法

Webhookルートごとに一意の署名用シークレット(64文字の16進数文字列)が自動生成されます。

  • ダッシュボードで確認: Webhookルートのメールボックス編集ページを開きます。シークレットはデフォルトでパスワードマスクされています。表示をクリックして確認するか、コピーをクリックしてクリップボードにコピーできます。
  • 再生成: メールボックス編集ページの下部にあるシークレットを再生成をクリックします。再生成すると古いシークレットは即座に無効になるため、再生成する前にアプリケーション側を更新してください。

Pythonでの検証例

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を使っても検証を行えます。

<?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が各添付ファイルに含まれます。

重要なポイント

  • URLの有効期限は24時間です。Webhook受信後、速やかにダウンロードしてください。
  • 各添付ファイルにはfilenamecontent_typesize(バイト単位)、urlが含まれます。
  • URLに認証パラメーターが含まれているため、ダウンロード時に追加のヘッダーは不要です。

Pythonでのダウンロード例

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

Webhookレシーバーのサンプル(Flask)

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エラー いいえ

ベストプラクティス

  • 200を素早く返す。 Webhookを受け取ったらすぐに応答し、メール処理は非同期で行います。
  • 冪等性を実装する。 X-KAI-Tracking-IDヘッダーを使って重複配信を検出してください。リトライでは同じトラッキングIDが使われます。
  • トラッキングIDを記録する。 KaiMailダッシュボードで配信の問題をデバッグする際に役立ちます。

認証結果

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)チェックに成功しました。転送チェーンの認証が保持されています。

活用方法

  • すべて成功: メールが正当なものである可能性が高いです。
  • SPF成功、DKIM失敗: メーリングリストや転送サービスがメール本文を変更した場合に起こることがあります。通常は正当なメールです。
  • すべて失敗: 注意が必要です。メールが偽装されている可能性があります。手動での確認を検討してください。
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は利用できません。

トラブルシューティング

MXレコードが確認できない

  • DNSの変更が反映されるまで最大48時間かかる場合があります。しばらく待ってからMXチェックを再度クリックしてください。
  • 対象ドメインのほかのMXレコードがすべて削除されていることを確認してください。
  • MXの値がmail.kaimail.netkaimail.netではない)であることを確認してください。

Webhookが配信されない

  • 本番環境では、エンドポイントURLがHTTPS(HTTPではなく)であることを確認してください。
  • サーバーが30秒以内に2xxステータスコードを返していることを確認してください。
  • KaiMailダッシュボードの配信ログでエラーの詳細を確認してください。

署名の検証が失敗する

  • パース・再シリアライズしたJSON文字列ではなく、生のリクエストボディ(バイト列)に対して検証していることを確認してください。
  • KaiMailダッシュボードのメールボックス編集ページから正しいWebhookシークレットを使用していることを確認してください。Webhookルートごとに個別のシークレットがあります。
  • フレームワークが読み取り前にリクエストボディを変更していないか確認してください。

添付ファイルがダウンロードできない

  • 署名付きURLの有効期限は24時間です。Webhook受信後すぐに添付ファイルをダウンロードしてください。
  • サーバーからkaimail-attachments.s3.amazonaws.comにアクセスできることを確認してください(ファイアウォールでブロックされていないか)。

メールは受信されるがWebhookにエラーが表示される

  • 4xxエラーはエンドポイントがリクエストを拒否したことを意味します。アプリケーションログを確認してください。
  • 5xxエラーはサーバーの内部エラーです。KaiMailは最大3回リトライします。
  • タイムアウトはエンドポイントの応答に30秒以上かかったことを意味します。すぐに200を返し、非同期で処理してください。