Initail project
This commit is contained in:
commit
5f749af298
15
.dockerignore
Normal file
15
.dockerignore
Normal file
@ -0,0 +1,15 @@
|
|||||||
|
.git
|
||||||
|
.gitignore
|
||||||
|
.vscode
|
||||||
|
.idea
|
||||||
|
.dotnet
|
||||||
|
.nuget
|
||||||
|
**/bin
|
||||||
|
**/obj
|
||||||
|
**/*.user
|
||||||
|
**/*.suo
|
||||||
|
**/*.swp
|
||||||
|
**/*.log
|
||||||
|
.env
|
||||||
|
.env.*
|
||||||
|
!/.env.example
|
||||||
4
.env.example
Normal file
4
.env.example
Normal file
@ -0,0 +1,4 @@
|
|||||||
|
ASPNETCORE_ENVIRONMENT=Development
|
||||||
|
ConnectionStrings__Default=Host=localhost;Database=send_engine;Username=postgres;Password=postgres
|
||||||
|
ESP__Provider=ses
|
||||||
|
ESP__ApiKey=change_me
|
||||||
81
.gitignore
vendored
Normal file
81
.gitignore
vendored
Normal file
@ -0,0 +1,81 @@
|
|||||||
|
# Build results
|
||||||
|
bin/
|
||||||
|
obj/
|
||||||
|
[Bb]uild/
|
||||||
|
[Bb]uilds/
|
||||||
|
|
||||||
|
# Visual Studio
|
||||||
|
.vs/
|
||||||
|
*.suo
|
||||||
|
*.user
|
||||||
|
*.userosscache
|
||||||
|
*.sln.docstates
|
||||||
|
*.vcxproj.user
|
||||||
|
*.vcxproj.filters
|
||||||
|
*.vcxproj.user
|
||||||
|
|
||||||
|
# Rider / JetBrains
|
||||||
|
.idea/
|
||||||
|
*.sln.iml
|
||||||
|
|
||||||
|
# VS Code
|
||||||
|
.vscode/
|
||||||
|
|
||||||
|
# Visual Studio Code local history
|
||||||
|
.history/
|
||||||
|
|
||||||
|
# .NET Core
|
||||||
|
project.lock.json
|
||||||
|
project.fragment.lock.json
|
||||||
|
artifacts/
|
||||||
|
|
||||||
|
# NuGet
|
||||||
|
*.nupkg
|
||||||
|
*.snupkg
|
||||||
|
# Local NuGet/global package caches in repo
|
||||||
|
.nuget/
|
||||||
|
# The packages folder can be ignored because of PackageReference
|
||||||
|
# Uncomment if using packages.config
|
||||||
|
#packages/
|
||||||
|
|
||||||
|
# Logs
|
||||||
|
*.log
|
||||||
|
|
||||||
|
# User-specific files
|
||||||
|
*.userprefs
|
||||||
|
|
||||||
|
# Resharper / DotCover
|
||||||
|
_ReSharper*/
|
||||||
|
*.[Rr]e[Ss]harper
|
||||||
|
*.DotSettings.user
|
||||||
|
|
||||||
|
# Test results
|
||||||
|
TestResults/
|
||||||
|
*.trx
|
||||||
|
|
||||||
|
# Coverage
|
||||||
|
*.coverage
|
||||||
|
*.coveragexml
|
||||||
|
coverage/
|
||||||
|
|
||||||
|
# ASP.NET Scaffolding
|
||||||
|
ScaffoldingReadMe.txt
|
||||||
|
|
||||||
|
# Secrets
|
||||||
|
secrets.json
|
||||||
|
appsettings.*.json
|
||||||
|
!appsettings.json
|
||||||
|
.env
|
||||||
|
.env.*
|
||||||
|
!.env.example
|
||||||
|
|
||||||
|
# OS
|
||||||
|
.DS_Store
|
||||||
|
Thumbs.db
|
||||||
|
|
||||||
|
# Others
|
||||||
|
*.swp
|
||||||
|
*.tmp
|
||||||
|
*.pid
|
||||||
|
*.pid.lock
|
||||||
|
.dotnet/
|
||||||
6
Directory.Build.props
Normal file
6
Directory.Build.props
Normal file
@ -0,0 +1,6 @@
|
|||||||
|
<Project>
|
||||||
|
<PropertyGroup>
|
||||||
|
<RunAnalyzers>false</RunAnalyzers>
|
||||||
|
<EnableNETAnalyzers>false</EnableNETAnalyzers>
|
||||||
|
</PropertyGroup>
|
||||||
|
</Project>
|
||||||
9
NuGet.Config
Normal file
9
NuGet.Config
Normal file
@ -0,0 +1,9 @@
|
|||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<configuration>
|
||||||
|
<config>
|
||||||
|
<add key="globalPackagesFolder" value=".nuget/packages" />
|
||||||
|
</config>
|
||||||
|
<packageSources>
|
||||||
|
<add key="nuget.org" value="https://api.nuget.org/v3/index.json" />
|
||||||
|
</packageSources>
|
||||||
|
</configuration>
|
||||||
57
README.md
Normal file
57
README.md
Normal file
@ -0,0 +1,57 @@
|
|||||||
|
# Send Engine(發送引擎)
|
||||||
|
|
||||||
|
此專案為獨立的發送引擎,負責寄送、節流、退信處理與追蹤。文件統一放在 `docs/`。
|
||||||
|
|
||||||
|
## 目標(MVP)
|
||||||
|
- 接收 Member Center 的訂閱事件(activated / unsubscribed / preferences.updated)
|
||||||
|
- 多租戶名單快照(依 tenant/list)且僅增量更新
|
||||||
|
- 管理 Campaign / Send Job 與排程
|
||||||
|
- 對接 ESP(SES / SendGrid / Mailgun)
|
||||||
|
- 記錄投遞結果與退信(必要時回寫 Member Center)
|
||||||
|
|
||||||
|
## 既定條件
|
||||||
|
- 與 Member Center 相同框架(C# .NET Core)
|
||||||
|
- Send engine 不提供跨租戶名單查詢
|
||||||
|
- 事件同步採 event/queue 或 webhook(二擇一即可)
|
||||||
|
- DB:PostgreSQL
|
||||||
|
|
||||||
|
## 範圍(第一階段)
|
||||||
|
- 事件 ingest 與名單增量同步
|
||||||
|
- Campaign / Send Job 基本流程
|
||||||
|
- Sender Adapter(至少一種 ESP)
|
||||||
|
- 投遞與退信記錄
|
||||||
|
|
||||||
|
## 非目標(暫不處理)
|
||||||
|
- 自建 SMTP server
|
||||||
|
- 跨租戶名單查詢
|
||||||
|
- 行銷自動化或行為追蹤
|
||||||
|
|
||||||
|
## 文件
|
||||||
|
- `docs/DESIGN.md`:系統設計總覽、核心模組與資料流
|
||||||
|
- `docs/FLOWS.md`:主要業務流程(同步、寄送、退信)
|
||||||
|
- `docs/OPENAPI.md`:API 規格說明與補充規則
|
||||||
|
- `docs/SCHEMA.sql`:資料庫 schema(PostgreSQL)
|
||||||
|
- `docs/TECH_STACK.md`:技術棧與選型
|
||||||
|
- `docs/INSTALL.md`:安裝、初始化與維運指令
|
||||||
|
- `docs/USE_CASES.md`:需求用例(角色、情境、驗收)
|
||||||
|
|
||||||
|
## 專案結構
|
||||||
|
```text
|
||||||
|
mass_mail_engine/
|
||||||
|
├── src/
|
||||||
|
│ ├── SendEngine.Api/ # REST API(事件 ingest、送信、管理 API)
|
||||||
|
│ ├── SendEngine.Installer/ # 安裝與初始化 CLI(migrate/init/admin)
|
||||||
|
│ ├── SendEngine.Application/ # 應用層介面與 DTO
|
||||||
|
│ ├── SendEngine.Infrastructure/# EF Core、ESP Adapter、服務實作
|
||||||
|
│ └── SendEngine.Domain/ # 領域實體與常數
|
||||||
|
├── docs/ # 專案文件
|
||||||
|
├── .vscode/ # 本機開發啟動與工作設定
|
||||||
|
├── SendEngine.sln # Solution
|
||||||
|
└── README.md
|
||||||
|
```
|
||||||
|
|
||||||
|
## 待確認事項
|
||||||
|
- 事件系統選擇(Kafka/RabbitMQ/SNS+SQS / Webhook)
|
||||||
|
- ESP 優先順序(SES / SendGrid / Mailgun)
|
||||||
|
- 退信回寫的規則(hard bounce / soft bounce)
|
||||||
|
- 追蹤事件範圍(open / click / unsubscribe)
|
||||||
4
SendEngine.sln
Normal file
4
SendEngine.sln
Normal file
@ -0,0 +1,4 @@
|
|||||||
|
Microsoft Visual Studio Solution File, Format Version 12.00
|
||||||
|
# Visual Studio Version 17
|
||||||
|
VisualStudioVersion = 17.0.31903.59
|
||||||
|
MinimumVisualStudioVersion = 10.0.40219.1
|
||||||
53
docs/DESIGN.md
Normal file
53
docs/DESIGN.md
Normal file
@ -0,0 +1,53 @@
|
|||||||
|
# Design
|
||||||
|
|
||||||
|
本文件描述 Send Engine 的核心模組、資料流、模組邊界與 Auth/Scope 設計。
|
||||||
|
本引擎只負責事件控制與執行,不提供 UI。
|
||||||
|
ESP 介接暫定為 Amazon SES。
|
||||||
|
|
||||||
|
## 核心模組
|
||||||
|
- Event Ingest
|
||||||
|
- List Store(多租戶)
|
||||||
|
- Campaign / Send Job
|
||||||
|
- Sender Adapter
|
||||||
|
- Delivery & Bounce Handling
|
||||||
|
|
||||||
|
## 信任邊界與 Auth 模型
|
||||||
|
### 外部角色
|
||||||
|
- Member Center:事件來源與名單權威來源(authority)
|
||||||
|
- 各租戶網站:內容來源(campaign/content producer)
|
||||||
|
- Send Engine:執行與狀態記錄(executor)
|
||||||
|
|
||||||
|
### 信任邊界
|
||||||
|
- Send Engine 不自行發放 tenant 身分;一律以 Member Center 代表的 tenant scope 為準
|
||||||
|
- 所有 API 必須攜帶 tenant_id,且在驗證後固定於 request context
|
||||||
|
|
||||||
|
### 驗證方式(建議)
|
||||||
|
1. **Member Center → Send Engine**
|
||||||
|
- 使用簽名 Webhook(主推),或 OAuth2 Client Credentials
|
||||||
|
- token 內含 `tenant_id` 與 scopes(例如 `newsletter:events.write`)
|
||||||
|
2. **租戶網站 → Send Engine**
|
||||||
|
- 使用 OAuth2 Client Credentials 或 JWT(由 Member Center 簽發)
|
||||||
|
- token 內含 `tenant_id` 與 scopes(例如 `newsletter:send.write`)
|
||||||
|
3. **Send Engine → Member Center**
|
||||||
|
- 使用 OAuth2 Client Credentials(Send Engine 作為 client)
|
||||||
|
- scopes 只允許 `newsletter:list.read` / `newsletter:events.read` / `newsletter:events.write`
|
||||||
|
|
||||||
|
### Tenant Scope 取得與約束
|
||||||
|
- **事件 ingest**:以 token 中的 `tenant_id` 為唯一來源,不接受 body 覆寫
|
||||||
|
- **Send Job 建立**:tenant_id 從 token 取得;list_id 必須屬於該 tenant
|
||||||
|
- **名單同步**:由 Member Center 主動推送(全量/增量),Send Engine 不主動拉取名單
|
||||||
|
- **回寫事件**:回寫時攜帶同一 tenant_id,Member Center 做二次驗證
|
||||||
|
|
||||||
|
### 防護與審計
|
||||||
|
- 所有跨服務呼叫需記錄 `tenant_id`、`client_id`、`scope`、`request_id`
|
||||||
|
- API 層進行 scope 檢查與 tenant 一致性檢查
|
||||||
|
- Event Ingest 與 Webhook 需具備重放防護(timestamp + nonce)
|
||||||
|
|
||||||
|
### 名單外流風險控制
|
||||||
|
- Send Engine 不提供「名單查詢 API」
|
||||||
|
- 全量同步由 Member Center 發動並推送
|
||||||
|
- 如需測試/診斷,僅回傳計數與版本資訊,不回傳名單內容
|
||||||
|
|
||||||
|
## 資料模型原則
|
||||||
|
- 事件為 append-only,快照表僅反映最新狀態
|
||||||
|
- 任何狀態變更必須可追溯(含來源與租戶)
|
||||||
93
docs/FLOWS.md
Normal file
93
docs/FLOWS.md
Normal file
@ -0,0 +1,93 @@
|
|||||||
|
# Flows
|
||||||
|
|
||||||
|
本文件描述 Send Engine 的三條核心流程:訂閱事件同步、發送排程、退信處理。
|
||||||
|
本引擎只負責事件控制與執行,不提供 UI。
|
||||||
|
ESP 介接暫定為 Amazon SES。
|
||||||
|
Member Center 為多租戶架構,信件內容由各租戶網站產生後送入 Send Engine。
|
||||||
|
|
||||||
|
## 1. 訂閱事件同步流程
|
||||||
|
目的:以事件為主,建立/更新本地名單快照(tenant/list scope),不做跨租戶查詢。
|
||||||
|
|
||||||
|
流程:
|
||||||
|
1. Member Center 發出事件(`subscription.activated` / `subscription.unsubscribed` / `preferences.updated`)。
|
||||||
|
2. Send Engine 的 Event Ingest 接收事件(Webhook 或 Queue 擇一)。
|
||||||
|
3. 進行驗證(tenant scope、簽章/Token、重放保護)。
|
||||||
|
4. 事件落地為 Inbox(append-only),標記 `received_at`。
|
||||||
|
5. Consumer 依 `event_id` 去重並處理:
|
||||||
|
- activated → upsert 訂閱者、掛到 tenant/list
|
||||||
|
- unsubscribed → 標記狀態為 unsubscribed(保留歷史)
|
||||||
|
- preferences.updated → 更新偏好欄位與訂閱狀態
|
||||||
|
6. 寫入 List Store 快照(只增量更新,不拉全量)。
|
||||||
|
7. 記錄處理結果與版本號(供重播與對帳)。
|
||||||
|
|
||||||
|
錯誤與重試:
|
||||||
|
- 驗證失敗 → 直接拒絕(401/403),不寫入 Inbox。
|
||||||
|
- DB 暫時性錯誤 → 重試(含指數退避),仍失敗則進 DLQ。
|
||||||
|
- 事件格式錯誤 → 標記為 invalid,記錄原因。
|
||||||
|
|
||||||
|
## 1b. 全量名單同步流程(由 Member Center 主動推送)
|
||||||
|
目的:避免 Send Engine 透過 API 拉取名單,降低名單外流風險。
|
||||||
|
|
||||||
|
流程:
|
||||||
|
1. Member Center 內部管理介面或排程觸發「名單重建」。
|
||||||
|
2. Member Center 以 webhook 推送全量名單或分批名單事件(推薦分批)。
|
||||||
|
3. Send Engine 驗證來源與 tenant scope。
|
||||||
|
4. 事件寫入 Inbox,Consumer 以批次方式重建 List Store。
|
||||||
|
5. 重建完成後標記同步版本(sync_version)。
|
||||||
|
|
||||||
|
注意事項:
|
||||||
|
- 建議以批次/游標推送,避免單筆 payload 過大
|
||||||
|
- 可於 Send Engine 端提供 `sync_received` 回應與進度回報
|
||||||
|
|
||||||
|
## 2. 發送排程流程
|
||||||
|
目的:從租戶網站送入的內容建立 Send Job,切分送信任務並控速。
|
||||||
|
|
||||||
|
流程:
|
||||||
|
1. 租戶網站以 Member Center 簽發的 token 呼叫 Send Engine API 建立 Campaign/Send Job:
|
||||||
|
- 必填:tenant_id、list_id、內容(subject/body/template 其一或組合)
|
||||||
|
- 選填:排程時間、發送窗口、追蹤設定(open/click)
|
||||||
|
2. Send Engine 驗證 tenant scope 與內容完整性,建立 Send Job。
|
||||||
|
- tenant_id 以 token 為準,body 的 tenant_id 僅作一致性檢查
|
||||||
|
- list_id 必須屬於 tenant scope
|
||||||
|
3. Scheduler 在排程時間點啟動 Send Job:
|
||||||
|
- 讀取 List Store 快照
|
||||||
|
- 依規則過濾(已退訂、bounced、黑名單)
|
||||||
|
4. 切分成可控批次(batch),寫入 Outbox。
|
||||||
|
5. Sender Worker 取出 batch,轉成 SES API 請求。
|
||||||
|
6. SES 回應 message_id → 記錄 delivery log。
|
||||||
|
7. 更新 Send Job 進度(成功/失敗/重試)。
|
||||||
|
|
||||||
|
控速策略(範例):
|
||||||
|
- 全域 TPS 上限 + tenant TPS 上限
|
||||||
|
- SES provider-specific rate limit
|
||||||
|
- 超限則延後批次並回寫排程
|
||||||
|
|
||||||
|
內容管理注意事項:
|
||||||
|
- Send Engine 不負責內容產生與編輯,僅接收已渲染或可渲染內容
|
||||||
|
- 若採模板渲染,模板需由租戶網站提供並由 Send Engine 以 tenant scope 渲染
|
||||||
|
|
||||||
|
## 3. 退信處理流程
|
||||||
|
目的:處理 ESP 回報的 bounce/complaint,並回寫本地名單狀態。
|
||||||
|
|
||||||
|
流程:
|
||||||
|
1. SES 透過 SNS/Webhook 回報事件(bounce/complaint/delivery/open/click)。
|
||||||
|
2. Webhook 驗證簽章與來源(SES/SNS 驗證)。
|
||||||
|
3. 將事件寫入 Inbox(append-only)。
|
||||||
|
4. Consumer 解析事件:
|
||||||
|
- hard bounce → 標記 bounced + 停用
|
||||||
|
- soft bounce → 記錄次數,超過門檻停用
|
||||||
|
- complaint → 立即停用並列入黑名單
|
||||||
|
5. 更新 List Store 快照與投遞記錄。
|
||||||
|
6. 回寫 Member Center 以停用訂閱(例如 hard bounce / complaint)。
|
||||||
|
|
||||||
|
回寫規則:
|
||||||
|
- Send Engine 僅回寫「停用原因」與必要欄位
|
||||||
|
- Member Center 需提供可標註來源的欄位(例如 `disabled_by=send_engine`)
|
||||||
|
|
||||||
|
資料一致性:
|
||||||
|
- 任何狀態改變需保留歷史(append-only events + current snapshot)
|
||||||
|
- 避免以 ESP 回報反向新增不存在的訂閱者(僅更新已存在者)
|
||||||
|
|
||||||
|
多租戶安全性補充:
|
||||||
|
- 所有事件與 Send Job 必須攜帶 tenant_id,且不可跨租戶讀寫
|
||||||
|
- API 僅允許 tenant scope 的操作(list/read/write)
|
||||||
4
docs/INSTALL.md
Normal file
4
docs/INSTALL.md
Normal file
@ -0,0 +1,4 @@
|
|||||||
|
# Install
|
||||||
|
|
||||||
|
- 需求:.NET SDK 8.x, PostgreSQL
|
||||||
|
- 設定:複製 `.env.example` → `.env`
|
||||||
254
docs/OPENAPI.md
Normal file
254
docs/OPENAPI.md
Normal file
@ -0,0 +1,254 @@
|
|||||||
|
# OpenAPI Notes
|
||||||
|
|
||||||
|
本文件描述 Send Engine 對外 API、Webhook 驗證與與 Member Center 的介接規則。
|
||||||
|
目標是讓 Member Center 與租戶網站可以清楚交換資料與責任邊界。
|
||||||
|
|
||||||
|
## Auth 與驗證
|
||||||
|
### 1. 租戶網站 → Send Engine API
|
||||||
|
使用 OAuth2 Client Credentials 或 JWT(由 Member Center 簽發)。
|
||||||
|
|
||||||
|
必要 claims:
|
||||||
|
- `tenant_id`
|
||||||
|
- `scope`(至少 `newsletter:send.write`)
|
||||||
|
|
||||||
|
規則:
|
||||||
|
- `tenant_id` 只能取自 token,不接受 body 覆寫
|
||||||
|
- `list_id` 必須屬於該 tenant
|
||||||
|
|
||||||
|
### 2. Member Center → Send Engine Webhook
|
||||||
|
使用簽名 Webhook(HMAC)或 OAuth2 Client Credentials(建議簽名)。
|
||||||
|
|
||||||
|
Header 建議:
|
||||||
|
- `X-Signature`: `hex(hmac_sha256(secret, body))`
|
||||||
|
- `X-Timestamp`: Unix epoch seconds
|
||||||
|
- `X-Nonce`: UUID
|
||||||
|
|
||||||
|
驗證規則:
|
||||||
|
- timestamp 在允許時間窗內(例如 ±5 分鐘)
|
||||||
|
- nonce 不可重複(重放防護)
|
||||||
|
- signature 必須匹配
|
||||||
|
|
||||||
|
### 3. Send Engine → Member Center 回寫
|
||||||
|
使用 OAuth2 Client Credentials(Send Engine 作為 client)
|
||||||
|
|
||||||
|
scope 最小化:
|
||||||
|
- `newsletter:events.write`(停用回寫)
|
||||||
|
- `newsletter:list.read`(若未來仍需查詢)
|
||||||
|
|
||||||
|
## 通用欄位
|
||||||
|
### Timestamp
|
||||||
|
- 欄位:`occurred_at`
|
||||||
|
- 格式:RFC3339,例如 `2026-02-10T09:30:00Z`
|
||||||
|
|
||||||
|
### ID
|
||||||
|
- `tenant_id`、`list_id`、`subscriber_id`、`event_id` 皆為 UUID
|
||||||
|
|
||||||
|
## 通用錯誤格式
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"error": "string_code",
|
||||||
|
"message": "human readable message",
|
||||||
|
"request_id": "uuid"
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## Webhook:Member Center → Send Engine
|
||||||
|
### A. 訂閱事件(增量)
|
||||||
|
用途:同步新增/取消/偏好變更。
|
||||||
|
|
||||||
|
Endpoint:
|
||||||
|
- `POST /webhooks/subscriptions`
|
||||||
|
|
||||||
|
Scope:
|
||||||
|
- `newsletter:events.write`
|
||||||
|
|
||||||
|
事件型別:
|
||||||
|
- `subscription.activated`
|
||||||
|
- `subscription.unsubscribed`
|
||||||
|
- `preferences.updated`
|
||||||
|
|
||||||
|
Request Body(示意):
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"event_id": "uuid",
|
||||||
|
"event_type": "subscription.activated",
|
||||||
|
"tenant_id": "uuid",
|
||||||
|
"list_id": "uuid",
|
||||||
|
"subscriber": {
|
||||||
|
"id": "uuid",
|
||||||
|
"email": "user@example.com",
|
||||||
|
"status": "active",
|
||||||
|
"preferences": { "topic": "news" }
|
||||||
|
},
|
||||||
|
"occurred_at": "2026-02-10T09:30:00Z"
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
Response:
|
||||||
|
- `200 OK`:accepted
|
||||||
|
- `401/403`:驗證失敗
|
||||||
|
- `409`:event_id 重複或 nonce 重放
|
||||||
|
- `422`:格式錯誤
|
||||||
|
|
||||||
|
### B. 全量名單同步
|
||||||
|
用途:由 Member Center 主動推送全量或分批名單,避免 Send Engine 拉取名單。
|
||||||
|
|
||||||
|
Endpoint:
|
||||||
|
- `POST /webhooks/lists/full-sync`
|
||||||
|
|
||||||
|
Scope:
|
||||||
|
- `newsletter:events.write`
|
||||||
|
|
||||||
|
Request Body(分批示意):
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"sync_id": "uuid",
|
||||||
|
"batch_no": 1,
|
||||||
|
"batch_total": 10,
|
||||||
|
"tenant_id": "uuid",
|
||||||
|
"list_id": "uuid",
|
||||||
|
"subscribers": [
|
||||||
|
{ "id": "uuid", "email": "a@example.com", "status": "active" },
|
||||||
|
{ "id": "uuid", "email": "b@example.com", "status": "unsubscribed" }
|
||||||
|
],
|
||||||
|
"occurred_at": "2026-02-10T09:30:00Z"
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
Response:
|
||||||
|
- `200 OK`:accepted
|
||||||
|
- `401/403`:驗證失敗
|
||||||
|
- `409`:sync_id + batch_no 重複
|
||||||
|
- `422`:格式錯誤
|
||||||
|
|
||||||
|
## API:租戶網站 → Send Engine
|
||||||
|
### C. 建立 Send Job
|
||||||
|
用途:租戶網站送入內容,建立 Campaign/Send Job 並排程。
|
||||||
|
|
||||||
|
Endpoint:
|
||||||
|
- `POST /api/send-jobs`
|
||||||
|
|
||||||
|
Scope:
|
||||||
|
- `newsletter:send.write`
|
||||||
|
|
||||||
|
Request Body:
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"tenant_id": "uuid",
|
||||||
|
"list_id": "uuid",
|
||||||
|
"name": "Weekly Update",
|
||||||
|
"subject": "Weekly Update",
|
||||||
|
"body_html": "<p>Hello</p>",
|
||||||
|
"body_text": "Hello",
|
||||||
|
"template": null,
|
||||||
|
"scheduled_at": "2026-02-11T02:00:00Z",
|
||||||
|
"window_start": "2026-02-11T02:00:00Z",
|
||||||
|
"window_end": "2026-02-11T05:00:00Z",
|
||||||
|
"tracking": { "open": true, "click": false }
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
欄位規則:
|
||||||
|
- `subject` 必填,最小長度 1
|
||||||
|
- `body_html` / `body_text` / `template` 至少擇一
|
||||||
|
- `window_start` 必須小於 `window_end`(若有提供)
|
||||||
|
|
||||||
|
Response:
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"send_job_id": "uuid",
|
||||||
|
"status": "pending"
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### D. 查詢 Send Job
|
||||||
|
Endpoint:
|
||||||
|
- `GET /api/send-jobs/{id}`
|
||||||
|
|
||||||
|
Scope:
|
||||||
|
- `newsletter:send.read`
|
||||||
|
|
||||||
|
Response:
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"id": "uuid",
|
||||||
|
"tenant_id": "uuid",
|
||||||
|
"list_id": "uuid",
|
||||||
|
"campaign_id": "uuid",
|
||||||
|
"status": "running",
|
||||||
|
"scheduled_at": "2026-02-11T02:00:00Z",
|
||||||
|
"window_start": "2026-02-11T02:00:00Z",
|
||||||
|
"window_end": "2026-02-11T05:00:00Z"
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### E. 取消 Send Job
|
||||||
|
Endpoint:
|
||||||
|
- `POST /api/send-jobs/{id}/cancel`
|
||||||
|
|
||||||
|
Scope:
|
||||||
|
- `newsletter:send.write`
|
||||||
|
|
||||||
|
Response:
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"id": "uuid",
|
||||||
|
"status": "cancelled"
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## Webhook:SES → Send Engine
|
||||||
|
### F. SES 事件回報
|
||||||
|
用途:接收 bounce/complaint/delivery/open/click 等事件。
|
||||||
|
|
||||||
|
Endpoint:
|
||||||
|
- `POST /webhooks/ses`
|
||||||
|
|
||||||
|
驗證:
|
||||||
|
- 依 SES/SNS 規格驗簽
|
||||||
|
|
||||||
|
Request Body(示意):
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"event_type": "bounce",
|
||||||
|
"message_id": "ses-id",
|
||||||
|
"tenant_id": "uuid",
|
||||||
|
"email": "user@example.com",
|
||||||
|
"bounce_type": "hard",
|
||||||
|
"occurred_at": "2026-02-10T09:45:00Z"
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
Response:
|
||||||
|
- `200 OK`
|
||||||
|
|
||||||
|
## 外部 API:Send Engine → Member Center
|
||||||
|
以下為 Member Center 端提供的 API,非 Send Engine 的 OpenAPI 規格範圍。
|
||||||
|
|
||||||
|
### G. 停用訂閱回寫
|
||||||
|
用途:因 hard bounce / complaint 停用訂閱,並在 Member Center 註記來源。
|
||||||
|
|
||||||
|
Endpoint(Member Center 提供):
|
||||||
|
- `POST /api/subscriptions/disable`
|
||||||
|
|
||||||
|
Scope:
|
||||||
|
- `newsletter:events.write`
|
||||||
|
|
||||||
|
Request Body(示意):
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"tenant_id": "uuid",
|
||||||
|
"subscriber_id": "uuid",
|
||||||
|
"list_id": "uuid",
|
||||||
|
"reason": "hard_bounce",
|
||||||
|
"disabled_by": "send_engine",
|
||||||
|
"occurred_at": "2026-02-10T09:45:00Z"
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## 狀態碼與錯誤
|
||||||
|
通用錯誤:
|
||||||
|
- `401/403`:Auth 或 scope 不符
|
||||||
|
- `409`:重放或事件重複(nonce / event_id)
|
||||||
|
- `422`:資料格式錯誤
|
||||||
|
- `500`:伺服器內部錯誤
|
||||||
224
docs/SCHEMA.sql
Normal file
224
docs/SCHEMA.sql
Normal file
@ -0,0 +1,224 @@
|
|||||||
|
-- PostgreSQL schema (MVP)
|
||||||
|
-- Note: This schema is optimized for low volume and long-term retention.
|
||||||
|
-- Future expansion: add delivery_detail / tracking_detail tables if needed.
|
||||||
|
|
||||||
|
-- Extensions
|
||||||
|
CREATE EXTENSION IF NOT EXISTS citext;
|
||||||
|
|
||||||
|
-- Tenants (optional; can be sourced from Member Center if not stored locally)
|
||||||
|
CREATE TABLE IF NOT EXISTS tenants (
|
||||||
|
id UUID PRIMARY KEY,
|
||||||
|
name TEXT,
|
||||||
|
created_at TIMESTAMPTZ NOT NULL DEFAULT now()
|
||||||
|
);
|
||||||
|
|
||||||
|
-- Lists (per tenant)
|
||||||
|
CREATE TABLE IF NOT EXISTS lists (
|
||||||
|
id UUID PRIMARY KEY,
|
||||||
|
tenant_id UUID NOT NULL,
|
||||||
|
name TEXT NOT NULL,
|
||||||
|
created_at TIMESTAMPTZ NOT NULL DEFAULT now()
|
||||||
|
);
|
||||||
|
CREATE INDEX IF NOT EXISTS idx_lists_tenant ON lists(tenant_id);
|
||||||
|
|
||||||
|
ALTER TABLE lists
|
||||||
|
ADD CONSTRAINT fk_lists_tenant
|
||||||
|
FOREIGN KEY (tenant_id) REFERENCES tenants(id);
|
||||||
|
|
||||||
|
-- Subscribers (per tenant)
|
||||||
|
CREATE TABLE IF NOT EXISTS subscribers (
|
||||||
|
id UUID PRIMARY KEY,
|
||||||
|
tenant_id UUID NOT NULL,
|
||||||
|
email CITEXT NOT NULL,
|
||||||
|
status TEXT NOT NULL, -- active/unsubscribed/bounced/complaint
|
||||||
|
preferences JSONB,
|
||||||
|
created_at TIMESTAMPTZ NOT NULL DEFAULT now(),
|
||||||
|
updated_at TIMESTAMPTZ NOT NULL DEFAULT now(),
|
||||||
|
UNIQUE (tenant_id, email)
|
||||||
|
);
|
||||||
|
CREATE INDEX IF NOT EXISTS idx_subscribers_tenant ON subscribers(tenant_id);
|
||||||
|
|
||||||
|
ALTER TABLE subscribers
|
||||||
|
ADD CONSTRAINT fk_subscribers_tenant
|
||||||
|
FOREIGN KEY (tenant_id) REFERENCES tenants(id);
|
||||||
|
|
||||||
|
-- List membership snapshot
|
||||||
|
CREATE TABLE IF NOT EXISTS list_members (
|
||||||
|
id UUID PRIMARY KEY,
|
||||||
|
tenant_id UUID NOT NULL,
|
||||||
|
list_id UUID NOT NULL,
|
||||||
|
subscriber_id UUID NOT NULL,
|
||||||
|
status TEXT NOT NULL, -- active/unsubscribed/bounced/complaint
|
||||||
|
created_at TIMESTAMPTZ NOT NULL DEFAULT now(),
|
||||||
|
updated_at TIMESTAMPTZ NOT NULL DEFAULT now(),
|
||||||
|
UNIQUE (tenant_id, list_id, subscriber_id)
|
||||||
|
);
|
||||||
|
CREATE INDEX IF NOT EXISTS idx_list_members_tenant ON list_members(tenant_id);
|
||||||
|
CREATE INDEX IF NOT EXISTS idx_list_members_list ON list_members(list_id);
|
||||||
|
|
||||||
|
ALTER TABLE list_members
|
||||||
|
ADD CONSTRAINT fk_list_members_tenant
|
||||||
|
FOREIGN KEY (tenant_id) REFERENCES tenants(id);
|
||||||
|
|
||||||
|
ALTER TABLE list_members
|
||||||
|
ADD CONSTRAINT fk_list_members_list
|
||||||
|
FOREIGN KEY (list_id) REFERENCES lists(id);
|
||||||
|
|
||||||
|
ALTER TABLE list_members
|
||||||
|
ADD CONSTRAINT fk_list_members_subscriber
|
||||||
|
FOREIGN KEY (subscriber_id) REFERENCES subscribers(id);
|
||||||
|
|
||||||
|
-- Event inbox (append-only)
|
||||||
|
CREATE TABLE IF NOT EXISTS events_inbox (
|
||||||
|
id UUID PRIMARY KEY,
|
||||||
|
tenant_id UUID NOT NULL,
|
||||||
|
event_type TEXT NOT NULL,
|
||||||
|
source TEXT NOT NULL, -- member_center / ses
|
||||||
|
payload JSONB NOT NULL,
|
||||||
|
received_at TIMESTAMPTZ NOT NULL DEFAULT now(),
|
||||||
|
processed_at TIMESTAMPTZ,
|
||||||
|
status TEXT NOT NULL DEFAULT 'received', -- received/processed/invalid/failed
|
||||||
|
error TEXT
|
||||||
|
);
|
||||||
|
CREATE INDEX IF NOT EXISTS idx_events_inbox_tenant ON events_inbox(tenant_id);
|
||||||
|
CREATE INDEX IF NOT EXISTS idx_events_inbox_type ON events_inbox(event_type);
|
||||||
|
CREATE INDEX IF NOT EXISTS idx_events_inbox_status ON events_inbox(status);
|
||||||
|
|
||||||
|
ALTER TABLE events_inbox
|
||||||
|
ADD CONSTRAINT fk_events_inbox_tenant
|
||||||
|
FOREIGN KEY (tenant_id) REFERENCES tenants(id);
|
||||||
|
|
||||||
|
-- Campaigns (content is provided by tenant sites)
|
||||||
|
CREATE TABLE IF NOT EXISTS campaigns (
|
||||||
|
id UUID PRIMARY KEY,
|
||||||
|
tenant_id UUID NOT NULL,
|
||||||
|
list_id UUID NOT NULL,
|
||||||
|
name TEXT,
|
||||||
|
subject TEXT,
|
||||||
|
body_html TEXT,
|
||||||
|
body_text TEXT,
|
||||||
|
template JSONB, -- optional template payload
|
||||||
|
created_at TIMESTAMPTZ NOT NULL DEFAULT now()
|
||||||
|
);
|
||||||
|
CREATE INDEX IF NOT EXISTS idx_campaigns_tenant ON campaigns(tenant_id);
|
||||||
|
|
||||||
|
ALTER TABLE campaigns
|
||||||
|
ADD CONSTRAINT fk_campaigns_tenant
|
||||||
|
FOREIGN KEY (tenant_id) REFERENCES tenants(id);
|
||||||
|
|
||||||
|
ALTER TABLE campaigns
|
||||||
|
ADD CONSTRAINT fk_campaigns_list
|
||||||
|
FOREIGN KEY (list_id) REFERENCES lists(id);
|
||||||
|
|
||||||
|
-- Send jobs
|
||||||
|
CREATE TABLE IF NOT EXISTS send_jobs (
|
||||||
|
id UUID PRIMARY KEY,
|
||||||
|
tenant_id UUID NOT NULL,
|
||||||
|
list_id UUID NOT NULL,
|
||||||
|
campaign_id UUID NOT NULL,
|
||||||
|
scheduled_at TIMESTAMPTZ,
|
||||||
|
window_start TIMESTAMPTZ,
|
||||||
|
window_end TIMESTAMPTZ,
|
||||||
|
status TEXT NOT NULL DEFAULT 'pending', -- pending/running/completed/failed/cancelled
|
||||||
|
created_at TIMESTAMPTZ NOT NULL DEFAULT now(),
|
||||||
|
updated_at TIMESTAMPTZ NOT NULL DEFAULT now()
|
||||||
|
);
|
||||||
|
CREATE INDEX IF NOT EXISTS idx_send_jobs_tenant ON send_jobs(tenant_id);
|
||||||
|
CREATE INDEX IF NOT EXISTS idx_send_jobs_status ON send_jobs(status);
|
||||||
|
|
||||||
|
ALTER TABLE send_jobs
|
||||||
|
ADD CONSTRAINT fk_send_jobs_tenant
|
||||||
|
FOREIGN KEY (tenant_id) REFERENCES tenants(id);
|
||||||
|
|
||||||
|
ALTER TABLE send_jobs
|
||||||
|
ADD CONSTRAINT fk_send_jobs_list
|
||||||
|
FOREIGN KEY (list_id) REFERENCES lists(id);
|
||||||
|
|
||||||
|
ALTER TABLE send_jobs
|
||||||
|
ADD CONSTRAINT fk_send_jobs_campaign
|
||||||
|
FOREIGN KEY (campaign_id) REFERENCES campaigns(id);
|
||||||
|
|
||||||
|
-- Outbox (batches to send)
|
||||||
|
CREATE TABLE IF NOT EXISTS send_batches (
|
||||||
|
id UUID PRIMARY KEY,
|
||||||
|
tenant_id UUID NOT NULL,
|
||||||
|
send_job_id UUID NOT NULL,
|
||||||
|
status TEXT NOT NULL DEFAULT 'queued', -- queued/sending/done/failed
|
||||||
|
size INT NOT NULL,
|
||||||
|
created_at TIMESTAMPTZ NOT NULL DEFAULT now(),
|
||||||
|
updated_at TIMESTAMPTZ NOT NULL DEFAULT now()
|
||||||
|
);
|
||||||
|
CREATE INDEX IF NOT EXISTS idx_send_batches_job ON send_batches(send_job_id);
|
||||||
|
CREATE INDEX IF NOT EXISTS idx_send_batches_status ON send_batches(status);
|
||||||
|
|
||||||
|
ALTER TABLE send_batches
|
||||||
|
ADD CONSTRAINT fk_send_batches_tenant
|
||||||
|
FOREIGN KEY (tenant_id) REFERENCES tenants(id);
|
||||||
|
|
||||||
|
ALTER TABLE send_batches
|
||||||
|
ADD CONSTRAINT fk_send_batches_job
|
||||||
|
FOREIGN KEY (send_job_id) REFERENCES send_jobs(id);
|
||||||
|
|
||||||
|
-- Delivery summary (no per-recipient details for MVP)
|
||||||
|
CREATE TABLE IF NOT EXISTS delivery_summary (
|
||||||
|
id UUID PRIMARY KEY,
|
||||||
|
tenant_id UUID NOT NULL,
|
||||||
|
send_job_id UUID NOT NULL,
|
||||||
|
total INT NOT NULL DEFAULT 0,
|
||||||
|
delivered INT NOT NULL DEFAULT 0,
|
||||||
|
bounced INT NOT NULL DEFAULT 0,
|
||||||
|
complained INT NOT NULL DEFAULT 0,
|
||||||
|
created_at TIMESTAMPTZ NOT NULL DEFAULT now(),
|
||||||
|
updated_at TIMESTAMPTZ NOT NULL DEFAULT now(),
|
||||||
|
UNIQUE (tenant_id, send_job_id)
|
||||||
|
);
|
||||||
|
CREATE INDEX IF NOT EXISTS idx_delivery_summary_job ON delivery_summary(send_job_id);
|
||||||
|
|
||||||
|
ALTER TABLE delivery_summary
|
||||||
|
ADD CONSTRAINT fk_delivery_summary_tenant
|
||||||
|
FOREIGN KEY (tenant_id) REFERENCES tenants(id);
|
||||||
|
|
||||||
|
ALTER TABLE delivery_summary
|
||||||
|
ADD CONSTRAINT fk_delivery_summary_job
|
||||||
|
FOREIGN KEY (send_job_id) REFERENCES send_jobs(id);
|
||||||
|
|
||||||
|
-- Auth: trusted clients for this service (prevent abuse)
|
||||||
|
-- Member Center and tenant sites are registered here.
|
||||||
|
CREATE TABLE IF NOT EXISTS auth_clients (
|
||||||
|
id UUID PRIMARY KEY,
|
||||||
|
tenant_id UUID, -- NULL for Member Center or global services
|
||||||
|
client_id TEXT NOT NULL UNIQUE,
|
||||||
|
name TEXT NOT NULL,
|
||||||
|
scopes TEXT[] NOT NULL,
|
||||||
|
status TEXT NOT NULL DEFAULT 'active', -- active/disabled
|
||||||
|
created_at TIMESTAMPTZ NOT NULL DEFAULT now()
|
||||||
|
);
|
||||||
|
CREATE INDEX IF NOT EXISTS idx_auth_clients_tenant ON auth_clients(tenant_id);
|
||||||
|
|
||||||
|
-- API keys for HMAC or simple client auth (store hash only)
|
||||||
|
CREATE TABLE IF NOT EXISTS auth_client_keys (
|
||||||
|
id UUID PRIMARY KEY,
|
||||||
|
client_id UUID NOT NULL,
|
||||||
|
key_hash TEXT NOT NULL,
|
||||||
|
created_at TIMESTAMPTZ NOT NULL DEFAULT now(),
|
||||||
|
revoked_at TIMESTAMPTZ
|
||||||
|
);
|
||||||
|
CREATE INDEX IF NOT EXISTS idx_auth_client_keys_client ON auth_client_keys(client_id);
|
||||||
|
|
||||||
|
ALTER TABLE auth_client_keys
|
||||||
|
ADD CONSTRAINT fk_auth_client_keys_client
|
||||||
|
FOREIGN KEY (client_id) REFERENCES auth_clients(id);
|
||||||
|
|
||||||
|
-- Replay protection for signed webhooks (optional)
|
||||||
|
CREATE TABLE IF NOT EXISTS webhook_nonces (
|
||||||
|
id UUID PRIMARY KEY,
|
||||||
|
client_id UUID NOT NULL,
|
||||||
|
nonce TEXT NOT NULL,
|
||||||
|
received_at TIMESTAMPTZ NOT NULL DEFAULT now(),
|
||||||
|
UNIQUE (client_id, nonce)
|
||||||
|
);
|
||||||
|
CREATE INDEX IF NOT EXISTS idx_webhook_nonces_client ON webhook_nonces(client_id);
|
||||||
|
|
||||||
|
ALTER TABLE webhook_nonces
|
||||||
|
ADD CONSTRAINT fk_webhook_nonces_client
|
||||||
|
FOREIGN KEY (client_id) REFERENCES auth_clients(id);
|
||||||
5
docs/TECH_STACK.md
Normal file
5
docs/TECH_STACK.md
Normal file
@ -0,0 +1,5 @@
|
|||||||
|
# Tech Stack
|
||||||
|
|
||||||
|
- C# .NET Core
|
||||||
|
- PostgreSQL
|
||||||
|
- ESP: SES / SendGrid / Mailgun
|
||||||
5
docs/USE_CASES.md
Normal file
5
docs/USE_CASES.md
Normal file
@ -0,0 +1,5 @@
|
|||||||
|
# Use Cases
|
||||||
|
|
||||||
|
- 管理者建立 Campaign
|
||||||
|
- 系統排程送信
|
||||||
|
- 退信與投遞追蹤
|
||||||
458
docs/openapi.yaml
Normal file
458
docs/openapi.yaml
Normal file
@ -0,0 +1,458 @@
|
|||||||
|
openapi: 3.1.0
|
||||||
|
info:
|
||||||
|
title: Send Engine API
|
||||||
|
version: 0.1.0
|
||||||
|
description: |
|
||||||
|
Send Engine external API and webhooks.
|
||||||
|
servers:
|
||||||
|
- url: https://send-engine.example.com
|
||||||
|
|
||||||
|
security:
|
||||||
|
- bearerAuth: []
|
||||||
|
|
||||||
|
paths:
|
||||||
|
/api/send-jobs:
|
||||||
|
post:
|
||||||
|
summary: Create send job
|
||||||
|
security:
|
||||||
|
- bearerAuth: []
|
||||||
|
requestBody:
|
||||||
|
required: true
|
||||||
|
content:
|
||||||
|
application/json:
|
||||||
|
schema:
|
||||||
|
$ref: '#/components/schemas/CreateSendJobRequest'
|
||||||
|
responses:
|
||||||
|
'200':
|
||||||
|
description: Created
|
||||||
|
content:
|
||||||
|
application/json:
|
||||||
|
schema:
|
||||||
|
$ref: '#/components/schemas/CreateSendJobResponse'
|
||||||
|
'409':
|
||||||
|
description: Conflict
|
||||||
|
content:
|
||||||
|
application/json:
|
||||||
|
schema:
|
||||||
|
$ref: '#/components/schemas/ErrorResponse'
|
||||||
|
'401':
|
||||||
|
description: Unauthorized
|
||||||
|
content:
|
||||||
|
application/json:
|
||||||
|
schema:
|
||||||
|
$ref: '#/components/schemas/ErrorResponse'
|
||||||
|
'403':
|
||||||
|
description: Forbidden
|
||||||
|
content:
|
||||||
|
application/json:
|
||||||
|
schema:
|
||||||
|
$ref: '#/components/schemas/ErrorResponse'
|
||||||
|
'422':
|
||||||
|
description: Validation error
|
||||||
|
content:
|
||||||
|
application/json:
|
||||||
|
schema:
|
||||||
|
$ref: '#/components/schemas/ErrorResponse'
|
||||||
|
/api/send-jobs/{id}:
|
||||||
|
get:
|
||||||
|
summary: Get send job
|
||||||
|
security:
|
||||||
|
- bearerAuth: []
|
||||||
|
parameters:
|
||||||
|
- name: id
|
||||||
|
in: path
|
||||||
|
required: true
|
||||||
|
schema:
|
||||||
|
type: string
|
||||||
|
format: uuid
|
||||||
|
responses:
|
||||||
|
'200':
|
||||||
|
description: OK
|
||||||
|
content:
|
||||||
|
application/json:
|
||||||
|
schema:
|
||||||
|
$ref: '#/components/schemas/SendJob'
|
||||||
|
'401':
|
||||||
|
description: Unauthorized
|
||||||
|
content:
|
||||||
|
application/json:
|
||||||
|
schema:
|
||||||
|
$ref: '#/components/schemas/ErrorResponse'
|
||||||
|
'403':
|
||||||
|
description: Forbidden
|
||||||
|
content:
|
||||||
|
application/json:
|
||||||
|
schema:
|
||||||
|
$ref: '#/components/schemas/ErrorResponse'
|
||||||
|
'404':
|
||||||
|
description: Not found
|
||||||
|
content:
|
||||||
|
application/json:
|
||||||
|
schema:
|
||||||
|
$ref: '#/components/schemas/ErrorResponse'
|
||||||
|
/api/send-jobs/{id}/cancel:
|
||||||
|
post:
|
||||||
|
summary: Cancel send job
|
||||||
|
security:
|
||||||
|
- bearerAuth: []
|
||||||
|
parameters:
|
||||||
|
- name: id
|
||||||
|
in: path
|
||||||
|
required: true
|
||||||
|
schema:
|
||||||
|
type: string
|
||||||
|
format: uuid
|
||||||
|
responses:
|
||||||
|
'200':
|
||||||
|
description: OK
|
||||||
|
content:
|
||||||
|
application/json:
|
||||||
|
schema:
|
||||||
|
$ref: '#/components/schemas/SendJobStatusResponse'
|
||||||
|
'401':
|
||||||
|
description: Unauthorized
|
||||||
|
content:
|
||||||
|
application/json:
|
||||||
|
schema:
|
||||||
|
$ref: '#/components/schemas/ErrorResponse'
|
||||||
|
'403':
|
||||||
|
description: Forbidden
|
||||||
|
content:
|
||||||
|
application/json:
|
||||||
|
schema:
|
||||||
|
$ref: '#/components/schemas/ErrorResponse'
|
||||||
|
'404':
|
||||||
|
description: Not found
|
||||||
|
content:
|
||||||
|
application/json:
|
||||||
|
schema:
|
||||||
|
$ref: '#/components/schemas/ErrorResponse'
|
||||||
|
|
||||||
|
/webhooks/subscriptions:
|
||||||
|
post:
|
||||||
|
summary: Member Center subscription events
|
||||||
|
security:
|
||||||
|
- webhookSignature: []
|
||||||
|
webhookTimestamp: []
|
||||||
|
webhookNonce: []
|
||||||
|
requestBody:
|
||||||
|
required: true
|
||||||
|
content:
|
||||||
|
application/json:
|
||||||
|
schema:
|
||||||
|
$ref: '#/components/schemas/SubscriptionEvent'
|
||||||
|
responses:
|
||||||
|
'200':
|
||||||
|
description: Accepted
|
||||||
|
'401':
|
||||||
|
description: Unauthorized
|
||||||
|
content:
|
||||||
|
application/json:
|
||||||
|
schema:
|
||||||
|
$ref: '#/components/schemas/ErrorResponse'
|
||||||
|
'403':
|
||||||
|
description: Forbidden
|
||||||
|
content:
|
||||||
|
application/json:
|
||||||
|
schema:
|
||||||
|
$ref: '#/components/schemas/ErrorResponse'
|
||||||
|
'409':
|
||||||
|
description: Duplicate event
|
||||||
|
content:
|
||||||
|
application/json:
|
||||||
|
schema:
|
||||||
|
$ref: '#/components/schemas/ErrorResponse'
|
||||||
|
'422':
|
||||||
|
description: Validation error
|
||||||
|
content:
|
||||||
|
application/json:
|
||||||
|
schema:
|
||||||
|
$ref: '#/components/schemas/ErrorResponse'
|
||||||
|
|
||||||
|
/webhooks/lists/full-sync:
|
||||||
|
post:
|
||||||
|
summary: Member Center full list sync
|
||||||
|
security:
|
||||||
|
- webhookSignature: []
|
||||||
|
webhookTimestamp: []
|
||||||
|
webhookNonce: []
|
||||||
|
requestBody:
|
||||||
|
required: true
|
||||||
|
content:
|
||||||
|
application/json:
|
||||||
|
schema:
|
||||||
|
$ref: '#/components/schemas/FullSyncBatch'
|
||||||
|
responses:
|
||||||
|
'200':
|
||||||
|
description: Accepted
|
||||||
|
'401':
|
||||||
|
description: Unauthorized
|
||||||
|
content:
|
||||||
|
application/json:
|
||||||
|
schema:
|
||||||
|
$ref: '#/components/schemas/ErrorResponse'
|
||||||
|
'403':
|
||||||
|
description: Forbidden
|
||||||
|
content:
|
||||||
|
application/json:
|
||||||
|
schema:
|
||||||
|
$ref: '#/components/schemas/ErrorResponse'
|
||||||
|
'409':
|
||||||
|
description: Duplicate batch
|
||||||
|
content:
|
||||||
|
application/json:
|
||||||
|
schema:
|
||||||
|
$ref: '#/components/schemas/ErrorResponse'
|
||||||
|
'422':
|
||||||
|
description: Validation error
|
||||||
|
content:
|
||||||
|
application/json:
|
||||||
|
schema:
|
||||||
|
$ref: '#/components/schemas/ErrorResponse'
|
||||||
|
|
||||||
|
/webhooks/ses:
|
||||||
|
post:
|
||||||
|
summary: SES/SNS events
|
||||||
|
security:
|
||||||
|
- sesSignature: []
|
||||||
|
requestBody:
|
||||||
|
required: true
|
||||||
|
content:
|
||||||
|
application/json:
|
||||||
|
schema:
|
||||||
|
$ref: '#/components/schemas/SesEvent'
|
||||||
|
responses:
|
||||||
|
'200':
|
||||||
|
description: OK
|
||||||
|
'401':
|
||||||
|
description: Unauthorized
|
||||||
|
content:
|
||||||
|
application/json:
|
||||||
|
schema:
|
||||||
|
$ref: '#/components/schemas/ErrorResponse'
|
||||||
|
'403':
|
||||||
|
description: Forbidden
|
||||||
|
content:
|
||||||
|
application/json:
|
||||||
|
schema:
|
||||||
|
$ref: '#/components/schemas/ErrorResponse'
|
||||||
|
|
||||||
|
components:
|
||||||
|
securitySchemes:
|
||||||
|
bearerAuth:
|
||||||
|
type: http
|
||||||
|
scheme: bearer
|
||||||
|
bearerFormat: JWT
|
||||||
|
webhookSignature:
|
||||||
|
type: apiKey
|
||||||
|
in: header
|
||||||
|
name: X-Signature
|
||||||
|
webhookTimestamp:
|
||||||
|
type: apiKey
|
||||||
|
in: header
|
||||||
|
name: X-Timestamp
|
||||||
|
webhookNonce:
|
||||||
|
type: apiKey
|
||||||
|
in: header
|
||||||
|
name: X-Nonce
|
||||||
|
sesSignature:
|
||||||
|
type: apiKey
|
||||||
|
in: header
|
||||||
|
name: X-Amz-Sns-Signature
|
||||||
|
|
||||||
|
schemas:
|
||||||
|
CreateSendJobRequest:
|
||||||
|
type: object
|
||||||
|
required: [list_id, subject]
|
||||||
|
properties:
|
||||||
|
tenant_id:
|
||||||
|
type: string
|
||||||
|
format: uuid
|
||||||
|
list_id:
|
||||||
|
type: string
|
||||||
|
format: uuid
|
||||||
|
name:
|
||||||
|
type: string
|
||||||
|
subject:
|
||||||
|
type: string
|
||||||
|
minLength: 1
|
||||||
|
body_html:
|
||||||
|
type: string
|
||||||
|
body_text:
|
||||||
|
type: string
|
||||||
|
template:
|
||||||
|
type: object
|
||||||
|
additionalProperties: true
|
||||||
|
scheduled_at:
|
||||||
|
type: string
|
||||||
|
format: date-time
|
||||||
|
window_start:
|
||||||
|
type: string
|
||||||
|
format: date-time
|
||||||
|
window_end:
|
||||||
|
type: string
|
||||||
|
format: date-time
|
||||||
|
tracking:
|
||||||
|
$ref: '#/components/schemas/TrackingOptions'
|
||||||
|
oneOf:
|
||||||
|
- required: [body_html]
|
||||||
|
- required: [body_text]
|
||||||
|
- required: [template]
|
||||||
|
|
||||||
|
CreateSendJobResponse:
|
||||||
|
type: object
|
||||||
|
required: [send_job_id, status]
|
||||||
|
properties:
|
||||||
|
send_job_id:
|
||||||
|
type: string
|
||||||
|
format: uuid
|
||||||
|
status:
|
||||||
|
type: string
|
||||||
|
enum: [pending, running, completed, failed, cancelled]
|
||||||
|
|
||||||
|
SendJob:
|
||||||
|
type: object
|
||||||
|
required: [id, tenant_id, list_id, campaign_id, status]
|
||||||
|
properties:
|
||||||
|
id:
|
||||||
|
type: string
|
||||||
|
format: uuid
|
||||||
|
tenant_id:
|
||||||
|
type: string
|
||||||
|
format: uuid
|
||||||
|
list_id:
|
||||||
|
type: string
|
||||||
|
format: uuid
|
||||||
|
campaign_id:
|
||||||
|
type: string
|
||||||
|
format: uuid
|
||||||
|
status:
|
||||||
|
type: string
|
||||||
|
enum: [pending, running, completed, failed, cancelled]
|
||||||
|
scheduled_at:
|
||||||
|
type: string
|
||||||
|
format: date-time
|
||||||
|
window_start:
|
||||||
|
type: string
|
||||||
|
format: date-time
|
||||||
|
window_end:
|
||||||
|
type: string
|
||||||
|
format: date-time
|
||||||
|
|
||||||
|
SendJobStatusResponse:
|
||||||
|
type: object
|
||||||
|
required: [id, status]
|
||||||
|
properties:
|
||||||
|
id:
|
||||||
|
type: string
|
||||||
|
format: uuid
|
||||||
|
status:
|
||||||
|
type: string
|
||||||
|
enum: [pending, running, completed, failed, cancelled]
|
||||||
|
|
||||||
|
TrackingOptions:
|
||||||
|
type: object
|
||||||
|
properties:
|
||||||
|
open:
|
||||||
|
type: boolean
|
||||||
|
click:
|
||||||
|
type: boolean
|
||||||
|
|
||||||
|
SubscriptionEvent:
|
||||||
|
type: object
|
||||||
|
required: [event_id, event_type, tenant_id, list_id, subscriber, occurred_at]
|
||||||
|
properties:
|
||||||
|
event_id:
|
||||||
|
type: string
|
||||||
|
format: uuid
|
||||||
|
event_type:
|
||||||
|
type: string
|
||||||
|
enum: [subscription.activated, subscription.unsubscribed, preferences.updated]
|
||||||
|
tenant_id:
|
||||||
|
type: string
|
||||||
|
format: uuid
|
||||||
|
list_id:
|
||||||
|
type: string
|
||||||
|
format: uuid
|
||||||
|
subscriber:
|
||||||
|
$ref: '#/components/schemas/SubscriberPayload'
|
||||||
|
occurred_at:
|
||||||
|
type: string
|
||||||
|
format: date-time
|
||||||
|
|
||||||
|
SubscriberPayload:
|
||||||
|
type: object
|
||||||
|
required: [id, email, status]
|
||||||
|
properties:
|
||||||
|
id:
|
||||||
|
type: string
|
||||||
|
format: uuid
|
||||||
|
email:
|
||||||
|
type: string
|
||||||
|
format: email
|
||||||
|
status:
|
||||||
|
type: string
|
||||||
|
enum: [active, unsubscribed, bounced, complaint]
|
||||||
|
preferences:
|
||||||
|
type: object
|
||||||
|
additionalProperties: true
|
||||||
|
|
||||||
|
FullSyncBatch:
|
||||||
|
type: object
|
||||||
|
required: [sync_id, batch_no, batch_total, tenant_id, list_id, subscribers, occurred_at]
|
||||||
|
properties:
|
||||||
|
sync_id:
|
||||||
|
type: string
|
||||||
|
format: uuid
|
||||||
|
batch_no:
|
||||||
|
type: integer
|
||||||
|
minimum: 1
|
||||||
|
batch_total:
|
||||||
|
type: integer
|
||||||
|
minimum: 1
|
||||||
|
tenant_id:
|
||||||
|
type: string
|
||||||
|
format: uuid
|
||||||
|
list_id:
|
||||||
|
type: string
|
||||||
|
format: uuid
|
||||||
|
subscribers:
|
||||||
|
type: array
|
||||||
|
items:
|
||||||
|
$ref: '#/components/schemas/SubscriberPayload'
|
||||||
|
occurred_at:
|
||||||
|
type: string
|
||||||
|
format: date-time
|
||||||
|
|
||||||
|
SesEvent:
|
||||||
|
type: object
|
||||||
|
required: [event_type, message_id, tenant_id, email, occurred_at]
|
||||||
|
properties:
|
||||||
|
event_type:
|
||||||
|
type: string
|
||||||
|
enum: [bounce, complaint, delivery, open, click]
|
||||||
|
message_id:
|
||||||
|
type: string
|
||||||
|
tenant_id:
|
||||||
|
type: string
|
||||||
|
format: uuid
|
||||||
|
email:
|
||||||
|
type: string
|
||||||
|
format: email
|
||||||
|
bounce_type:
|
||||||
|
type: string
|
||||||
|
enum: [hard, soft]
|
||||||
|
occurred_at:
|
||||||
|
type: string
|
||||||
|
format: date-time
|
||||||
|
|
||||||
|
ErrorResponse:
|
||||||
|
type: object
|
||||||
|
required: [error, message, request_id]
|
||||||
|
properties:
|
||||||
|
error:
|
||||||
|
type: string
|
||||||
|
message:
|
||||||
|
type: string
|
||||||
|
request_id:
|
||||||
|
type: string
|
||||||
6
global.json
Normal file
6
global.json
Normal file
@ -0,0 +1,6 @@
|
|||||||
|
{
|
||||||
|
"sdk": {
|
||||||
|
"version": "8.0.417",
|
||||||
|
"rollForward": "latestFeature"
|
||||||
|
}
|
||||||
|
}
|
||||||
Loading…
x
Reference in New Issue
Block a user