- 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.
7.9 KiB
OpenAPI Notes
本文件描述 Send Engine 對外 API、Webhook 驗證與與 Member Center 的介接規則。 目標是讓 Member Center 與租戶網站可以清楚交換資料與責任邊界。
Auth 與驗證
1. 租戶網站 → Send Engine API
使用 OAuth2 Client Credentials 或 JWT(由 Member Center 簽發)。
必要 claims:
tenant_idscope(至少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 secondsX-Nonce: UUIDX-Client-Id: Auth client UUID(對應auth_clients.id)
驗證規則:
- timestamp 在允許時間窗內(例如 ±5 分鐘)
- nonce 不可重複(重放防護)
- signature 必須匹配
- client 必須存在且為 active
auth_clients.tenant_id必須與 payloadtenant_id一致- 不使用任何
X-Client-Idfallback;缺少 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
通用錯誤格式
{
"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.activatedsubscription.unsubscribedpreferences.updated
Request Body(示意):
{
"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:accepted401/403:驗證失敗409:event_id 重複或 nonce 重放422:格式錯誤
B. 全量名單同步
用途:由 Member Center 主動推送全量或分批名單,避免 Send Engine 拉取名單。
Endpoint:
POST /webhooks/lists/full-sync
Scope:
newsletter:events.write
Request Body(分批示意):
{
"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:accepted401/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:
{
"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必填,最小長度 1body_html/body_text/template至少擇一window_start必須小於window_end(若有提供)tenant_id必須已存在(不存在回422 tenant_not_found)list_id若不存在,會在該 tenant 下自動建立 placeholder list 後建立 send job
Response:
{
"send_job_id": "uuid",
"status": "pending"
}
C-1. Sending Proxy Submit Job(整合規格)
用途:對齊內容網站/會員平台呼叫發信代理的標準接口。
Endpoint:
POST /v1/send-jobs
Request Body(欄位):
message_type:newsletter|transactionalfrom:發件人to:收件人陣列subject:主旨html:HTML 內容text:純文字內容headers:自定義 header(白名單)list_unsubscribe.url:退訂 URLlist_unsubscribe.mailto:可選tags.campaign_id/tags.site_id/tags.list_id/tags.segmentidempotency_key:冪等鍵
Response:
job_idstatus=queued
規則:
- 必須帶 Configuration Set + Message Tags 後才能呼叫 SES
newsletter類型需帶:List-UnsubscribeList-Unsubscribe-Post: List-Unsubscribe=One-Click
D. 查詢 Send Job
Endpoint:
GET /api/send-jobs/{id}
Scope:
newsletter:send.read
Response:
{
"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:
{
"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(示意):
{
"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 Centersuppression:設為黑名單(suppressed)
回寫 Member Center 條件:
hard_bounced:設黑名單後回寫soft_bounced:達門檻設黑名單後回寫complaint:立即回寫suppression:設黑名單後回寫
回寫原因碼(固定):
hard_bouncesoft_bounce_thresholdcomplaintsuppression
外部 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(示意):
{
"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)