이벤트 유형
웹훅을 만들 때 이 이벤트 유형을 원하는 대로 조합해 구독할 수 있습니다.
| 이벤트 | 발생 시점 | last_event 설정값 |
|---|---|---|
email.scheduled |
scheduled_at이 있는 이메일이 접수되었습니다. |
scheduled |
email.sent |
이메일이 Amazon SES에 전달되었습니다. | sent |
email.delivered |
수신자의 메일 서버가 이메일을 수락했습니다. | delivered |
email.delivery_delayed |
전달이 지연되거나 일시적으로 실패하고 있으며, SES가 계속 재시도합니다. | delivery_delayed |
email.bounced |
영구적인 실패입니다(주소가 존재하지 않거나 도메인이 거부함). | bounced |
email.complained |
수신자가 이메일을 스팸으로 신고했습니다. | complained |
email.opened |
수신자가 이메일을 열었습니다(열람 추적 필요). | opened |
email.clicked |
수신자가 링크를 클릭했습니다(클릭 추적 필요). | clicked |
email.failed |
SpacePost 또는 SES가 이메일을 보내지 못했습니다(거부, 렌더링 실패, 일시 중지된 팀 또는 테넌트). | failed |
email.suppressed |
수신자가 발송 제외 목록에 있어 건너뛰었습니다. | suppressed |
페이로드 형식
섹션 제목: “페이로드 형식”웹훅 본문은 Resend의 형식과 정확히 같으므로 Resend용으로 작성한 핸들러가 그대로 동작합니다.
{ "type": "email.delivered", "created_at": "2026-10-23T14:02:10.000Z", "data": { "email_id": "0192e1c4-7a3b-7c1e-9d2f-3b5a8e6f4c10", "from": "Acme <hello@acme.com>", "to": ["user@example.com"], "subject": "Welcome to Acme", "created_at": "2026-10-23T14:02:09.512Z", "broadcast_id": "0192e1c4-0000-7000-8000-0000000000bb", "tenant_id": "0192e1c4-5555-7000-8000-0000000000cc", "tags": { "category": "welcome" } }}type은 이벤트이고,created_at은 이벤트가 발생한 시각입니다.data.email_id,from,to(영향받은 수신자만이 아니라 이메일의 모든 수신자),subject,created_at(이메일이 생성된 시각)은 항상 포함됩니다.data.broadcast_id는 브로드캐스트 이메일일 때만 포함됩니다.data.tenant_id는 이메일이 테넌트를 위해 발송된 경우(플랫폼 모드)에만 포함됩니다. 사용자의external_id가 아니라 SpacePost의 테넌트 ID입니다.data.tags는 이메일에 태그가 있을 때만 객체({ "name": "value" })로 포함됩니다.- 일부 이벤트는
data에 이벤트 이름을 딴 객체를 하나 더 추가합니다:bounce,click,open,failed,suppressed. 아래에 정리되어 있습니다. 그 외에는 추가되는 것이 없습니다. 최상위bounceType,delayType,reason키는 없습니다.
email.scheduled
섹션 제목: “email.scheduled”{ "type": "email.scheduled", "created_at": "2026-10-23T14:02:09.700Z", "data": { "email_id": "0192e1c4-7a3b-7c1e-9d2f-3b5a8e6f4c10", "from": "Acme <hello@acme.com>", "to": ["user@example.com"], "subject": "Your trial ends tomorrow", "created_at": "2026-10-23T14:02:09.512Z" }}email.sent
섹션 제목: “email.sent”{ "type": "email.sent", "created_at": "2026-10-23T14:02:10.100Z", "data": { "email_id": "0192e1c4-7a3b-7c1e-9d2f-3b5a8e6f4c10", "from": "Acme <hello@acme.com>", "to": ["user@example.com"], "subject": "Welcome to Acme", "created_at": "2026-10-23T14:02:09.512Z" }}email.delivered
섹션 제목: “email.delivered”{ "type": "email.delivered", "created_at": "2026-10-23T14:02:11.350Z", "data": { "email_id": "0192e1c4-7a3b-7c1e-9d2f-3b5a8e6f4c10", "from": "Acme <hello@acme.com>", "to": ["user@example.com"], "subject": "Welcome to Acme", "created_at": "2026-10-23T14:02:09.512Z" }}email.delivery_delayed
섹션 제목: “email.delivery_delayed”SES 전달 지연과 일시적 반송에서 발생합니다. 단순 지연에는 추가 필드가 없습니다. 일시적 반송에는 bounce 객체도 포함됩니다(아래 참고, type: "Transient").
{ "type": "email.delivery_delayed", "created_at": "2026-10-23T14:07:00.000Z", "data": { "email_id": "0192e1c4-7a3b-7c1e-9d2f-3b5a8e6f4c10", "from": "Acme <hello@acme.com>", "to": ["user@example.com"], "subject": "Welcome to Acme", "created_at": "2026-10-23T14:02:09.512Z" }}email.bounced
섹션 제목: “email.bounced”영구 반송만 이 이벤트로 전달되며, 해당 주소는 발송 제외 목록에 추가됩니다. data.bounce에는 type(Permanent), subType, 수신 서버의 message가 들어 있습니다.
{ "type": "email.bounced", "created_at": "2026-10-23T14:02:12.000Z", "data": { "email_id": "0192e1c4-7a3b-7c1e-9d2f-3b5a8e6f4c10", "from": "Acme <hello@acme.com>", "to": ["nobody@example.com"], "subject": "Welcome to Acme", "created_at": "2026-10-23T14:02:09.512Z", "bounce": { "type": "Permanent", "subType": "General", "message": "smtp; 550 5.1.1 The email account that you tried to reach does not exist." } }}반송 하위 유형에는 General, NoEmail, Suppressed, OnAccountSuppressionList, MailboxFull, MessageTooLarge, ContentRejected, AttachmentRejected가 있습니다.
email.complained
섹션 제목: “email.complained”추가 필드는 없습니다. 해당 주소는 발송 제외 목록에 추가됩니다.
{ "type": "email.complained", "created_at": "2026-10-23T15:30:00.000Z", "data": { "email_id": "0192e1c4-7a3b-7c1e-9d2f-3b5a8e6f4c10", "from": "Acme <hello@acme.com>", "to": ["user@example.com"], "subject": "Our monthly newsletter", "created_at": "2026-10-23T14:02:09.512Z" }}email.opened
섹션 제목: “email.opened”{ "type": "email.opened", "created_at": "2026-10-23T14:05:44.000Z", "data": { "email_id": "0192e1c4-7a3b-7c1e-9d2f-3b5a8e6f4c10", "from": "Acme <hello@acme.com>", "to": ["user@example.com"], "subject": "Welcome to Acme", "created_at": "2026-10-23T14:02:09.512Z", "open": { "ipAddress": "203.0.113.7", "userAgent": "Mozilla/5.0 (Macintosh)" } }}email.clicked
섹션 제목: “email.clicked”{ "type": "email.clicked", "created_at": "2026-10-23T14:06:10.000Z", "data": { "email_id": "0192e1c4-7a3b-7c1e-9d2f-3b5a8e6f4c10", "from": "Acme <hello@acme.com>", "to": ["user@example.com"], "subject": "Welcome to Acme", "created_at": "2026-10-23T14:02:09.512Z", "click": { "link": "https://acme.com/get-started", "ipAddress": "203.0.113.7", "userAgent": "Mozilla/5.0 (Macintosh)", "timestamp": "2026-10-23T14:06:10.000Z" } }}email.failed
섹션 제목: “email.failed”{ "type": "email.failed", "created_at": "2026-10-23T14:02:10.000Z", "data": { "email_id": "0192e1c4-7a3b-7c1e-9d2f-3b5a8e6f4c10", "from": "Acme <hello@acme.com>", "to": ["user@example.com"], "subject": "Welcome to Acme", "created_at": "2026-10-23T14:02:09.512Z", "failed": { "reason": "Attachment https://files.acme.com/report.pdf returned HTTP 404" } }}email.suppressed
섹션 제목: “email.suppressed”suppressed.reason은 suppression_list(수신자가 발송 제외 목록에 있음) 또는 recipient_domain_does_not_accept_mail(수신자 도메인에 메일 서버가 없음)입니다.
{ "type": "email.suppressed", "created_at": "2026-10-23T14:02:10.000Z", "data": { "email_id": "0192e1c4-7a3b-7c1e-9d2f-3b5a8e6f4c10", "from": "Acme <hello@acme.com>", "to": ["user@example.com", "other@example.com"], "subject": "Welcome to Acme", "created_at": "2026-10-23T14:02:09.512Z", "suppressed": { "recipients": ["other@example.com"], "reason": "suppression_list" } }}TypeScript에서 이벤트 타입 지정하기
섹션 제목: “TypeScript에서 이벤트 타입 지정하기”Node SDK는 type으로 구분되는 WebhookPayload 유니언을 내보내고 spacepost.webhooks.verify()가 이를 반환하므로, event.type으로 좁히면 올바른 data 필드를 얻을 수 있습니다.
import type { WebhookPayload } from "@spacepost/node";
function handle(event: WebhookPayload) { switch (event.type) { case "email.bounced": return event.data.bounce?.subType; case "email.clicked": return event.data.click?.link; case "email.suppressed": return event.data.suppressed.recipients; case "email.failed": return event.data.failed.reason; default: return event.data.tenant_id; // present when the email was sent for a tenant }}