홈으로 돌아가기

SMTPDog 개발자 문서

도메인 인증, SMTP와 Email API, API 키, 웹훅 서명 검증, 문제 해결까지 한눈에 볼 수 있는 연동 가이드입니다.

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. 네 가지 상태 이벤트

이벤트는 sent, bounced, deferred, failed입니다. opened, clicked, delivered는 없습니다. payload 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는 없습니다.