ホームに戻る

SMTPDog 開発者ドキュメント

ドメイン検証、SMTP と Email API、API キー、Webhook の署名検証、トラブルシューティングまで、わかりやすい導入ガイドです。

06 · webhooks

Webhook

メール状態イベントを受信し、署名検証と冪等処理を行います。

1. 受信エンドポイントを作る

セッショントークンで/webhooksを管理し、作成時はurlのみ送ります。公開HTTPSを推奨し、私設・ループバックは不可、リダイレクトしません。PATCHでurlやactive/disabledを変更できますがイベント選択は未対応です。secretは作成・取得でも返るため安全に保存してください。API Keyではありません。

curl -X POST "${SMTPDOG_API_ORIGIN}/webhooks" \
  -H "Authorization: Bearer ${SMTPDOG_SESSION_TOKEN}" \
  -H "Content-Type: application/json" \
  --data '{"url":"https://example.com/hooks/email"}'

curl -X PATCH "${SMTPDOG_API_ORIGIN}/webhooks/${SMTPDOG_WEBHOOK_ID}" \
  -H "Authorization: Bearer ${SMTPDOG_SESSION_TOKEN}" \
  -H "Content-Type: application/json" \
  --data '{"status":"disabled"}'

curl -X GET "${SMTPDOG_API_ORIGIN}/webhooks/${SMTPDOG_WEBHOOK_ID}/deliveries" \
  -H "Authorization: Bearer ${SMTPDOG_SESSION_TOKEN}"

2. 4種類の状態イベント

イベントはsent、bounced、deferred、failedです。opened、clicked、deliveredはありません。本文timestampは登録時刻で署名時刻とは異なります。同じ配信の再試行ではevent_idが一定ですが、別端点や重複状態では別IDもあり、順序や必達は保証しません。

フィールド / 設定型 / 状態説明
event_id / message_idUUID string配信イベントIDとメールIDは別の用途
event_type / statusstringsent / bounced / deferred / failed
from / tostring送信者・受信者各1名
timestampRFC3339 string登録時刻、署名Unix秒とは異なる
smtp_responsestring任意のSMTP情報
{
  "event_id": "00000000-0000-4000-8000-000000000001",
  "event_type": "sent",
  "message_id": "00000000-0000-4000-8000-000000000000",
  "from": "sender@example.com",
  "to": "recipient@example.com",
  "status": "sent",
  "timestamp": "2026-01-01T00:00:00Z"
}

3. 生の本文で署名を検証

X-Webhook-Signatureはt=<Unix秒>,v1=<hex>です。完全なsecretでt + "." + 生の本文にHMAC-SHA256を計算します。JSON再シリアル化やsecret接頭辞の削除は不可です。定時間比較を使います。時間許容幅は受信側が設定し、プラットフォーム固定値ではありません。

package webhookexample

import (
    "crypto/hmac"
    "crypto/sha256"
    "encoding/hex"
    "strconv"
    "strings"
    "time"
)

// raw must be the original body bytes, before JSON parsing.
// tolerance is configured and reviewed by the receiver.
func Verify(secret string, raw []byte, header string,
    now time.Time, tolerance time.Duration) bool {
    if tolerance <= 0 { return false }
    parts := strings.Split(header, ",")
    if len(parts) != 2 { return false }
    fields := map[string]string{}
    for _, part := range parts {
        pair := strings.SplitN(strings.TrimSpace(part), "=", 2)
        if len(pair) != 2 { return false }
        if _, exists := fields[pair[0]]; exists { return false }
        fields[pair[0]] = pair[1]
    }
    ts, err := strconv.ParseInt(fields["t"], 10, 64)
    if err != nil || ts <= 0 || strconv.FormatInt(ts, 10) != fields["t"] {
        return false
    }
    delta := now.Sub(time.Unix(ts, 0))
    if delta > tolerance || delta < -tolerance { return false }
    sig, err := hex.DecodeString(fields["v1"])
    if err != nil || len(sig) != sha256.Size { return false }
    mac := hmac.New(sha256.New, []byte(secret))
    mac.Write([]byte(fields["t"] + "."))
    mac.Write(raw)
    return hmac.Equal(mac.Sum(nil), sig)
}

// Read the raw request body and verify before parsing JSON.
// Then durably deduplicate event_id and apply business idempotency.
// Return 2xx only when safe; this function is not a full receiver.

4. 重複排除と確認、記録

検証後に永続的な重複排除と冪等処理をして2xxを返します。非2xx・通信失敗は初回と最大5回の再試行となり、時刻は稼働状態によります。GET /webhooks/{id}/deliveriesは直近50件でpayloadやevent_idはありません。停止で待機を消さず、削除は履歴も消します。テスト・再配信・secret更新APIはありません。