- Created migration file for rebaseline of the database schema. - Added tables: auth_clients, tenants, auth_client_keys, webhook_nonces, events_inbox, lists, campaigns, subscriptions, send_jobs, delivery_summary, and send_batches. - Defined relationships and constraints between tables. - Updated DbContext and model snapshot to reflect new entities and their configurations. - Removed deprecated ListMember entity and its references. - Introduced Dockerfile for building and running the SendEngine application. - Enhanced installer program to support tenant creation and webhook client management with Member Center integration.
326 lines
7.9 KiB
Markdown
326 lines
7.9 KiB
Markdown
# 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
|
||
- `X-Client-Id`: Auth client UUID(對應 `auth_clients.id`)
|
||
|
||
驗證規則:
|
||
- timestamp 在允許時間窗內(例如 ±5 分鐘)
|
||
- nonce 不可重複(重放防護)
|
||
- signature 必須匹配
|
||
- client 必須存在且為 active
|
||
- `auth_clients.tenant_id` 必須與 payload `tenant_id` 一致
|
||
- 不使用任何 `X-Client-Id` fallback;缺少 tenant 對應 client 時應由來源端跳過發送
|
||
- 預設拒絕 `auth_clients.tenant_id = NULL` 的通用 client(可透過 `Webhook__AllowNullTenantClient=true` 明確開啟)
|
||
|
||
### 3. Send Engine → Member Center 回寫
|
||
使用 OAuth2 Client Credentials(Send Engine 作為 client)
|
||
|
||
scope 最小化:
|
||
- `newsletter:events.write`(停用回寫)
|
||
- `newsletter:list.read`(若未來仍需查詢)
|
||
|
||
實作約定:
|
||
- 優先走 token endpoint(client credentials)
|
||
- `ApiToken` 僅作暫時 fallback(建議逐步移除)
|
||
|
||
## 通用欄位
|
||
### 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`(若有提供)
|
||
- `tenant_id` 必須已存在(不存在回 `422 tenant_not_found`)
|
||
- `list_id` 若不存在,會在該 tenant 下自動建立 placeholder list 後建立 send job
|
||
|
||
Response:
|
||
```json
|
||
{
|
||
"send_job_id": "uuid",
|
||
"status": "pending"
|
||
}
|
||
```
|
||
|
||
### C-1. Sending Proxy Submit Job(整合規格)
|
||
用途:對齊內容網站/會員平台呼叫發信代理的標準接口。
|
||
|
||
Endpoint:
|
||
- `POST /v1/send-jobs`
|
||
|
||
Request Body(欄位):
|
||
- `message_type`:`newsletter` | `transactional`
|
||
- `from`:發件人
|
||
- `to`:收件人陣列
|
||
- `subject`:主旨
|
||
- `html`:HTML 內容
|
||
- `text`:純文字內容
|
||
- `headers`:自定義 header(白名單)
|
||
- `list_unsubscribe.url`:退訂 URL
|
||
- `list_unsubscribe.mailto`:可選
|
||
- `tags.campaign_id` / `tags.site_id` / `tags.list_id` / `tags.segment`
|
||
- `idempotency_key`:冪等鍵
|
||
|
||
Response:
|
||
- `job_id`
|
||
- `status=queued`
|
||
|
||
規則:
|
||
- 必須帶 Configuration Set + Message Tags 後才能呼叫 SES
|
||
- `newsletter` 類型需帶:
|
||
- `List-Unsubscribe`
|
||
- `List-Unsubscribe-Post: List-Unsubscribe=One-Click`
|
||
|
||
### 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/hard_bounced/soft_bounced/complaint/suppression/delivery/open/click 等事件。
|
||
|
||
推薦架構(正式):
|
||
- `SES Configuration Set -> SNS -> SQS -> ECS Worker`
|
||
- 由 Worker 消費事件,不要求對外公開 webhook
|
||
|
||
相容模式(可選):
|
||
- `POST /webhooks/ses`
|
||
|
||
驗證:
|
||
- 依 SES/SNS 規格驗簽(可用 `Ses__SkipSignatureValidation=true` 暫時略過)
|
||
|
||
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`
|
||
|
||
事件對應規則(固定):
|
||
- `hard_bounced`:立即設為黑名單(`suppressed`)
|
||
- `soft_bounced`:累計達門檻後設為黑名單(`suppressed`)
|
||
- `complaint`:取消訂閱並回寫 Member Center
|
||
- `suppression`:設為黑名單(`suppressed`)
|
||
|
||
回寫 Member Center 條件:
|
||
- `hard_bounced`:設黑名單後回寫
|
||
- `soft_bounced`:達門檻設黑名單後回寫
|
||
- `complaint`:立即回寫
|
||
- `suppression`:設黑名單後回寫
|
||
|
||
回寫原因碼(固定):
|
||
- `hard_bounce`
|
||
- `soft_bounce_threshold`
|
||
- `complaint`
|
||
- `suppression`
|
||
|
||
## 外部 API:Send Engine → Member Center
|
||
以下為 Member Center 端提供的 API,非 Send Engine 的 OpenAPI 規格範圍。
|
||
|
||
### G. 停用訂閱回寫
|
||
用途:因 hard bounce / complaint 停用訂閱,並在 Member Center 註記來源。
|
||
|
||
Endpoint(Member Center 提供):
|
||
- `POST /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`:伺服器內部錯誤
|
||
|
||
## Retry 策略(整合規格)
|
||
- Throttle:指數退避重試
|
||
- Temporary network error:重試
|
||
- Hard failure:不重試
|
||
- Retry 上限可設定(例如 5 次)
|
||
|
||
## 相關環境參數
|
||
- `Bounce__SoftBounceThreshold`:soft bounce 轉黑名單門檻(預設 `5`)
|